-
v0.20.0 Stable
released this
2026-09-16 00:55:17 +00:00 | 109 commits to main since this releaseAdded
BlockEditortakeshint,emptyTextandaddLabel(typeLabel), the
three visible strings it used to hard-code in English: the reorder hint
above the list, the edit-mode empty message under theemptyslot, and the
add-button label built from each block type'slabel. The English defaults
are unchanged, so a Dutch consumer can passTekst toevoegenand a Dutch
hint without CSS overrides or replacing the add row. The aria-only labels
and announcements are still English.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.19.0 Stable
released this
2026-09-16 00:31:09 +00:00 | 112 commits to main since this releaseAdded
- The English accessibility text the icon-only controls and landmarks used
to hardcode is now a prop with that text as its default, following
AppLayout'sskipLabel/drawerLabel/navigationLabel, so a
single-language consumer can name them in its own language.Pagination
takeslabel(the<nav>landmark,Pagination),previousLabel
(Previous page) andnextLabel(Next page);Breadcrumbtakes
label(Breadcrumb);NumberFieldtakesincreaseLabel(Increase)
anddecreaseLabel(Decrease);TabletakesselectAllLabel
(Select all rows), whichDataTableforwards alongside its own
selectRowLabel, a string or a function of the row that replaces the
defaultSelect <first cell>name;ToasttakescloseLabel(Close),
andToastHosttakes the samecloseLabelfor every toast it renders
next to its existinglabelfor the notifications region.
PaginationFooterhas no text of its own and is unchanged.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The English accessibility text the icon-only controls and landmarks used
-
v0.18.0 Stable
released this
2026-09-16 00:00:32 +00:00 | 119 commits to main since this releaseChanged
NumberFieldtakessteppers(defaulttrue);falserenders a plain
number input without the +/− column.
Fixed
Checkbox,RadioGroupandSwitchinputs are their declared em size
again. The baseinput, textarea, selectrule padded them by--space-s,
which inflated a 1.15em checkbox to 35px; all three resetpaddingto 0.
Added
FilterCheckbox,FilterSelect,FilterMultiSelectandFilterRange
(@[secure]/ui/components/Filter*.vue), the filter controls an index
definition declares. Each binds to the index that owns the query — the
stateIndexprovides to itsfiltersslot, the one a definition provides
withprovideIndexQuery, or an explicitstateprop — and reads and
writes only its own filter names; mounted without a state it warns once
and does nothing. The checkbox writes'true'or deletes; the select
prepends anemptyLabeloption that deletes; the multi-select is a
fieldset of checkboxes whose legend readsLabel (n)with a Clear while
n > 0, optional per-option counts, an optional client-side search, and
values joined on the comma convention; the range is two NumberFields
without steppers writing<name>_minand<name>_maxon change, each
cleared on its own.
injectIndexQuery()joins the composable exports for a control that needs
the provided state without throwing when there is none.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.17.0 Stable
released this
2026-09-15 23:40:41 +00:00 | 131 commits to main since this releaseAdded
splitFilterValuesandjoinFilterValuesin@[secure]/ui/utils/cms, for
the multi-value filter convention the backend contract now documents: a
filter such asfilter[type]=1,2carries its values as one comma-separated
string of identifiers or enum tokens, and a numeric range is two ordinary
allowlisted filters,<name>_minand<name>_max. Both stay one string per
filter[name], sodecodeListQuery,encodeListQuery, the URL sync and
the allowlist are unchanged.splitFilterValues('')is[]and
joinFilterValues([])isundefined, so clearing deletes the parameter
rather than sending an empty string. See ADR 0009.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.16.0 Stable
released this
2026-09-15 23:35:38 +00:00 | 135 commits to main since this releaseBreaking
RecordLayoutis retired, one release after it shipped: with the page
container providing the main column and the newPageAsidetaking the
sidebar's place on the right, nothing of it was left but a gap.Panel
stays. Migration: drop theRecordLayoutwrapper and itssidebarslot,
render the main column'sTabsandPanels directly inRecordForm's
fieldsslot, and move the sidebarPanel's children into
<PageAside :title="…">inRecordForm's newasideslot (the title
comes from the panel;sidebar-labelledbyandtitle-idgo, the aside
names itself). The./components/RecordLayout.vueexport is gone.
Changed
- The page container no longer stacks the aside below the main row under
56em.PageAsideowns that breakpoint: the column becomes a rail at the
right edge and the toggle opens the content as a drawer. The aside track is
nowautoand the component sets its own width, so a consumer-written
<aside class="pui-page__aside">without the component gets no size; use
PageAside. RecordDetailsstacks each label over its value when its own container is
narrower than 24rem, so it reads in aPageAsideor any other narrow
column without the consumer overriding its grid.
Added
PageAside, the right band of a CMS page: filters beside a module index,
settings beside a record form.title(required; heads the column and
names the landmark),description, a default slot,toggle(default on)
withtoggle-label, andv-model:open(default open). Open, it is the
sticky, self-scrolling aside column with a flat header and a left rule;
closed, a rail one toggle wide in the same column, its contenthidden
but mounted so fields in the band still submit; below 56em the rail stays
and the toggle opens a drawer from the right with the Sidebar drawer's
mechanics (backdrop, focus trap, Escape, the same toggle closing it). The
toggle keeps thesidebar-righticon in both states and carries the state
inaria-expandedand its name, "Hide {title}" / "Show {title}".Formhas anasideslot rendered as a direct child of<form>, between
the body and the actions row, andRecordFormforwards it. APageAside
in it becomes the page's aside column through the form'sdisplay: contentswhile its fields keep their form owner. The page's:has()
switch matches that path too.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.15.0 Stable
released this
2026-09-15 22:46:35 +00:00 | 139 commits to main since this releaseChanged
AppLayout's main region no longer has padding. A page's rows carry their
own gutter (see the page container under Added), so a sticky band sits
flush against the region's edges without negative margins. Consumer-visible:
a page that has not adopted.pui-pageloses its inset. The seams stay,
defaulting to0; one line restores the previous behaviour:
.pui-app-layout__main { --pui-app-layout-main-padding: var(--space-m) var(--space-l) var(--space-xl); --pui-app-layout-main-padding-mobile: var(--space-s); }.Formwraps everything that is not the actions row — the error summary,
the default slot, the status message — in a new<div class="pui-form__body">.
This changes the DOM for every consumer, page-placed or not; selectors and
tests that assumed the fields were direct children of the<form>need the
extra level. The body inherits the form'sgap, so a.pui-formoverride
still spaces the fields.
Added
.pui-page, the page container forAppLayout's default slot: one grid of
four full-width rows placed by named area —pui-page__band(the breadcrumb
strip),__header,__main(takes the leftover height) and__footer
(sticky at the region's bottom) — plus an optional__asidebeside the main
row: a stretched, size-contained column whose child sticks to the top of the
scrollport with its own scroll, stacking below the content under 56em. The
bands span the whole region without knowing the aside exists. A row has one
occupant: two elements in the same named area overlap..pui-gutter, the one inline padding every page row takes, in the
utilitieslayer so it replaces a bar's own padding without a specificity
fight. It reads--pui-gutter-inline,--space-lon the shell and
--space-sbelow the 48rem breakpoint.- A module
Indexor aForm(so aRecordForm) that is a direct child of
.pui-pagesupplies the page's rows itself: its header, body and pagination
footer, or its body and its actions row as the sticky footer band. Inferred
from the position throughdisplay: contents, no prop; a related index and
any form insidepui-page__mainare unaffected.
Fixed
- A long record title in a
Breadcrumbwraps inside its item instead of
widening the trail.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.14.0 Stable
released this
2026-09-15 22:18:12 +00:00 | 144 commits to main since this releaseChanged
-
AppLayout's main region is now the scrollport. The shell is one viewport
tall (100dvh),.pui-app-layout__maintakes the height left under the
header and scrolls withoverflow: auto, and the document never scrolls.
The header and navigation rail lose theirposition: sticky; they simply
stay put. This gives a page a definite height to work against: a sticky
pagination or action band pins to the region's bottom edge, a1frrow
takes the real leftover space, and a short page no longer grows a scrollbar
to fit a sticky aside.overscroll-behavior-block: containon the region
keeps an iOS rubber-band from dragging the whole shell, and a print rule
(block-size: auto,overflow: visible) prints the whole page rather than
one screenful.Consumer-visible: anything that scrolled the window must now scroll the
region. Vue Router'sscrollBehavioronly scrolls the window, so return
falsefrom it and setmain.scrollTopyourself on route change (and on
back, if you restore); move ascroll-padding-block-endfor sticky bands
fromhtmlto<main>, and drop anyscroll-padding-block-startfor the
header, which is now outside the scrollport. A sticky band pins at the
region's content edge, inside its padding, so a page that ends in one
sets--pui-app-layout-main-paddingwith a0block-end instead of
pulling the band down with a negative margin. Fragment links to targets
inside the region keep working natively. Escape hatch, one line:
.pui-app-layout { block-size: auto; min-block-size: 100dvh }makes the
document the scrollport again (pin the header yourself if you want it
fixed).
Added
sidebar-leftandsidebar-righticons: a panel outline with the divider on
the left or the right, for toggling a side column.sidebar-leftis
Feather'ssidebar;sidebar-rightis its mirror on the same 24px grid.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v0.13.0 Stable
released this
2026-09-15 21:17:33 +00:00 | 149 commits to main since this releaseAdded
- View mode for
BlockEditor:readonlyrenders the block list as content.
The drag handle, move, remove and add controls and the reorder hint go, every
mutation returns early, and#blockreceivesreadonlybesidedisabledso
the fields render read-only. The collapse toggles stay — they are the escape
hatch for a long list — and blocks start expanded. Each block title now sits
in a heading element around its collapse button, so heading navigation works
in both modes;level(2–6, default 3) picks it. A read-only editor carries
data-readonly="true", andPanelkeeps its border, background and padding
around one instead of dropping them as it does for an editable editor. imagefield type onRecordFormField: a URLtype="url"TextInput while
editing, the image itself when read-only, capped so a full-size asset cannot
push the layout, with the same em dash asRecordDetailswhen the value is
empty.altNamepoints at a sibling field in the same list for the
alternative text, so it works for nested and indexed paths.
Fixed
- A read-only
Textareagrows with its value instead of clipping it inside a
box nobody can type in;rowsbecomes its minimum height.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- View mode for
-
v0.12.0 Stable
released this
2026-09-15 17:37:05 +00:00 | 159 commits to main since this releaseAdded
RecordLayoutandPanel: the sidebar-plus-panels shape a record page is
built from.RecordLayoutplaces asidebarslot (anasidenamed through
sidebar-labelledby) beside a main column and stacks them below 56em.
Panelis one bordered section with its heading inside it (title,
description,level,title-id); it honourshidden, so it serves as a
tab panel as-is, and drops its border, background and padding when its
direct child is aBlockEditor. Both set--pui-record-fields-columnsfor
the RecordFields inside them — one column in the sidebar, two in a panel
from 48em with textarea fields spanning the row — and the sidebar stacks
RecordDetails labels over their values, so a consumer page no longer
carries any of that CSS.
Fixed
Tabswraps its buttons inside a narrow container instead of pushing the
tablist past the container's edge.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.11.0 Stable
released this
2026-09-15 13:55:30 +00:00 | 164 commits to main since this releaseAdded
- The Vue CMS playground defines its contacts collection once and mounts it as
both the/contactsmodule index and the related index on a company's
Contacts tab, demonstratingIndexplacement and prefixed query syncing with
realistic consumer-owned columns, loaders, cells and navigation.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The Vue CMS playground defines its contacts collection once and mounts it as