• v0.20.0 cdb5ff7aa3

    v0.20.0 Stable

    pienter-release-bot released this 2026-09-16 00:55:17 +00:00 | 109 commits to main since this release

    Added

    • BlockEditor takes hint, emptyText and addLabel(typeLabel), the
      three visible strings it used to hard-code in English: the reorder hint
      above the list, the edit-mode empty message under the empty slot, and the
      add-button label built from each block type's label. The English defaults
      are unchanged, so a Dutch consumer can pass Tekst toevoegen and a Dutch
      hint without CSS overrides or replacing the add row. The aria-only labels
      and announcements are still English.
    Downloads
  • v0.19.0 00ffcc9047

    v0.19.0 Stable

    pienter-release-bot released this 2026-09-16 00:31:09 +00:00 | 112 commits to main since this release

    Added

    • 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's skipLabel / drawerLabel / navigationLabel, so a
      single-language consumer can name them in its own language. Pagination
      takes label (the <nav> landmark, Pagination), previousLabel
      (Previous page) and nextLabel (Next page); Breadcrumb takes
      label (Breadcrumb); NumberField takes increaseLabel (Increase)
      and decreaseLabel (Decrease); Table takes selectAllLabel
      (Select all rows), which DataTable forwards alongside its own
      selectRowLabel, a string or a function of the row that replaces the
      default Select <first cell> name; Toast takes closeLabel (Close),
      and ToastHost takes the same closeLabel for every toast it renders
      next to its existing label for the notifications region.
      PaginationFooter has no text of its own and is unchanged.
    Downloads
  • v0.18.0 48d1bb2a54

    v0.18.0 Stable

    pienter-release-bot released this 2026-09-16 00:00:32 +00:00 | 119 commits to main since this release

    Changed

    • NumberField takes steppers (default true); false renders a plain
      number input without the +/− column.

    Fixed

    • Checkbox, RadioGroup and Switch inputs are their declared em size
      again. The base input, textarea, select rule padded them by --space-s,
      which inflated a 1.15em checkbox to 35px; all three reset padding to 0.

    Added

    • FilterCheckbox, FilterSelect, FilterMultiSelect and FilterRange
      (@[secure]/ui/components/Filter*.vue), the filter controls an index
      definition declares. Each binds to the index that owns the query — the
      state Index provides to its filters slot, the one a definition provides
      with provideIndexQuery, or an explicit state prop — 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 an emptyLabel option that deletes; the multi-select is a
      fieldset of checkboxes whose legend reads Label (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>_min and <name>_max on 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
  • v0.17.0 74f81f66cc

    v0.17.0 Stable

    pienter-release-bot released this 2026-09-15 23:40:41 +00:00 | 131 commits to main since this release

    Added

    • splitFilterValues and joinFilterValues in @[secure]/ui/utils/cms, for
      the multi-value filter convention the backend contract now documents: a
      filter such as filter[type]=1,2 carries its values as one comma-separated
      string of identifiers or enum tokens, and a numeric range is two ordinary
      allowlisted filters, <name>_min and <name>_max. Both stay one string per
      filter[name], so decodeListQuery, encodeListQuery, the URL sync and
      the allowlist are unchanged. splitFilterValues('') is [] and
      joinFilterValues([]) is undefined, so clearing deletes the parameter
      rather than sending an empty string. See ADR 0009.
    Downloads
  • v0.16.0 e10a3b857b

    v0.16.0 Stable

    pienter-release-bot released this 2026-09-15 23:35:38 +00:00 | 135 commits to main since this release

    Breaking

    • RecordLayout is retired, one release after it shipped: with the page
      container providing the main column and the new PageAside taking the
      sidebar's place on the right, nothing of it was left but a gap. Panel
      stays. Migration: drop the RecordLayout wrapper and its sidebar slot,
      render the main column's Tabs and Panels directly in RecordForm's
      fields slot, and move the sidebar Panel's children into
      <PageAside :title="…"> in RecordForm's new aside slot (the title
      comes from the panel; sidebar-labelledby and title-id go, the aside
      names itself). The ./components/RecordLayout.vue export is gone.

    Changed

    • The page container no longer stacks the aside below the main row under
      56em. PageAside owns that breakpoint: the column becomes a rail at the
      right edge and the toggle opens the content as a drawer. The aside track is
      now auto and the component sets its own width, so a consumer-written
      <aside class="pui-page__aside"> without the component gets no size; use
      PageAside.
    • RecordDetails stacks each label over its value when its own container is
      narrower than 24rem, so it reads in a PageAside or 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)
      with toggle-label, and v-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 content hidden
      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 the sidebar-right icon in both states and carries the state
      in aria-expanded and its name, "Hide {title}" / "Show {title}".
    • Form has an aside slot rendered as a direct child of <form>, between
      the body and the actions row, and RecordForm forwards it. A PageAside
      in it becomes the page's aside column through the form's display: contents while its fields keep their form owner. The page's :has()
      switch matches that path too.
    Downloads
  • v0.15.0 1b1ac6e381

    v0.15.0 Stable

    pienter-release-bot released this 2026-09-15 22:46:35 +00:00 | 139 commits to main since this release

    Changed

    • 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-page loses its inset. The seams stay,
      defaulting to 0; 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); }.
    • Form wraps 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's gap, so a .pui-form override
      still spaces the fields.

    Added

    • .pui-page, the page container for AppLayout'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 __aside beside 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
      utilities layer so it replaces a bar's own padding without a specificity
      fight. It reads --pui-gutter-inline, --space-l on the shell and
      --space-s below the 48rem breakpoint.
    • A module Index or a Form (so a RecordForm) that is a direct child of
      .pui-page supplies 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 through display: contents, no prop; a related index and
      any form inside pui-page__main are unaffected.

    Fixed

    • A long record title in a Breadcrumb wraps inside its item instead of
      widening the trail.
    Downloads
  • v0.14.0 e80ffebb5b

    v0.14.0 Stable

    pienter-release-bot released this 2026-09-15 22:18:12 +00:00 | 144 commits to main since this release

    Changed

    • AppLayout's main region is now the scrollport. The shell is one viewport
      tall (100dvh), .pui-app-layout__main takes the height left under the
      header and scrolls with overflow: auto, and the document never scrolls.
      The header and navigation rail lose their position: 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, a 1fr row
      takes the real leftover space, and a short page no longer grows a scrollbar
      to fit a sticky aside. overscroll-behavior-block: contain on 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's scrollBehavior only scrolls the window, so return
      false from it and set main.scrollTop yourself on route change (and on
      back, if you restore); move a scroll-padding-block-end for sticky bands
      from html to <main>, and drop any scroll-padding-block-start for 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-padding with a 0 block-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-left and sidebar-right icons: a panel outline with the divider on
      the left or the right, for toggling a side column. sidebar-left is
      Feather's sidebar; sidebar-right is its mirror on the same 24px grid.
    Downloads
  • v0.13.0 b657c941ba

    v0.13.0 Stable

    pienter-release-bot released this 2026-09-15 21:17:33 +00:00 | 149 commits to main since this release

    Added

    • View mode for BlockEditor: readonly renders the block list as content.
      The drag handle, move, remove and add controls and the reorder hint go, every
      mutation returns early, and #block receives readonly beside disabled so
      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", and Panel keeps its border, background and padding
      around one instead of dropping them as it does for an editable editor.
    • image field type on RecordFormField: a URL type="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 as RecordDetails when the value is
      empty. altName points at a sibling field in the same list for the
      alternative text, so it works for nested and indexed paths.

    Fixed

    • A read-only Textarea grows with its value instead of clipping it inside a
      box nobody can type in; rows becomes its minimum height.
    Downloads
  • v0.12.0 34f1241405

    v0.12.0 Stable

    pienter-release-bot released this 2026-09-15 17:37:05 +00:00 | 159 commits to main since this release

    Added

    • RecordLayout and Panel: the sidebar-plus-panels shape a record page is
      built from. RecordLayout places a sidebar slot (an aside named through
      sidebar-labelledby) beside a main column and stacks them below 56em.
      Panel is one bordered section with its heading inside it (title,
      description, level, title-id); it honours hidden, so it serves as a
      tab panel as-is, and drops its border, background and padding when its
      direct child is a BlockEditor. Both set --pui-record-fields-columns for
      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

    • Tabs wraps its buttons inside a narrow container instead of pushing the
      tablist past the container's edge.
    Downloads
  • v0.11.0 d2b4e88308

    v0.11.0 Stable

    pienter-release-bot released this 2026-09-15 13:55:30 +00:00 | 164 commits to main since this release

    Added

    • The Vue CMS playground defines its contacts collection once and mounts it as
      both the /contacts module index and the related index on a company's
      Contacts tab, demonstrating Index placement and prefixed query syncing with
      realistic consumer-owned columns, loaders, cells and navigation.
    Downloads