@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.
@@ -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="Select all rows"
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="Breadcrumb" class="pui-breadcrumb">
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
- defineProps<{
40
- items: { label: string; href?: string }[];
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="Pagination">
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="Previous page"
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="Next page"
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.17.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",