@pienter/ui 0.5.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 (31) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/CONVENTIONS.md +45 -0
  3. package/README.md +30 -0
  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/form/block-editor/BlockEditor.vue +454 -0
  8. package/components/form/block-editor/block-editor.css +149 -0
  9. package/components/form/block-editor/types.ts +15 -0
  10. package/components/form/combobox/Combobox.vue +21 -34
  11. package/components/form/number-field/NumberField.vue +0 -1
  12. package/components/form/record-form/RecordFields.vue +128 -0
  13. package/components/form/record-form/RecordForm.vue +116 -0
  14. package/components/form/record-form/fields.ts +20 -0
  15. package/components/form/record-form/record-form.css +15 -0
  16. package/components/form/record-form/types.ts +28 -0
  17. package/components/form/text-input/text-input.css +2 -0
  18. package/components/layout/index/Index.vue +353 -0
  19. package/components/layout/index/index.css +114 -0
  20. package/components/layout/index/useIndex.ts +390 -0
  21. package/components/layout/table/table.css +2 -1
  22. package/components/navigation/breadcrumb/Breadcrumb.vue +24 -5
  23. package/components/navigation/breadcrumb/breadcrumb.css +15 -0
  24. package/components/navigation/sidebar/Sidebar.vue +11 -8
  25. package/components/navigation/tabs/Tabs.vue +6 -0
  26. package/composables/useMenu.ts +20 -27
  27. package/package.json +12 -2
  28. package/styles/0-settings/colors.css +10 -0
  29. package/utils/a11y/focus.ts +9 -3
  30. package/utils/cms/index.ts +283 -0
  31. package/utils/cms/schema.json +126 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
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
+
5
63
  ## 0.5.0 - 2026-08-30
6
64
 
7
65
  ### Breaking
package/CONVENTIONS.md CHANGED
@@ -1409,3 +1409,48 @@ Pinned upstream SHAs used across the table:
1409
1409
  | toggle | https://github.com/shadcn-ui/ui/blob/7865621397708917369251d67029ad920e390a38/apps/v4/registry/new-york-v4/ui/toggle.tsx | 7865621397708917369251d67029ad920e390a38 | https://github.com/radix-ui/primitives/tree/22473d16404bfd446305db5b6c9308aece99fdec/packages/react/toggle/src | 22473d16404bfd446305db5b6c9308aece99fdec | 2026-05-27 |
1410
1410
  | toggle-group | https://github.com/shadcn-ui/ui/blob/7865621397708917369251d67029ad920e390a38/apps/v4/registry/new-york-v4/ui/toggle-group.tsx | 7865621397708917369251d67029ad920e390a38 | https://github.com/radix-ui/primitives/tree/22473d16404bfd446305db5b6c9308aece99fdec/packages/react/toggle-group/src | 22473d16404bfd446305db5b6c9308aece99fdec | 2026-05-27 |
1411
1411
  | tooltip | https://github.com/shadcn-ui/ui/blob/7865621397708917369251d67029ad920e390a38/apps/v4/registry/new-york-v4/ui/tooltip.tsx | 7865621397708917369251d67029ad920e390a38 | https://github.com/radix-ui/primitives/tree/22473d16404bfd446305db5b6c9308aece99fdec/packages/react/tooltip/src | 22473d16404bfd446305db5b6c9308aece99fdec | 2026-05-27 |
1412
+
1413
+ ## CMS page composition
1414
+
1415
+ `Index`, `RecordForm`, and `RecordDetails` compose existing primitives. Consumers
1416
+ register routes and supply endpoint-specific data functions, rendering and actions.
1417
+
1418
+ Index accepts `load(query, { signal })`, `columns` and `queryOptions`. It owns
1419
+ query state, rows, metadata, loading, failures and retry. Search is debounced;
1420
+ search/filter/sort/page-size changes reset page to 1. New requests and unmount
1421
+ abort earlier work; stale responses are ignored even when loaders ignore the signal.
1422
+ `sync-query` opts into Vue Router query decoding and history synchronization.
1423
+ Without it, state stays local. Index imports the optional `vue-router` peer, but
1424
+ requires an installed router instance only for URL synchronization.
1425
+
1426
+ Index applies the existing default sort cycle; Table/DataTable still emit a column
1427
+ key as intent, per ADR 0005. A toolbar select shares that sort state and includes
1428
+ all allowed fields, including those without visible columns. Sortable columns
1429
+ must belong to the allowed sort vocabulary; labels may be overridden with
1430
+ `sortLabels`. Filter slots can update one key without replacing siblings. Action
1431
+ and selection slots receive loading state and an awaitable refresh callback.
1432
+ Structured backend query errors offer reset; other load failures offer retry.
1433
+ Use DataTable when the consumer needs controlled data
1434
+ and request state. Index's compact row spacing uses
1435
+ `--pui-table-cell-padding-block`, which Table otherwise defaults to `--space-xs`.
1436
+
1437
+ RecordForm emits detached JSON snapshots, preserving every supplied property.
1438
+ Consumers construct an explicit writable object from backend records. Typed path
1439
+ segments identify nested fields and array indices; fields display issues for
1440
+ their path and descendants. The form summary retains unmatched issues.
1441
+ `--pui-record-form-columns` customizes the field grid, defaulting to one column.
1442
+
1443
+ RecordForm's `fields` slot replaces its default RecordFields rendering for
1444
+ custom sidebar/tab layouts. RecordFields takes the same draft and renders a
1445
+ subset, with `--pui-record-fields-columns` as its grid seam. It never renders a
1446
+ form element. Callers expose the relevant tab when backend issues arrive.
1447
+
1448
+ BlockEditor owns an ordered array of stable `id`/`type` blocks and emits updates
1449
+ for edits, addition, removal and reordering. Creation factories and slot content
1450
+ stay caller-owned. Optional `v-model:collapsed` lets pages reveal invalid blocks.
1451
+ Its colocated audit records pointer, keyboard, focus and cancellation behavior.
1452
+
1453
+ The backend contract lives in `docs/cms/backend-contract.md` with a distributable
1454
+ JSON Schema at `utils/cms/schema.json`. It defines request/response data only;
1455
+ endpoint URLs, authentication, permissions and domain operations belong to the
1456
+ application.
package/README.md CHANGED
@@ -32,6 +32,36 @@ import { icons } from '@pienter/ui/icons';
32
32
  import { generateId } from '@pienter/ui/utils/a11y/id.js';
33
33
  ```
34
34
 
35
+ ### CMS pages
36
+
37
+ Compose ordinary Vue routes with `Index`, `RecordForm`, and `RecordDetails`:
38
+
39
+ ```ts
40
+ import Index from '@pienter/ui/components/Index.vue';
41
+ import RecordForm from '@pienter/ui/components/RecordForm.vue';
42
+ import RecordFields from '@pienter/ui/components/RecordFields.vue';
43
+ import BlockEditor from '@pienter/ui/components/BlockEditor.vue';
44
+ import RecordDetails from '@pienter/ui/components/RecordDetails.vue';
45
+ import type { RecordFormField } from '@pienter/ui/components/RecordForm.types';
46
+ import { decodeListQuery, encodeListQuery } from '@pienter/ui/utils/cms';
47
+ ```
48
+
49
+ Index receives `load(query, { signal })`, columns and query options. It handles
50
+ loading, search, pagination, errors and retry; `sync-query` enables Vue Router URL
51
+ synchronization. Index requires the `vue-router` package; an installed router
52
+ instance is needed only when synchronization is enabled. RecordForm receives `v-model`, fields and validation issues,
53
+ emitting a JSON payload on `submit`. RecordDetails receives a record and fields.
54
+ Use slots for custom cells, controls, values and actions. Your pages own route
55
+ declarations, endpoint functions and saving. Use RecordForm’s `fields` slot and
56
+ RecordFields to put groups in sidebars and tab panels while retaining one form.
57
+ BlockEditor accepts `v-model`, block creation factories and a `block` slot; it
58
+ provides drag-and-drop, keyboard movement, add/remove and collapse controls.
59
+
60
+ The [CMS guide](../../docs/cms/README.md) contains working examples. The
61
+ [backend contract](../../docs/cms/backend-contract.md) defines query parameters,
62
+ response envelopes and validation paths; its JSON Schema ships at
63
+ `@pienter/ui/utils/cms/schema.json`.
64
+
35
65
  ### Themes
36
66
 
37
67
  The default theme follows the operating-system color preference, using
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ export type { RecordDetailsField } from './types.js';
3
+ </script>
4
+
5
+ <script setup lang="ts" generic="T extends object">
6
+ import { readPath } from '../../../utils/cms/index.js';
7
+ import type { RecordDetailsField } from './types.js';
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ record: T;
12
+ fields: readonly RecordDetailsField[];
13
+ emptyValue?: string;
14
+ }>(),
15
+ { emptyValue: '—' },
16
+ );
17
+
18
+ function valueFor(field: RecordDetailsField): unknown {
19
+ return readPath(props.record, field.path ?? [field.name]);
20
+ }
21
+
22
+ function displayValue(value: unknown): string {
23
+ if (value === undefined || value === null || value === '')
24
+ return props.emptyValue;
25
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
26
+ if (typeof value === 'object') return JSON.stringify(value);
27
+ return String(value);
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <div class="pui-record-details">
33
+ <dl class="pui-record-details__list">
34
+ <div
35
+ v-for="field in fields"
36
+ :key="field.name"
37
+ class="pui-record-details__field"
38
+ >
39
+ <dt class="pui-record-details__label">{{ field.label }}</dt>
40
+ <dd class="pui-record-details__value">
41
+ <slot
42
+ :name="`field:${field.name}`"
43
+ :field="field"
44
+ :value="valueFor(field)"
45
+ :record="record"
46
+ >
47
+ {{ displayValue(valueFor(field)) }}
48
+ </slot>
49
+ </dd>
50
+ </div>
51
+ </dl>
52
+ <slot />
53
+ <div v-if="$slots.actions" class="pui-record-details__actions">
54
+ <slot name="actions" :record="record" />
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <style>
60
+ @import './record-details.css';
61
+ </style>
@@ -0,0 +1,37 @@
1
+ @layer components {
2
+ .pui-record-details {
3
+ display: grid;
4
+ gap: var(--space-s);
5
+
6
+ & .pui-record-details__list {
7
+ display: grid;
8
+ margin: 0;
9
+ }
10
+
11
+ & .pui-record-details__field {
12
+ display: grid;
13
+ grid-template-columns: minmax(0, 1fr) minmax(0, 3fr);
14
+ gap: var(--space-s);
15
+ padding-block: var(--space-xs);
16
+ border-block-end: var(--stroke-sm) solid var(--border-clr-base);
17
+ }
18
+
19
+ & .pui-record-details__label {
20
+ color: var(--text-clr-dim);
21
+ font-size: var(--step--1);
22
+ }
23
+
24
+ & .pui-record-details__value {
25
+ margin: 0;
26
+ min-inline-size: 0;
27
+ overflow-wrap: anywhere;
28
+ white-space: pre-wrap;
29
+ }
30
+
31
+ & .pui-record-details__actions {
32
+ display: flex;
33
+ flex-wrap: wrap;
34
+ gap: var(--space-xs);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,8 @@
1
+ import type { ValuePath } from '../../../utils/cms/index.js';
2
+
3
+ export interface RecordDetailsField {
4
+ name: string;
5
+ label: string;
6
+ /** Defaults to the literal property name, without splitting dots. */
7
+ path?: ValuePath;
8
+ }