@uni-design-system/uni-angular 9.0.1 → 10.1.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
@@ -1,5 +1,548 @@
1
1
  # @uni-design-system/uni-angular
2
2
 
3
+ ## 10.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`f28c951`](https://github.com/uni-design-system/uni/commit/f28c95117f2844b58a265b59c2ea8b9715156670) Thanks [@gaenglish](https://github.com/gaenglish)! - `uni-drawer` gains the API an editor panel needs, and a close it can refuse.
8
+
9
+ **Closing can now be vetoed.** Escape, the backdrop, the header's close button
10
+ and the footer's cancel all funnel through one decision that emits
11
+ `closeRequest` with a `reason` — `'escape' | 'backdrop' | 'close-button'`. Set
12
+ `disableAutoClose` and the drawer stops acting on its own: it asks, and waits
13
+ for you to set `open`.
14
+
15
+ That split exists because the confirmation it has to accommodate is
16
+ _asynchronous_. A synchronous veto — a preventable event — cannot express
17
+ "ask the user, then decide", so every consumer would prevent unconditionally
18
+ and close manually anyway. Leave `disableAutoClose` off and behaviour is
19
+ unchanged, so adding a listener alone breaks nothing.
20
+
21
+ ```html
22
+ <uni-drawer [(open)]="open" [disableAutoClose]="form.dirty()" (closeRequest)="confirmDiscard()" />
23
+ ```
24
+
25
+ New inputs: `width` (per-instance override of the theme's width — a nav drawer
26
+ is 280 and an editor panel 480, and both live in one app), `headline` and
27
+ `defaultCloseButton` for the header row, and `initialFocus`, a selector resolved
28
+ inside the panel when it opens.
29
+
30
+ **`ariaLabel` no longer defaults to `'Navigation'`.** A drawer with a header is
31
+ labelled by that header via `aria-labelledby`; without one, `ariaLabel` is used;
32
+ with neither, the drawer is unnamed. The old default meant every drawer that
33
+ wasn't a nav drawer announced itself as one, and a wrong accessible name is
34
+ worse than a missing one — the missing one is at least caught by an audit. If
35
+ you relied on it, set `ariaLabel="Navigation"` explicitly.
36
+
37
+ **`scrim` turns the dimming off** without changing the modality. As a `scrim`
38
+ input or a `drawer.behavior.scrim` theme option, false leaves `::backdrop`
39
+ transparent so the page behind stays legible — an editor panel beside a board
40
+ the user is still reading. Focus is still trapped and the page behind is still
41
+ inert: it is a visibility choice, not a modality one. `background` joins it,
42
+ selecting `solid`, `glass` or `gradient` as a token choice rather than per-app
43
+ CSS.
44
+
45
+ **Fixed: a closed overlay drawer rendered in normal flow behind the page.** The
46
+ panel's `display: flex` outranks the UA stylesheet's
47
+ `dialog:not([open]) { display: none }`, so the drawer was visible on first
48
+ paint and reappeared behind the content after every close. An explicit
49
+ `&:not([open])` rule restores it. The closing animation is unaffected — `open`
50
+ is only removed once it ends.
51
+
52
+ - [`f28c951`](https://github.com/uni-design-system/uni/commit/f28c95117f2844b58a265b59c2ea8b9715156670) Thanks [@gaenglish](https://github.com/gaenglish)! - `uni-drawer` is a three-row panel, and is no longer its own scroll container.
53
+
54
+ **The `<dialog>` used to be the scroller.** `over` mode set `overflowY: 'auto'`
55
+ on the panel and put the theme's padding there too, which made a pinned header
56
+ or footer impossible: padding on a scrolling box scrolls away with its content,
57
+ and any row you pinned against it could not sit flush to the panel edge. It also
58
+ set only the one axis — and a single explicit overflow axis computes the other
59
+ to `auto`, which is exactly how a container becomes an accidental scroller.
60
+ `side` had it right already, setting both.
61
+
62
+ The panel is now a flex column of three rows — an optional
63
+ `[uni-drawer-header]`, the projected body, an optional `[uni-drawer-buttons]`
64
+ (alias `[drawer-buttons]`) — and **only the body scrolls**. The panel itself is
65
+ `overflow: clip` on both axes, explicitly, never the shorthand. The body carries
66
+ `overscroll-behavior: contain`, so scrolling to its end does not start scrolling
67
+ the page behind it, and `position: relative`, so a stray absolutely positioned
68
+ descendant is contained rather than re-homed into an ancestor.
69
+
70
+ Those two must travel together: a positioned body _without_ a clipped shell is
71
+ worse than the status quo, because it pulls phantom overflow into the scroller
72
+ instead of out of the panel.
73
+
74
+ **`drawer.behavior.padding` is now the body's padding, not the panel's.** A
75
+ drawer with no header or footer looks the same as before. One that gains either
76
+ gets rows flush to the panel edge, which is the point.
77
+
78
+ Two new theme entries, `drawerHeader` and `drawerButtons`, mirror the dialog
79
+ pair knob for knob but default to a panel's posture rather than a dialog's: the
80
+ header's title is left-aligned rather than centered, and the footer trails its
81
+ actions rather than centering them.
82
+
83
+ Note one deliberate divergence from `[dialog-buttons]`: the drawer footer's
84
+ **confirm button does not close the drawer**. A panel's save is usually async
85
+ and can fail, so closing is left to the consumer via `(confirmed)`.
86
+
87
+ - [`67e17ea`](https://github.com/uni-design-system/uni/commit/67e17ea927e3ca24b7abecdcccf134d53ac3656b) Thanks [@gaenglish](https://github.com/gaenglish)! - `uni-toggle` honours `size`, and the checked colour has a theme home.
88
+
89
+ **`size` did nothing.** `uni-toggle` inherits a bindable `size` input from
90
+ `BaseComponent`, but its geometry came from a single `toggle.behavior.size`
91
+ number, so `<uni-toggle size="sm">` compiled, read as deliberate, and rendered
92
+ identically to every other switch. The theme now carries a `sizes` block and the
93
+ input selects from it.
94
+
95
+ **Geometry is stated per size, not derived from ratios.** Each size token is a
96
+ `width` / `height` / `padding` triple — `padding` being the knob's inset — and
97
+ the rest falls out: knob is `height - 2 * padding`, travel is `width - height`,
98
+ radius is `height / 2`. Real switch designs do not hold a constant proportion
99
+ across sizes (a consumer's 32x18 and 28x16 pair differs in both track and knob
100
+ ratio), so a single ratio token could match one size or the other but never
101
+ both.
102
+
103
+ `lg` is `BaseComponent`'s default and reproduces the previous geometry exactly —
104
+ 40x20, knob 16 — so **no existing toggle moves**. A theme still setting the
105
+ legacy `toggle.behavior.size` number keeps the old derived-ratio behaviour; that
106
+ option is now deprecated in favour of the `sizes` block.
107
+
108
+ **Fixed: the knob would have overhung the track at any other proportion.** The
109
+ checked transform was hardcoded to translate by one track height, which is only
110
+ correct while the track is `2x` wide and the knob `0.8x`. It is now derived, so
111
+ the knob lands the same distance from each edge at every size.
112
+
113
+ **New `checkedColor`, as an input and a theme option.** The on-state used to be
114
+ the instance's `variant`, which meant an app wanting one switch colour repeated
115
+ an attribute on every call site. `toggle.behavior.checkedColor` sets it once;
116
+ the input overrides per instance; `variant` remains the fallback when neither is
117
+ set. The focus ring follows the resolved colour rather than staying on `variant`.
118
+ An input as well as an option is necessary because `variant` defaults to
119
+ `'primary'` and so cannot be distinguished from unset — without one, a themed
120
+ `checkedColor` would have silently made `variant` inert with no way back.
121
+
122
+ **Toggle transitions are a motion token.** It hardcoded `0.3s ease` while
123
+ `uni-radio` already read `theme.motion(options.motion ?? 'control')`, so a theme
124
+ setting a motion scale moved the radio and not the switch.
125
+
126
+ ### Patch Changes
127
+
128
+ - [`f28c951`](https://github.com/uni-design-system/uni/commit/f28c95117f2844b58a265b59c2ea8b9715156670) Thanks [@gaenglish](https://github.com/gaenglish)! - Document the drawer as an editor panel, and assert the layout that makes it one.
129
+
130
+ Both existing Drawer stories were navigation shells, so the shape that actually
131
+ stresses the component — a pinned header, a long scrolling form, a pinned save
132
+ bar — had no worked example. `EditorPanel` is that example, with a
133
+ `uni-number-input` and a `uni-quantity-stepper` near the bottom of the scroll,
134
+ where the sr-only overflow bug used to surface.
135
+
136
+ It carries a play function that asserts the scroll geometry: the panel's
137
+ `scrollHeight` equals its `clientHeight`, setting `scrollTop` on it does
138
+ nothing, the footer has no scrollable content of its own, the body scrolls to
139
+ its last element and stops, and no descendant has escaped its scroll container
140
+ to land on the panel. Those assertions live in the story rather than the unit
141
+ spec on purpose — jsdom has no layout engine, so every one of them would pass
142
+ vacuously there.
143
+
144
+ The MDX gains the three-row layout, the close-request contract, an input table,
145
+ and theme-option blocks for the two new component entries.
146
+
147
+ - [`8f85b48`](https://github.com/uni-design-system/uni/commit/8f85b48b06f67e7f44ce29149dd32aae856e2f19) Thanks [@gaenglish](https://github.com/gaenglish)! - New **Experiments → Forms layout pressure** docs page: every form control in an
148
+ `auto 1fr` grid, with a bar showing what it left for its sibling.
149
+
150
+ It exists to make one class of bug visible, because nothing else can see it. A
151
+ `1fr` track is `minmax(auto, 1fr)`, and that `auto` floor is the control's own
152
+ min-content size — so a control reporting a larger intrinsic width than it needs
153
+ quietly steals track width from whatever sits beside it, while still looking
154
+ correct in isolation. That shipped once: `uni-quantity-stepper`'s value cell is a
155
+ native `<input>` defaulting to `size="20"`, so it measured ~230px instead of
156
+ ~92px and collapsed a consumer's grid column.
157
+
158
+ Neither the specs nor `build-storybook` catch it — the specs assert computed
159
+ styles and ARIA, and jsdom does not do layout — so it took a consumer report.
160
+ The page is deliberately width-constrained, since on a wide canvas nothing
161
+ competes for the track and the defect cannot appear. The measurements are live
162
+ and update as the viewport changes.
163
+
164
+ - [`67e17ea`](https://github.com/uni-design-system/uni/commit/67e17ea927e3ca24b7abecdcccf134d53ac3656b) Thanks [@gaenglish](https://github.com/gaenglish)! - Signal Forms docs said `[field]`; the directive is `[formField]`.
165
+
166
+ `[field]` was the selector in the Angular 21.0 Signal Forms preview and was
167
+ renamed before release. In 21.2 the directive is `FormField`, selector
168
+ `[formField]`, with its required input aliased to `formField` — `[field]` does
169
+ not exist at all. Every form control in this library was documented with the
170
+ name that had been removed, across seven component doc comments and five MDX
171
+ files, and all of it flowed into the MCP index and the generated API reference.
172
+ A consuming app found this, not us.
173
+
174
+ Nothing about the components changed: they already satisfy `FormValueControl` /
175
+ `FormCheckboxControl` and always bound correctly.
176
+
177
+ **The reason it rotted is that nothing compiled a binding.** No spec or story in
178
+ the library imported `@angular/forms/signals` — the toggle's "Form Signals" story
179
+ hand-bound `[checked]` and `[touched]` as plain props, which demonstrates
180
+ nothing about Signal Forms and would keep passing through any rename. There is
181
+ now a spec that binds a real `form()` to `uni-toggle` through `[formField]` and
182
+ asserts the round trip in both directions, plus `touched` and `required`
183
+ propagation, so the next rename fails CI instead of the docs. The story binds a
184
+ real form too.
185
+
186
+ - [`f28c951`](https://github.com/uni-design-system/uni/commit/f28c95117f2844b58a265b59c2ea8b9715156670) Thanks [@gaenglish](https://github.com/gaenglish)! - Visually hidden text no longer inflates a consuming app's scroll containers.
187
+
188
+ **Eighteen controls quietly added scrollable distance to whatever box happened
189
+ to be above them.** `visuallyHidden` was `position: absolute`, and the controls
190
+ that emit it — `uni-number-input`, `uni-quantity-stepper`, the toggle's hidden
191
+ `<input>`, and fifteen others — are `position: static`. An absolutely positioned
192
+ box resolves its containing block to the nearest _positioned_ ancestor, so each
193
+ 1x1 span skipped every `overflow: auto` between it and that ancestor and landed
194
+ in the distant ancestor's scrollable overflow. A consumer reported a fixed side
195
+ panel measuring `scrollHeight: 1891` against `clientHeight: 793` — the whole
196
+ 1098px difference came from seven invisible spans that had escaped the panel's
197
+ body scroller.
198
+
199
+ The helper is now `position: fixed`, whose containing block is the viewport, so
200
+ it joins no ancestor's scrollable overflow at all. The element stays 1x1 and
201
+ clipped to nothing, so screen reader behaviour is unchanged. Inside a
202
+ `transform`ed ancestor a fixed box re-anchors to that ancestor, which is
203
+ harmless here: where the box lands never mattered, only what it overflowed.
204
+
205
+ This class of bug is invisible in isolation — it needs a consumer to nest the
206
+ control inside a scrolling shell before it appears — so it is now covered by a
207
+ test that renders the emitting controls and asserts what actually reaches the
208
+ DOM, not just the recipe.
209
+
210
+ - [`7545ff3`](https://github.com/uni-design-system/uni/commit/7545ff3e6dd85d29157963488f75f9e3681947c7) Thanks [@gaenglish](https://github.com/gaenglish)! - Stepper buttons now leave focus in their field, and `uni-quantity-stepper`
211
+ follows the theme's field chrome.
212
+
213
+ **Clicking `+` or `−` focused nothing.** Taking pointer capture means calling
214
+ `preventDefault()` on `pointerdown`, which also suppresses the browser's default
215
+ focus handling — and the buttons carry `tabindex="-1"`, so focus landed on
216
+ `<body>`. The arrow keys then did nothing, exactly when a user reaching for `+`
217
+ is most likely to try them. `createPressRepeat` gained a `focus` callback,
218
+ invoked on press and handed the pressed button; `uni-number-input` and
219
+ `uni-quantity-stepper` point it at their text field, the way a native spinner
220
+ does. Where there is no field — a read-only quantity stepper, whose buttons are
221
+ themselves the tab stops — focus goes to the button instead. **This affected
222
+ `uni-number-input` as well**, not just the stepper.
223
+
224
+ **`uni-quantity-stepper` ignored a theme's field styling.** It carried its own
225
+ `color` / `border` / `borderRadius` tokens, so a theme that restyles `input` —
226
+ Wellsourced fills its fields `#F3F2EF` — left the stepper stark white beside
227
+ them. Those three now default to the shared `input` chrome and are unset in the
228
+ base theme, so the stepper tracks whatever a theme does to its fields; they
229
+ remain available as per-component overrides for parting them deliberately. With
230
+ the focus indicator and the dividers already sourced this way, the container is
231
+ now consistently the field chrome unless a theme says otherwise.
232
+
233
+ - [`8f85b48`](https://github.com/uni-design-system/uni/commit/8f85b48b06f67e7f44ce29149dd32aae856e2f19) Thanks [@gaenglish](https://github.com/gaenglish)! - `uni-quantity-stepper` no longer claims ~230px of width it does not need.
234
+
235
+ The value cell is a native `<input>`, which defaults to `size="20"`. Its
236
+ `flex: 1 1 auto` meant `flex-basis` resolved to that intrinsic ~20-character
237
+ width rather than to `valueWidth`, so the control measured ~230px instead of the
238
+ ~92px its buttons and a 3ch value actually need. Worse, that inflated width is
239
+ the control's `auto` size, and a `1fr` grid track is `minmax(auto, 1fr)` — so a
240
+ stepper stole track width from whatever sat beside it in a grid. Reported by
241
+ Wellsourced, who worked around it with `uni-quantity-stepper input { width: 0 }`.
242
+
243
+ The input is now sized from its content (`size` bound to the rendered value's
244
+ length), so the intrinsic width tells the truth. `valueWidth` stays the floor via
245
+ `min-width`, which is what keeps stepping 9 → 10 from reflowing the row, and the
246
+ cell still grows past it with the digits — measured at 92px for one to three
247
+ digits, 116px at `12,000`, 140px at `1,234,567`.
248
+
249
+ This is deliberately not the `width`-instead-of-`min-width` fix that was also
250
+ suggested: a fixed cell would have pinned the value at `valueWidth` and clipped
251
+ longer numbers, losing the growth the option documents.
252
+
253
+ **For consumers carrying the workaround:** it is safe to leave in place — the
254
+ control measures the same 92px either way — but remove it to get the growth
255
+ back, since `width: 0` forces `flex-basis: 0` and pins the cell at the floor.
256
+
257
+ ## 10.0.0
258
+
259
+ ### Major Changes
260
+
261
+ - [`21b655d`](https://github.com/uni-design-system/uni/commit/21b655df0f93e2e2de6a22ccf38050b474d4e5ab) Thanks [@gaenglish](https://github.com/gaenglish)! - `uni-slider` is rebuilt on custom thumbs instead of `<input type="range">`,
262
+ gaining range mode, marks, a value readout and an exact decimal step model. This
263
+ is a breaking change to both the component's value shape and its theme options.
264
+
265
+ **Why it could not stay native.** One `<input type="range">` cannot carry two
266
+ thumbs, so range mode, thumb crossing and per-thumb ARIA bounds were all
267
+ unreachable; marks and a value tooltip were unstyleable through it. The
268
+ alternative — a second component for the range case — would have meant two
269
+ keyboard implementations to keep in step, which is the drift the shared step
270
+ model exists to prevent. What the platform was giving us (the slider ARIA
271
+ pattern and the keyboard map) is reimplemented explicitly and covered by 29
272
+ specs.
273
+
274
+ **Breaking: value shape.**
275
+
276
+ ```ts
277
+ // before
278
+ value = model<number>(0);
279
+ // after — the shape follows `mode`, and `null` is empty
280
+ value = model<number | UniNumberRange | null>(null);
281
+ ```
282
+
283
+ A `single` slider still reads and writes a plain number, so
284
+ `[(value)]="volume"` is unchanged. Code that relied on `value()` being
285
+ non-nullable, or that read it without narrowing, now needs to handle `null` and
286
+ the `{ start, end }` range.
287
+
288
+ **Breaking: theme options.** `slider.options.color` is **removed**. Fill and
289
+ thumb colour now come from the `variant` role pair, the rule every other
290
+ component follows, so `variant="warn"` recolours a slider with no theme edit. A
291
+ theme that set `color` should delete it and pass `variant` at the call site.
292
+ `trackColor` now defaults to `primary-container` rather than `surface-variant`.
293
+
294
+ New options: `thumbBorderRadius`, `minTouchTarget`, `markSize`, `markColor`,
295
+ `labelTypeface`, `labelColor`, `tooltipColor`, `tooltipTextColor`,
296
+ `tooltipShadow`, `tooltipBorderRadius`, `transitionMs`. `trackHeight`,
297
+ `thumbSize` and `borderRadius` are unchanged.
298
+
299
+ **New capability.**
300
+ - `mode="range"` — two thumbs, a `{ start, end }` value, `minGap` to fence the
301
+ ends apart. Thumbs may cross and swap, and the dragged one keeps focus.
302
+ - `marks` and `snapToMarks` — labelled stops, spoken in place of the number.
303
+ - `valueDisplay` — `none`, `inline`, `tooltip`, or `input`, which seats a
304
+ compact `uni-number-input` at the trailing edge, two-way bound to the same
305
+ value: drag for the ballpark, type for the exact figure. Single mode only.
306
+ - `origin` — anchor the fill somewhere other than `min`, for sliders spanning ±.
307
+ - `sliding` and `changed` outputs, replacing an implicit per-frame model write.
308
+ **Bind `changed` in forms**; `sliding` fires every frame of a drag.
309
+ - `largeStep`, defaulting to a tenth of the range, for `PageUp`/`PageDown` and
310
+ `Shift+Arrow`.
311
+ - Right-to-left support: the horizontal arrows and the track mirror; the value
312
+ does not.
313
+
314
+ **Also new: the `cdk/number` primitives** this is built on, shared with the
315
+ numeric input family still to come — exact scaled-`BigInt` decimal arithmetic
316
+ (`stepDecimal`, `roundDecimal`, `clampDecimal`), locale-aware
317
+ `parseNumber`/`formatNumber` over `Intl`, and `createPressRepeat` for
318
+ hold-to-repeat stepper buttons. Stepping `0.1` twenty times from `0` now lands
319
+ on exactly `2`, and `1.15` rounds to `1.2` where `(1.15).toFixed(1)` gives
320
+ `'1.1'`.
321
+
322
+ ### Minor Changes
323
+
324
+ - [`2a79bb8`](https://github.com/uni-design-system/uni/commit/2a79bb8ebd8fd0b5cd792697972b6f48b444c80f) Thanks [@gaenglish](https://github.com/gaenglish)! - New `uni-number-input`: the field for a quantity, a price, a percentage or a
325
+ measurement, with locale-aware parsing, `Intl` formatting on commit,
326
+ prefix/suffix adornments, min/max/step fences and steppers that hold to repeat.
327
+
328
+ ```html
329
+ <uni-number-input label="Quantity" [(value)]="qty" [min]="1" />
330
+ <uni-number-input label="Unit price" currency="USD" [(value)]="price" />
331
+ ```
332
+
333
+ **Why not `<input type="number">`.** Per the HTML value sanitization algorithm,
334
+ a number input whose text is not a valid floating-point number reports
335
+ `value === ''`. Type `12,50` as most of Europe does, or paste `1,234.56` from a
336
+ spreadsheet, and the app reads an empty field with no way to tell that from a
337
+ blank one — a data-loss bug, and the reason this is `type="text"` with
338
+ `role="spinbutton"`. The platform control also cannot group thousands, cannot
339
+ place an affix outside the editable text, has ~10px unstyleable spinners below
340
+ the WCAG 2.2 target minimum, changes value on the scroll wheel while focused,
341
+ and steps in floats.
342
+
343
+ **What it does instead.**
344
+ - **Presets** — `decimal`, `integer`, `currency`, `percent` — supply decimals,
345
+ grouping, affix and `inputmode` together, so a money field is `label`,
346
+ `currency="USD"`, `[(value)]`. `numberFormat` is the escape hatch, merged over
347
+ the preset.
348
+ - **Parses what people actually type**: canonical ASCII always, locale grouping
349
+ (`1.234,56` in German), pasted affixes and currency symbols, accounting
350
+ negatives `(1,234.56)`, localized digit systems, compact `1.5k`, and — behind
351
+ `allowExpressions` — spreadsheet arithmetic like `12*3`, via a shunting-yard
352
+ parser that never calls `eval`.
353
+ - **Unreadable text stays in the field**, flagged with a dashed underline and a
354
+ `rejected` event, rather than being silently swallowed.
355
+ - **Exact arithmetic.** Stepping `0.1` twenty times from `0` lands on exactly
356
+ `2`; `1.15` rounds to `1.2` where `(1.15).toFixed(1)` gives `'1.1'`. A second
357
+ `valueAsString` model carries digits a `number` cannot, and a dev-mode warning
358
+ fires when a bound `value` cannot round-trip.
359
+ - **Percent never divides behind your back**: `preset="percent"` shows `15%` for
360
+ `15`. Models that really are fractions set `valueIsFraction`.
361
+ - Clamping on commit rather than per keystroke, visible fences that disable the
362
+ matching stepper and announce, `wrap` for cyclic fields, `emptyStepValue`,
363
+ four stepper layouts, and hold-to-repeat that announces once on release.
364
+
365
+ `uni-slider` gains `valueDisplay="input"`, which seats one of these as its
366
+ readout — drag for the ballpark, type for the exact value.
367
+
368
+ Adds `numberInput` to `ComponentName` with a theme entry. Field chrome is not
369
+ duplicated there: colour, border, radius and focus come from the shared `input`
370
+ options via `uni-input-box`, so a number field restyles with every other field.
371
+
372
+ `uni-input-box` gains a `managedInset` input. The themed leading inset normally
373
+ rides the inner `<input>`, which is right while the text is the field's leading
374
+ edge and wrong the moment an adornment sits in front — a currency prefix would
375
+ hug the border while the number it belongs to sat indented past it. A field with
376
+ adornments sets `managedInset` and places the inset on whichever element is
377
+ actually first. Existing fields are unaffected: the default is `false`.
378
+
379
+ - [`e7875ee`](https://github.com/uni-design-system/uni/commit/e7875ee81030a703f4ca1904bb94cb8ddc7f57b9) Thanks [@gaenglish](https://github.com/gaenglish)! - New `uni-number-range-input`: two linked numeric fields in one chrome with a
380
+ single `{ start, end }` value — price filters, thresholds, tolerances. This
381
+ completes the numeric family alongside `uni-number-input`,
382
+ `uni-quantity-stepper` and the rebuilt `uni-slider`.
383
+
384
+ ```html
385
+ <uni-number-range-input label="Price range" currency="USD" [(value)]="price" [minGap]="50" />
386
+ ```
387
+
388
+ `start`/`end` deliberately match `UniDateRange`, so the library has one range
389
+ vocabulary, and they never collide with the `min`/`max` **inputs**, which mean
390
+ the fence rather than the value.
391
+
392
+ **The rules that make it one field rather than two glued together:**
393
+ - **Either end alone is a valid value.** `{ start: 50 }` means "50 and up",
394
+ which is a real filter. This is where it diverges from `uni-date-time-input`,
395
+ whose two parts are two halves of one answer.
396
+ - **Stepping is fenced; typing swaps.** A stepper can never walk one end through
397
+ the other — its wall is the other end, held off by `minGap`, and each end's
398
+ `aria-valuemin`/`aria-valuemax` report that wall rather than the outer bounds.
399
+ A _typed_ backwards commit is swapped and announced instead, the rule
400
+ `uni-calendar` applies to a backwards date range: clamping against the other
401
+ end would destroy the number just entered.
402
+ - **`minGap` pushes the end you edited**, not the other one, which is what makes
403
+ stepping behave as a fence rather than dragging the range along.
404
+ - A refused draft flags only the end it was typed into; the other stays valid.
405
+ - `preset`, `currency`, `prefix`, `suffix`, `decimals`, `grouping`, `locale` and
406
+ `roundingMode` are forwarded to both ends so the halves always read alike.
407
+
408
+ It owns its commit path rather than nesting two `uni-number-input`s, because the
409
+ two behaviours above need _different_ bounds — a stepper must be fenced at the
410
+ other end while a typed commit must arrive un-clamped — and a child field
411
+ applies one bound pair to both. The arithmetic, parsing and formatting are still
412
+ the shared `cdk/number` primitives.
413
+
414
+ Adds `numberRangeInput` to `ComponentName` with a theme entry (`partGap`,
415
+ `dividerText`, `dividerColor`). Field chrome is not duplicated there: colour,
416
+ border, radius and focus come from the shared `input` options via
417
+ `uni-input-box`. `dividerText` is literal punctuation rather than an icon token —
418
+ an en dash between two numbers is not a glyph a theme swaps artwork for.
419
+
420
+ - [`589cecb`](https://github.com/uni-design-system/uni/commit/589cecb178d9119eea3fbc3f3cd9149eefdaa036) Thanks [@gaenglish](https://github.com/gaenglish)! - New `uni-quantity-stepper`: `− 3 +` for cart lines, table cells and seat counts
421
+ — the numeric core with no field chrome, no label and no room for either.
422
+
423
+ ```html
424
+ <uni-quantity-stepper label="Quantity, Blue T-shirt (M)" [(value)]="qty" [min]="1" />
425
+ ```
426
+
427
+ A separate component rather than a `chrome="bare"` flag on `uni-number-input`,
428
+ because this control is defined by what it does _not_ have — presets, affixes,
429
+ expressions, four stepper layouts — and eight inputs are easier to write
430
+ correctly than forty plus a list of which ones to leave alone. The arithmetic,
431
+ parsing and hold-to-repeat come from the same `cdk/number` primitives, so
432
+ `1,200` and the keyboard map behave identically in both.
433
+ - **`deleteAtMin`** is the cart pattern in one attribute: at the floor the
434
+ decrement becomes a remove affordance, renamed `Remove {label}`, and emits
435
+ `removed` rather than stepping to zero. Without it every shop reimplements the
436
+ same `value === 1 ? remove() : step(-1)` branch outside the component. (The
437
+ spec called this output `emptied`; that is a native `HTMLMediaElement` event
438
+ name, which `@angular-eslint/no-output-native` rightly rejects, and `removed`
439
+ is already what `uni-tag` calls the same request.)
440
+ - The middle is a real input by default — typing `12` beats tapping `+` eleven
441
+ times, and it takes the same grouped and locale-aware entry the field does.
442
+ `editable=false` renders the number as text for read-mostly tables, and the
443
+ buttons become the tab stops since there is nothing else to focus.
444
+ - `size` is `sm` / `md` / `lg` at 24 / 32 / 40px _outer_ height, so an `md`
445
+ stepper lines up with a 32px field beside it, with the buttons square at that
446
+ height. `md` and `lg` clear the 24×24 pointer target of WCAG 2.2 SC 2.5.8;
447
+ `sm` leaves 22px inside its border and is the dense desktop option.
448
+
449
+ Adds `quantityStepper` to `ComponentName` with a theme entry. Unlike the other
450
+ numeric controls it does **not** inherit the shared `input` chrome — it is not a
451
+ field — so it carries its own container tokens, defaulted to the same values
452
+ `input` uses so a cart stepper and a form field look related out of the box.
453
+ Height comes from the entry's `sizes` block rather than an option.
454
+
455
+ ### Patch Changes
456
+
457
+ - [`ce94c8a`](https://github.com/uni-design-system/uni/commit/ce94c8a6acb5c70351fecfaca3469c46727c2aa4) Thanks [@gaenglish](https://github.com/gaenglish)! - `uni-angular`'s build now runs `tsc --noEmit` first, closing the last gap left
458
+ by the earlier "typecheck the builds" change — core and react already did this,
459
+ angular did not.
460
+
461
+ `ng-packagr` alone does not surface every type error in the package. A real one
462
+ reached a Storybook build unnoticed: a form control declaring
463
+ `min = input(0)` where `FormValueControl` types the property as
464
+ `InputSignal<number | undefined>` (Signal Forms syncs it from `min()`
465
+ validators), which is a variance error `pnpm build` reported as success. Every
466
+ form control must declare `min`/`max` as `input<number | undefined>(…)` and read
467
+ a `resolvedMin()` computed internally.
468
+
469
+ Also here:
470
+ - A `type-check` script, matching core and react, so `turbo type-check` covers
471
+ the whole workspace.
472
+ - `prototypes/**` is excluded from the package tsconfig. Those are standalone
473
+ design explorations that reference modules and dependencies which do not
474
+ exist in this package — excluding them is what makes a real typecheck
475
+ possible over the code that ships.
476
+ - Three latent type errors fixed: `vitest.config.ts` took `defineConfig` from
477
+ `vite`, whose overload does not accept the `test` block (it comes from
478
+ `vitest/config`); `spacing.spec.ts` lost callback inference through an
479
+ untyped `vi.spyOn` return; and `radio.motion.spec.ts` typed a `motion`
480
+ argument as `Record<string, unknown>` rather than `Motions`.
481
+
482
+ - [`5cf120b`](https://github.com/uni-design-system/uni/commit/5cf120b9cb268e74a2ed062d8df2cf5cf9749750) Thanks [@gaenglish](https://github.com/gaenglish)! - Lint MDX prose for stray `{`, which MDX compiles to a JSX expression.
483
+
484
+ Writing `named "Increase {label}"` in a docs bullet makes `label` a reference to
485
+ an undefined variable, and the page dies at runtime with
486
+ `ReferenceError: label is not defined` under Storybook's "The component failed to
487
+ render properly" banner. Nothing caught it: it is a React render error rather
488
+ than a compile error, so **`build-storybook` passes**, and `check-doc-links.mjs`
489
+ only validates link ids. Only opening the page found it — twice.
490
+
491
+ `scripts/check-mdx-braces.mjs` now runs as part of the package's `lint` script,
492
+ so `turbo run lint` (and therefore CI) fails on it. It skips the four places a
493
+ brace is legitimate — fenced code blocks, inline code spans including ones that
494
+ soft-wrap across a line, ESM `import`/`export` statements, and JSX tags such as
495
+ `of={Stories.X}` or `rows={[…]}` — plus MDX comment containers. Hits are
496
+ reported as `file:line:column` with the offending line and the fix: backtick the
497
+ text, or escape the brace as `\{`.
498
+
499
+ Also available on its own as `pnpm lint:mdx`.
500
+
501
+ - [`05f991f`](https://github.com/uni-design-system/uni/commit/05f991f0cc6b0895777763ede7605b5e274dc0a1) Thanks [@gaenglish](https://github.com/gaenglish)! - Three cosmetic fixes in the numeric family.
502
+
503
+ **`uni-quantity-stepper`'s dividers were heavier than its frame.** The rules
504
+ either side of the value took `dividerColor: 'outline'` — a solid grey — against
505
+ an outer border of the 8%-alpha `light` token, so the control read as three
506
+ pieces stuck together rather than one frame. They now take the **same `border`
507
+ token as the container** and move with it on focus, so a focused stepper is not
508
+ accented on the outside and grey down the middle. `dividerColor` remains as an
509
+ opt-in override for a deliberately distinct rule, and is unset in the base theme.
510
+
511
+ **`uni-quantity-stepper` had no focus state.** Every other field gets its focus
512
+ chrome from `uni-input-box`, which the stepper deliberately does not use — and
513
+ its inner input clears its own outline via `removeInputPlatformStyling`, so
514
+ focusing the middle showed nothing at all. The container now carries the same
515
+ `:has(input:focus)` rule and the same `input` theme tokens the box applies
516
+ (`focusOutline`, `focusOutlineOffset`, and the optional `focusBorder` /
517
+ `focusShadow` / `focusColor`), so a stepper highlights exactly like the field
518
+ beside it — including in themes such as Wellsourced that express focus as a
519
+ border and ring rather than an outline. Error state still wins, keeping a
520
+ flagged control visibly flagged while it is corrected.
521
+
522
+ **A trailing suffix sat against the right border.** The leading inset was
523
+ already handled, so the two sides did not match. `uni-number-range-input` — which
524
+ has no steppers — now insets both edges of its row, and `uni-number-input` insets
525
+ the trailing edge whenever no stepper occupies it (`stepperLayout="none"`, or a
526
+ read-only field). Where a stepper _is_ present the trailing edge is still left
527
+ to it, because a button is meant to reach the border.
528
+
529
+ Both insets ride the row rather than the `<input>`: `uni-input-box` styles
530
+ `& input` at a higher specificity than a component class can reach, so padding
531
+ set on the input itself is silently dropped.
532
+
533
+ - [`1fe8941`](https://github.com/uni-design-system/uni/commit/1fe89415c65d38c97d610fb5725e8a050432192f) Thanks [@gaenglish](https://github.com/gaenglish)! - `uni-tag-input`'s first chip no longer rides the left border.
534
+
535
+ The themed leading inset was applied by `uni-input-box` to the inner `<input>`,
536
+ which is the field's leading edge only while it is empty. Once a chip existed,
537
+ the chip sat flush against the border while the text after it stayed indented.
538
+ The chip row now owns the inset — it is the leading content — via the
539
+ `managedInset` input added alongside `uni-number-input`, so the first chip and
540
+ an empty field's placeholder both start at the same 8px as every other field's
541
+ text. Wrapped chip rows are unaffected; vertical padding already handled those.
542
+
543
+ - Updated dependencies [[`2a79bb8`](https://github.com/uni-design-system/uni/commit/2a79bb8ebd8fd0b5cd792697972b6f48b444c80f), [`e7875ee`](https://github.com/uni-design-system/uni/commit/e7875ee81030a703f4ca1904bb94cb8ddc7f57b9), [`589cecb`](https://github.com/uni-design-system/uni/commit/589cecb178d9119eea3fbc3f3cd9149eefdaa036), [`21b655d`](https://github.com/uni-design-system/uni/commit/21b655df0f93e2e2de6a22ccf38050b474d4e5ab)]:
544
+ - @uni-design-system/uni-core@10.0.0
545
+
3
546
  ## 9.0.1
4
547
 
5
548
  ## 9.0.0