-
v0.10.0 Stable
released this
2026-09-15 13:42:23 +00:00 | 177 commits to main since this releaseChanged
- Index renders its title, description and actions through the new
PageHeader, so thepui-index__header,pui-index__heading,
pui-index__title,pui-index__descriptionandpui-index__actions
classes are gone. They were never documented as public API; a consumer
style that targeted them moves topui-page-headerand its
__heading,__title,__descriptionand__actionselements. Markup,
spacing and type are unchanged in both placements.
Added
PageHeaderat@[secure]/ui/components/PageHeader.vue: the title,
description and actions band above a module index or a record page,
extracted from Index so detail and form pages no longer hand-roll the same
header.titleanddescriptionare plain strings,level(1|2|
3, default1) picks the heading element and sizes levels 2 and 3 as
section headings,title-idsets the title'sidforaria-labelledby,
and#actionsfills the trailing row. It renders nothing when it has no
title, description or actions. Index uses it at level 1 in module placement
and level 2 in related placement.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Index renders its title, description and actions through the new
-
v0.9.0 Stable
released this
2026-09-15 13:34:03 +00:00 | 183 commits to main since this releaseAdded
useUrlTab(keys, defaultKey, param = 'tab')at
@[secure]/ui/composables/useUrlTabkeeps the activeTabskey in the URL
query, the single source of truth, so a shared link or a reload opens the
same tab. It returns a writable computed forv-model: the default key
drops the parameter, an unknown value falls back to it, and writes go
throughrouter.replaceso tab switches stay out of the back stack and
other query parameters survive. Requires the optionalvue-routerpeer,
likeuseUrlSort.formFailure(cause, fallback)in@[secure]/ui/utils/cmsturns a caught
save error into RecordForm's{ issues, errors }: a value carrying the
ErrorResponseenvelope contributes its field issues and leaveserrors
empty; otherwiseerrorsholds the caughtErrormessage orfallback.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.8.0 Stable
released this
2026-09-15 12:51:25 +00:00 | 187 commits to main since this releaseBreaking
- Index drops
headingLevel. The heading level now followsplacement:h1
in the default module placement,h2in related placement. Replace
heading-level="2"on an embedded list withplacement="related".
Added
- Index
placement(module|related, defaultmodule), exposed as
data-placementon.pui-index. Module is the page itself and renders flat:
the heading and actions above a divider, an unboxed toolbar, the table with
DataTable's own border and radius, and the pagination footer as a band
below. Related is a list inside another record's detail page and renders as
one bordered region holding the header, toolbar, table and pagination
footer, with a second-level heading. Existing consumers are unchanged; the
toolbar and results no longer share a bordered body in module placement, so
consumer styles targeting that box no longer apply. - Index
sync-queryaccepts a string as well as a boolean. The string is a
key prefix:sync-query="contacts"reads and writescontacts.page,
contacts.page_size,contacts.search,contacts.sortand
contacts.filter[...], so a related index can share its parent page's URL.
Unrelated parameters, including the barepage, survive untouched.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Index drops
-
v0.7.1 Stable
released this
2026-09-15 09:49:22 +00:00 | 219 commits to main since this releaseChanged
-
RecordFields reuses field error arrays while draft values change, avoiding
repeated issue scans and unnecessary error-prop updates during typing. -
Sidebar and Tabs skip navigation DOM scans for unrelated key presses; Sidebar
also handles Space activation before collecting visible entries. -
Menus initialize roving focus with one enabled-item search and avoid repeating
full tabindex updates when keyboard navigation triggers a focus event. -
NumberField stepper clicks emit one model update while preserving native input
and change events, so consumer validation and autosave handlers run once.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v0.7.0 Stable
released this
2026-09-15 09:13:52 +00:00 | 245 commits to main since this releaseChanged
-
Index preserves structured backend errors and offers reset for rejected queries.
Refresh callbacks wait for navigation and loading to settle. Sortable columns
are checked against the endpoint's allowed fields before loading. -
Breadcrumbis now a bar with an optionalactionsslot on the
inline-end side. Thepui-breadcrumbblock class moved from the<ol>
to the<nav>; the list ispui-breadcrumb__trail. -
Index renders its pagination footer as a page-level band after the
results card instead of inside it, mirroring the header. Pages can pin
it to the viewport bottom; consumer styles targeting the footer's card
corner radii no longer apply.
Added
- Index's sort select shares state with table headers and supports allowed fields
without visible columns. Sort labels can be customized independently of columns. - Individual filter updates and refresh/loading access in Index action and
selection slots reduce consumer wiring. normalizeListQueryvalidates structured query state directly, without a URL
encoding round trip.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v0.6.0 Stable
released this
2026-09-11 12:21:50 +00:00 | 249 commits to main since this releaseChanged
- CMS surfaces use softer decorative borders and a lower-contrast canvas; the block editor no longer sits inside a second card. New
--bg-clr-canvasand--border-clr-subtlesemantic tokens leave control, text and focus contrast intact. - Form validation focuses a visible, enabled invalid control, including when other errors are in hidden tab panels.
- Index separates page headings and actions from a bordered search, filter and results surface.
- Compact CMS layouts use tighter table rows and inline pagination controls. Form fields no longer double label spacing or stretch controls beside hints.
Added
RecordFieldsand RecordForm’sfieldsslot for sidebar and tabbed layouts within one form; Tabs items can supply panel IDs and relationships.BlockEditorfor typed content blocks, with model updates, app-provided fields/factories, pointer drag-and-drop, keyboard movement, add/remove and controlled collapse.Breadcrumblink slot for router links andRecordFormbefore-field slot for headings and other content between fields.Index,RecordForm, andRecordDetailscomponents for CMS pages composed with ordinary Vue routes, props, events, and slots. Index accepts an async loader and owns query state, debounced search, loading, retry, stale-request protection and optional Vue Router query synchronization.- Backend query and response types, JSON Schema, URL query codecs, and immutable record path helpers under
@[secure]/ui/utils/cms.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- CMS surfaces use softer decorative borders and a lower-contrast canvas; the block editor no longer sits inside a second card. New
-
v0.5.0 Stable
released this
2026-08-30 08:55:16 +00:00 | 254 commits to main since this releaseBreaking
- The framework-agnostic controllers are gone; their behavior now lives in
Vue composables.@[secure]/ui/utils/controllers/*(mountDialog,
mountPopover,mountDisclosure,mountMenuand their types) no longer
exists — importuseDialog,usePopover,useDisclosure, anduseMenu
from@[secure]/ui/composables/<name>instead. The composables carry the
same behavior and options but own the lifecycle themselves: they take an
element ref plus options at setup, wire on mount, follow a controlled
opengetter, and clean up on unmount, so the mount/watch/teardown dance
every component repeated is gone.Placementmoved to
@[secure]/ui/composables/usePopover. Component behavior is unchanged.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The framework-agnostic controllers are gone; their behavior now lives in
-
v0.4.0 Stable
released this
2026-08-28 13:45:01 +00:00 | 260 commits to main since this releaseBreaking
Sidebarfollows the D13 layered split instead of a shared controller, so
mountSidebaris no longer exported from@[secure]/ui/utils/controllers.
utils/controllers/is for code both tiers run; Sidebar's imperative
orchestration is Astro-only and now lives beside the component, exported as
@[secure]/ui/components/Sidebar.mount. Vue no longer wraps it at all —
Sidebar.vueis reactive throughout, which is what D13 already required
("wrapping an imperative DOM controller from Vue fights Vue's data flow…
the controller mutates the DOM, Vue patches it back, and race conditions
ensue"). Both tiers now derive from shared primitives in
@[secure]/ui/utils/navigation/sidebar:isHrefActive,deriveGroupKey,
computeSubmenuPosition,isSubmenuActive,groupContainsActive.
isHrefActiveandderiveGroupKeymoved there from the controller barrel.Sidebar'sitemsprop is nowtopItems, alongside a newbottomItems.
The column has two nav regions — the top nav, which takes the leftover
height and scrolls, and the bottom nav, pinned to the foot below a divider
for settings, account and the like. Entries in either get identical wiring:
active-href matching, keyboard navigation, and collapsed flyouts for their
groups. The#footerslot is renamed#bottomand still takes arbitrary
content besidebottomItems. Migrate:items="nav"to:top-items="nav".- The private recursive renderer is
SidebarMenuItem(was
SidebarItemRender), matching thedata-sidebar-menu-itemhook it emits.
Internal to the component — no export changed — but the file names did. - The nav list classes are
.pui-sidebar__top-navand
.pui-sidebar__bottom-nav(was.pui-sidebar__menu), and the foot wrapper
is.pui-sidebar__bottom(was.pui-sidebar__footer). Consumers styling
those internals will need to follow. Sidebarno longer uses a roving tabindex. Every menu entry is its own tab
stop, as an ordinary<nav>full of links should be. Roving left the whole
sidebar with a single tab stop, so a rail of four destinations had three
that Tab could never reach. This also matches CONVENTIONS.md, which reserves
roving for "single-focus-per-group controls … where one item represents the
entire group's focus target" — a navigation landmark is not that. Arrow keys,
Home and End still work, now as an accelerator on top of the natural order
rather than a replacement for it. Consumers relying on exactly one sidebar tab
stop will see more; nothing else changes.
Changed
--space-sidebar-width-collapsedis4.75rem(was4rem). At 4rem the new
rail captions had about 2rem of usable width once the panel's padding was
taken out, and real labels ellipsised to two characters.- The
Sidebarfocus ring colour follows the entry's tone via
--pui-sidebar-ring, matching how--btn-ringworks for Button: neutral by
default, brand on the entry wearing the brand wash, so the ring no longer
fights the fill underneath it. - The collapsed rail marks which icon the open flyout belongs to. Previously
only hover did, so a flyout opened from the keyboard left nothing pointing
back at its icon once focus moved into the panel. - The collapsed submenu flyout now sits on
--layer-below, so it genuinely
passes under the rail rather than across it — the panel's own background
hides where the slide starts. The open-flyout rule pairs the rung with
isolation: isolate, because "behind the rail" only holds while the sidebar
is the stacking context containing the flyout; without it the panel would go
behind the page in any shell that doesn't happen to give the root a
context. - The collapsed submenu flyout slides in from behind the rail when it opens,
and disappears instantly when it closes. The transition is declared only on
the open state — a transition belongs to the state being moved to — so
there is nothing to undeclare for the closing direction. An exit animation on
a dismissal only delays the content behind it, and the panel covers page
content.visibilitystays untransitioned so the entries are focusable the
instant the group takes focus; underprefers-reduced-motionthe panel
simply appears. - The collapsed
Sidebarrail stacks a caption under each icon instead of
going icon-only, so it is navigable without hovering. The rail sets its own
inline padding rather than inheriting the expanded panel's, and captions wrap
to a second line instead of truncating — an ellipsis hides the one thing a
caption is for. - A collapsed rail group whose subtree contains the current page carries the
brand wash. On the rail its children are hidden, so the group is the only
indicator of location, and a tint alone lost to the hover fill — a hovered
entry read as more current than the current one. The wash is reverted in
drawer mode, where the sidebar is a full expanded list: there the group sits
directly above its own current child and two identical fills merged into a
single block. Brand ink alone carries "this section holds the current page"
there, and the wash is left to the entry that actually is the current page. - The
Sidebarfocus ring uses--outline-width/--outline-offsetinstead
of hard-coded3px/2px. It is the standard outset ring, so it reads as a
ring around the entry rather than a line cutting into it and stays legible
over the selected entry's wash; the collapsed rail reserves inline padding so
the column edge never clips it. - A
Sidebarlink's selected state uses--bg-clr-brand-softrather than
--bg-clr-accent, which is what hover already uses — the current page was
indistinguishable from whatever the pointer was over.
Fixed
Sidebar's focus ring is no longer clipped in the top nav. That list is a
scroll container —overflow-y: autoforcesoverflow-x: auto— and a
scroll container clips at its padding box, so an entry filling it
edge-to-edge had the ring sliced off both sides. The list now reserves the
ring's width aspadding-inlinewith a matching negativemargin-inline,
so nothing moves.- Consumers must import
@[secure]/ui/stylesbefore any component. Each
component stylesheet is a bare@layer components { … }, and layer order is
fixed by first encounter — so a component loading first registers
componentsahead ofbase, and every base element rule starts outranking
component rules. In the Vue playground that made base'sa { color: … }beat
.pui-sidebar__link, turning sidebar links brand-coloured. The playground
entry point now imports the stylesheet first; the library contract is
unchanged (main.css declares the order once). - The mobile
Sidebardrawer no longer paints under the consumer's app header.
The rule that lifts the sidebar while a collapsed flyout is open applied at
every width, and az-indexother thanautomakes a grid or flex item a
stacking context even whileposition: static— which is what a sidebar in a
layout shell normally is. The drawer's ownz-index: 50was therefore sealed
inside that context and lost to any header above 30. The lift is now scoped
above the drawer breakpoint, where the flyout actually exists, and the drawer
lifts the root itself while open. Sidebar's focus handler no longer rewrites every entry'stabindex. The
roving pattern was removed from the initial render but survived in the
focusinhandler, which demoted every other entry to-1the moment one was
focused — so the sidebar looked fully tabbable until you actually tabbed into
it, and then collapsed to a single stop.- A collapsed submenu flyout's entries are reachable by Tab.
visibilitywas in
the flyout's transition list, so it flipped partway through the fade and the
entries were still unfocusable when Tab arrived — landing on a group jumped
straight past its flyout to the next rail item.visibilitynow steps
immediately on open and is delayed only on close, so the fade-out still plays. - The collapsed rail no longer stays visible below
drawerBreakpoint. The
drawer'swidth: 0sat on a bare.pui-sidebarinside a media query, which
grants no specificity, so.pui-sidebar[data-state='collapsed']outranked it
and left an empty bordered strip down the page on mobile. - The collapsed submenu flyout aligns flush with the top of the sidebar column
instead of the menu region. The menu's box starts inside the panel's padding,
which left a visible seam above the flyout. With a#brandslot filled the
flyout starts below the brand, so it still never covers the wordmark. Icon.astroforwards aclassprop instead of silently dropping it. It
hardcodedclass="pui-icon"and spread the rest of the props after, so an
incoming class was discarded — Vue applied it, Astro did not. One casualty
wasSidebar'spui-sidebar__group-carethook, which meant the caret never
rotated on expand in Astro.- A
Sidebarbrand slot no longer spills across the page when collapsed. The
rule that was meant to hide everything withoutdata-keep-when-collapsedwas
an empty placeholder, so a wordmark kept its full width in a rail narrower
than most product names. - The collapsed submenu flyout no longer renders behind page content. It is
position: fixed, which escapes overflow but not a stacking context, and
consumers routinely make the sidebarposition: stickyfor scroll — which
creates one. The root now raises its own z-index while a flyout is open. - A closed flyout is positioned as soon as it enters panel mode. A fixed
element with no coordinates resolves to its static position inside the rail,
so anything that made it briefly visible flashed the panel over the icons.
Added
Sidebartakes abottomItemsprop — the bottom nav. Same
SidebarItem[]shape astopItems, rendered at the foot of the column
below a divider, for settings / account / help. See the Breaking entry for
the accompanyingitems→topItemsrename.Sidebartakes acollapsedSubmenuprop ('hidden' | 'panel', default
'hidden'). With'panel', a collapsed rail's groups are no longer a dead
end: hover, focus, or click opens a group's children in a full-height flyout
beside the rail. Escape closes it and returns focus to the group toggle;
clicking the toggle pins the flyout so it survives pointer-out; only one
flyout is open at a time. The default keeps the previous behavior, so
existing consumers are unaffected.mountSidebargainsopenSubmenu(key),closeSubmenu(), and
openSubmenuKey(), plus acollapsedSubmenuconfig field. They are no-ops
unless the policy is active — which needs the sidebar collapsed and the
viewport abovedrawerBreakpoint, since below it there is no rail.- A stacking ladder:
--layer-below,--layer-0…--layer-5,--layer-top.
Every positioned layer in the library names a rung instead of inventing a
number, so the whole order is legible in one place and two components cannot
accidentally tie.Toast,Table's loading pane,NumberField's focused
step button and everySidebarlayer moved onto it; the numbers they
resolve to are unchanged. --shadow-lg, for a surface that covers page content rather than sitting on
it.--shadow-mdis tuned for a card in the flow and reads flat under
something as large as a sidebar flyout.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.3.0 Stable
released this
2026-08-26 13:26:54 +00:00 | 267 commits to main since this releaseAdded
-
--bg-clr-selected— the neutral "selected" background rung, one step past
the hover fill so a chosen item stays distinct from a merely hovered one. It
rides--clr-surface-1, the only neutral that keeps a similar distance from
the surface in both themes: darker than the surface in light, lighter in dark.
Pair it with--text-clr-base. Never with--text-clr-inverse— that is
near-white in both themes and belongs on a fill or an inverted bar, so on a
light neutral it produces near-white on near-white in the light theme.
Previously the neutral ramp jumped straight from--bg-clr-surface-2(91%
lightness) to--bg-clr-dark(27%), so a selected state could only be
invisible or a heavy slab. -
PaginationFooter(Vue + Astro) — the bar that seats a result summary
opposite a pagination control: top border, summary at the leading edge,
control at the trailing edge. Pure markup with slots; the summary text comes
from a#summaryslot so pluralization and locale stay with the consumer,
and the control comes from the default slot. Below a30remcontainer both
regions stack and centre, with the control leading and the summary reading as
a caption beneath it.Paginationitself gained no props.<PaginationFooter> <template #summary>Showing 21–30 of 96 domains</template> <Pagination v-model:current-page="page" :total-pages="10" /> </PaginationFooter>
Changed
Paginationis restyled to the admin-surface "table footer bar" direction.
The<ul>is now a single bordered strip whose cells share dividers, instead
of a row of individually bordered boxes separated by--space-3xs. Cells use
--fw-control(was--fw-semibold), tabular numerals, and
--opacity-disabled(was a hard-coded0.4). The public API, markup
contract, and ARIA are unchanged.Paginationis now--control-height-sm(30px) tall overall, so it lines up
with asmButtonplaced beside it. It was effectively 32px: the height
was applied to the cells, and the strip's own 1px border sat outside them.
The height moved to the<ul>and the cells stretch to fill it, landing at
28px — still clear of the 24px WCAG 2.5.8 target.Pagination's current page is now--bg-clr-selected+--text-clr-base,
replacing--bg-clr-dark+--text-clr-inverse. The old pairing read as a
heavy slab in the light theme and sank into the bar in the dark one, because
--bg-clr-darkis a scrim colour rather than a selection colour.Pagination's focus ring now uses a negativeoutline-offsetplus a
z-indexlift. The cells are adjacent now, so the previous positive offset
drew the ring underneath the neighbouring cell. Same token, same width, still
:focus-visible-only.Paginationreduces to just the prev / next arrows and the current page below
an18remcontainer. Previously the full page run stayed put and overflowed.
This is pure CSS — a container query on the component's own<nav>wrapper —
so it responds to the space the control is given rather than to the viewport,
and the page list itself is unchanged.Pagination's<nav>wrapper now carries apui-pagination-navclass, so it
can act as that query container. It is a new public child class and carries no
visual styling of its own.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
1 download
-
-
v0.2.0
Stablereleased this
2026-08-24 21:54:20 +00:00 | 277 commits to main since this releaseBreaking
TableandDataTableno longer supportv-model:sort. Sort is a read-only
sortprop plus asortevent carrying the activated column key; the consumer
decides the next state. Migratev-model:sort="sort"to
:sort="sort" @sort="key => sort = toggleSort(sort, key)". See ADR 0005.useUrlSort()returns{ sort, onSort }instead of a writable computed:
:sort="sort" @sort="onSort".SortEventis removed; the{ key, direction }shape is nowSort.
Changed
--duration-slowis now2s(was1s). It drives exactly one thing — the
Iconspin animation — so this slows every spinner, includingButton's
loading state, which read as hurried.ButtonandIconButtonrenderSpinnerinstead of an inline spinning
Icon, so every spinner in the library now shares the stepped chasing-tail
look. Sizes are unchanged (1rem/1.25rem), held via--pui-spinner-size.- The
loadericon keeps its eight spokes as separate<path>elements in
clockwise order so they can be faded individually. Visually identical on its
own; onlySpinnerramps them. - A
ButtonorIconButtonin itsloadingstate no longer fades to
--opacity-disabled. It is stilldisabledfor interaction purposes, but a
busy control is not an unavailable one, and the fade compounded with the
spinner's own opacity ramp — the dial's dimmest spokes landed near 6% alpha
and effectively disappeared.
Added
Spinner(Vue + Astro) — the indeterminate "working" indicator, composing
Icon'sloaderglyph. Sizessm | md | lg | xl; supplylabelwhen the
spinner is the only signal, omit it when the surrounding element already
carriesaria-busy. With alabelthe wrapper becomes arole="status"
region and the label is rendered as visually-hidden text in a
.pui-spinner__labelspan — a live region announces its text content, and one
whose only child isaria-hiddenhas none.Table(Vue + Astro) andDataTabletakeloading. The<tbody>frosts
under an overlay and is markedinert, so its links and checkboxes are not
merely un-clickable but genuinely out of reach; the header stays crisp and
clickable, the table is markedaria-busy, andDataTablesuppresses its
empty state.rowsis passed through untouched. The overlay is one extra
<tr class="pui-table__loading">inside<tbody>, rendered only while
loading; the public DOM root is unchanged. Its spinner stays centred in the
visible scroll port on a horizontally scrolled table, and a body with no rows
is held open while busy by--pui-table-loading-min-h(default9rem) so the
overlay has a pane to sit in on a first fetch.@[secure]/ui/utilsexports the sort domain:Sort,SortDirection,
SortState,toggleSort,parseSort,formatSort. The wire form is the
column key with a-prefix for descending (title,-title), which means
a column key must not itself start with-.Table.astrogained a sort surface. Sortable headers render links to the URL
each column would produce next, with the samearia-sortand chevron contract
as the Vue tier, and work without JavaScript.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads