@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.
- package/CHANGELOG.md +58 -0
- package/CONVENTIONS.md +45 -0
- package/README.md +30 -0
- package/components/display/record-details/RecordDetails.vue +61 -0
- package/components/display/record-details/record-details.css +37 -0
- package/components/display/record-details/types.ts +8 -0
- package/components/form/block-editor/BlockEditor.vue +454 -0
- package/components/form/block-editor/block-editor.css +149 -0
- package/components/form/block-editor/types.ts +15 -0
- package/components/form/combobox/Combobox.vue +21 -34
- package/components/form/number-field/NumberField.vue +0 -1
- package/components/form/record-form/RecordFields.vue +128 -0
- package/components/form/record-form/RecordForm.vue +116 -0
- package/components/form/record-form/fields.ts +20 -0
- package/components/form/record-form/record-form.css +15 -0
- package/components/form/record-form/types.ts +28 -0
- package/components/form/text-input/text-input.css +2 -0
- package/components/layout/index/Index.vue +353 -0
- package/components/layout/index/index.css +114 -0
- package/components/layout/index/useIndex.ts +390 -0
- package/components/layout/table/table.css +2 -1
- package/components/navigation/breadcrumb/Breadcrumb.vue +24 -5
- package/components/navigation/breadcrumb/breadcrumb.css +15 -0
- package/components/navigation/sidebar/Sidebar.vue +11 -8
- package/components/navigation/tabs/Tabs.vue +6 -0
- package/composables/useMenu.ts +20 -27
- package/package.json +12 -2
- package/styles/0-settings/colors.css +10 -0
- package/utils/a11y/focus.ts +9 -3
- package/utils/cms/index.ts +283 -0
- 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
|
+
}
|