@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 CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.19.0 - 2026-09-16
6
+
7
+ ### Added
8
+
9
+ - The English accessibility text the icon-only controls and landmarks used
10
+ to hardcode is now a prop with that text as its default, following
11
+ `AppLayout`'s `skipLabel` / `drawerLabel` / `navigationLabel`, so a
12
+ single-language consumer can name them in its own language. `Pagination`
13
+ takes `label` (the `<nav>` landmark, `Pagination`), `previousLabel`
14
+ (`Previous page`) and `nextLabel` (`Next page`); `Breadcrumb` takes
15
+ `label` (`Breadcrumb`); `NumberField` takes `increaseLabel` (`Increase`)
16
+ and `decreaseLabel` (`Decrease`); `Table` takes `selectAllLabel`
17
+ (`Select all rows`), which `DataTable` forwards alongside its own
18
+ `selectRowLabel`, a string or a function of the row that replaces the
19
+ default `Select <first cell>` name; `Toast` takes `closeLabel` (`Close`),
20
+ and `ToastHost` takes the same `closeLabel` for every toast it renders
21
+ next to its existing `label` for the notifications region.
22
+ `PaginationFooter` has no text of its own and is unchanged.
23
+
24
+ ## 0.18.0 - 2026-09-16
25
+
26
+ ### Changed
27
+
28
+ - `NumberField` takes `steppers` (default `true`); `false` renders a plain
29
+ number input without the +/− column.
30
+
31
+ ### Fixed
32
+
33
+ - `Checkbox`, `RadioGroup` and `Switch` inputs are their declared em size
34
+ again. The base `input, textarea, select` rule padded them by `--space-s`,
35
+ which inflated a 1.15em checkbox to 35px; all three reset `padding` to 0.
36
+
37
+ ### Added
38
+
39
+ - `FilterCheckbox`, `FilterSelect`, `FilterMultiSelect` and `FilterRange`
40
+ (`@pienter/ui/components/Filter*.vue`), the filter controls an index
41
+ definition declares. Each binds to the index that owns the query — the
42
+ state `Index` provides to its `filters` slot, the one a definition provides
43
+ with `provideIndexQuery`, or an explicit `state` prop — and reads and
44
+ writes only its own filter names; mounted without a state it warns once
45
+ and does nothing. The checkbox writes `'true'` or deletes; the select
46
+ prepends an `emptyLabel` option that deletes; the multi-select is a
47
+ fieldset of checkboxes whose legend reads `Label (n)` with a Clear while
48
+ `n > 0`, optional per-option counts, an optional client-side search, and
49
+ values joined on the comma convention; the range is two NumberFields
50
+ without steppers writing `<name>_min` and `<name>_max` on change, each
51
+ cleared on its own.
52
+ `injectIndexQuery()` joins the composable exports for a control that needs
53
+ the provided state without throwing when there is none.
54
+
5
55
  ## 0.17.0 - 2026-09-15
6
56
 
7
57
  ### Added
@@ -15,6 +65,22 @@
15
65
  the allowlist are unchanged. `splitFilterValues('')` is `[]` and
16
66
  `joinFilterValues([])` is `undefined`, so clearing deletes the parameter
17
67
  rather than sending an empty string. See ADR 0009.
68
+ - `useIndexQuery`, `provideIndexQuery` and `useIndexFilters` in
69
+ `@pienter/ui/composables/useIndexQuery`. `useIndexQuery` is the query state
70
+ `Index` has always owned — query, rows, meta, loading, errors, the setters,
71
+ `reload` and `reset` — exported so an index definition can create it, pass
72
+ it to `Index` through the new `state` prop and share it with filter
73
+ controls rendered outside `Index`, such as a page sidebar. `Index` provides
74
+ its state to its subtree, and `useIndexFilters()` gives a control the small
75
+ surface it needs: `filters`, `filterNames`, `loading`, `setFilter`,
76
+ `setFilters`, `clearFilters`. Without `state` nothing changes; `load` and
77
+ `queryOptions` are required unless `state` is passed.
78
+ - `Index` shows its active filters as a chips row between the toolbar and the
79
+ table: one chip per filter value (a comma-separated multi-value filter is
80
+ one chip per value), each with a "Remove <label> <value>" button, and a
81
+ Clear all that empties `filters` only. New `filterLabels` and
82
+ `filterValueLabels` props name the chips; the raw name and value are the
83
+ fallback.
18
84
 
19
85
  ## 0.16.0 - 2026-09-15
20
86
 
package/CONVENTIONS.md CHANGED
@@ -60,6 +60,7 @@ Current consumers:
60
60
  - `components/form/select/` — `Select` + `Segmented` (the segmented control is a tab-like single-select visual; shares form-primitive scaffolding with Select; will be retrofit in Phase 4)
61
61
  - `components/action/button/` — `Button` + `IconButton` (the icon-only control is a square button. `button.css` shares the base/variant/size/state/focus rules across both via a `.pui-btn, .pui-icon-btn` selector group; a `.pui-btn`-only block holds the label, link variant, block layout, and loading fade that must not reach the icon button; a `.pui-icon-btn` rule adds the square box)
62
62
  - `components/feedback/toast/` — `Toast` + `ToastHost` (Toast renders one notification; ToastHost orchestrates the visible family and owns the shared `pui-toast-region` block in `toast.css`)
63
+ - `components/layout/index-filters/` — `FilterCheckbox` + `FilterSelect` + `FilterMultiSelect` + `FilterRange` (the filter controls of an index definition; one binding composable and the fieldset chrome the two grouped controls share in `filters.css`)
63
64
 
64
65
  Rules:
65
66
 
@@ -1253,59 +1254,63 @@ Each component keeps its own audit next to its source, so a decision and the
1253
1254
  code it governs move in the same diff. Add an entry when you add a component;
1254
1255
  follow the shape of an existing one.
1255
1256
 
1256
- | Component | Audit |
1257
- | ------------ | ------------------------------------------------------------------------------------------ |
1258
- | Accordion | [`components/layout/accordion/AUDIT.md`](./components/layout/accordion/AUDIT.md) |
1259
- | Alert | [`components/feedback/alert/AUDIT.md`](./components/feedback/alert/AUDIT.md) |
1260
- | AlertDialog | [`components/overlay/alert-dialog/AUDIT.md`](./components/overlay/alert-dialog/AUDIT.md) |
1261
- | AppLayout | [`components/layout/app-layout/AUDIT.md`](./components/layout/app-layout/AUDIT.md) |
1262
- | Avatar | [`components/display/avatar/AUDIT.md`](./components/display/avatar/AUDIT.md) |
1263
- | AvatarStack | [`components/display/avatar/AUDIT.md`](./components/display/avatar/AUDIT.md) |
1264
- | Badge | [`components/display/badge/AUDIT.md`](./components/display/badge/AUDIT.md) |
1265
- | Breadcrumb | [`components/navigation/breadcrumb/AUDIT.md`](./components/navigation/breadcrumb/AUDIT.md) |
1266
- | Button | [`components/action/button/AUDIT.md`](./components/action/button/AUDIT.md) |
1267
- | Card | [`components/layout/card/AUDIT.md`](./components/layout/card/AUDIT.md) |
1268
- | Checkbox | [`components/form/checkbox/AUDIT.md`](./components/form/checkbox/AUDIT.md) |
1269
- | Collapsible | [`components/layout/collapsible/AUDIT.md`](./components/layout/collapsible/AUDIT.md) |
1270
- | Combobox | [`components/form/combobox/AUDIT.md`](./components/form/combobox/AUDIT.md) |
1271
- | Command | [`components/overlay/command/AUDIT.md`](./components/overlay/command/AUDIT.md) |
1272
- | DataTable | [`components/layout/table/AUDIT.md`](./components/layout/table/AUDIT.md) |
1273
- | DateInput | [`components/form/date-input/AUDIT.md`](./components/form/date-input/AUDIT.md) |
1274
- | DropdownMenu | [`components/overlay/dropdown-menu/AUDIT.md`](./components/overlay/dropdown-menu/AUDIT.md) |
1275
- | Empty | [`components/display/empty/AUDIT.md`](./components/display/empty/AUDIT.md) |
1276
- | Form | [`components/form/form/AUDIT.md`](./components/form/form/AUDIT.md) |
1277
- | Icon | [`components/display/icon/AUDIT.md`](./components/display/icon/AUDIT.md) |
1278
- | IconButton | [`components/action/button/AUDIT.md`](./components/action/button/AUDIT.md) |
1279
- | InputOTP | [`components/form/input-otp/AUDIT.md`](./components/form/input-otp/AUDIT.md) |
1280
- | Label | [`components/form/label/AUDIT.md`](./components/form/label/AUDIT.md) |
1281
- | Modal | [`components/overlay/modal/AUDIT.md`](./components/overlay/modal/AUDIT.md) |
1282
- | Navbar | [`components/navigation/navbar/AUDIT.md`](./components/navigation/navbar/AUDIT.md) |
1283
- | NumberField | [`components/form/number-field/AUDIT.md`](./components/form/number-field/AUDIT.md) |
1284
- | PageAside | [`components/layout/page-aside/AUDIT.md`](./components/layout/page-aside/AUDIT.md) |
1285
- | Pagination | [`components/navigation/pagination/AUDIT.md`](./components/navigation/pagination/AUDIT.md) |
1286
- | Panel | [`components/layout/record-layout/AUDIT.md`](./components/layout/record-layout/AUDIT.md) |
1287
- | Popover | [`components/overlay/popover/AUDIT.md`](./components/overlay/popover/AUDIT.md) |
1288
- | Progress | [`components/feedback/progress/AUDIT.md`](./components/feedback/progress/AUDIT.md) |
1289
- | RadioGroup | [`components/form/radio-group/AUDIT.md`](./components/form/radio-group/AUDIT.md) |
1290
- | Segmented | [`components/form/select/AUDIT.md`](./components/form/select/AUDIT.md) |
1291
- | Select | [`components/form/select/AUDIT.md`](./components/form/select/AUDIT.md) |
1292
- | Separator | [`components/layout/separator/AUDIT.md`](./components/layout/separator/AUDIT.md) |
1293
- | Sheet | [`components/overlay/sheet/AUDIT.md`](./components/overlay/sheet/AUDIT.md) |
1294
- | Sidebar | [`components/navigation/sidebar/AUDIT.md`](./components/navigation/sidebar/AUDIT.md) |
1295
- | Skeleton | [`components/feedback/skeleton/AUDIT.md`](./components/feedback/skeleton/AUDIT.md) |
1296
- | Slider | [`components/form/slider/AUDIT.md`](./components/form/slider/AUDIT.md) |
1297
- | Spinner | [`components/feedback/spinner/AUDIT.md`](./components/feedback/spinner/AUDIT.md) |
1298
- | Switch | [`components/form/switch/AUDIT.md`](./components/form/switch/AUDIT.md) |
1299
- | Table | [`components/layout/table/AUDIT.md`](./components/layout/table/AUDIT.md) |
1300
- | Tabs | [`components/navigation/tabs/AUDIT.md`](./components/navigation/tabs/AUDIT.md) |
1301
- | TagsInput | [`components/form/tags-input/AUDIT.md`](./components/form/tags-input/AUDIT.md) |
1302
- | Textarea | [`components/form/textarea/AUDIT.md`](./components/form/textarea/AUDIT.md) |
1303
- | TextInput | [`components/form/text-input/AUDIT.md`](./components/form/text-input/AUDIT.md) |
1304
- | Toast | [`components/feedback/toast/AUDIT.md`](./components/feedback/toast/AUDIT.md) |
1305
- | ToastHost | [`components/feedback/toast/AUDIT.md`](./components/feedback/toast/AUDIT.md) |
1306
- | Toggle | [`components/action/toggle/AUDIT.md`](./components/action/toggle/AUDIT.md) |
1307
- | ToggleGroup | [`components/action/toggle-group/AUDIT.md`](./components/action/toggle-group/AUDIT.md) |
1308
- | Tooltip | [`components/overlay/tooltip/AUDIT.md`](./components/overlay/tooltip/AUDIT.md) |
1257
+ | Component | Audit |
1258
+ | ----------------- | ------------------------------------------------------------------------------------------ |
1259
+ | Accordion | [`components/layout/accordion/AUDIT.md`](./components/layout/accordion/AUDIT.md) |
1260
+ | Alert | [`components/feedback/alert/AUDIT.md`](./components/feedback/alert/AUDIT.md) |
1261
+ | AlertDialog | [`components/overlay/alert-dialog/AUDIT.md`](./components/overlay/alert-dialog/AUDIT.md) |
1262
+ | AppLayout | [`components/layout/app-layout/AUDIT.md`](./components/layout/app-layout/AUDIT.md) |
1263
+ | Avatar | [`components/display/avatar/AUDIT.md`](./components/display/avatar/AUDIT.md) |
1264
+ | AvatarStack | [`components/display/avatar/AUDIT.md`](./components/display/avatar/AUDIT.md) |
1265
+ | Badge | [`components/display/badge/AUDIT.md`](./components/display/badge/AUDIT.md) |
1266
+ | Breadcrumb | [`components/navigation/breadcrumb/AUDIT.md`](./components/navigation/breadcrumb/AUDIT.md) |
1267
+ | Button | [`components/action/button/AUDIT.md`](./components/action/button/AUDIT.md) |
1268
+ | Card | [`components/layout/card/AUDIT.md`](./components/layout/card/AUDIT.md) |
1269
+ | Checkbox | [`components/form/checkbox/AUDIT.md`](./components/form/checkbox/AUDIT.md) |
1270
+ | Collapsible | [`components/layout/collapsible/AUDIT.md`](./components/layout/collapsible/AUDIT.md) |
1271
+ | Combobox | [`components/form/combobox/AUDIT.md`](./components/form/combobox/AUDIT.md) |
1272
+ | Command | [`components/overlay/command/AUDIT.md`](./components/overlay/command/AUDIT.md) |
1273
+ | DataTable | [`components/layout/table/AUDIT.md`](./components/layout/table/AUDIT.md) |
1274
+ | DateInput | [`components/form/date-input/AUDIT.md`](./components/form/date-input/AUDIT.md) |
1275
+ | DropdownMenu | [`components/overlay/dropdown-menu/AUDIT.md`](./components/overlay/dropdown-menu/AUDIT.md) |
1276
+ | Empty | [`components/display/empty/AUDIT.md`](./components/display/empty/AUDIT.md) |
1277
+ | FilterCheckbox | [`components/layout/index-filters/AUDIT.md`](./components/layout/index-filters/AUDIT.md) |
1278
+ | FilterMultiSelect | [`components/layout/index-filters/AUDIT.md`](./components/layout/index-filters/AUDIT.md) |
1279
+ | FilterRange | [`components/layout/index-filters/AUDIT.md`](./components/layout/index-filters/AUDIT.md) |
1280
+ | FilterSelect | [`components/layout/index-filters/AUDIT.md`](./components/layout/index-filters/AUDIT.md) |
1281
+ | Form | [`components/form/form/AUDIT.md`](./components/form/form/AUDIT.md) |
1282
+ | Icon | [`components/display/icon/AUDIT.md`](./components/display/icon/AUDIT.md) |
1283
+ | IconButton | [`components/action/button/AUDIT.md`](./components/action/button/AUDIT.md) |
1284
+ | InputOTP | [`components/form/input-otp/AUDIT.md`](./components/form/input-otp/AUDIT.md) |
1285
+ | Label | [`components/form/label/AUDIT.md`](./components/form/label/AUDIT.md) |
1286
+ | Modal | [`components/overlay/modal/AUDIT.md`](./components/overlay/modal/AUDIT.md) |
1287
+ | Navbar | [`components/navigation/navbar/AUDIT.md`](./components/navigation/navbar/AUDIT.md) |
1288
+ | NumberField | [`components/form/number-field/AUDIT.md`](./components/form/number-field/AUDIT.md) |
1289
+ | PageAside | [`components/layout/page-aside/AUDIT.md`](./components/layout/page-aside/AUDIT.md) |
1290
+ | Pagination | [`components/navigation/pagination/AUDIT.md`](./components/navigation/pagination/AUDIT.md) |
1291
+ | Panel | [`components/layout/record-layout/AUDIT.md`](./components/layout/record-layout/AUDIT.md) |
1292
+ | Popover | [`components/overlay/popover/AUDIT.md`](./components/overlay/popover/AUDIT.md) |
1293
+ | Progress | [`components/feedback/progress/AUDIT.md`](./components/feedback/progress/AUDIT.md) |
1294
+ | RadioGroup | [`components/form/radio-group/AUDIT.md`](./components/form/radio-group/AUDIT.md) |
1295
+ | Segmented | [`components/form/select/AUDIT.md`](./components/form/select/AUDIT.md) |
1296
+ | Select | [`components/form/select/AUDIT.md`](./components/form/select/AUDIT.md) |
1297
+ | Separator | [`components/layout/separator/AUDIT.md`](./components/layout/separator/AUDIT.md) |
1298
+ | Sheet | [`components/overlay/sheet/AUDIT.md`](./components/overlay/sheet/AUDIT.md) |
1299
+ | Sidebar | [`components/navigation/sidebar/AUDIT.md`](./components/navigation/sidebar/AUDIT.md) |
1300
+ | Skeleton | [`components/feedback/skeleton/AUDIT.md`](./components/feedback/skeleton/AUDIT.md) |
1301
+ | Slider | [`components/form/slider/AUDIT.md`](./components/form/slider/AUDIT.md) |
1302
+ | Spinner | [`components/feedback/spinner/AUDIT.md`](./components/feedback/spinner/AUDIT.md) |
1303
+ | Switch | [`components/form/switch/AUDIT.md`](./components/form/switch/AUDIT.md) |
1304
+ | Table | [`components/layout/table/AUDIT.md`](./components/layout/table/AUDIT.md) |
1305
+ | Tabs | [`components/navigation/tabs/AUDIT.md`](./components/navigation/tabs/AUDIT.md) |
1306
+ | TagsInput | [`components/form/tags-input/AUDIT.md`](./components/form/tags-input/AUDIT.md) |
1307
+ | Textarea | [`components/form/textarea/AUDIT.md`](./components/form/textarea/AUDIT.md) |
1308
+ | TextInput | [`components/form/text-input/AUDIT.md`](./components/form/text-input/AUDIT.md) |
1309
+ | Toast | [`components/feedback/toast/AUDIT.md`](./components/feedback/toast/AUDIT.md) |
1310
+ | ToastHost | [`components/feedback/toast/AUDIT.md`](./components/feedback/toast/AUDIT.md) |
1311
+ | Toggle | [`components/action/toggle/AUDIT.md`](./components/action/toggle/AUDIT.md) |
1312
+ | ToggleGroup | [`components/action/toggle-group/AUDIT.md`](./components/action/toggle-group/AUDIT.md) |
1313
+ | Tooltip | [`components/overlay/tooltip/AUDIT.md`](./components/overlay/tooltip/AUDIT.md) |
1309
1314
 
1310
1315
  ## Form submit response contract
1311
1316
 
@@ -1470,6 +1475,37 @@ value is a key prefix: `sync-query="contacts"` reads and writes `contacts.page`,
1470
1475
  `contacts.search`, `contacts.filter[...]` and so on, so a related index shares
1471
1476
  its parent page's URL without touching the page's own parameters.
1472
1477
 
1478
+ That query state is a composable, `useIndexQuery({ load, queryOptions,
1479
+ syncQuery, searchDebounce })` from `@pienter/ui/composables/useIndexQuery`.
1480
+ Index calls it when given `load` and `queryOptions`; an index definition
1481
+ whose filter controls sit outside Index's subtree (a page sidebar) calls it
1482
+ itself, passes the result as `state`, and shares it with
1483
+ `provideIndexQuery(state)`. A control reads `useIndexFilters()` — `filters`,
1484
+ `filterNames`, `loading`, `setFilter`, `setFilters`, `clearFilters` — from
1485
+ the injected state, so the same control works in the `filters` slot, where
1486
+ Index provides its own state, or anywhere the definition provides one. The
1487
+ `filters` slot stays for the toolbar. Index shows the active filters as a
1488
+ chips row above the table: one chip per value, split on the comma convention
1489
+ (ADR 0009), each with a remove button, plus Clear all, which empties
1490
+ `filters` and nothing else. `filterLabels` and `filterValueLabels` name the
1491
+ chips the way `sortLabels` names the sort select.
1492
+
1493
+ The controls a definition declares are `FilterCheckbox` (`name`, `label`;
1494
+ `'true'` or deleted), `FilterSelect` (`name`, `label`, `options`, an
1495
+ `emptyLabel` option that deletes), `FilterMultiSelect` (`name`, `label`,
1496
+ `options` with an optional `count`, `searchable`; a fieldset whose legend
1497
+ counts the selected values, with a Clear, joining values on the comma
1498
+ convention) and `FilterRange` (`name`, `label`, `min`, `max`, `step`; two
1499
+ NumberFields writing `<name>_min` and `<name>_max` on change). Each binds
1500
+ through `useIndexFilters` to the provided state, or to a `state` prop, and
1501
+ touches only its own names; without a state it warns and does nothing. On a
1502
+ module index they live in the `PageAside`: the definition renders them
1503
+ inside `<PageAside title="Filters">` beside `<Index :state>`, both under the
1504
+ component that created and provided the state, and the aside's content grid
1505
+ stacks them. The `filters` slot stays for a toolbar placement; elsewhere a
1506
+ column of them is a consumer grid with a spacing token for its gap. Options
1507
+ and counts are the consumer's (`CONTEXT.md`: Facet).
1508
+
1473
1509
  `placement` selects the chrome for the two CMS placements (`CONTEXT.md`, ADR
1474
1510
  0008), exposed as `data-placement` on `.pui-index`. `module` (default) is the
1475
1511
  page itself: an `h1` header above a divider, an unboxed toolbar, the table with
@@ -19,7 +19,7 @@
19
19
  v-if="dismissible"
20
20
  class="pui-toast__close"
21
21
  type="button"
22
- aria-label="Close"
22
+ :aria-label="closeLabel"
23
23
  @click="$emit('dismiss')"
24
24
  >
25
25
  <Icon name="x" size="sm" />
@@ -48,6 +48,8 @@ const props = withDefaults(
48
48
  dismissible?: boolean;
49
49
  icon?: IconName;
50
50
  action?: { label: string };
51
+ /** Accessible name of the icon-only close button. */
52
+ closeLabel?: string;
51
53
  }>(),
52
54
  {
53
55
  tone: undefined,
@@ -55,6 +57,7 @@ const props = withDefaults(
55
57
  dismissible: true,
56
58
  icon: undefined,
57
59
  action: undefined,
60
+ closeLabel: 'Close',
58
61
  },
59
62
  );
60
63
 
@@ -17,11 +17,15 @@ interface Timer {
17
17
  const props = withDefaults(
18
18
  defineProps<{
19
19
  controller?: ToastController;
20
+ /** Accessible name of the notifications region. */
20
21
  label?: string;
22
+ /** Accessible name of every toast's close button. */
23
+ closeLabel?: string;
21
24
  }>(),
22
25
  {
23
26
  controller: () => defaultController,
24
27
  label: 'Notifications',
28
+ closeLabel: 'Close',
25
29
  },
26
30
  );
27
31
  const items = computed(() => props.controller.visible.value);
@@ -151,6 +155,7 @@ onBeforeUnmount(() => {
151
155
  :dismissible="item.dismissible"
152
156
  :icon="item.icon"
153
157
  :action="item.action"
158
+ :close-label="closeLabel"
154
159
  aria-keyshortcuts="Escape"
155
160
  @dismiss="dismiss(item.id)"
156
161
  @action="runAction(item)"
@@ -3,6 +3,7 @@
3
3
  appearance: none;
4
4
  flex: none;
5
5
  margin: 0;
6
+ padding: 0;
6
7
  width: 1.15em;
7
8
  height: 1.15em;
8
9
  border: var(--stroke-md) solid var(--border-clr-strong);
@@ -60,18 +61,18 @@
60
61
  }
61
62
 
62
63
  /* The label is the click target for the box, so it reads as one. */
63
- .pui-field:has(.pui-checkbox) .pui-field__label {
64
+ .pui-field:has(> .pui-checkbox) > .pui-field__label {
64
65
  cursor: pointer;
65
66
  user-select: none;
66
67
  }
67
68
 
68
69
  /* Mute the label + cursor when the input is disabled. */
69
- .pui-field:has(.pui-checkbox:disabled) .pui-field__label {
70
+ .pui-field:has(> .pui-checkbox:disabled) > .pui-field__label {
70
71
  color: var(--text-clr-muted);
71
72
  cursor: not-allowed;
72
73
  }
73
74
 
74
- .pui-field[data-readonly='true']:has(.pui-checkbox) .pui-field__label {
75
+ .pui-field[data-readonly='true']:has(> .pui-checkbox) > .pui-field__label {
75
76
  cursor: default;
76
77
  }
77
78
  }
@@ -35,12 +35,15 @@
35
35
  @input="onInput"
36
36
  @blur="emit('blur')"
37
37
  />
38
- <div v-if="!readonly" class="pui-number-field__steppers">
38
+ <div
39
+ v-if="!readonly && steppers"
40
+ class="pui-number-field__steppers"
41
+ >
39
42
  <button
40
43
  type="button"
41
44
  class="pui-number-field__step"
42
45
  data-direction="increment"
43
- aria-label="Increase"
46
+ :aria-label="increaseLabel"
44
47
  :disabled="disabled"
45
48
  @click="handleStep('increment')"
46
49
  >
@@ -50,7 +53,7 @@
50
53
  type="button"
51
54
  class="pui-number-field__step"
52
55
  data-direction="decrement"
53
- aria-label="Decrease"
56
+ :aria-label="decreaseLabel"
54
57
  :disabled="disabled"
55
58
  @click="handleStep('decrement')"
56
59
  >
@@ -95,6 +98,12 @@ const props = withDefaults(
95
98
  disabled?: boolean;
96
99
  /** Read-only: the value shows in the field chrome without steppers, stays focusable and cannot be edited. */
97
100
  readonly?: boolean;
101
+ /** The +/− buttons beside the input; off, the field is a plain number input. */
102
+ steppers?: boolean;
103
+ /** Accessible name of the + stepper. */
104
+ increaseLabel?: string;
105
+ /** Accessible name of the − stepper. */
106
+ decreaseLabel?: string;
98
107
  hint?: string;
99
108
  errors?: string[];
100
109
  status?: 'error' | 'success';
@@ -112,6 +121,9 @@ const props = withDefaults(
112
121
  required: false,
113
122
  disabled: false,
114
123
  readonly: false,
124
+ steppers: true,
125
+ increaseLabel: 'Increase',
126
+ decreaseLabel: 'Decrease',
115
127
  hint: undefined,
116
128
  errors: () => [],
117
129
  status: undefined,
@@ -42,8 +42,9 @@
42
42
  margin: 0;
43
43
  }
44
44
 
45
- /* Read-only renders no stepper column, so the input keeps its own corners. */
46
- .pui-field[data-readonly='true'] .pui-number-field__input {
45
+ /* Without a stepper column the input keeps its own corners. */
46
+ .pui-number-field:not(:has(.pui-number-field__steppers))
47
+ .pui-number-field__input {
47
48
  border-top-right-radius: var(--radius-sm);
48
49
  border-bottom-right-radius: var(--radius-sm);
49
50
  }
@@ -66,6 +66,7 @@
66
66
  appearance: none;
67
67
  flex: none;
68
68
  margin: 0;
69
+ padding: 0;
69
70
  width: 1.15em;
70
71
  height: 1.15em;
71
72
  border: var(--stroke-md) solid var(--border-clr-strong);
@@ -3,6 +3,7 @@
3
3
  appearance: none;
4
4
  flex: none;
5
5
  margin: 0;
6
+ padding: 0;
6
7
  width: 2.4em;
7
8
  height: 1.4em;
8
9
  background: var(--border-clr-base);