• v0.10.0 0d53ceb085

    v0.10.0 Stable

    pienter-release-bot released this 2026-09-15 13:42:23 +00:00 | 177 commits to main since this release

    Changed

    • Index renders its title, description and actions through the new
      PageHeader, so the pui-index__header, pui-index__heading,
      pui-index__title, pui-index__description and pui-index__actions
      classes are gone. They were never documented as public API; a consumer
      style that targeted them moves to pui-page-header and its
      __heading, __title, __description and __actions elements. Markup,
      spacing and type are unchanged in both placements.

    Added

    • PageHeader at @[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. title and description are plain strings, level (1 | 2 |
      3, default 1) picks the heading element and sizes levels 2 and 3 as
      section headings, title-id sets the title's id for aria-labelledby,
      and #actions fills 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
  • v0.9.0 65f86c932e

    v0.9.0 Stable

    pienter-release-bot released this 2026-09-15 13:34:03 +00:00 | 183 commits to main since this release

    Added

    • useUrlTab(keys, defaultKey, param = 'tab') at
      @[secure]/ui/composables/useUrlTab keeps the active Tabs key 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 for v-model: the default key
      drops the parameter, an unknown value falls back to it, and writes go
      through router.replace so tab switches stay out of the back stack and
      other query parameters survive. Requires the optional vue-router peer,
      like useUrlSort.
    • formFailure(cause, fallback) in @[secure]/ui/utils/cms turns a caught
      save error into RecordForm's { issues, errors }: a value carrying the
      ErrorResponse envelope contributes its field issues and leaves errors
      empty; otherwise errors holds the caught Error message or fallback.
    Downloads
  • v0.8.0 baab290ea1

    v0.8.0 Stable

    pienter-release-bot released this 2026-09-15 12:51:25 +00:00 | 187 commits to main since this release

    Breaking

    • Index drops headingLevel. The heading level now follows placement: h1
      in the default module placement, h2 in related placement. Replace
      heading-level="2" on an embedded list with placement="related".

    Added

    • Index placement (module | related, default module), exposed as
      data-placement on .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-query accepts a string as well as a boolean. The string is a
      key prefix: sync-query="contacts" reads and writes contacts.page,
      contacts.page_size, contacts.search, contacts.sort and
      contacts.filter[...], so a related index can share its parent page's URL.
      Unrelated parameters, including the bare page, survive untouched.
    Downloads
  • v0.7.1 f4f199e543

    v0.7.1 Stable

    pienter-release-bot released this 2026-09-15 09:49:22 +00:00 | 219 commits to main since this release

    Changed

    • 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
  • v0.7.0 20394a7bfe

    v0.7.0 Stable

    pienter-release-bot released this 2026-09-15 09:13:52 +00:00 | 245 commits to main since this release

    Changed

    • 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.

    • Breadcrumb is now a bar with an optional actions slot on the
      inline-end side. The pui-breadcrumb block class moved from the <ol>
      to the <nav>; the list is pui-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.
    • normalizeListQuery validates structured query state directly, without a URL
      encoding round trip.
    Downloads
  • v0.6.0 ee41a94d5a

    v0.6.0 Stable

    pienter-release-bot released this 2026-09-11 12:21:50 +00:00 | 249 commits to main since this release

    Changed

    • CMS surfaces use softer decorative borders and a lower-contrast canvas; the block editor no longer sits inside a second card. New --bg-clr-canvas and --border-clr-subtle semantic 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

    • RecordFields and RecordForm’s fields slot for sidebar and tabbed layouts within one form; Tabs items can supply panel IDs and relationships.
    • BlockEditor for typed content blocks, with model updates, app-provided fields/factories, pointer drag-and-drop, keyboard movement, add/remove and controlled collapse.
    • Breadcrumb link slot for router links and RecordForm before-field slot for headings and other content between fields.
    • Index, RecordForm, and RecordDetails components 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
  • v0.5.0 3303567812

    v0.5.0 Stable

    pienter-release-bot released this 2026-08-30 08:55:16 +00:00 | 254 commits to main since this release

    Breaking

    • The framework-agnostic controllers are gone; their behavior now lives in
      Vue composables. @[secure]/ui/utils/controllers/* (mountDialog,
      mountPopover, mountDisclosure, mountMenu and their types) no longer
      exists — import useDialog, usePopover, useDisclosure, and useMenu
      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
      open getter, and clean up on unmount, so the mount/watch/teardown dance
      every component repeated is gone. Placement moved to
      @[secure]/ui/composables/usePopover. Component behavior is unchanged.
    Downloads
  • v0.4.0 b0cb7a92e8

    v0.4.0 Stable

    pienter-release-bot released this 2026-08-28 13:45:01 +00:00 | 260 commits to main since this release

    Breaking

    • Sidebar follows the D13 layered split instead of a shared controller, so
      mountSidebar is 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.vue is 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.
      isHrefActive and deriveGroupKey moved there from the controller barrel.
    • Sidebar's items prop is now topItems, alongside a new bottomItems.
      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 #footer slot is renamed #bottom and still takes arbitrary
      content beside bottomItems. Migrate :items="nav" to :top-items="nav".
    • The private recursive renderer is SidebarMenuItem (was
      SidebarItemRender), matching the data-sidebar-menu-item hook it emits.
      Internal to the component — no export changed — but the file names did.
    • The nav list classes are .pui-sidebar__top-nav and
      .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.
    • Sidebar no 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-collapsed is 4.75rem (was 4rem). 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 Sidebar focus ring colour follows the entry's tone via
      --pui-sidebar-ring, matching how --btn-ring works 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. visibility stays untransitioned so the entries are focusable the
      instant the group takes focus; under prefers-reduced-motion the panel
      simply appears.
    • The collapsed Sidebar rail 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 Sidebar focus ring uses --outline-width / --outline-offset instead
      of hard-coded 3px / 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 Sidebar link's selected state uses --bg-clr-brand-soft rather 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: auto forces overflow-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 as padding-inline with a matching negative margin-inline,
      so nothing moves.
    • Consumers must import @[secure]/ui/styles before any component. Each
      component stylesheet is a bare @layer components { … }, and layer order is
      fixed by first encounter — so a component loading first registers
      components ahead of base, and every base element rule starts outranking
      component rules. In the Vue playground that made base's a { 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 Sidebar drawer 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 a z-index other than auto makes a grid or flex item a
      stacking context even while position: static — which is what a sidebar in a
      layout shell normally is. The drawer's own z-index: 50 was 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's tabindex. The
      roving pattern was removed from the initial render but survived in the
      focusin handler, which demoted every other entry to -1 the 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. visibility was 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. visibility now 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's width: 0 sat on a bare .pui-sidebar inside 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 #brand slot filled the
      flyout starts below the brand, so it still never covers the wordmark.
    • Icon.astro forwards a class prop instead of silently dropping it. It
      hardcoded class="pui-icon" and spread the rest of the props after, so an
      incoming class was discarded — Vue applied it, Astro did not. One casualty
      was Sidebar's pui-sidebar__group-caret hook, which meant the caret never
      rotated on expand in Astro.
    • A Sidebar brand slot no longer spills across the page when collapsed. The
      rule that was meant to hide everything without data-keep-when-collapsed was
      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 sidebar position: sticky for 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

    • Sidebar takes a bottomItems prop — the bottom nav. Same
      SidebarItem[] shape as topItems, rendered at the foot of the column
      below a divider, for settings / account / help. See the Breaking entry for
      the accompanying itemstopItems rename.
    • Sidebar takes a collapsedSubmenu prop ('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.
    • mountSidebar gains openSubmenu(key), closeSubmenu(), and
      openSubmenuKey(), plus a collapsedSubmenu config field. They are no-ops
      unless the policy is active — which needs the sidebar collapsed and the
      viewport above drawerBreakpoint, 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 every Sidebar layer 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-md is tuned for a card in the flow and reads flat under
      something as large as a sidebar flyout.
    Downloads
  • v0.3.0 14de3890c0

    v0.3.0 Stable

    pienter-release-bot released this 2026-08-26 13:26:54 +00:00 | 267 commits to main since this release

    Added

    • --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 #summary slot so pluralization and locale stay with the consumer,
      and the control comes from the default slot. Below a 30rem container both
      regions stack and centre, with the control leading and the summary reading as
      a caption beneath it. Pagination itself gained no props.

      <PaginationFooter>
          <template #summary>Showing 2130 of 96 domains</template>
          <Pagination v-model:current-page="page" :total-pages="10" />
      </PaginationFooter>
      

    Changed

    • Pagination is 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-coded 0.4). The public API, markup
      contract, and ARIA are unchanged.
    • Pagination is now --control-height-sm (30px) tall overall, so it lines up
      with a sm Button placed 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-dark is a scrim colour rather than a selection colour.
    • Pagination's focus ring now uses a negative outline-offset plus a
      z-index lift. 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.
    • Pagination reduces to just the prev / next arrows and the current page below
      an 18rem container. 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 a pui-pagination-nav class, so it
      can act as that query container. It is a new public child class and carries no
      visual styling of its own.
    Downloads
  • v0.2.0 c6b850a98b

    v0.2.0
    All checks were successful
    ci / typecheck (push) Successful in 12s
    ci / release (push) Successful in 11s
    ci / lint (push) Successful in 14s
    ci / format (push) Successful in 15s
    Stable

    pienter-release-bot released this 2026-08-24 21:54:20 +00:00 | 277 commits to main since this release

    Breaking

    • Table and DataTable no longer support v-model:sort. Sort is a read-only
      sort prop plus a sort event carrying the activated column key; the consumer
      decides the next state. Migrate v-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".
    • SortEvent is removed; the { key, direction } shape is now Sort.

    Changed

    • --duration-slow is now 2s (was 1s). It drives exactly one thing — the
      Icon spin animation — so this slows every spinner, including Button's
      loading state, which read as hurried.
    • Button and IconButton render Spinner instead 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 loader icon keeps its eight spokes as separate <path> elements in
      clockwise order so they can be faded individually. Visually identical on its
      own; only Spinner ramps them.
    • A Button or IconButton in its loading state no longer fades to
      --opacity-disabled. It is still disabled for 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's loader glyph. Sizes sm | md | lg | xl; supply label when the
      spinner is the only signal, omit it when the surrounding element already
      carries aria-busy. With a label the wrapper becomes a role="status"
      region and the label is rendered as visually-hidden text in a
      .pui-spinner__label span — a live region announces its text content, and one
      whose only child is aria-hidden has none.
    • Table (Vue + Astro) and DataTable take loading. The <tbody> frosts
      under an overlay and is marked inert, so its links and checkboxes are not
      merely un-clickable but genuinely out of reach; the header stays crisp and
      clickable, the table is marked aria-busy, and DataTable suppresses its
      empty state. rows is 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 (default 9rem) so the
      overlay has a pane to sit in on a first fetch.
    • @[secure]/ui/utils exports 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.astro gained a sort surface. Sortable headers render links to the URL
      each column would produce next, with the same aria-sort and chevron contract
      as the Vue tier, and work without JavaScript.
    Downloads