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
alphadist-tag. Install@arrai-innovations/vueda@alphato select the alpha channel.
- The client package publishes to public npm with the
ViewListrenders its grid flush again (ViewList,ViewList.objectsGrid):- When the default theme moved
ObjectsGrid.rootfrom aborderto thehairlineinset box-shadow, theborder-0inViewList.objectsGridstopped 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.objectsGridnow uses!shadow-noneinstead. - The default
before-listslot 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 patchedViewList.objectsGridorObjectsGrid.rootto hide that frame can remove the workaround.
- When the default theme moved
Tablerow dividers paint (TableRow,TableHeader,TableBody,TableFooter,TableHead):Table.tableuses 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.rootapplies[&>*]:border-b-hairline,TableBody.rootremoves it from the last row's cells, andTableFooter.rootadds a top divider to its first row's cells.TableHeader.rootis empty by default, because the header row's own divider covers it. A stickyTableHead.rootno longer adds a 1px box-shadow edge; the cell's divider travels with it. Tables now show dividers. An application that patchedTableRow.root,TableHeader.root,TableBody.root, orTableFooter.rootwith row or section borders should move them onto cells, or remove them if the defaults now suffice.
ObjectsGridcolumn headers stay on one line (ObjectsGrid.headerCell):- Body cells and
TableHeadalready usedwhitespace-nowrap, butObjectsGrid.headerCelldid not. A multi-word header over short values (for example "Unit Price" over0.38) wrapped onto two lines, so a table-layout header row mixed one-line and bottom-aligned two-line labels.ObjectsGrid.headerCellnow setswhitespace-nowrap, and the column widens to fit its label instead. Wide grids may scroll horizontally sooner. An application that wants a wrapping header can patchObjectsGrid.headerCellor render a shorter label in theheader(<field>)slot.
- Body cells and
ViewListcard layout no longer shows an empty "Selected" row (ViewList):ViewListalways adds the syntheticselected_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, theselected_label and value are hidden in card layout, so each card starts with its first real field. A consumerfield(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
ComboboxInputsearch field and the floatingSidebarvariant's edge used a plain 1pxborder, 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-hairlineandborder-{t,b,l,r,x,y}-hairlineutilities now set their border style as well as their width, like Tailwind's ownborder-*utilities. They relied on preflight forborder-style: solidbefore, 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 useborder-*-hairlinerather thanborderfor the same reason; see Customize VUEDA Appearance.
- The divider under the
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-strongand darkens to--foregroundon hover._ButtonOutline.rootandToggle's outline variant usehairline-border-strong hover:hairline-foreground;ToggleGroupItem's outline seams useborder-border-strong hover:border-foreground. Everything that composes_ButtonOutline.rootfollows, including the pagination controls, the calendar previous and next buttons, and theFileUploadtrigger. An application that wants the previous weight can patch_ButtonOutline.rootback tohairline-foreground.
- The neutral outline recipe drew its edge in
Editable fields have square corners (
--vueda-field-radius):- A new
--vueda-field-radiustoken (0) androunded-vueda-fieldutility give editable fields their own corner, separate from--vueda-control-radius(2px), which buttons and toggles keep.Input,Textarea,NativeSelect,SelectTrigger,InputGroupand its attachedInputGroupButton,DateField,DateRangeField,TimeField,TagsInput,NumberFieldInput, and theWidgetComboboxtrigger 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-radiusafter thebase.cssimport.
- A new
A change of authenticated user rechecks the route on screen (
makeCRUDRoutes,requireModelInfo):- The checks
makeCRUDRoutesattaches run throughbeforeEnter, 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 leftViewActionNotFoundin place of the redirect the checks would have produced on entry.makeCRUDRoutesnow watchesidentityGenerationand 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, orgroupsRedirect. requireModelInfonow returnsfalsewhen the authenticated user changes while it is fetching metadata, which cancels that navigation. It returnedundefinedbefore, which Vue Router reads as approval, so a navigation checked against the previous user's metadata completed. No action required.makeCRUDRoutesreturns the same two route records, and an application registers them as it did before. An application callingrequireModelInfofrom a guard of its own now receivesfalserather thanundefinedwhen the user changes mid-fetch.
- The checks
Breaking:
useViewListowns rich list filter state (useViewList,FilterGroup,ViewList,useFilter):useViewListis now the single owner of rich filter state, URL restoration, query parameter synchronization, and saved filter preferences. Itslist.listStategroup losesfilterArgs(the old flat query-param object);useViewListexposes a newfiltergroup instead:filter.state.addedFilters(the active-filter list) andfilter.filterables/filter.filterableDetails/filter.validFilterables(resolved from model config;validFilterablesnarrows the other two to fields with a usable, non-hidden filter type).ViewListbindsfilter.state.addedFilterstoFilterGroup'sv-modeland passes the three resolved values straight through asFilterGroup'sfilterables/filterableDetails/validFilterablesprops. This removes a race whereFilterGroupanduseViewListeach watched the route and could write back to the URL independently, and stopsFilterGroupfrom recomputing whatuseViewListalready resolved. It also closes a related race withinuseViewListitself: 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'sfilterablesandfilterableDetailsprops change meaning: both are now required, and a new requiredvalidFilterablesprop joins them.FilterGroupno 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 whenfilterables/filterableDetailswere left unset) is gone; everyFilterGroupusage must now supply all three, typically fromuseViewList'sfiltergroup.FilterGroup'sv-modelnow carries the rich active-filter array (the same shapeFilterMenu,FilterChip, andFilterFieldFormalready used) instead of a flat query-param object.FilterGroupno 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. Itsfilter-changeandquery-changeevents are removed, since the state they bridged is now owned directly byuseViewList. Thehide-filter-formevent is unchanged.ViewList's ownfilter-changeevent is renamed tofilteredand, along withquery-change, is now emitted once on mount as a live ref (toRef(() => ...)), matching the existingobjects/order/sortedevents, rather than as a repeated event fired with a plain value on every change.- New module
@vueda/use/useFilterables.jsexportsuseFilterables(modelConfig, props, state?): the filterables/details merge extracted out ofuseFilter, so a caller such asuseViewListcomputes it once and passesfilterables/filterableDetailsdown instead ofuseFilterresolving them itself.useFilter.jsno longer performs this merge or holds its own copy of the filterable field list; it only resolves field/widget components from itsfilterables/filterableDetailsprops. A custom shell built directly onuseViewList(bypassingViewList) must replace any read or write of the removedlist.listState.filterArgswithfilter.state.addedFilters, and bindfilter-group'sv-modelto it, passingfilter.filterables/filter.filterableDetails/filter.validFilterablesas the matching props. A consumer ofFilterGroupon its own (outsideuseViewList) must now resolvefilterables/filterableDetails/validFilterablesitself (e.g. viauseFilterablesfrom@vueda/use/useFilterables.js, paired with its own filtering for hidden/typeless fields) and pass all three, update itsv-modelto bind an active-filter array instead of a flat query-param object, and drop any@filter-change/@query-changelisteners — own the URL restoration and query-param conversion itself, or wrapuseViewListfor it. AViewListconsumer listening for@filter-changeshould rename its listener to@filteredand, like the existing@objects/@order/@sortedlisteners, 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):historyObjectHistorybecomeshistoryWorkflowStateHistory, 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.storeWorkflowread this URL under the keyworkflowObjectHistory, which no URL table defined. The lookup returnedundefinedunless a project registered that key itself. The store now readshistoryWorkflowStateHistoryand substitutes:app,:model, and:pkthe way the other workflow URL builders do. Rename anysetUrl("historyObjectHistory", ...)orsetUrl("workflowObjectHistory", ...)call tosetUrl("historyWorkflowStateHistory", ...), and give it a template carrying:app,:model, and:pkplaceholders.
ViewHistoryListpresents an object's history as the actions behind it (ViewHistoryList):- The server's
history_listaction now returns action groups rather than one django-simple-history record per row.ViewHistoryListrenders 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
historyexpand from model info. Thefieldsprop picks and orders fromrecorded_at,actor,kind,label,model,relation,type,field,old, andnew. The default omitsrelation, 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-startbeside the existingdata-rev-startanddata-rev-childattributes, and the theme gainskindPill,cellModel,cellModelObject, anddiffFieldkeys plusdata-missingstates ondiffandcellUserName. Therestoredhistory type is gone, because the server publishes onlycreated,updated, anddeleted; thetypePilltheme key drops itsdata-[kind=restored]classes and the Font Awesome icon pack dropstypeRestored. Replace anyfieldsvalue naminghistory_id,history_date,history_change_reason,history_type,history_user, orhistory_relationwith the new column names. Afield(history_date),field(history_user), orfield(history_type)slot override becomesfield(recorded_at),field(actor), orfield(type); theactorslot receives{ id, display, missing }rather than a name. A demo or test fixture that supplied ahistoryexpand descriptor in model info can drop it.
- The server's
Transition codes with no project view now resolve to
ViewExecuteTransition, notViewWorkflowTransition(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, becauseViewActionRouteronly routed to a transition view for the literal, reserved action name"transition", while the route guard admits the actual transition codes returned bypermitted_transitions. The router now recognizes any admitted transition code and falls back to the newViewExecuteTransition, which composesModelActionFormwith arun-actionbacked bystoreWorkflow.executeTransition(so the exact route code is submitted astransition_code, unrecased) and forwardsActionForm's dry-run and warning-acknowledgement arguments through the same confirm-then-retry flow every other model action uses.ViewAction{App}{Model}{Code}.vueandViewAction{Code}.vueproject overrides still take priority over this fallback, exactly as they do forViewAction. ViewWorkflowTransitionand 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 ofViewExecuteTransition; a project that wants an in-page transition picker (rather than one route per transition) supplies its own view.ViewActionRouternow checks for a transition before it checkscrudComponents, so a transition code always resolves through the naming-convention overrides andViewExecuteTransition, even when that code collides with acrudComponentskey. A workflow transition codedactivate,update, ordestroy(a default registry key) now rendersViewExecuteTransitioninstead of the built-in CRUD view it reached on the previous release. The same applies to a project's own key registered throughsetCrudComponents.ViewExecuteTransitionaccepts theme overrides only.ViewWorkflowTransitionacceptedICON_OVERRIDE_PROPSand resolved aflagicon throughuseIcons; that icon-override surface has no equivalent onViewExecuteTransition. Breaking: direct imports of@vueda/views/ViewWorkflowTransition.vueand itsViewWorkflowTransitiontheme key must move to@vueda/views/ViewExecuteTransition.vueand theViewExecuteTransitiontheme 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 transitioncode. A project with a transition code matching acrudComponentskey (default or custom) must rename the transition code or thecrudComponentskey to avoid the collision, since the transition now always wins. A project passing icon overrides toViewWorkflowTransitionhas no override surface to move them to onViewExecuteTransition.
- Selecting a workflow transition previously reached a confirmation form that submitted to the generic model-action endpoint and never sent
ActionFormgains adry-run-targetprop for per-target dry-run re-validation (ActionForm, useActionForm, useModelAction, ModelActionForm):- The dry-run latch moved from
useModelActionintouseActionForm, keyed by a newdryRunTargetidentity 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 matchingdryRunTargetcomputed, andModelActionFormforwards it toActionFormautomatically. No action required throughModelActionForm. A view composingActionFormdirectly for a target that can change after mount must passdry-run-targetitself to get re-validation.
- The dry-run latch moved from
Server feedback has a shared base class (
ServerFeedbackError,FormValidationError,ConfirmationRequiredError):- VUEDA now exports
ServerFeedbackErrorfrom@vueda/utils/errors.js. It is the public base class for feedback errors the form system can ingest.FormValidationErrorandConfirmationRequiredErrorextend it. They preserve their existing names, messages, response data, and concreteinstanceofchecks. useObjectForm,useActionForm, andErrorDisplaynow treat customServerFeedbackErrorsubclasses as ingestible form feedback. They excludeConfirmationRequiredError; that class remains reserved for warning confirmation when it carries a digest. Custom CRUDL adapters that need automatic form feedback should throw aServerFeedbackErrorsubclass. Populateerrorsand/ormessages. Continue to throwConfirmationRequiredErrorfor warning confirmation.
- VUEDA now exports
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.
FieldRenderernow contains a field's failure to that field. The surrounding form still renders, and the failed field shows anErrorDisplaynaming itself and the widget that failed, which reports to Sentry like any other displayed error. useFieldRenderergainederror,errored,renderFailureText, andclearError, following theerror/erroredshape 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
fieldComponentsorwidgetComponentsentry naming a component that is not registered now throws instead of resolving toundefined. 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.
- 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.
mergeThemeapplies a merge customizer and never returns a caller's object (mergeTheme):mergeThemetakes(...themes)and reduced every argument as a theme layer, so the customizerbuildFormpasses 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, matchingmergeWith.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 returningundefineddefers to the built-in callback, so class combining andcomposesreplacement still apply at the levels it leaves alone.- Called with a single theme,
mergeThemereturned that argument by reference, so mutating the result mutated the caller's theme module. It now throws aTypeErrorfor 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 asbuildFormdoes:mergeTheme({}, ...layers). No action required unless your application callsmergeThemewith 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 infieldPropsorwidgetPropstheme overrides.
Entity-encoded feedback and template text renders as literal text (
sanitizeMessage,sanitizeMessages,WidgetPreviewableTemplate):sanitizeMessageran 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<b>bold</b>rendered bold, and<script>decoded to a real tag that DOMPurify then removed, deleting text the sender meant a reader to see. The pre-pass is gone andsanitizeMessagepasses 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, andliwith thehref,target, andrelattributes, so markup a server sends deliberately still renders. This was never a sanitization bypass; DOMPurify ran after the decode throughout. - The
escapeHtmlexport 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 importedescapeHtmlfrom@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):storeModelInfocaches permission-filtered model metadata (and memoized fetch errors) byapp.model, andstoreModelConfig,storeWorkflow, andstoreModelChoicescache 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 aclearAuthScoped()action, andstoreUsercalls 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
AuthScopeInvalidatedErrorfrom@vueda/utils/errors.jsrather than resolving with data fetched for the previous user, and their failures are not memoized, so the next call issues a fresh request.requireModelInfocancels the navigation that met that error, and reports no router error. storeUsergainsprincipalId(the current user's id,nullwhen anonymous,undefinedbefore the first current-user response) andidentityGeneration(a counter incremented once per change of user, after the caches are dropped).useNavigation,useModelInfo,useModelConfig,useWorkflowTransitions, anduseModelChoiceswatchidentityGenerationand 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'sgenericConfigsandspecificConfigsare integrator input and are left alone. Choice lists seeded throughstoreModelChoices.setChoicesorsetFilterChoicesare cleared along with fetched ones, because the store cannot tell them apart.storeTheme,storeDarkMode,storeCollapseNav,storeListPreference, andsetUsingVuedaWorkfloware unaffected. No action required. If your application seedsstoreModelChoicesthroughsetChoicesorsetFilterChoices, reseed it after a change of user (watchstoreUser().identityGeneration). If you catch errors fromfetchModelInfo,getConfig, thestoreWorkflowfetches, orfetchChoices, treatAuthScopeInvalidatedErroras "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-foregroundwhile the surrounding label stays muted, and the "Page N of M" report gainsfont-medium, so both read-outs weight their numbers consistently and match the CRUDL list mockup. A newPaginationFooter.rangeEmphasistheme key styles the emphasized figures; themetaslot'sreportprop still resolves to the flat unstyled string. No action required; visual only. Restyle or drop the emphasis via thePaginationFooter.rangeEmphasistheme key.
- The "Showing X to Y of N" range read-out now lifts the row range and the total-record count to
Auth forms expose their supported programmatic update surface (AuthForm, AuthorizingForm, ViewSignIn):
AuthForm,AuthorizingForm, andViewSignInnow emitform-contexton mount. The existingform-objectevent remains a readonly ref for observing current values, whileform-context.updateValue(name, value)provides controlled programmatic updates.ViewTwoFactorAuthnow uses the form context when its recovery-code toggle changes the selected method, avoiding Vue readonly-state warnings. Replace direct assignments throughform-objectwith 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):
useModelActionprovides 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
bulkDeleteorexecuteAction; single-object actions use the object'sdeleteorexecuteAction. - Default list and object adapters now include
executeAction. Delete adapters acceptformDataand treat dry-run200responses as success only during dry runs; real deletes still require204. ModelActionFormaccepts aninstanceListprop so host views can keep selected rows in sync after real bulk destroys.useModelAction().buildRequestis no longer returned. Requires@arrai-innovations/reactive-helpers>= 24.1.0 for the per-callkeepObjects/keepObjectoptions. CustombulkDeleteadapters should acceptformDataand treat a dry-run200as success. Code that calleduseModelAction().buildRequestshould register anexecuteActionadapter instead.
Model action plumbing is reusable outside confirmation forms (useModelAction, ModelActionForm, ViewDestroy, ViewActivate):
ModelActionFormkeeps the same public props, slots, and theme keys while delegating action execution touseModelAction.ViewDestroydry-run pre-flights accept the server's200dry-run response and keep selected records visible until confirmation.- Destroy actions target the standard viewset routes: bulk destroy sends
DELETEto the model's list URL and single destroy to its detail URL, with nodestroypath segment. Every other action keeps its segment, matching the dynamic routes the server generates for@actionmethods. No action required. Custom action buttons and custom action screens can import@vueda/use/useModelAction.jswhen they need server action plumbing without theModelActionFormconfirmation page.
ViewDestroyandViewActivateno longer fail to mount (ModelActionForm, ActionForm):ModelActionFormestablishes a form context when none is injected, soViewDestroyandViewActivatecan submit, route server 400 field errors, and render validation state throughActionForm.ActionFormmounted without a form context throws an explicit error naming the missing provider. No action required. If you renderActionFormdirectly rather than throughModelActionForm, wrap it in auseForm()scope and provide the context underFormContextSymbol.
Read-only widgets resolve static choice labels (WidgetReadOnly, ViewRead):
WidgetReadOnlynow accepts the same static choice props as editable choice widgets (options,optionLabel, andoptionValue) and displays the matching option label in read-only forms.ViewReadnow shows labels such asEnterprise,USD, andNoinstead of stored values such asenterprise,usd, andfalsewhen field metadata includes static choices. The read-only value slots now also receiverawValuefor custom renderers that intentionally show stored values.- Read-only forms now pass model-info
display_choicesmetadata toWidgetReadOnlyahead of editablechoices. This lets the server label stored values for read-only display without changing the editable widget type.ChoiceFieldbacked byBooleanFieldalso 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'srawValueprop.
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-texton<Sonner>instead of targeting[data-description].
- 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
Breaking: reactive-helpers and VueUse peer floors are raised (package):
- VUEDA now requires
@arrai-innovations/reactive-helpers^25.0.0and@vueuse/core^14.0.0. The VueUse floor aligns VUEDA withreka-ui, which already depends on VueUse 14. It also stopsvuedaViteConfigfrom dedupingreka-uidown 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, readstate.objectsVersion, use related-rulepkKey, or importListError. Every reactive-helpers export VUEDA imports still exists in v25. - reactive-helpers v25 drops its
vue-deepunrefdependency and implementsdeepUnrefitself. VUEDA importsdeepUnreffrom reactive-helpers rather than fromvue-deepunref, so the import path does not change. One behavior change reaches callers: aDate,RegExp,Map, orSetnested inside an object or array now survivesdeepUnrefby 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 defaultflush: "sync", which can re-run once per dependency change in a tick instead of once per tick. Upgrade@arrai-innovations/reactive-helpersto>= 25.0.0and@vueuse/coreto>= 14.0.0together. reactive-helpers v23 and newer require Node>=22. v25 removes the related-rulepkKeyalias and theListErrorexport. Rename related-rulepkKeytofkKeyin your ownrelatedObjectsRulesandrelatedObjectRules; instanceprops.pkKeyis a different option and is unchanged. See the reactive-helpers changelog for full migration notes.
- VUEDA now requires
Badge gains
info,success, andwarningvariants (Badge):Badgeaccepted onlydefault,secondary,destructive, andoutline, 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/--warningtokens 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--warningnow shifts these badges andAlerttogether. Replace open-coded tone pills withvariant="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.executeTransitionnow maps a409 Conflictresponse to aConfirmationRequiredErrorinstead of a genericWorkflowError, and accepts anacknowledgeWarningsargument (its final parameter) that is sent as theAcknowledge-Warningsrequest 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.executeTransitionrecords which form it requested on the error'sbulkflag. No action required. CatchConfirmationRequiredErroron a409fromexecuteTransitionand pass itsdigestback asacknowledgeWarningsto proceed. Readbulkoff the error to know which warnings shape you received.
Breaking:
useObjectsWorkflowTransitionsis removed (DetailView,useDetailView):useDetailViewnow readsvalid_transitionsoff the object payload it already fetches, instead of issuing a second per-object request throughuseObjectsWorkflowTransitions/storeWorkflow. The model info'sfieldsmap only exposesvalid_transitionsfor models with a workflow, so the request is skipped entirely for models without one. If you imported@vueda/use/useObjectsWorkflowTransitions.jsdirectly, readvalid_transitionsoff the fetched object instead (asuseDetailView'sactions.availableTransitionsnow does).ViewWorkflowTransitionandstoreWorkfloware unaffected.
All-pages list loading clears stale rows immediately (listCrud):
allPagePaginatedListCrudAdaptornow clears the existing list when a replacement request starts. It previously checked a literalpageparameter instead of VUEDA's configuredpparameter, 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-pxon abg-borderelement) 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. Newh-hairline/w-hairlineutilities key a filled rule's thickness to--vueda-hairline-width; every separator and inline divider now uses them, andSuggestionListrow rules move to a DPR-trackeddivide-y. SidebarMenuButtonChild'soutlinevariant painted its ring throughhsl(var(--sidebar-border)), an invalid wrap of an OKLCH token, so the edge did not render. It now uses thehairlineutility with the sidebar tokens.SystemMessageCarddropped a stray cardbox-shadowthat collided with its own hairline border (only one could paint). No action required; visual fixes. Custom filled rules should useh-hairline/w-hairlineinstead ofh-px/w-pxso they DPR-track.
- Filled 1px rules (
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 deprecatedDetailedViewalias is removed; import@vueda/views/DetailView.vueinstead.form/andobjects-grid/are new top-level folders;support/holds renderless utilities.New import paths, grouped by destination:
Component New path under @vueda/ClickToCopyText display/click-to-copy-text/ClickToCopyText.vueConsequencesBullets display/consequences-bullets/ConsequencesBullets.vueConstraintsBar display/constraints-bar/ConstraintsBar.vueDateTimeDisplay, DateRangeDisplay display/date-display/ErrorDisplay display/error-display/ErrorDisplay.vueFieldPickerMenuList display/field-picker/FieldPickerMenuList.vueLoadingHeartbeatStrip, LoadingSkeletonGhost, LoadingSpinnerBlock, LoadingSpinnerInline display/loading/ResponsiveMenu display/responsive-menu/ResponsiveMenu.vueSortChip, SortControl, SortGroup display/sort/SystemMessageCard, TriedUrlCallout, SuggestionList, DiagnosticStrip display/system-message/FilterChip, FilterFieldForm, FilterForm, FilterGroup, FilterMenu form/filter/FieldSetMany, FieldSetRange, FieldSetSingularStackedInline, FieldSetStackedInline, FieldSetStackedInlineRow, FieldSetTabularInline form/field-set/FormModel, FieldRenderer, FormField, FormMessage form/form-model/FormGrid, FormSection, FormSectionTitle form/layout/FormConfirmDialog, TypedConfirmField form/confirm/ObjectsGrid, ObjectsGridBodyCell, ObjectsGridBodyCellSkeleton, ObjectsGridCardCell, ObjectsGridCardCellSkeleton, ObjectsGridTableHeader, ColumnText, ColumnDateTime, ColumnModelLink objects-grid/StickyBar, StickyChrome, StickyStackProvider shell/sticky/PageTitle, PageActions shell/page-title/NavigationItem navigation/item/NavigationItem.vueLinkModelView navigation/link-model-view/LinkModelView.vueActionForm, AuthForm, AuthorizingForm, DetailView, ModelActionForm views/EmptyComponent, LazyRender support/PageTitle's theme registration moved from theviewstheme family toshell(@vueda/theme/vueda-tailwind/shell/PageTitle.theme.js). The theme key is stillPageTitle, 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 exampledisplay/sort/SortChip.vue). Replace@vueda/components/DetailedView.vuewith@vueda/views/DetailView.vue. If you import theme families per family,PageTitlenow registers viashell/index.jsinstead ofviews/index.js. Theme overrides need no changes.
Sonner now loads vue-sonner's base stylesheet (Sonner):
Sonner.vuenow importsvue-sonner/style.cssas a side effect. Without it the mountedToasterhad 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 importedvue-sonner/style.cssyourself to work around this, you can drop that import.
Sonner toasts show a close button by default (Sonner):
closeButtonnow defaults totrueinstead of falling through to vue-sonner's own default offalse. 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.cssnow 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.
- vue-sonner's default close button is a circular badge positioned so it sits half outside the toast's border.
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.cssnow setscursor: grab(andgrabbingwhile dragging) anduser-select: noneon 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.
- vue-sonner only suppresses text selection once a swipe is already underway (
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.cssnow resolves all four through--vueda-duration-interaction/--vueda-ease-interaction. No action required.
- vue-sonner's stock transform / opacity / height / box-shadow transition on
Sonner
richColorsnow themed with VUEDA tokens (Sonner):richColorspreviously fell through to vue-sonner's stock palette (its own reds/greens/blues), unrelated to the VUEDA theme.Sonner.vuenow 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--popoverso the surface stays opaque.richColorscan be set globally on<Sonner>or per-toast viatoast.success(msg, { richColors: true }). No action required. Re-toning--success/--info/--warning/--destructivenow 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.vuedepends 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 overvue-sonner, so install@arrai-innovations/vue-sonnerand importtoastfrom it. An earlier unreleased build of this line used annpm:alias onvue-sonner; drop that alias if you picked it up.
- 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.
New default sign-in view (new ViewSignIn):
ViewSignInis a ready-to-route sign-in view: an email and password form in anAuthorizingFormcard (centered chrome), wired tostoreUser.loginthrough the innerActionForm, with post-login routing and MFA pending-flow detection handled byAuthorizingFormviauseSignInFlow. 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 toAuthorizingForm(header,subTitle,redirect,formProps,requireRecentLogin, theme overrides) plusfield(email),widget(email),field(password),widget(password), andaction-barslots, with any otherAuthorizingFormorActionFormslot forwarded. Additive. Point your sign-in route at@vueda/views/ViewSignIn.vueto 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 totype="submit"and triggered submission (running the action) instead of only cancelling. It is nowtype="button". No action required; bug fix. Affects every view usingActionForm's default cancel button (for exampleViewChangePasswordand the CRUD action views).
- The default "Cancel, go back" button had no explicit
AuthorizingForm now centers against the viewport, not an ancestor's height (AuthorizingForm):
theme.rootusedmin-h-full, which only resolves against an ancestor with an explicitheight(notmin-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.rootnow usesmin-h-svh, which sizes against the viewport directly regardless of ancestor cooperation. No action required unless you overrodetheme.rootyourself; 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/typographywas 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 newtext-titleutility (no separatePageTitlesits above its card),AuthForm's uses the newtext-headingutility (a page-levelPageTitlealready does), and both subtitles matchFieldDescription's supporting-text recipe.text-heading,text-title, andtext-displayjointext-bodyas 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 overrodetheme.titleexpecting it to size the heading text, retarget your override to the newtheme.header/theme.subTitlekeys.
- Both components' title region used
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
ResponsiveMenushell (a popover on desktop, a full-screen dialog on mobile) wrapping a sharedFieldPickerMenuList(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'sdialog*andaddMenu*keys andFilterMenu'seyebrow/item/emptykeys are removed; that styling now lives under the newResponsiveMenu.*(mobile dialog chrome) andFieldPickerMenuList.*(eyebrow / list / item / empty) keys.FilterMenukeeps its drill-inback/separator/drillInkeys. If you themedSortControl.addMenu*/SortControl.dialog*orFilterMenu.eyebrow/FilterMenu.item/FilterMenu.empty, move those overrides toResponsiveMenu.*andFieldPickerMenuList.*. The trigger's leading icon and the field-row chevron now resolve from the icon registry underResponsiveMenu/FieldPickerMenuList(orDefault); registersort/filter/caretDown/chevronRightaccordingly.
- The sort and filter toolbar menus now present through a shared
ObjectsGrid column headers no longer sort (ObjectsGrid, ObjectsGridTableHeader, ViewList):
- Sorting is driven entirely from the toolbar
Sortcontrol and the active-sort chips;ObjectsGridcolumn headers are no longer interactive, so the data surface carries no sort affordance.ObjectsGriddrops thesortablesandsortedprops, theupdate:sortedevent, and thesort-icon/sort-icon(<field>)slots.ObjectsGridTableHeaderdrops itssortable/ascending/descending/multiSortIndexprops and its sort-icon and multi-sort-priority rendering; theObjectsGridTableHeader.sortIconandObjectsGridTableHeader.multiSortNumbertheme 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-iconslot for header sorting, that sorting is removed; drive sort throughSortControland the sort chips (asViewListdoes), or through your own controls writing the samesortedarray. Thesort-iconslot override no longer applies to grid headers.
- Sorting is driven entirely from the toolbar
Active filters and sorts share one sticky constraints band; sort gains a chip strip (new ConstraintsBar, SortChip, SortGroup; FilterGroup, ViewList, useViewList):
ViewListnow 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 filtersfor filters,Clear sortfor 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/sortsActiveprops,filters/sortslots),SortGroup(the sort chips strip:sorted/fieldDetailsprops,update:sortedevent, drag-to-reorder), andSortChip(one sort field as a removable pill, with ashowOrdinalprop gating the ordinal / drag handle).FilterGroupgains ahostedprop that renders its chips bare for placement inside the band, and shows itsClear filtersonly with more than one filter. New theme keys:ConstraintsBar.*,SortChip.*,SortGroup.*, andFilterGroup.subgroup. Additive forViewListconsumers; no action required. RegistersortDown,close, andgripVerticalicon-registry entries (for example Font Awesome'sfaSortDown/faXmark/faGripVertical) underDefaultor forSortChipto 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;
SortControlbecomes an add-field menu;SortEditorandMobileSortComponentare removed (SortControl; removes SortEditor, MobileSortComponent):- The toolbar
Sorttrigger 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 groupClear sortappear only with more than one sort. SortEditor(the reorderable-list editor body) and the deprecatedMobileSortComponentare removed, along with their theme entries andSortEditor'sdrag-handle/toggle-order-button/sort-icon/remove-sort-button/add-sort-button/clear-sort-buttonslot contract.SortControlno longer hostsSortEditoror forwards body slots. If you usedSortEditororMobileSortComponentdirectly, or overrodeSortControl's body slots, move toSortControl(the add-field menu) plusSortGroup/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.
- The toolbar
Checkbox keeps a stable footprint across checked and unchecked states (Checkbox):
- The checkbox root now centers its indicator in an
inline-flexbox and sits onalign-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 defaultbaselinealignment 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-layoutObjectsGridgrid. No action required; visual fix.
- The checkbox root now centers its indicator in an
Checkbox and radio edges stay visible on hovered rows in dark mode (Checkbox, RadioGroupItem):
- The unchecked control edge painted the opaque
--inputtoken, which in dark mode sits at the same lightness as thebg-accent/50row-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--inputis a dark-on-light edge that never collided). Checked, focus, and invalid states are unchanged. No action required; dark-mode visual fix.
- The unchecked control edge painted the opaque
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-awareborder-b-hairline/border-t-hairlinesilently 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 rawborder-b/border-tform still works. No action required; additive. Preferborder-b-hairline/border-t-hairlineon Card headers and footers (and blockInputGroupAddons) for a divider that tracks device-pixel ratio.
- The header, footer, and block-addon divider-padding hooks previously fired only for a raw
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
checkicon-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 keysViewList.selectionCount,ViewList.selectionCountIcon, andViewList.selectionCountValuestyle the wrapper, glyph, and figure; the read-out carriesdata-qa="view-list-selection-count". No action required; additive. Register acheckentry in your icon registry (for example Font Awesome'sfaCheck) underDefaultor forViewListto show the glyph; without it the count text renders alone. Restyle via the new theme keys.
- When rows are selected, the bulk-actions strip now opens with a "✓ N selected" read-out (a primary-tinted check glyph from the
data-qahooks added across the pagination family (Pagination, PaginationContent, PaginationItem, PaginationEllipsis, PaginationFirst/Previous/Next/Last, PaginationBar, PaginationMeta, PaginationFooter):- Each pagination primitive now carries a
data-qaattribute mirroring itsdata-slot(e.g.data-qa="pagination-first"), andPaginationFootertags 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, soViewList's footer keeps itsdata-qa="view-list-pagination". No action required; additive. End-to-end suites can target the new hooks.
- Each pagination primitive now carries a
Pagination footer rebuilt on the PaginationBar substrate, renamed
PaginationComponenttoPaginationFooterand 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 renamedPaginationFooterand relocated tonavigation/pagination/PaginationFooter.vue, joining the rest of the pagination family. Its theme key is renamedPaginationComponent→PaginationFooter. PaginationFooteris rebuilt as aPaginationBarcomposition: 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.
PaginationFooterdrops theallowShowAllPagesandshowingAllPagesprops, theupdate:showingAllPagesevent, and thetotal-records/show-all-pagesslots; it adds theperPageprop (a number or"all") with anupdate:perPageevent, apageSizeOptionsprop, andmeta/rows-per-pageslots. ViewListdrops theallowShowAllPagesandalwaysShowAllPagesprops and addspageSizeOptions(default[25, 50, 100, 200, "all"]) anddefaultPageSize(default25). The matching model-config flagsallowShowAllPages/alwaysShowAllPagesare 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 thepspage-size query param for a numeric selection so the server'sperPagematches the chosen value.ViewHistoryListis converted onto the same footer: it drops theallowShowAllPagesandalwaysShowAllPagesprops and addspageSizeOptionsanddefaultPageSize. The history page size is not persisted (it resets each visit).- The
ViewList.paginationWrappertheme key is removed; the embeddedPaginationBar(theme keyNavigationPaginationBar) 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 nowPaginationFooter. RemoveallowShowAllPages/alwaysShowAllPagesfrom<ViewList>/<ViewHistoryList>usage and from model configs. To offer fewer or different page sizes, passpageSizeOptions(drop the"all"entry to forbid loading every page); to change the initial size, passdefaultPageSize. To always load all pages, setdefaultPageSize="all". A model that previously setalwaysShowAllPagesmaps todefaultPageSize="all". If you consumed the footer directly, replace theshowingAllPages/allowShowAllPagesprops and theupdate:showingAllPageshandler withperPage/pageSizeOptionsand anupdate:perPagehandler, and move anytotal-records/show-all-pagesslot content to themeta/rows-per-pageslots. If you styledViewList.paginationWrapper, move those overrides toNavigationPaginationBar.
- The composed footer (formerly
Pagination navigation buttons restyled to icon-only (PaginationFirst, PaginationPrevious, PaginationNext, PaginationLast):
- The four pagination edge controls now render as compact icon-only outline squares (
NavigationPaginationNavButtoncomposes_ButtonOutlineat thesmsquare size) instead of labeled ghost buttons. ThePrevious/Next/First/Lasttext moves to ansr-onlylabel, 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. PaginationFirstandPaginationLastnow request theanglesLeft/anglesRighticon-registry keys (previouslychevronLeft/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. RegisteranglesLeftandanglesRightentries in your icon registry (for example Font Awesome'sfaAnglesLeft/faAnglesRight), underDefaultor per-component forPaginationFirst/PaginationLast. Without them the First / Last buttons render theirsr-onlylabel only (no visible glyph). Previous / Next still usechevronLeft/chevronRight. If you relied on the visiblePrevious/Nexttext, it is nowsr-only; restyle via theNavigationPaginationNavButtontheme key or the button slots if you need it visible.
- The four pagination edge controls now render as compact icon-only outline squares (
Button resolves on two axes:
toneandemphasis; the bare default is now neutral, not primary (Button; new_Button*primitives):Buttonnow resolves its look from two independent props:tone(neutral/primary/destructive, the color) andemphasis(fill/outline/ghost/link, the structure). The previousvariantshorthand is removed.- Breaking: a bare
<Button>with notone/emphasisnow renders as a neutral filled chip (equivalent to the oldsecondaryvariant) 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 passtone="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.Buttonemitsdata-toneanddata-emphasisfor styling and test hooks. - The full tone × emphasis matrix is now realized as composition primitives. New
_Button*theme keys fill in the cells the singlevariantenum could not express:_ButtonPrimaryOutline,_ButtonDestructiveOutline,_ButtonPrimaryGhost,_ButtonDestructiveGhost,_ButtonNeutralLink, and_ButtonDestructiveLink._ButtonNeutralLinkis a foreground-toned text button (no--primarytint), for quiet text actions that should not spread the accent across every secondary affordance. - Outline cells now paint their edge with the DPR-aware
hairlineutility instead of CSSborder, including newhairline-primaryandhairline-foregroundcolor 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 settone="primary", otherwise they render neutral. Replace oldvariantvalues withtone/emphasis:defaultbecomestone="primary",secondarybecomes neutral fill,outlinebecomesemphasis="outline",ghostbecomesemphasis="ghost",destructivebecomestone="destructive", andlinkbecomestone="primary" emphasis="link". To restyle a cell app-wide (e.g. low-emphasis deletes), override its_Button*key rather thanButton.
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
Buttontone and emphasis from the action instead of a fixed style. Layer 1 (tone, model-wide): adelete/destroyaction is destructive-toned, everything else neutral, overridable per action viaactionDetails[name].tone. Layer 2 (emphasis, contextual): each view's hero action is promoted to the filled primary CTA (createon a list,update/edit on a read or detail view), while the rest rest at a placement emphasis:outlinein a page title or action bar,ghostin a dense bulk strip. - New public util
@vueda/utils/actionVariant.js(resolveActionVariant,actionTone) holds the mapping.LinkModelViewis the single chokepoint that applies it and gains propsemphasis(placement emphasis),primary(promote to the hero CTA), andtone(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, andDetailViewgain aprimaryActionsprop (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'sactionDetails[name].tone, passprimaryActions, or replace the per-action slot.
- 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
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 }. Therevealstrategy is selectable:always(never hides),scroll-up(hides until a deliberate scroll up, no idle reveal), andscroll-up-or-idle(also reveals when scrolling settles).revealalso accepts a boolean (or boolean ref/getter) to hand visibility control to the caller entirely.scrollRootaccepts an element, a ref, or a getter, and the scroll listener and idle timer tear down automatically on scope dispose.StickyBaris unchanged for consumers: same props, same markup, same default behavior (it uses thescroll-up-or-idlestrategy). 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, importuseScrollRevealfrom@vueda/use/useScrollReveal.jsrather than reaching intoStickyBar.
Framework-owned sticky page chrome (new
StickyStackProvider,StickyChrome,useStickyStack):- New opt-in layout primitives for pinning page chrome.
StickyStackProviderwraps the scrolling region (the window keeps scrolling; the provider adds nooverflow) and lays out an ordered stack of independently-revealing bars pinned to the top and bottom of the viewport. Place the page title in itstopslot; 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. StickyChrometeleports a view's chrome into a zone (zone="top"/zone="bottom") at a givenorder, with its ownrevealbehavior (auseScrollRevealstrategy or a boolean), degrading to in-place rendering when no provider is present.useStickyStackis the underlying context (provider role establishes it; view role registers a bar), mirroringusePageTitle. Each bar's sticky offset is the cumulative height of the visible bars between it and the viewport edge, computed internally (via theresolveStickyStackutility) 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-topcustom property for a sticky grid header to offset against, and warns in development when an ancestor's non-visibleoverflowwould silently break window-relativeposition: sticky. Opt-in and additive; nothing changes unless you placeStickyStackProviderin your shell. ExistingStickyBarusage is unaffected. Integration requirement: keep the provider free of any ancestor that setsoverflowto a non-visible value (auto/scroll/hidden/clip), or window-relative sticky will pin to that ancestor instead of the viewport.
- New opt-in layout primitives for pinning page chrome.
List and form chrome migrated onto the sticky stack (ViewList, ViewCreate, ViewUpdate, ViewRead, DetailView, StickyBar; integrator templates):
ViewListnow 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) throughStickyChrome. 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 noStickyStackProviderpresent, all three render inline (the bulk-actions bar now below the grid, above pagination), so lists without the provider are unchanged in behavior.StickyBargains a zone-managed mode. New propszone(top/bottom),order, andreveallet a bar teleport its surface into aStickyStackProviderzone and hand positioning and reveal to that zone, so it stacks with the page title and other chrome instead of self-sticking. Whenzoneis omitted the bar is standalone and behaves exactly as before (self-sticky to the window orscrollRoot, defaultscroll-up-or-idlereveal). The newrevealprop also exposes the reveal strategy for standalone bars, which was previously fixed atscroll-up-or-idle. In zone-managed mode the bar renders only its inner surface; thedata-qa="sticky-bar-root"wrapper (which carried the self-sticky positioning) is omitted because the zone provides it.ViewCreate,ViewUpdate,ViewRead, andDetailViewnow render their form-action / detail-action bar withzone="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-monorepoandintegrator-monorepo-dxtemplates now wrap<RouterView>inStickyStackProviderwithPageTitlein thetopslot. If you renderViewList,ViewCreate,ViewUpdate,ViewRead, orDetailViewin a shell without aStickyStackProvider, their toolbar, bulk-actions bar, action bar, and pagination now render inline (in normal flow) instead of pinned. To keep them pinned, place aStickyStackProvideraround your routed content withPageTitle(or your own title component) in itstopslot; see the updated templates. Standalone<StickyBar>usage and thescrollRootprop are unchanged.
Type-aware list columns (ViewList; new ColumnText / ColumnDateTime / ColumnModelLink,
availableColumns,columnMappings,resolveColumns):ViewListnow derives a column renderer from each field's type, the same wayModelFormderives a widget. Date, time, and datetime columns render throughDateTimeDisplay; foreign-key and one-to-one columns render as links to the related object's detail view throughLinkModelView. Every other column falls back toColumnText, which reproduces the previous plain formatted-text cell, so non-relation, non-date columns are unchanged. Object and array values that reachColumnText(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
ViewListlayer, so the grid cells stay presentation-neutral. Precedence, highest first: a consumer#field(<col>)slot, thecolumnComponentsprop on<ViewList>,modelConfig.config.columnComponents[<col>], the type default fromcolumnMappings, thenColumnText. A matchingcolumnPropsmap (prop or model config) supplies extra props to the resolved adapter. - New public surface: the
availableColumnsregistry (@vueda/utils/columnLookups.js), thecolumnMappingstable plusmergeColumnMappings(@vueda/utils/columnMappings.js), and theresolveColumnsresolver (@vueda/utils/resolveColumnComponents.js). New componentsColumnText,ColumnDateTime,ColumnModelLink. NewViewListpropscolumnComponents/columnProps, plus new model config keyscolumnComponents/columnProps(merged per field, likefieldComponents/fieldProps). ColumnModelLinkresolves its link target from the field's ownappLabel/modelmetadata (provided by model-info for writable relations), falling back toapp/modelsupplied viacolumnProps, 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 itscolumnComponentsentry 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):
storeModelInfonow camelCases each expand descriptor's own keys (for exampleapp_labelbecomesappLabel,requires_permissionbecomesrequiresPermission), matching how it already camelCases field details. The field-name keys under an expand'sfmap are still preserved verbatim (they are server lookup keys), and eachFieldInfovalue underfis still camelCased. This removes a long-standing inconsistency where an expand root keptsnake_casekeys while field details were camelCase. If you read an expand descriptor's related-model identity directly (for exampleexpandDetail.app_label), switch toexpandDetail.appLabel. Themodelkey is unchanged. DefaultViewListandModelFormusage needs no change.
Layout-independent sort control in the list toolbar (new SortControl; ViewList; MobileSortComponent deprecated):
ViewListnow shows aSortcontrol in the under-actions toolbar next toFilterswhenever 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
SortControlcomponent wraps the sharedSortEditorand teleports its trigger into a host zone.MobileSortComponentis deprecated in favor ofSortControl; it remains exported and functional for direct consumers.useViewListaddssort.canShowSorter(layout-independent) and deprecatessort.canShowMobileSorterandsort.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 SortandClear alloutside the available viewport. The actions share a row at thesmbreakpoint and stack on narrower screens.DialogContentadds afullScreenprop so the default theme emits full-screen geometry instead of combining conflicting centered-modal and viewport classes. New mobile shell theme keys areSortControl.dialog,SortControl.dialogHeader, andSortControl.dialogBody;MobileSortComponentexposes matching keys. No action is required for defaultViewListusage; the sort control appears automatically. If you renderedMobileSortComponentdirectly, migrate toSortControl(propssortables/sorted/fieldDetails/triggerTarget, eventupdate:sorted); register asorticon (andsortDownfor the direction toggle) in youruseIconsregistry 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 Sortcontrol 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-buttonslot 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 theadd-sort-buttonslot and called its click handler to append a sort, the slot now only needs to render a trigger button; the menu handles appending.
- The sort editor's
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 newSortEditorcomponent.MobileSortComponentis now a thin full-screen dialog shell that hostsSortEditor, owning only the trigger button, the dialog open/close state, and the applied-sort count badge.SortControlreuses 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
MobileSortComponenttheme namespace to a newSortEditornamespace:drawerInneris nowSortEditor.root, anddraggable,draggableItem,draggableItemInner,dragHandle,sortOrderText,select,sortInlineActionBar, andactionBarare now underSortEditor.MobileSortComponentnow ownsdialog,dialogHeader, anddialogBody; its formerdrawerkey no longer applies because the shell is no longer a drawer. If you override any of those moved keys viathemeOverride/setTheme, re-target them underSortEditor(for example,MobileSortComponent.draggableItembecomesSortEditor.draggableItem). Replace aMobileSortComponent.draweroverride with the appropriatedialog,dialogHeader, ordialogBodyoverride. - The editor no longer ships emoji as the default direction and remove glyphs. The direction toggle now renders the
sortDownicon from theuseIconsregistry (rotated 180 degrees for ascending), and the remove control renders the registrycloseicon; both fall back to no glyph when the icon is not registered, consistent with the existing drag-handle (gripVertical). Thetoggle-order-buttonandremove-sort-buttonslots still override the full controls. Register asortDownentry in youruseIconsregistry (most apps already registercloseandgripVertical) to show the direction glyph. If you relied on the emoji defaults, the controls will render without a glyph until you do.
- The multi-field sort editor body (reorderable rows, per-row field select, direction toggle, remove,
List sorting survives reloads and round-trips through the URL (storeListPreference, ViewList):
storeListPreference.getSortingreturned the stored sort as a plain object ({ 0: "-updated", 1: "mrr" }) instead of an array. On restore,ViewListfed that object straight back intosetSorting, 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.getSortingnow 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
oquery 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 withoutodoes 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
owithout removing active filters, and changing filters or search preservesowithout storing it as a filter preference. No action is required. If you readgetSortingdirectly, it now returnsstring[] | 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-backgroundnow also set the pairedtext-foregroundtoken. This prevents dark dialogs and sheets from inheriting a light-mode text color from an unrelated page ancestor.
- Portal surfaces that paint
Dependency security floor:
- The client package now requires
dompurify3.4.11 or newer so installs resolve to versions with the published DOMPurify sanitization fixes. No action is required unless your application pinsdompurifybelow 3.4.11.
- The client package now requires
Model-backed filter choices render correctly (WidgetModel):
WidgetModelno longer forwards relationappandmodelmetadata 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 defaultViewListfilters.
Choice fetch URLs no longer 301-redirect (storeModelChoices):
model_info_choicesandmodel_info_filter_choicesfetches now include the trailing slash before the query string (.../{field}/?ps=200). Previously the missing slash made Django'sAPPEND_SLASHanswer 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
Filterscontrol 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, orClear allto reset every filter. FilterComponentand its theme entry (FilterComponent) have been removed. Its per-field form controller now lives in the newFilterFieldForm, the active-filter pill in the newFilterChip, and the add-filter trigger/menu in the newFilterMenu. New theme entries:FilterFieldForm,FilterChip,FilterMenu.FilterGroupis 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 importedFilterComponentdirectly, or overrode theFilterComponenttheme key or thefilter-component/filter-dropdown-button*/filter-clear-button*slots, migrate to the new components and their slots / theme keys. Most consumers use these throughViewListand need no change.ViewList: the filter trigger moved into the under-actions toolbar (teleported fromFilterGroupinto a newfilterTriggerZone), the mobile sort affordance moved alongside it, and the active-filter chips render in a strip below. The standaloneStickyBar-wrapped filter strip is gone, so the empty error band it used to reserve no longer appears. Theme keysViewList.filterGroupBar,ViewList.filterGroupBarEyebrow, andViewList.sortComponentDivwere removed;ViewList.filterControlsandViewList.filterTriggerZonewere 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, andbuildFilterFromQueryto@vueda/use/useFilterForm.js(and exported theFilterFieldMappingstable) 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__indeep-link filter (fromVuedaFilterSet/IdInFilterSet, whose widget is aHiddenInputand which the filter metadata reports ashidden: true) has no mapped input widget; opening its filter form previously threwNo 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
keepOpenOverNestedPopperhelper (@vueda/shell/popover/keepOpenOverNestedPopper.js) wired to their content's@interact-outside. - Fixed a
cannot run an inactive effect scopewarning emitted when a filter form unmounted (drilling back, closing the popover, applying, or removing).useReactiveHookRegistryno longer runs its deferred aggregate update after its effect scope has been disposed, which also hardens any on-demand-mounted form built onuseForm. No action is required for defaultViewListusage. Filter behavior (apply, edit, remove, clear, URL round-trip) is unchanged; only the presentation and component structure changed.
- The list view no longer renders one persistent dashed "+ Field" button per filterable. Instead, a single
Sticky chrome fade gradients removed (StickyBar, PageTitle, ViewList):
- The fade gradient beneath pinned chrome has been removed everywhere.
StickyBarno longer renders itsgradientelement, andPageTitleno longer renders agradientcap instickymode. The default theme treats protection/fade gradients beneath floating UI as disallowed with no exceptions. - The
StickyBar.gradientandPageTitle.gradienttheme keys (and thedata-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 onStickyBar.inner;ViewList.filterGroupBaronly retints the strip by setting the new--vueda-sticky-bar-surfacecustom property thatStickyBar.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 overrideStickyBar.gradientorPageTitle.gradientviathemeOverride/setTheme, remove those overrides; they no longer resolve. To retint aStickyBarfrom 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 competingbg-*class on the root.
- The fade gradient beneath pinned chrome has been removed everywhere.
CRUD view default themes (form body gutter):
ViewCreate,ViewRead, andViewUpdatenow register their own theme entries, each withrootandbodyslots. Thebodyslot applies a defaultpx-5 py-5gutter 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 carrydata-qa="create-form-root"anddata-qa="create-form", matching theread-form-root/read-formandupdate-form-root/update-formmarkers already present onViewReadandViewUpdate.- The submit/action button cluster's
data-qais now plural across all three views, since the element is a container that wraps multiple buttons:create-action-buttons,read-action-buttons, andupdate-action-buttons. This renames the previous singularread-action-buttonandupdate-action-buttonmarkers (and fixesViewCreate, which was mislabeledupdate-action-buttons). If you target these clusters bydata-qain tests or selectors, updateread-action-buttontoread-action-buttonsandupdate-action-buttontoupdate-action-buttons.No action is required. Existing styling hooks still apply and merge with the theme slots:ViewUpdate'sclass/outerClassprops,ViewCreate'sclassprop, andViewRead's forwarded$attrsclass. Override thebodyslot of any of these viathemeOverride(orsetTheme) 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.useObjectFormexposes aconfirmationcontroller and aonSubmissionWarningsRequireConfirmationhook; on confirm it resubmits once, acknowledging the warnings, and on cancel it leaves the form unsaved with the warnings displayed.ViewCreateandViewUpdaterender the newFormConfirmDialogwired to that controller, and render its warnings throughFieldWarningsList. - Added
ConfirmationRequiredError(@vueda/utils/errors.js), thrown bydefaultObjectCreate/defaultObjectUpdateon a 409; both adaptors also accept anacknowledgeWarningsdigest and send it as theAcknowledge-Warningsheader. Its third constructor argument,{ bulk }, records which warnings shape the response carries; both single-object adaptors passfalse. - The
confirmationcontroller 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.FormConfirmDialogregisters itself on mount; a custom dialog must callconfirmation.register()andconfirmation.unregister(). No action is required for the defaultViewCreateandViewUpdateflows. Custom create or update shells that calluseObjectFormdirectly should render aFormConfirmDialog(or their own dialog that registers itself) bound toobjectForm.confirmation, otherwise warned saves are cancelled with a console warning. Requires a server release that implements theget_warningsconfirmation gate.
- Create/update submissions that the server answers with
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.useActionFormexposes the sameconfirmationcontroller andonSubmissionWarningsRequireConfirmationhook asuseObjectForm; 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. ActionFormmounts theFormConfirmDialogitself, soViewAction,ViewDestroy, and custom shells built onActionFormget confirmation without extra markup. This differs from object forms, where the view shell renders the dialog.ModelActionForm'sdefaultRunActionanddefaultObjectsDelete(@vueda/utils/listCrud.js) now throwConfirmationRequiredErroron a 409, and both accept anacknowledgeWarningsdigest that they send as theAcknowledge-Warningsheader. Both mark the errorbulk: true, because both issue the bulk request form.useViewDestroy'shandleDeleteforwardsacknowledgeWarningsand clears aConfirmationRequiredErrorout 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 theconfirmationcontrollers ofuseObjectFormanduseActionForm. Registration (register()/unregister()) and fail-closed semantics are unchanged. - A bulk request receives warnings in the per-object
{object_id: {field: [messages]}}shape, keyed bystr(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.ConfirmationRequiredErrorcarries that knowledge on abulkflag, set through its third constructor argument, and theconfirmationcontroller exposes it asconfirmation.bulk. - Added
FieldWarningsList(@vueda/form/confirm/FieldWarningsList.vue), which renders one object's field-keyed warnings.non_field_errorsrenders 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. Itsentryslot replaces one field's layout and receivesfieldandmessages. ModelActionFormgroups a bulk confirmation's warnings into one display group per object id, resolves each id to a display label through the sameWidgetReadOnlylink its selected-objects list uses, and renders each group's field warnings throughFieldWarningsList. It reads the shape from the error'sbulkflag rather than from the selection count, so a customrun-actionthat issues a bulk request for one object keeps its object attribution. Itsform-confirm-dialog-warningsslot addsnormalizedWarnings(the resolved groups) to the scope, and itswarning-entryslot forwards to every group'sFieldWarningsListwithpkadded.FormConfirmDialog'swarningsslot scope changed. It now carrieswarnings(the controller's raw mapping, whichever shape it is in),flatWarnings(every message flattened into one array), andbulk. The scope'swarningspreviously held the flattened array; that value is nowflatWarnings. The dialog's own default rendering still showsflatWarnings, so it makes no assumption about the shape. No action is required forViewAction,ViewDestroy, or shells built onActionForm. Callers that useuseActionFormwithout theActionFormshell should render aFormConfirmDialogbound to the returnedconfirmationcontroller, otherwise warned actions resolve as cancelled with a console warning. A customrun-actionthat issues a bulk request must reportbulk: trueon itsConfirmationRequiredError, orModelActionFormrenders each object id as a field name and drops object attribution. A slot override readingwarningsas a flat message array should readflatWarningsinstead. Requires a server release with the action and bulk warning gate (viewsetget_warnings_for_object(action, obj)andget_warnings(action, objs),gate_warnings,@action(confirm=True)).
- Action submissions that the server answers with
Breaking:
FormValidationErrorno longer parses a.warningschannel:FormValidationError(@vueda/utils/errors.js) no longer splits flattened response paths on a.warningsregex. Every path in a 400 response now populates.errors;.messagesis 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 the409/ConfirmationRequiredErrorconfirmation gate now, not through a.warnings-shaped 400 body. No repository production code raisedis_warning=True, so no shipped server response ever produced the.warnings-shaped 400 body this regex parsed. If a model has a field literally namedwarnings, a validation error on it now correctly lands in.errorsinstead of being diverted into.messages.
useWarnings (removed):
- The proactive warning fetch has been removed. The
useWarningscomposable, itssetUsingWarningstoggle, and theonRetrieveErrorHandlerhelper are gone, along with the automaticGET /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 a409that the client resolves viaConfirmationRequiredError) is unchanged, andhandleServerFormValidationErrorandFormMessage(type="message") still route and render them intostate.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 anysetUsingWarnings(...)calls, which no longer exist.
- The proactive warning fetch has been removed. The
Page title (PageTitle, usePageTitle, PageActions):
- The page
<h1>is no longer rendered inside each view. Views contribute their title and loading state through the newusePageTitlecomposable, andPageTitleis now a layout-level display that the integrator places above<RouterView>. Page-level action buttons are wrapped in the newPageActionscomponent, which teleports them into the title bar's action zone (with an inline fallback when no zone exists). PageTitleno longer accepts thetitleorloadingprops, nor theeyebrow,title-suffix,subtitle,under-actions,footer, orbuttonslots. It reads the title and loading state fromusePageTitleand exposes atitleslot plus the action zone.ViewList's search and column controls now render in the view body instead of the title bar.AuthFormrenders its own heading markup rather than embeddingPageTitle. Establish the context once in your root layout: callusePageTitle()inTheApp.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 callingusePageTitle().
- The page
Theme registration and lazy loading:
- The built-in
vueda-tailwindtheme is now authored as per-component*.theme.jsmodules. Components can register only the theme entries they need, while the existing globalsetTheme(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 globalsetTheme(vuedaTailwind)call and let components register their own theme entries as they render. Keep importing@vueda/theme/vueda-tailwind/base.css.
- The built-in
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 exportsfontAwesomeFreeIconsand a default registry object that can be passed tosetIcons(), plusinstallFontAwesomeFreeIcons()for apps that want a one-call installer. To use it, install@fortawesome/fontawesome-svg-core,@fortawesome/free-solid-svg-icons, and@fortawesome/vue-fontawesomein the consuming app, import Font Awesome CSS according to your app setup, then callsetIcons(fontAwesomeFreeIcons)during app bootstrap. Apps using another icon system can keep registering their own icon registry.
- The default Tailwind theme now ships an opt-in Font Awesome Free icon registry at
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 arecalendar,search,upload,minus, andhourglass; the pass also reuses existingclose,gripVertical, andpluskeys.ViewLoadingnow delegates its crest icon toSystemMessageCard, usingloadingnormally andhourglasson 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 underDefaultor under the named component to replace the text fallback.
- These components now resolve their built-in icon affordances through
Semantic icon slots removed (ActionForm, ModelActionForm, ViewDestroy, SystemMessageCard):
ActionFormno longer exposesvalidation-icon;ModelActionFormandViewDestroyno longer exposebanner-icon;SystemMessageCardno longer exposescrest-icon. These icons now render throughuseIcons()or, forSystemMessageCard, through itsiconNameandiconPropsprops backed by the registry. Replace those slots with registry entries oriconOverride. UseActionForm.triangleExclamation,ModelActionForm.info/circleCheck/triangleExclamation,ViewDestroy.triangleExclamation, andSystemMessageCard.<iconName>orDefault.<iconName>.
Deep icon pass-through slots removed (Calendar, ViewList):
Calendarno longer forwardscalendar-prev-iconorcalendar-next-iconinto its navigation buttons, andViewListno longer forwardscolumns-select-dropdown-iconinto the columnsSelectTrigger.CalendarandRangeCalendarnow accepticonOverrideand provide it to their descendant navigation buttons. Replace these deep icon slots withiconOverride: useCalendarPrevButton.chevronLeftandCalendarNextButton.chevronRightforCalendar,RangeCalendarPrevButton.chevronLeftandRangeCalendarNextButton.chevronRightforRangeCalendar, andSelectTrigger.caretDownfor theViewListcolumns 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()andiconOverride. Replace#icon,#search-icon,#indicator-icon,#check-icon,#close-icon, and Sonner's toast icon slots with registry entries oriconOverride. Common keys arecaretDown,chevronRight,check,close,search,ellipsis,toggle,loading,info, andtriangleExclamation; for Sonner, success usescheck, info usesinfo, warning usestriangleExclamation, error and close useclose, and loading usesloading.
- 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
Sort direction icon slots removed (SortChip, SortEditor):
SortChipandSortEditorno longer expose thesort-iconslot. Their direction glyphs resolve through thesortDownicon registry key;SortEditorstill exposestoggle-order-buttonfor replacing the whole direction control. Replace#sort-iconcustomizations with asortDownregistry entry underDefault,SortChip, orSortEditor, or replace the full SortEditor control through#toggle-order-button.
Toast dependencies:
@arrai-innovations/vue-sonneris now a peer dependency of@arrai-innovations/vueda, and the Copier client templates install it directly. Add@arrai-innovations/vue-sonnerto consuming applications and importtoastfrom it, so direct imports and VUEDA's toaster resolve the same package instance.
Field shell layout:
- Horizontal
Fieldlabels now use a shrinkable, capped column, andFieldContentcan shrink inside flex rows. This prevents controls and helper text from overflowing narrow horizontal field containers.
- Horizontal
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
scrollRootprop. 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 toscrollRootinstead of the window. No action is required. The prop defaults tonull, which preserves the existing window-based behavior.
- Added a
WidgetImage, WidgetFile:
- Both widgets now consume the
{name, url}representation produced by the serverFileFieldandImageFieldserializer fields.WidgetImageunwrapsurlfor the preview image and distinguishes a freshly pickedFile(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 readsurlinstead of the previously unpopulatedobjectURL. WidgetImagenow 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)Filein a new tab via a local object URL, revoked when the selection changes or the widget unmounts. Previously the link pointed theFileobject at itshref, which navigated to[object Object]. Persisted{name, url}references continue to link to theirurlin 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 mapsmodels.FileFieldandmodels.ImageFieldto VUEDA's serializer fields.
- Both widgets now consume the
Redundant
*Classprops removed (LinkModelView, DetailView, InputOTP):LinkModelViewno longer accepts abuttonClassprop. The component renders a single root (theButton) and inherits attributes, so aclassset on<LinkModelView>already falls through to the underlying button and merges with its theme classes. Replace:button-class="…"with:class="…"(or a plainclass="…"). To restyle deeper button slots, forwardthemeOverrideinstead.DetailViewno longer accepts theheaderClass,titleClass,bodyClass, orloadingClassprops. These targeted a header/title region thatDetailViewno longer renders (the page title now lives in the layout-levelPageTitle), so the props had no effect. Remove these props from<DetailView>usage. Style the page title viaPageTitle'sthemeOverride;DetailView'sclass(root) andouterClass(form wrapper) props are unchanged.InputOTPno longer accepts acontainerClassprop. It was always overridden internally by the computed container class (theme('root')plus theclassprop). Use theclassprop to add classes to the OTP container, orthemeOverrideagainstInputOTP.root.
*Classprops folded intothemeOverride(PageTitle, ViewUpdate, DetailView):PageTitleno longer accepts aheaderClassprop. The class it added is now supplied through the theme: target therootslot viathemeOverride(orsetTheme). Replace:header-class="…"on<PageTitle>with:theme-override="{ PageTitle: { root: { class: '…' } } }".ViewUpdateno longer accepts anouterClassprop, and now acceptsthemeOverride. The form-body wrapper's classes come from thebodytheme slot; merge extra classes by overriding that slot. Replace:outer-class="…"on<ViewUpdate>with:theme-override="{ ViewUpdate: { body: { class: '…' } } }".DetailViewnow registers its own theme entry withrootandbodyslots (matchingViewCreate/ViewRead/ViewUpdate) and acceptsthemeOverride; itsouterClassprop is removed. Thebodyslot applies the samepx-5 py-5content 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 thebodyslot to reset the padding.
themeOverridenow accepted by all themed components:- A set of themed components did not expose the
themeOverrideprop, so per-instance overrides passed to them were ignored even though they resolve their classes through the theme system. They now acceptthemeOverrideconsistently with the rest of the library:ClickToCopyText,FieldRenderer,FilterForm,FilterGroup,MobileSortComponent,ModelActionForm,FieldSetStackedInline,FieldSetSingularStackedInline,ViewCreate,ViewRead,ViewHistoryList,ViewSetupDevice,ViewRecoveryCodes,ViewTwoFactorAuth, andWidgetPreviewableTemplate. No action is required. To restyle one of these per instance, pass:theme-override="{ <Component>: { <slot>: { class: '…' } } }"instead of relying on a globalsetTheme/patchTheme.FieldRendererresolves theFormModeltheme key.
- A set of themed components did not expose the
verbslot prop removed (ActionForm, FieldSetSingularStackedInline, FieldSetStackedInline, FieldSetTabularInline, FieldSetStackedInlineRow, ViewAction, ViewActivate):- The
verbslot 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 byuseIconsand theiconOverrideprop (ICON_OVERRIDE_PROPS). If your slot overrides read theverbprop 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 } } }".
- The
severityslot prop removed (MobileSortComponent, FilterGroup, DetailView, ViewRead, ViewUpdate):MobileSortComponentno longer forwards aseverityslot prop to itstoggle-drawer-button,remove-sort-button,add-sort-button, orclear-sort-buttonslots. The default button renders in the appropriate variant without it.FilterGroupno longer forwards aseverityslot prop to itsclear-filters-buttonslot.DetailView,ViewRead, andViewUpdateno longer passseverityto the action and workflow-transition button slots.severitywas a PrimeVue-specific button styling prop that has no meaning in the current tone / emphasis button model. If your slot overrides readseverityto style a button, switch totone/emphasisinstead.
iconOverrideprop andICON_OVERRIDE_PROPS(useIcons):- Components can now accept a per-instance
iconOverrideprop, the icon-registry counterpart tothemeOverride. SpreadICON_OVERRIDE_PROPS(from@vueda/use/useIcons.js) into a component's props and passpropsas the new second argument touseIcons(componentName, props). The component's own icons then resolve against itsiconOverride, 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 removedverbslot prop was meant to serve.useIconscalled with only a component name is unchanged. - Every current Vue component that renders icons through
useIcons()now spreadsICON_OVERRIDE_PROPSand passespropstouseIcons, soiconOverrideworks 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; previouslyuseIconsOverridedeep-cloned the base registry, which would have structurally cloned the component definition. That path had no callers until now. Opt a component in by spreadingICON_OVERRIDE_PROPSand passingpropstouseIcons. Pass overrides as:icon-override="{ <Component>: { <iconName>: { component, props } } }", or use aDefaultbucket to cover every component in the subtree; aDefaultoverride outranks a component-specific entry in the default registry.markRawany component placed in the prop, since Vue makes the prop reactive before the registry can mark it.
- Components can now accept a per-instance
vuedaViteConfigallow-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@vuedaalias (for example a list renderingColumnText).vuedaViteConfignow returns aserver.fs.allowlisting the workspace root plus the linked vueda source realpath, so linked source serves without manual configuration. Registry/workspace installs resolve insidenode_modulesand get noserverfragment, unchanged. No action required if you spread the helper result and do not set your ownserverblock (the integrator templates do this). If you declare your ownserver, do not let a plain spread overwrite the helper's: merge instead, e.g.mergeConfig(vuedaViteConfig(...), { server: { ... } }), or preservefs.allowinside yourserverblock (fs: { allow: [...(vueda.server?.fs?.allow ?? []), ...yourEntries] }). Otherwise the linked-source allow-list is dropped and the dev server rejects vueda source again.
- When vueda is wired in with
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 thehairlineinset box-shadow (recoloured via--vueda-hairline-color); single-side edges useborder-*-hairline; floating surfaces useoverlay-hairline. Colours, widths (at DPR 1.0), and states are unchanged. Alert,Sonnertoasts, 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.Sonneralso overrides vue-sonner's built-inborderand 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 onebox-shadow) and itsoverlay-hairline-elevatedmodifier (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 ahairline-*variant) instead ofborder-*. If a theme key set bothhairlineand ashadow-*utility, useoverlay-hairlineinstead. They both writebox-shadowand cannot coexist.
- Component edges that were real 1px CSS
Combobox search input behavior for a selected value (WidgetCombobox):
- Selecting an option in an API-backed
WidgetComboboxleft the search box showing the raw value instead of the label. An API-backedWidgetComboboxnow 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
WidgetComboboxafter 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.
- Selecting an option in an API-backed
Tabular inline columns show their field labels again (FieldSetTabularInline):
FieldSetTabularInlineforwards aheader(fieldName)slot toObjectsGridfor 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'slabelin both layouts, matching theObjectsGridheader defaults (in card layout the label keeps the card header class and thedata-card-headerattribute). A suppliedheader(fieldName)slot still replaces the label entirely, and the synthetic item-action column stays unlabeled. No action required. If you added aheader(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
orderingpayload is now{ default: string[], fields: OrderInfo[] }instead of a bareOrderInfo[]array;fieldsis the same per-field metadata as before, anddefaultlists the field names (each optionally reversed via that field'sascending: false) the server applies when a request carries no explicit sort.ModelConfig.sortedis now derived fromordering.defaultinstead of always starting empty. - A
ViewList(or anyuseViewListconsumer) 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 returnedsortobject gainsdefaultSorted, the sanitized default sort array. - A default the viewer never chose is shown without being sent. It stays out of the
oquery 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:VuedaSearchFilterBackendranks a search by relevance only whileois 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 arouter.replaceand sent aso, 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 onlyupdateSortedrecords a choice. A pristine list URL and its outgoing request are unchanged: a route with noostill gets none. If you drive your own request offsort.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 fromClear sort(emptied the sort entirely) toReset 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 newdefaultSortedprop (same--prefixed array shape assorted) and itsdata-qachanged fromsort-cleartosort-reset.- Reset sort emits an empty
update:sortedrather thandefaultSorted, leaving the host to decide what "the default" means;SortGroupreadsdefaultSortedonly to know whether the control has anything to do.useViewListanswers that empty array by clearing the stored sort preference and applyingdefaultSorted, 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. SortChipgains aremovableprop (defaulttrue) gating its trailing remove ("x") control and divider.SortGrouppassesfalsewhenever 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 driveSortGroup/SortChipdirectly (outsideViewList), passdefaultSortedtoSortGroup(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 emptyupdate:sortedto your own default if you want reset rather than clear semantics, and updatedata-qa="sort-clear"selectors tosort-reset. If you relied onModelConfig.sortedalways starting empty, it now reflects the server's default; pass an explicitsortedoverride insetConfigif you need the old empty behavior.SortChipconsumers that always want a visible remove control should pass:removable="true"explicitly.
- The model-info
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 fromavailableFields.FormFieldis now the generic field renderer. Replace direct imports or string references such asFieldString,FieldNumber, orFieldDatewithFormFieldplus the appropriate validation mode.
- Type-specific
makeCRUDRoutes:makeCRUDRoutesnow requires an explicitactionRedirectroute for missing model/action guard paths. Pass a redirect route that is not itself gated byrequireModelInfo, for example{ name: "not-found" }.
ViewReadattribute forwarding:ViewReadno longer delegates its full shell toDetailView. Arbitrary HTML attributes now land on the inner content wrapper instead of the previous inner form element. If you passedclass,style, ordata-*attributes to style the previous form node, retarget those selectors.
DetailedView:DetailViewis now the canonical detail base component.DetailedViewis deprecated. Import@vueda/components/DetailView.vueinstead 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-tailwindas 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.
- Added
- View composables:
- Added
useViewList,useViewUpdate, anduseDetailViewso custom shells can reuse VUEDA's default list and detail behavior without copying default view components.
- Added
- View and form safety components:
- Added
TypedConfirmField,ConsequencesBullets, andconfirmTextsupport on destructive action flows. - Added
UserAvatarandSidebarUserBlockfor account and audit UI.
- Added
- History and grid presentation:
- Improved
ViewHistoryListwith user avatars, history-type pills, a meta strip, filter slot support, and table/card layout controls. - Improved
ObjectsGridcard mode, empty states, marked-destroy rows, and embedded-grid border behavior.
- Improved
- 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.
ViewUpdateredirects:redirectAfternow redirects after a successful update.
ViewReadevents:ViewReadnow emits declaredrelated-objectandcalculated-objectevents.
FieldSetSingularStackedInlineinitial values:- Singular stacked inline rows now wait for
fieldObjectsbefore auto-creating the row and loading initial values.
- Singular stacked inline rows now wait for
SidebarTriggericon behavior:- The sidebar toggle glyph now resolves through the icon registry instead of rendering the old placeholder character. Register a
SidebarTrigger.toggleicon or provide theiconslot to show a visible glyph.
- The sidebar toggle glyph now resolves through the icon registry instead of rendering the old placeholder character. Register a