@pienter/ui 0.17.0 → 0.19.0
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 +66 -0
- package/CONVENTIONS.md +89 -53
- package/components/feedback/toast/Toast.vue +4 -1
- package/components/feedback/toast/ToastHost.vue +5 -0
- package/components/form/checkbox/checkbox.css +4 -3
- package/components/form/number-field/NumberField.vue +15 -3
- package/components/form/number-field/number-field.css +3 -2
- package/components/form/radio-group/radio-group.css +1 -0
- package/components/form/switch/switch.css +1 -0
- package/components/layout/index/Index.vue +109 -27
- package/components/layout/index/index.css +18 -0
- package/components/layout/index/{useIndex.ts → useIndexQuery.ts} +62 -8
- package/components/layout/index-filters/FilterCheckbox.vue +29 -0
- package/components/layout/index-filters/FilterMultiSelect.vue +110 -0
- package/components/layout/index-filters/FilterRange.vue +80 -0
- package/components/layout/index-filters/FilterSelect.vue +38 -0
- package/components/layout/index-filters/filters.css +65 -0
- package/components/layout/index-filters/useFilterControl.ts +40 -0
- package/components/layout/table/DataTable.vue +10 -0
- package/components/layout/table/Table.vue +4 -1
- package/components/navigation/breadcrumb/Breadcrumb.vue +9 -4
- package/components/navigation/pagination/Pagination.vue +12 -3
- package/package.json +6 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* The two grouped controls share one fieldset chrome; the legend takes
|
|
3
|
+
the small control height so a Clear button beside it lines up. */
|
|
4
|
+
.pui-filter-multi-select,
|
|
5
|
+
.pui-filter-range {
|
|
6
|
+
position: relative;
|
|
7
|
+
|
|
8
|
+
& > .pui-field__label {
|
|
9
|
+
line-height: var(--control-height-sm);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.pui-filter-multi-select {
|
|
14
|
+
& > .pui-field__label {
|
|
15
|
+
padding-inline-end: var(--space-2xl);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* A rendered legend is not a grid item, so the Clear sits beside it by
|
|
20
|
+
position rather than by a grid row. */
|
|
21
|
+
.pui-filter-multi-select__clear {
|
|
22
|
+
position: absolute;
|
|
23
|
+
inset-block-start: 0;
|
|
24
|
+
inset-inline-end: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* About six option rows, then the list scrolls; the padding keeps the
|
|
28
|
+
focus ring inside the scrollport. */
|
|
29
|
+
.pui-filter-multi-select__options {
|
|
30
|
+
display: grid;
|
|
31
|
+
align-content: start;
|
|
32
|
+
gap: var(--space-2xs);
|
|
33
|
+
max-block-size: calc(6 * var(--space-l));
|
|
34
|
+
padding: var(--space-3xs);
|
|
35
|
+
overflow-y: auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.pui-filter-multi-select__option {
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: var(--space-2xs);
|
|
42
|
+
min-inline-size: 0;
|
|
43
|
+
margin-block-end: 0;
|
|
44
|
+
font-size: var(--step--1);
|
|
45
|
+
font-weight: var(--fw-regular);
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.pui-filter-multi-select__label {
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
text-overflow: ellipsis;
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.pui-filter-multi-select__count {
|
|
56
|
+
flex: none;
|
|
57
|
+
color: var(--text-clr-muted);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.pui-filter-range__bounds {
|
|
61
|
+
display: grid;
|
|
62
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
63
|
+
gap: var(--space-xs);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { computed, watch, type ComputedRef } from 'vue';
|
|
2
|
+
import type { ListQuery } from '../../../utils/cms/index.js';
|
|
3
|
+
import {
|
|
4
|
+
injectIndexQuery,
|
|
5
|
+
useIndexFilters,
|
|
6
|
+
type IndexQuery,
|
|
7
|
+
} from '../index/useIndexQuery.js';
|
|
8
|
+
|
|
9
|
+
export interface FilterBinding {
|
|
10
|
+
filters: ComputedRef<Readonly<ListQuery['filters']>>;
|
|
11
|
+
setFilter: (name: string, value: string | undefined) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Binds a control to its index; without one it warns and every write is a no-op. */
|
|
15
|
+
export function useFilterControl(
|
|
16
|
+
names: () => readonly string[],
|
|
17
|
+
state?: IndexQuery<unknown>,
|
|
18
|
+
): FilterBinding {
|
|
19
|
+
const index = state ?? injectIndexQuery();
|
|
20
|
+
if (!index) {
|
|
21
|
+
console.warn(
|
|
22
|
+
`[pui] Filter "${names().join('", "')}" has no index: render it inside an Index or pass the state from useIndexQuery.`,
|
|
23
|
+
);
|
|
24
|
+
return { filters: computed(() => ({})), setFilter: () => {} };
|
|
25
|
+
}
|
|
26
|
+
const { filters, filterNames, setFilter } = useIndexFilters(index);
|
|
27
|
+
let warned = false;
|
|
28
|
+
watch(
|
|
29
|
+
() => names().filter((name) => !filterNames.value.includes(name)),
|
|
30
|
+
(missing) => {
|
|
31
|
+
if (!missing.length || warned) return;
|
|
32
|
+
warned = true;
|
|
33
|
+
console.warn(
|
|
34
|
+
`[pui] Filter "${missing.join('", "')}" is not in queryOptions.filters; the index rejects it.`,
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
{ immediate: true },
|
|
38
|
+
);
|
|
39
|
+
return { filters, setFilter };
|
|
40
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
:selectable="selectable"
|
|
11
11
|
:clickable="clickable"
|
|
12
12
|
:all-selected="allSelected"
|
|
13
|
+
:select-all-label="selectAllLabel"
|
|
13
14
|
:label="label"
|
|
14
15
|
:labelledby="labelledby"
|
|
15
16
|
@toggle-all="toggleAll"
|
|
@@ -76,6 +77,10 @@ const props = withDefaults(
|
|
|
76
77
|
selectable?: boolean;
|
|
77
78
|
/** Show a pointer cursor on rows. */
|
|
78
79
|
clickable?: boolean;
|
|
80
|
+
/** Accessible name of the header select-all checkbox. */
|
|
81
|
+
selectAllLabel?: string;
|
|
82
|
+
/** Accessible name of a row's checkbox; a function receives the row. Default: `Select <first cell>`, or `Select row` when that cell is empty. */
|
|
83
|
+
selectRowLabel?: string | ((row: T) => string);
|
|
79
84
|
/** Which column is sorted, and how. `rows` is passed through untouched — ordering them is the consumer's job (ADR 0005). */
|
|
80
85
|
sort?: SortState;
|
|
81
86
|
/** Frosts and disables the body while the rows are stale, and suppresses the empty state. */
|
|
@@ -90,6 +95,8 @@ const props = withDefaults(
|
|
|
90
95
|
selected: () => [],
|
|
91
96
|
selectable: false,
|
|
92
97
|
clickable: false,
|
|
98
|
+
selectAllLabel: 'Select all rows',
|
|
99
|
+
selectRowLabel: undefined,
|
|
93
100
|
sort: null,
|
|
94
101
|
loading: false,
|
|
95
102
|
label: undefined,
|
|
@@ -112,6 +119,9 @@ function keyOf(row: T): string | number {
|
|
|
112
119
|
return cellValue(row, props.rowKey) as string | number;
|
|
113
120
|
}
|
|
114
121
|
function rowLabel(row: T): string {
|
|
122
|
+
if (typeof props.selectRowLabel === 'function')
|
|
123
|
+
return props.selectRowLabel(row);
|
|
124
|
+
if (props.selectRowLabel !== undefined) return props.selectRowLabel;
|
|
115
125
|
const first = props.columns[0];
|
|
116
126
|
const value = first ? String(cellValue(row, first.key) ?? '').trim() : '';
|
|
117
127
|
return value ? `Select ${value}` : 'Select row';
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
class="pui-table__check"
|
|
28
28
|
type="checkbox"
|
|
29
29
|
:checked="allSelected"
|
|
30
|
-
aria-label="
|
|
30
|
+
:aria-label="selectAllLabel"
|
|
31
31
|
@change="emit('toggle-all')"
|
|
32
32
|
/>
|
|
33
33
|
</th>
|
|
@@ -106,6 +106,8 @@ const props = withDefaults(
|
|
|
106
106
|
clickable?: boolean;
|
|
107
107
|
/** Checked state of the header select-all checkbox. */
|
|
108
108
|
allSelected?: boolean;
|
|
109
|
+
/** Accessible name of the header select-all checkbox. */
|
|
110
|
+
selectAllLabel?: string;
|
|
109
111
|
/** Which column is sorted, and how. Rendered, never changed here; `null` is unsorted (ADR 0005). */
|
|
110
112
|
sort?: SortState;
|
|
111
113
|
/** Frosts and disables the body while the rows are stale, leaving the header usable. */
|
|
@@ -119,6 +121,7 @@ const props = withDefaults(
|
|
|
119
121
|
selectable: false,
|
|
120
122
|
clickable: false,
|
|
121
123
|
allSelected: false,
|
|
124
|
+
selectAllLabel: 'Select all rows',
|
|
122
125
|
sort: null,
|
|
123
126
|
loading: false,
|
|
124
127
|
label: undefined,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<nav aria-label="
|
|
2
|
+
<nav :aria-label="label" class="pui-breadcrumb">
|
|
3
3
|
<ol class="pui-breadcrumb__trail">
|
|
4
4
|
<template v-for="(item, index) in items" :key="index">
|
|
5
5
|
<li
|
|
@@ -36,9 +36,14 @@
|
|
|
36
36
|
</template>
|
|
37
37
|
|
|
38
38
|
<script setup lang="ts">
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
39
|
+
withDefaults(
|
|
40
|
+
defineProps<{
|
|
41
|
+
items: { label: string; href?: string }[];
|
|
42
|
+
/** Accessible name of the `<nav>` landmark. */
|
|
43
|
+
label?: string;
|
|
44
|
+
}>(),
|
|
45
|
+
{ label: 'Breadcrumb' },
|
|
46
|
+
);
|
|
42
47
|
|
|
43
48
|
defineSlots<{
|
|
44
49
|
link?: (scope: {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<nav class="pui-pagination-nav" aria-label="
|
|
2
|
+
<nav class="pui-pagination-nav" :aria-label="label">
|
|
3
3
|
<ul class="pui-pagination">
|
|
4
4
|
<li>
|
|
5
5
|
<button
|
|
6
6
|
class="pui-pagination__item"
|
|
7
7
|
:disabled="currentPage <= 1"
|
|
8
|
-
aria-label="
|
|
8
|
+
:aria-label="previousLabel"
|
|
9
9
|
@click="emit('update:currentPage', currentPage - 1)"
|
|
10
10
|
>
|
|
11
11
|
<Icon name="chevron-left" size="sm" />
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
<button
|
|
34
34
|
class="pui-pagination__item"
|
|
35
35
|
:disabled="currentPage >= totalPages"
|
|
36
|
-
aria-label="
|
|
36
|
+
:aria-label="nextLabel"
|
|
37
37
|
@click="emit('update:currentPage', currentPage + 1)"
|
|
38
38
|
>
|
|
39
39
|
<Icon name="chevron-right" size="sm" />
|
|
@@ -52,9 +52,18 @@ const props = withDefaults(
|
|
|
52
52
|
currentPage: number;
|
|
53
53
|
totalPages: number;
|
|
54
54
|
maxVisible?: number;
|
|
55
|
+
/** Accessible name of the `<nav>` landmark. */
|
|
56
|
+
label?: string;
|
|
57
|
+
/** Accessible name of the icon-only previous button. */
|
|
58
|
+
previousLabel?: string;
|
|
59
|
+
/** Accessible name of the icon-only next button. */
|
|
60
|
+
nextLabel?: string;
|
|
55
61
|
}>(),
|
|
56
62
|
{
|
|
57
63
|
maxVisible: 5,
|
|
64
|
+
label: 'Pagination',
|
|
65
|
+
previousLabel: 'Previous page',
|
|
66
|
+
nextLabel: 'Next page',
|
|
58
67
|
},
|
|
59
68
|
);
|
|
60
69
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pienter/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Shared Pienter UI components, styles, icons, and browser utilities.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"./components/Sheet.vue": "./components/overlay/sheet/Sheet.vue",
|
|
87
87
|
"./components/Tooltip.vue": "./components/overlay/tooltip/Tooltip.vue",
|
|
88
88
|
"./composables/useDialog": "./composables/useDialog.ts",
|
|
89
|
+
"./composables/useIndexQuery": "./components/layout/index/useIndexQuery.ts",
|
|
89
90
|
"./composables/useDisclosure": "./composables/useDisclosure.ts",
|
|
90
91
|
"./composables/useMenu": "./composables/useMenu.ts",
|
|
91
92
|
"./composables/usePopover": "./composables/usePopover.ts",
|
|
@@ -98,6 +99,10 @@
|
|
|
98
99
|
"./toast": "./components/feedback/toast/toast.ts",
|
|
99
100
|
"./utils/a11y": "./utils/a11y/index.ts",
|
|
100
101
|
"./components/Index.vue": "./components/layout/index/Index.vue",
|
|
102
|
+
"./components/FilterCheckbox.vue": "./components/layout/index-filters/FilterCheckbox.vue",
|
|
103
|
+
"./components/FilterSelect.vue": "./components/layout/index-filters/FilterSelect.vue",
|
|
104
|
+
"./components/FilterMultiSelect.vue": "./components/layout/index-filters/FilterMultiSelect.vue",
|
|
105
|
+
"./components/FilterRange.vue": "./components/layout/index-filters/FilterRange.vue",
|
|
101
106
|
"./components/RecordForm.vue": "./components/form/record-form/RecordForm.vue",
|
|
102
107
|
"./components/RecordForm.types": "./components/form/record-form/types.ts",
|
|
103
108
|
"./components/RecordDetails.vue": "./components/display/record-details/RecordDetails.vue",
|