@pienter/ui 0.3.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/CONVENTIONS.md +342 -385
  3. package/README.md +33 -18
  4. package/components/display/record-details/RecordDetails.vue +61 -0
  5. package/components/display/record-details/record-details.css +37 -0
  6. package/components/display/record-details/types.ts +8 -0
  7. package/components/feedback/toast/toast.css +1 -1
  8. package/components/form/block-editor/BlockEditor.vue +454 -0
  9. package/components/form/block-editor/block-editor.css +149 -0
  10. package/components/form/block-editor/types.ts +15 -0
  11. package/components/form/combobox/Combobox.vue +42 -72
  12. package/components/form/combobox/combobox.css +1 -1
  13. package/components/form/form/Form.vue +1 -2
  14. package/components/form/label/label.css +1 -1
  15. package/components/form/number-field/NumberField.vue +1 -2
  16. package/components/form/number-field/number-field.css +1 -1
  17. package/components/form/radio-group/RadioGroup.vue +2 -5
  18. package/components/form/record-form/RecordFields.vue +128 -0
  19. package/components/form/record-form/RecordForm.vue +116 -0
  20. package/components/form/record-form/fields.ts +20 -0
  21. package/components/form/record-form/record-form.css +15 -0
  22. package/components/form/record-form/types.ts +28 -0
  23. package/components/form/slider/slider.css +2 -3
  24. package/components/form/tags-input/tags-input.css +1 -2
  25. package/components/form/text-input/text-input.css +2 -0
  26. package/components/form/textarea/textarea.css +1 -1
  27. package/components/layout/accordion/Accordion.vue +6 -27
  28. package/components/layout/collapsible/Collapsible.vue +9 -19
  29. package/components/layout/index/Index.vue +353 -0
  30. package/components/layout/index/index.css +114 -0
  31. package/components/layout/index/useIndex.ts +390 -0
  32. package/components/layout/table/table.css +5 -4
  33. package/components/navigation/breadcrumb/Breadcrumb.vue +24 -5
  34. package/components/navigation/breadcrumb/breadcrumb.css +15 -0
  35. package/components/navigation/sidebar/Sidebar.vue +329 -84
  36. package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
  37. package/components/navigation/sidebar/context.ts +21 -0
  38. package/components/navigation/sidebar/sidebar.css +346 -78
  39. package/components/navigation/sidebar/types.ts +13 -1
  40. package/components/navigation/tabs/Tabs.vue +6 -0
  41. package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
  42. package/components/overlay/command/Command.vue +10 -38
  43. package/components/overlay/command/command.css +1 -1
  44. package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
  45. package/components/overlay/modal/Modal.vue +7 -28
  46. package/components/overlay/popover/Popover.vue +9 -31
  47. package/components/overlay/sheet/Sheet.vue +7 -28
  48. package/components/overlay/tooltip/Tooltip.vue +14 -19
  49. package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
  50. package/composables/useDisclosure.ts +113 -0
  51. package/composables/useEventListener.ts +16 -0
  52. package/composables/useMenu.ts +212 -0
  53. package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
  54. package/package.json +16 -40
  55. package/styles/0-settings/colors.css +10 -0
  56. package/styles/0-settings/layout.css +18 -0
  57. package/styles/0-settings/motion.css +2 -2
  58. package/styles/0-settings/spacing.css +3 -1
  59. package/utils/a11y/focus.ts +9 -3
  60. package/utils/cms/index.ts +283 -0
  61. package/utils/cms/schema.json +126 -0
  62. package/utils/navigation/sidebar.ts +97 -0
  63. package/utils/validation/form.ts +6 -9
  64. package/components/action/button/Button.astro +0 -95
  65. package/components/action/button/IconButton.astro +0 -86
  66. package/components/display/avatar/Avatar.astro +0 -17
  67. package/components/display/avatar/AvatarStack.astro +0 -9
  68. package/components/display/badge/Badge.astro +0 -15
  69. package/components/display/empty/Empty.astro +0 -9
  70. package/components/display/icon/Icon.astro +0 -52
  71. package/components/feedback/alert/Alert.astro +0 -52
  72. package/components/feedback/progress/Progress.astro +0 -68
  73. package/components/feedback/skeleton/Skeleton.astro +0 -32
  74. package/components/feedback/spinner/Spinner.astro +0 -25
  75. package/components/feedback/toast/Toast.astro +0 -50
  76. package/components/form/checkbox/Checkbox.astro +0 -79
  77. package/components/form/date-input/DateInput.astro +0 -105
  78. package/components/form/form/Form.astro +0 -106
  79. package/components/form/input-otp/InputOTP.astro +0 -147
  80. package/components/form/label/Label.astro +0 -13
  81. package/components/form/number-field/NumberField.astro +0 -142
  82. package/components/form/radio-group/RadioGroup.astro +0 -105
  83. package/components/form/select/Select.astro +0 -105
  84. package/components/form/slider/Slider.astro +0 -205
  85. package/components/form/switch/Switch.astro +0 -75
  86. package/components/form/tags-input/TagsInput.astro +0 -153
  87. package/components/form/text-input/TextInput.astro +0 -84
  88. package/components/form/textarea/Textarea.astro +0 -86
  89. package/components/layout/card/Card.astro +0 -13
  90. package/components/layout/separator/Separator.astro +0 -31
  91. package/components/layout/table/Table.astro +0 -116
  92. package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
  93. package/components/navigation/navbar/Navbar.astro +0 -62
  94. package/components/navigation/pagination/PaginationFooter.astro +0 -24
  95. package/components/navigation/sidebar/Sidebar.astro +0 -132
  96. package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
  97. package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
  98. package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
  99. package/components/overlay/modal/Modal.astro +0 -66
  100. package/utils/controllers/disclosure.ts +0 -117
  101. package/utils/controllers/form.ts +0 -524
  102. package/utils/controllers/index.ts +0 -39
  103. package/utils/controllers/menu.ts +0 -255
  104. package/utils/controllers/number-field.ts +0 -103
  105. package/utils/controllers/otp.ts +0 -252
  106. package/utils/controllers/sidebar.ts +0 -610
  107. package/utils/controllers/slider.ts +0 -336
  108. package/utils/controllers/tags-input.ts +0 -255
  109. package/utils/controllers/toast.ts +0 -426
package/CHANGELOG.md CHANGED
@@ -2,6 +2,264 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.1 - 2026-09-15
6
+
7
+ ### Changed
8
+
9
+ - RecordFields reuses field error arrays while draft values change, avoiding
10
+ repeated issue scans and unnecessary error-prop updates during typing.
11
+
12
+ - Sidebar and Tabs skip navigation DOM scans for unrelated key presses; Sidebar
13
+ also handles Space activation before collecting visible entries.
14
+
15
+ - Menus initialize roving focus with one enabled-item search and avoid repeating
16
+ full tabindex updates when keyboard navigation triggers a focus event.
17
+
18
+ - NumberField stepper clicks emit one model update while preserving native input
19
+ and change events, so consumer validation and autosave handlers run once.
20
+
21
+ ## 0.7.0 - 2026-09-15
22
+
23
+ ### Changed
24
+
25
+ - Index preserves structured backend errors and offers reset for rejected queries.
26
+ Refresh callbacks wait for navigation and loading to settle. Sortable columns
27
+ are checked against the endpoint's allowed fields before loading.
28
+
29
+ - `Breadcrumb` is now a bar with an optional `actions` slot on the
30
+ inline-end side. The `pui-breadcrumb` block class moved from the `<ol>`
31
+ to the `<nav>`; the list is `pui-breadcrumb__trail`.
32
+ - Index renders its pagination footer as a page-level band after the
33
+ results card instead of inside it, mirroring the header. Pages can pin
34
+ it to the viewport bottom; consumer styles targeting the footer's card
35
+ corner radii no longer apply.
36
+
37
+ ### Added
38
+
39
+ - Index's sort select shares state with table headers and supports allowed fields
40
+ without visible columns. Sort labels can be customized independently of columns.
41
+ - Individual filter updates and refresh/loading access in Index action and
42
+ selection slots reduce consumer wiring.
43
+ - `normalizeListQuery` validates structured query state directly, without a URL
44
+ encoding round trip.
45
+
46
+ ## 0.6.0 - 2026-09-11
47
+
48
+ ### Changed
49
+
50
+ - 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.
51
+ - Form validation focuses a visible, enabled invalid control, including when other errors are in hidden tab panels.
52
+ - Index separates page headings and actions from a bordered search, filter and results surface.
53
+ - Compact CMS layouts use tighter table rows and inline pagination controls. Form fields no longer double label spacing or stretch controls beside hints.
54
+
55
+ ### Added
56
+
57
+ - `RecordFields` and RecordForm’s `fields` slot for sidebar and tabbed layouts within one form; Tabs items can supply panel IDs and relationships.
58
+ - `BlockEditor` for typed content blocks, with model updates, app-provided fields/factories, pointer drag-and-drop, keyboard movement, add/remove and controlled collapse.
59
+ - `Breadcrumb` link slot for router links and `RecordForm` before-field slot for headings and other content between fields.
60
+ - `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.
61
+ - Backend query and response types, JSON Schema, URL query codecs, and immutable record path helpers under `@pienter/ui/utils/cms`.
62
+
63
+ ## 0.5.0 - 2026-08-30
64
+
65
+ ### Breaking
66
+
67
+ - The framework-agnostic controllers are gone; their behavior now lives in
68
+ Vue composables. `@pienter/ui/utils/controllers/*` (`mountDialog`,
69
+ `mountPopover`, `mountDisclosure`, `mountMenu` and their types) no longer
70
+ exists — import `useDialog`, `usePopover`, `useDisclosure`, and `useMenu`
71
+ from `@pienter/ui/composables/<name>` instead. The composables carry the
72
+ same behavior and options but own the lifecycle themselves: they take an
73
+ element ref plus options at setup, wire on mount, follow a controlled
74
+ `open` getter, and clean up on unmount, so the mount/watch/teardown dance
75
+ every component repeated is gone. `Placement` moved to
76
+ `@pienter/ui/composables/usePopover`. Component behavior is unchanged.
77
+
78
+ ## 0.4.0 - 2026-08-28
79
+
80
+ ### Breaking
81
+
82
+ - The library is now Vue-only. The Astro tier is removed in full: every
83
+ `*.astro` component export, the Astro-only controllers (`mountForm`,
84
+ `mountNumberField`, `mountInputOTP`, `mountSlider`, `mountTagsInput`,
85
+ `mountToast`) and their types, and the `Sidebar.mount` entrypoint no longer
86
+ exist, and `astro` is no longer an optional peer dependency. Every removed
87
+ component has a Vue counterpart — migrate
88
+ `@pienter/ui/components/X.astro` imports to
89
+ `@pienter/ui/components/X.vue`; there is no Astro migration path. The
90
+ shared controllers Vue consumes (`mountDialog`, `mountDisclosure`,
91
+ `mountPopover`, `mountMenu`) are unchanged.
92
+ - `Sidebar` follows the D13 layered split instead of a shared controller, so
93
+ `mountSidebar` is no longer exported from `@pienter/ui/utils/controllers`.
94
+ `utils/controllers/` is for code both tiers run; Sidebar's imperative
95
+ orchestration is Astro-only and now lives beside the component, exported as
96
+ `@pienter/ui/components/Sidebar.mount`. Vue no longer wraps it at all —
97
+ `Sidebar.vue` is reactive throughout, which is what D13 already required
98
+ ("wrapping an imperative DOM controller from Vue fights Vue's data flow…
99
+ the controller mutates the DOM, Vue patches it back, and race conditions
100
+ ensue"). Both tiers now derive from shared primitives in
101
+ `@pienter/ui/utils/navigation/sidebar`: `isHrefActive`, `deriveGroupKey`,
102
+ `computeSubmenuPosition`, `isSubmenuActive`, `groupContainsActive`.
103
+ `isHrefActive` and `deriveGroupKey` moved there from the controller barrel.
104
+ - `Sidebar`'s `items` prop is now `topItems`, alongside a new `bottomItems`.
105
+ The column has two nav regions — the top nav, which takes the leftover
106
+ height and scrolls, and the bottom nav, pinned to the foot below a divider
107
+ for settings, account and the like. Entries in either get identical wiring:
108
+ active-href matching, keyboard navigation, and collapsed flyouts for their
109
+ groups. The `#footer` slot is renamed `#bottom` and still takes arbitrary
110
+ content beside `bottomItems`. Migrate `:items="nav"` to `:top-items="nav"`.
111
+ - The private recursive renderer is `SidebarMenuItem` (was
112
+ `SidebarItemRender`), matching the `data-sidebar-menu-item` hook it emits.
113
+ Internal to the component — no export changed — but the file names did.
114
+ - The nav list classes are `.pui-sidebar__top-nav` and
115
+ `.pui-sidebar__bottom-nav` (was `.pui-sidebar__menu`), and the foot wrapper
116
+ is `.pui-sidebar__bottom` (was `.pui-sidebar__footer`). Consumers styling
117
+ those internals will need to follow.
118
+ - `Sidebar` no longer uses a roving tabindex. Every menu entry is its own tab
119
+ stop, as an ordinary `<nav>` full of links should be. Roving left the whole
120
+ sidebar with a **single** tab stop, so a rail of four destinations had three
121
+ that Tab could never reach. This also matches CONVENTIONS.md, which reserves
122
+ roving for "single-focus-per-group controls … where one item represents the
123
+ entire group's focus target" — a navigation landmark is not that. Arrow keys,
124
+ Home and End still work, now as an accelerator on top of the natural order
125
+ rather than a replacement for it. Consumers relying on exactly one sidebar tab
126
+ stop will see more; nothing else changes.
127
+
128
+ ### Changed
129
+
130
+ - `--space-sidebar-width-collapsed` is `4.75rem` (was `4rem`). At 4rem the new
131
+ rail captions had about 2rem of usable width once the panel's padding was
132
+ taken out, and real labels ellipsised to two characters.
133
+ - The `Sidebar` focus ring colour follows the entry's tone via
134
+ `--pui-sidebar-ring`, matching how `--btn-ring` works for Button: neutral by
135
+ default, brand on the entry wearing the brand wash, so the ring no longer
136
+ fights the fill underneath it.
137
+ - The collapsed rail marks which icon the open flyout belongs to. Previously
138
+ only hover did, so a flyout opened from the keyboard left nothing pointing
139
+ back at its icon once focus moved into the panel.
140
+ - The collapsed submenu flyout now sits on `--layer-below`, so it genuinely
141
+ passes under the rail rather than across it — the panel's own background
142
+ hides where the slide starts. The open-flyout rule pairs the rung with
143
+ `isolation: isolate`, because "behind the rail" only holds while the sidebar
144
+ is the stacking context containing the flyout; without it the panel would go
145
+ behind the *page* in any shell that doesn't happen to give the root a
146
+ context.
147
+ - The collapsed submenu flyout slides in from behind the rail when it opens,
148
+ and disappears instantly when it closes. The transition is declared only on
149
+ the open state — a transition belongs to the state being moved *to* — so
150
+ there is nothing to undeclare for the closing direction. An exit animation on
151
+ a dismissal only delays the content behind it, and the panel covers page
152
+ content. `visibility` stays untransitioned so the entries are focusable the
153
+ instant the group takes focus; under `prefers-reduced-motion` the panel
154
+ simply appears.
155
+ - The collapsed `Sidebar` rail stacks a caption under each icon instead of
156
+ going icon-only, so it is navigable without hovering. The rail sets its own
157
+ inline padding rather than inheriting the expanded panel's, and captions wrap
158
+ to a second line instead of truncating — an ellipsis hides the one thing a
159
+ caption is for.
160
+ - A collapsed rail group whose subtree contains the current page carries the
161
+ brand wash. On the rail its children are hidden, so the group is the only
162
+ indicator of location, and a tint alone lost to the hover fill — a hovered
163
+ entry read as more current than the current one. The wash is reverted in
164
+ drawer mode, where the sidebar is a full expanded list: there the group sits
165
+ directly above its own current child and two identical fills merged into a
166
+ single block. Brand ink alone carries "this section holds the current page"
167
+ there, and the wash is left to the entry that actually is the current page.
168
+ - The `Sidebar` focus ring uses `--outline-width` / `--outline-offset` instead
169
+ of hard-coded `3px` / `2px`. It is the standard outset ring, so it reads as a
170
+ ring around the entry rather than a line cutting into it and stays legible
171
+ over the selected entry's wash; the collapsed rail reserves inline padding so
172
+ the column edge never clips it.
173
+ - A `Sidebar` link's selected state uses `--bg-clr-brand-soft` rather than
174
+ `--bg-clr-accent`, which is what hover already uses — the current page was
175
+ indistinguishable from whatever the pointer was over.
176
+
177
+ ### Fixed
178
+
179
+ - `Sidebar`'s focus ring is no longer clipped in the top nav. That list is a
180
+ scroll container — `overflow-y: auto` forces `overflow-x: auto` — and a
181
+ scroll container clips at its padding box, so an entry filling it
182
+ edge-to-edge had the ring sliced off both sides. The list now reserves the
183
+ ring's width as `padding-inline` with a matching negative `margin-inline`,
184
+ so nothing moves.
185
+ - Consumers must import `@pienter/ui/styles` before any component. Each
186
+ component stylesheet is a bare `@layer components { … }`, and layer order is
187
+ fixed by first encounter — so a component loading first registers
188
+ `components` ahead of `base`, and every base element rule starts outranking
189
+ component rules. In the Vue playground that made base's `a { color: … }` beat
190
+ `.pui-sidebar__link`, turning sidebar links brand-coloured. The playground
191
+ entry point now imports the stylesheet first; the library contract is
192
+ unchanged (main.css declares the order once).
193
+ - The mobile `Sidebar` drawer no longer paints under the consumer's app header.
194
+ The rule that lifts the sidebar while a collapsed flyout is open applied at
195
+ every width, and a `z-index` other than `auto` makes a **grid or flex item** a
196
+ stacking context even while `position: static` — which is what a sidebar in a
197
+ layout shell normally is. The drawer's own `z-index: 50` was therefore sealed
198
+ inside that context and lost to any header above 30. The lift is now scoped
199
+ above the drawer breakpoint, where the flyout actually exists, and the drawer
200
+ lifts the root itself while open.
201
+ - `Sidebar`'s focus handler no longer rewrites every entry's `tabindex`. The
202
+ roving pattern was removed from the initial render but survived in the
203
+ `focusin` handler, which demoted every other entry to `-1` the moment one was
204
+ focused — so the sidebar looked fully tabbable until you actually tabbed into
205
+ it, and then collapsed to a single stop.
206
+ - A collapsed submenu flyout's entries are reachable by Tab. `visibility` was in
207
+ the flyout's transition list, so it flipped partway through the fade and the
208
+ entries were still unfocusable when Tab arrived — landing on a group jumped
209
+ straight past its flyout to the next rail item. `visibility` now steps
210
+ immediately on open and is delayed only on close, so the fade-out still plays.
211
+ - The collapsed rail no longer stays visible below `drawerBreakpoint`. The
212
+ drawer's `width: 0` sat on a bare `.pui-sidebar` inside a media query, which
213
+ grants no specificity, so `.pui-sidebar[data-state='collapsed']` outranked it
214
+ and left an empty bordered strip down the page on mobile.
215
+ - The collapsed submenu flyout aligns flush with the top of the sidebar column
216
+ instead of the menu region. The menu's box starts inside the panel's padding,
217
+ which left a visible seam above the flyout. With a `#brand` slot filled the
218
+ flyout starts below the brand, so it still never covers the wordmark.
219
+ - `Icon.astro` forwards a `class` prop instead of silently dropping it. It
220
+ hardcoded `class="pui-icon"` and spread the rest of the props after, so an
221
+ incoming class was discarded — Vue applied it, Astro did not. One casualty
222
+ was `Sidebar`'s `pui-sidebar__group-caret` hook, which meant the caret never
223
+ rotated on expand in Astro.
224
+ - A `Sidebar` brand slot no longer spills across the page when collapsed. The
225
+ rule that was meant to hide everything without `data-keep-when-collapsed` was
226
+ an empty placeholder, so a wordmark kept its full width in a rail narrower
227
+ than most product names.
228
+ - The collapsed submenu flyout no longer renders behind page content. It is
229
+ `position: fixed`, which escapes overflow but not a stacking context, and
230
+ consumers routinely make the sidebar `position: sticky` for scroll — which
231
+ creates one. The root now raises its own z-index while a flyout is open.
232
+ - A closed flyout is positioned as soon as it enters panel mode. A fixed
233
+ element with no coordinates resolves to its static position inside the rail,
234
+ so anything that made it briefly visible flashed the panel over the icons.
235
+
236
+ ### Added
237
+
238
+ - `Sidebar` takes a `bottomItems` prop — the bottom nav. Same
239
+ `SidebarItem[]` shape as `topItems`, rendered at the foot of the column
240
+ below a divider, for settings / account / help. See the Breaking entry for
241
+ the accompanying `items` → `topItems` rename.
242
+ - `Sidebar` takes a `collapsedSubmenu` prop (`'hidden' | 'panel'`, default
243
+ `'hidden'`). With `'panel'`, a collapsed rail's groups are no longer a dead
244
+ end: hover, focus, or click opens a group's children in a full-height flyout
245
+ beside the rail. Escape closes it and returns focus to the group toggle;
246
+ clicking the toggle pins the flyout so it survives pointer-out; only one
247
+ flyout is open at a time. The default keeps the previous behavior, so
248
+ existing consumers are unaffected.
249
+ - `mountSidebar` gains `openSubmenu(key)`, `closeSubmenu()`, and
250
+ `openSubmenuKey()`, plus a `collapsedSubmenu` config field. They are no-ops
251
+ unless the policy is active — which needs the sidebar collapsed and the
252
+ viewport above `drawerBreakpoint`, since below it there is no rail.
253
+ - A stacking ladder: `--layer-below`, `--layer-0` … `--layer-5`, `--layer-top`.
254
+ Every positioned layer in the library names a rung instead of inventing a
255
+ number, so the whole order is legible in one place and two components cannot
256
+ accidentally tie. `Toast`, `Table`'s loading pane, `NumberField`'s focused
257
+ step button and every `Sidebar` layer moved onto it; the numbers they
258
+ resolve to are unchanged.
259
+ - `--shadow-lg`, for a surface that covers page content rather than sitting on
260
+ it. `--shadow-md` is tuned for a card in the flow and reads flat under
261
+ something as large as a sidebar flyout.
262
+
5
263
  ## 0.3.0 - 2026-08-26
6
264
 
7
265
  ### Added