srcdev-nuxt-components 9.6.7 → 9.6.8

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.
Files changed (58) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +14 -0
  2. package/.claude/skills/component-compliance-checklist.md +6 -0
  3. package/.claude/skills/component-naming.md +2 -2
  4. package/.claude/skills/components/input-checkbox-radio.md +19 -3
  5. package/.claude/skills/components/input-checkbox.md +14 -3
  6. package/.claude/skills/components/input-description.md +1 -1
  7. package/.claude/skills/components/input-error.md +1 -1
  8. package/.claude/skills/components/input-label.md +2 -2
  9. package/.claude/skills/components/input-number.md +44 -13
  10. package/.claude/skills/components/input-radio.md +15 -1
  11. package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
  12. package/.claude/skills/components/input-textarea-core.md +4 -4
  13. package/.claude/skills/index.md +5 -5
  14. package/.claude/skills/testing-add-unit-test.md +17 -0
  15. package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
  16. package/.vscode/srcdev-component-input-range.code-snippets +38 -18
  17. package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
  18. package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
  19. package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
  20. package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
  21. package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
  22. package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
  23. package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
  24. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
  25. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
  26. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
  27. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
  28. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
  29. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
  30. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
  31. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
  32. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
  33. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
  34. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +2 -2
  35. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +2 -2
  36. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +42 -2
  37. package/app/components/05.forms/input-number/InputNumber.vue +37 -25
  38. package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
  39. package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
  40. package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
  41. package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
  42. package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
  43. package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
  44. package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
  45. package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
  46. package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
  47. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
  48. package/app/components/05.forms/input-range/InputRange.vue +247 -0
  49. package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
  50. package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
  51. package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
  52. package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
  53. package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
  54. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +5 -5
  55. package/package.json +1 -1
  56. package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
  57. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
  58. package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
@@ -0,0 +1,14 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.vue"
4
+ ---
5
+
6
+ # Element ids come from `useId()`, never from data
7
+
8
+ Build `id`/`for` pairs from `useId()` (plus a loop index for repeated items), never from props or
9
+ data values like `` `${name}-${item.value}` ``.
10
+
11
+ Why: two instances sharing a `name` and values produce duplicate ids, and a value with spaces makes
12
+ an invalid one, so labels toggle the wrong control (`MultipleCheckboxes`, 2026-10-16). Test
13
+ uniqueness with both instances inside one host component: separate `mountSuspended` calls each
14
+ restart the `useId()` counter. Detail: skill `testing-add-unit-test.md` (srcdev-nuxt-components).
@@ -262,6 +262,12 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
262
262
  the edits in private memory. Note that an inline-size container can't size itself from its
263
263
  content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
264
264
  - Grid tracks that hold text are `minmax(0, …)` so an unbroken string can't widen the grid.
265
+ - An `auto-fit` column minimum taken from a token or a measured width is wrapped in
266
+ `min(100%, …)`, or one long item makes the column wider than its container.
267
+ - A component whose root is a `<fieldset>` sets `min-inline-size: 0` on it: a native fieldset
268
+ defaults to its min-content width and overflows a narrow column (`MultipleCheckboxes`,
269
+ 2026-10-16).
270
+ - Element ids come from `useId()`, never from data values (rule `consumer/ids-from-useid.md`).
265
271
  - Layout measured in script (connector lines, indicators) re-measures when any measured item
266
272
  resizes or changes class, not only when the container resizes (`measured-layout-observers.md`,
267
273
  pitfall #28).
@@ -83,8 +83,8 @@ external consumer, free to rename. Run `/check-component-usage` for the current
83
83
  | InputTextWithLabel | `InputTextField` | yes |
84
84
  | InputPasswordWithLabel | `InputPasswordField` | yes |
85
85
  | InputTextAsNumberWithLabel | `InputTextAsNumberField` | - |
86
- | InputRangeCore | `InputRange` | - |
87
- | InputRangeDefault | `InputRangeField` | - |
86
+ | ~~InputRangeCore~~ | ✅ `InputRange` (2026-10-16) | - |
87
+ | ~~InputRangeDefault~~ | ✅ `InputRangeField` (2026-10-16) | - |
88
88
  | InputTextareaCore | `InputTextarea` | - |
89
89
  | InputTextareaWithLabel | `InputTextareaField` | yes |
90
90
  | InputSelectCore | `InputSelect` | yes |
@@ -28,7 +28,6 @@ Use these three directly only for a custom group or layout:
28
28
 
29
29
  <InputCheckboxRadioButton
30
30
  v-for="option in options"
31
- :id="`service-${option.value}`"
32
31
  :key="option.value"
33
32
  v-model="state.services"
34
33
  type="checkbox"
@@ -85,9 +84,9 @@ Same props as the control minus `isButton` (always on), plus:
85
84
 
86
85
  | Prop | Type | Default | Notes |
87
86
  |---|---|---|---|
88
- | `id` | `string` | required | Used for both the input and `label[for]`. |
87
+ | `id` | `string` | generated | Used for both the input and `label[for]`. Optional since 2026-10-16: omitted, it comes from `useId()`. Never build it from data values (rule `ids-from-useid.md`). |
89
88
  | `label` | `string` | `""` | Replaced by the `labelContent` slot when given. |
90
- | `:options-layout` | `OptionsLayout` | `"equal-widths"` | `data-options-layout`; `inline` stops the label wrapping. |
89
+ | `:options-layout` | `OptionsLayout` | `"equal-widths"` | `data-options-layout`, a styling hook only (until 2026-10-16 `inline` made the label `nowrap`). |
91
90
  | `direction` | `"row" \| "row-reverse"` | `"row"` | `data-direction`; `row-reverse` puts the control on the right. Reacts to changes. |
92
91
  | `:is-pill` | `boolean` | `false` | `data-pill`, fully rounded. |
93
92
  | `:item-icon` | `string` | `"material-symbols:add-2"` | Decorator icon name. The `itemIcon` slot replaces it entirely. |
@@ -124,6 +123,15 @@ Full detail in `app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md
124
123
 
125
124
  ---
126
125
 
126
+ ## Long and hostile content
127
+
128
+ Both labels wrap long unbroken words and URLs (`overflow-wrap: anywhere`, `min-inline-size: 0` on
129
+ label and root), and the control and decorator icon keep their size (`flex-shrink: 0`). No
130
+ line-clamp tokens: an option label names the choice and must stay readable. The Field and Button
131
+ `StressTest` stories cover it; the bare control renders no text, so it has none.
132
+
133
+ ---
134
+
127
135
  ## History
128
136
 
129
137
  **Migrated 2026-09-27** (3/5 → full compliance):
@@ -147,3 +155,11 @@ Full detail in `app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md
147
155
  - Button radius, gap, paddings and the Field label colour became tokens; label colour default moved
148
156
  from `--colour-text-default` to `--theme-text`. Removed an undeclared `--_box-shadow` and an
149
157
  invalid unitless `min-height`.
158
+
159
+ **Stress pass 2026-10-16**:
160
+
161
+ - Removed the inline-layout `nowrap` on the button label (private `--_white-space`), which made an
162
+ unbroken label overflow the row; labels now wrap and the control/icon no longer shrink.
163
+ - `InputCheckboxRadioButton`'s `id` became optional with a `useId()` fallback.
164
+ - Specs now unmount every wrapper in `afterEach`; stories moved to `StoryObj` with Docs
165
+ descriptions, parent-story links, `CanvasSwitcher` (Field, Button) and `StressTest` stories.
@@ -24,7 +24,7 @@ instead.
24
24
 
25
25
  | Prop (template form) | Type | Default | Notes |
26
26
  |---|---|---|---|
27
- | `name` | `string` | (required) | Shared `name` on every checkbox; also used to build each checkbox `id` (`${name}-${item.value}`) and the error/description ids. |
27
+ | `name` | `string` | (required) | Shared `name` on every checkbox; also used for the error/description ids. Button-presentation checkbox ids come from `useId()` plus the option index (until 2026-10-16 they were `${name}-${item.value}`, which duplicated across two groups sharing a name and broke on values with spaces). |
28
28
  | `legend` | `string` | (required) | Fieldset `<legend>` text. |
29
29
  | `:error-message` | `string \| object` | (required) | Shown by `InputError` when `fieldHasError` is true. Pass `""` when there's no error. An array renders as a list. |
30
30
  | `:field-has-error` | `boolean` | `false` | Shows the error, sets `aria-invalid` on the fieldset and points each checkbox's `aria-describedby` at the error. |
@@ -36,7 +36,7 @@ instead.
36
36
  | `:display-as-disc` | `boolean` | `false` | Round checkbox symbol. |
37
37
  | `:theme` | `FormUiTheme` | `"default"` | Forwarded to each control. |
38
38
  | `:input-variant` | `InputUiVariant` | `"normal"` | Forwarded to each control and the error message. |
39
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset>`. Read once at mount. |
39
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset>`; updates when the prop changes. |
40
40
  | `data-testid` | `string` | `"multiple-checkboxes"` | On the root `<fieldset>`. |
41
41
  | `label` | `string` | `""` | **Ignored.** Kept only so existing call sites that pass it don't break; it has never rendered. Use `legend` or the description slots. |
42
42
 
@@ -84,7 +84,7 @@ instead.
84
84
  | `:options-layout` | `OptionsLayout` | `"equal-widths"` | Rendered as `data-options-layout` on the items container. |
85
85
  | `:theme` | `FormUiTheme` | `"default"` | |
86
86
  | `:input-variant` | `InputUiVariant` | `"normal"` | |
87
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset>`. Read once at mount. |
87
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset>`; updates when the prop changes. |
88
88
  | `data-testid` | `string` | `"single-checkbox"` | On the root `<fieldset>`. |
89
89
 
90
90
  ### v-model
@@ -122,6 +122,17 @@ controls' own colours are the `--input-checkbox-*` tokens in `input-checkbox-rad
122
122
 
123
123
  ---
124
124
 
125
+ ## Long and hostile content
126
+
127
+ Both root fieldsets set `min-inline-size: 0` (a native `<fieldset>` otherwise grows to its
128
+ min-content width and overflows a narrow column) and `overflow-wrap: anywhere`, which inherits
129
+ into the legend, description, labels and error, so long unbroken words and URLs wrap. Equal-width
130
+ columns are capped at `100%` of the container. Button-mode labels wrap too (`InputCheckboxRadioButton` dropped its inline `nowrap`
131
+ 2026-10-16). No line-clamp tokens: the legend, option labels and error name the choice or the
132
+ problem and must stay readable. The `StressTest` story in each story file covers this.
133
+
134
+ ---
135
+
125
136
  ## Migration notes (2026-09-25)
126
137
 
127
138
  Brought to full compliance. Behaviour changes a consumer could notice:
@@ -4,7 +4,7 @@
4
4
 
5
5
  `InputDescription` renders a form field's help text. Every `*Field` wrapper in `05.forms`
6
6
  (`InputTextWithLabel`, `InputNumberField`, `InputSelectWithLabel`, `InputTextareaWithLabel`,
7
- `InputRangeDefault`, `InputTextAsNumberWithLabel`, `SingleCheckbox`, `MultipleCheckboxes`,
7
+ `InputRangeField`, `InputTextAsNumberWithLabel`, `SingleCheckbox`, `MultipleCheckboxes`,
8
8
  `MultipleRadiobuttons`) already renders one and forwards its own `descriptionText` /
9
9
  `descriptionHtml` slots into it, so consumers normally just pass those slots to the wrapper:
10
10
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  `InputError` renders the red error message strip under a form control. Every `*Field` wrapper in
6
6
  `05.forms` (`InputTextWithLabel`, `InputNumberField`, `InputSelectWithLabel`, `InputTextareaWithLabel`,
7
- `InputRangeDefault`, `ToggleSwitchWithLabel`, `SingleCheckbox`, `MultipleCheckboxes`,
7
+ `InputRangeField`, `ToggleSwitchWithLabel`, `SingleCheckbox`, `MultipleCheckboxes`,
8
8
  `MultipleRadiobuttons`, `InputTextAsNumberWithLabel`) already renders one, so consumers rarely use
9
9
  it directly. Use it yourself only when building a custom field wrapper.
10
10
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  `InputLabel` renders a form field's `<label>`. Every labelled wrapper in `05.forms`
6
6
  (`InputTextWithLabel`, `InputTextAsNumberWithLabel`, `InputNumberField`, `InputSelectWithLabel`,
7
- `InputTextareaWithLabel`, `InputRangeDefault`, `ToggleSwitchWithLabel`,
7
+ `InputTextareaWithLabel`, `InputRangeField`, `ToggleSwitchWithLabel`,
8
8
  `ToggleSwitchWithLabelInline`) already renders one from its `label` prop, so consumers normally
9
9
  never use it directly.
10
10
 
@@ -62,7 +62,7 @@ export default defineAppConfig({
62
62
  - `"required"` mode: a marker on fields with `required`; `"optional"` mode: a marker on fields
63
63
  without it; `"none"`: nothing (default, so existing sites are unchanged).
64
64
  - `InputTextWithLabel`, `InputTextAsNumberWithLabel`, `InputNumberField`, `InputSelectWithLabel`,
65
- `InputTextareaWithLabel` and `InputRangeDefault` forward `:required`.
65
+ `InputTextareaWithLabel` and `InputRangeField` forward `:required`.
66
66
  `ToggleSwitchWithLabel`/`ToggleSwitchWithLabelInline` pass `indicator="none"`: a switch always
67
67
  has a value, so neither marker means anything there.
68
68
  - The wrappers don't expose the indicator props per field; use `app.config`.
@@ -29,17 +29,17 @@ Most consumers should reach for **InputNumberField** rather than `InputNumber` d
29
29
  | `:placeholder` | `string` | `""` | Native `placeholder`. |
30
30
  | `:required` | `boolean` | `false` | Native `required`. |
31
31
  | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
32
- | `:weight` | `FormWeight` | `"normal"` | Adds an `input-number--{weight}` class to the input, for prop-shape consistency with sibling `05.forms` components. **No CSS in the library currently styles this class — it's a no-op today**, same convention as `InputRangeCore`'s `weight` prop. |
33
- | `:input-variant` | `InputUiVariant` | `"normal"` | Adds the variant name as a class on the wrapper — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
32
+ | `:weight` | `FormWeight` | `"normal"` | Adds an `input-number--{weight}` class to the input, for prop-shape consistency with sibling `05.forms` components. **No CSS in the library currently styles this class — it's a no-op today**, same convention as `InputRange`'s `weight` prop. |
33
+ | `:input-variant` | `InputUiVariant` | `"normal"` | Rendered as `data-input-variant` on the wrapper (a bare class until 2026-10-16) — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
34
34
  | `:field-has-error` | `boolean` | `false` | Drives `data-invalid` on the wrapper. |
35
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. |
35
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. Reacts to changes. |
36
36
  | `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
37
37
 
38
38
  ### v-model
39
39
 
40
40
  `v-model` — `number | readonly number[]`, **required** (`defineModel({ required: true })`). Bound
41
41
  directly to the native number input. Required rather than defaulted deliberately — same reasoning
42
- as `InputRangeCore`'s `v-model` (see that skill doc) — this also satisfies `vue/require-default-prop`.
42
+ as `InputRange`'s `v-model` (see that skill doc) — this also satisfies `vue/require-default-prop`.
43
43
 
44
44
  ---
45
45
 
@@ -47,15 +47,15 @@ as `InputRangeCore`'s `v-model` (see that skill doc) — this also satisfies `vu
47
47
 
48
48
  | Slot | Notes |
49
49
  |------|-------|
50
- | `left` | Rendered before the input (e.g. a step-down button). Adds `has-left-slot` to the wrapper. |
51
- | `right` | Rendered after the input (e.g. a step-up button). Adds `has-right-slot` to the wrapper. |
50
+ | `left` | Rendered before the input (e.g. a step-down button) in `.input-number-slot-left`. Sets `data-has-left-slot` on the wrapper. |
51
+ | `right` | Rendered after the input (e.g. a step-up button) in `.input-number-slot-right`. Sets `data-has-right-slot` on the wrapper. |
52
52
 
53
- Any `.input-button` inside `.slot` gets its own border/outline stripped and is sized to
53
+ Any `.input-button` inside `.input-number-slot` gets its own border/outline stripped and is sized to
54
54
  match the input (`aspect-ratio: 1`, `width: var(--input-min-height)`) regardless of which slot
55
55
  it's in. The divider border is then added directly to that button element specifically when its
56
- slot is present — `&.has-left-slot .left-slot .input-button { border-right: ... }` and the
57
- right-hand mirror — so it sits flush against the button's own edge rather than around the
58
- containing `.slot` div. This makes step buttons read as embedded segments of the same control
56
+ slot is present (`.input-number-slot-left .input-button { border-right: ... }` and the
57
+ right-hand mirror), so it sits flush against the button's own edge rather than around the
58
+ containing `.input-number-slot` div. This makes step buttons read as embedded segments of the same control
59
59
  rather than separate floating buttons. The divider reuses the wrapper's own border colour token
60
60
  (no separate divider token), same as `InputTextCore`/`InputTextAsNumberWithLabel`.
61
61
 
@@ -83,7 +83,7 @@ score-1/5 state. Several latent bugs were fixed alongside the standard checklist
83
83
  - The native `<input>` had no static `input-number` class — only the dynamic
84
84
  `elementClasses` passthrough — so the entire `.input-number { ... }` style block (font,
85
85
  padding, colour, focus box-shadow) never matched anything. Fixed by adding the class directly.
86
- - `placeholder` was declared as a prop but never bound on the `<input>` — unlike `InputRangeCore`
86
+ - `placeholder` was declared as a prop but never bound on the `<input>` — unlike `InputRange`
87
87
  (where a range input genuinely has no placeholder concept and the no-op is intentional and
88
88
  documented), a number input *does* support `placeholder` meaningfully, so this was a real gap,
89
89
  not a deliberate no-op. Fixed by binding `:placeholder`.
@@ -107,9 +107,9 @@ score-1/5 state. Several latent bugs were fixed alongside the standard checklist
107
107
  aligned with the rest of `05.forms`.
108
108
  - `weight` was passed from `InputNumberField` to `InputNumber` but `InputNumber` had no
109
109
  matching prop, so it fell through as a raw, meaningless `weight="normal"` HTML attribute on the
110
- wrapper `<div>`. Fixed by declaring the prop on `InputNumber` too, matching `InputRangeCore`.
110
+ wrapper `<div>`. Fixed by declaring the prop on `InputNumber` too, matching `InputRange`.
111
111
  - `defineModel()` had no default, tripping `vue/require-default-prop` — fixed the same way as
112
- `InputRangeCore`: `defineModel({ required: true })`, not a rule disable or an artificial default.
112
+ `InputRange`: `defineModel({ required: true })`, not a rule disable or an artificial default.
113
113
  - A single-use private token, `--_border`, was inlined to the public
114
114
  `var(--input-number-border, var(--theme-border))` directly at its point of use — it had no
115
115
  composition or state-swap behind it, just a bare pass-through (see pitfall #20). `--_surface`
@@ -221,3 +221,34 @@ button icon content itself is consumer-supplied).
221
221
 
222
222
  Step-down is disabled (`readonly` on its `InputButton`) once the value reaches `min`, and
223
223
  step-up once it reaches `max`; clicking either button while at that boundary is a no-op.
224
+ Each press adds or subtracts `step` (its absolute value, or 1 when it's 0), rounds away
225
+ floating-point drift (0.2 + 0.1 gives 0.3) and clamps to `min`/`max`, so a step that would overshoot
226
+ lands on the limit and a typed out-of-range value is pulled back in. From an empty field the first
227
+ press sets 0, clamped into range. `:style-class-passthrough` lands on the root `.input-number-field`
228
+ only (it also reached the inner input until 2026-10-16) and reacts to changes. The root carries
229
+ `data-input-variant`, `data-theme` and `data-invalid`; its old `error`, `theme-*` and variant
230
+ classes are gone.
231
+
232
+ ---
233
+
234
+ ## Stress pass 2026-10-16
235
+
236
+ - **Stepping** (`InputNumberField`): 0.1 steps drifted (0.30000000000000004); a step that didn't
237
+ divide the range overshot `max`/`min` (8 + 5 with max 10 gave 13); an empty field turned into
238
+ `NaN`. Now rounded to the step's precision, clamped to `min`/`max`, and an empty field starts at
239
+ 0, clamped. A zero or negative `step` steps by its size, or 1.
240
+ - **Mobile keyboard**: `inputmode="numeric"` and `pattern="[0-9]+"` were hardcoded, so a negative
241
+ or decimal value couldn't be typed on phones (and number inputs ignore `pattern`). `inputmode` is
242
+ now `numeric` for whole steps, `decimal` for fractional ones, and omitted when `min` is negative;
243
+ `pattern` is gone.
244
+ - **Width**: the input's minimum width was `max`'s digit count in `em`, uncapped and blind to a
245
+ longer negative `min` or the step's decimals. It now counts the longest of `min`/`max` plus the
246
+ step's decimals, capped at `12em`; the wrapper has `max-inline-size: 100%` and the step buttons
247
+ don't shrink. The Field wraps long label, description and error copy.
248
+ - **State hooks**: bare state classes became data attributes and the slot holders got prefixed
249
+ class names (CONSUMER-STYLING.md has the mapping). Removed the non-existent `body-normal-bold`
250
+ utility class from the label and a dead `.label-description` rule. `InputError` now gets
251
+ `input-variant` like the label does.
252
+ - Specs unmount in `afterEach`; stories moved to `StoryObj` with Docs descriptions, a
253
+ `DecimalSteps` story, `CanvasSwitcher` and a `StressTest` story on the Field. The bare
254
+ control's step-button demo uses `InputButton`, not raw buttons.
@@ -41,7 +41,7 @@ option in `fieldData.data`, and an `InputError`. It's the single-choice sibling
41
41
  | `:options-layout` | `OptionsLayout` | `"equal-widths"` | `"equal-widths"` (auto-fit grid sized to the longest label), `"inline"` (wrapping row) or `"block"` (column). Rendered as `data-options-layout` on the items container. |
42
42
  | `:theme` | `FormUiTheme` | `"default"` | Passed to each radio. |
43
43
  | `:input-variant` | `InputUiVariant` | `"normal"` | Passed to each radio, the description and the error. |
44
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset>`. Read once at mount. |
44
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset>`; updates when the prop changes. |
45
45
  | `data-testid` | `string` | `"multiple-radio-buttons"` | On the root `<fieldset>`. |
46
46
 
47
47
  ### v-model
@@ -93,3 +93,17 @@ tokens from `input-checkbox-radio`.
93
93
  to `data-options-layout`; `MultipleCheckboxes` and `SingleCheckbox` got the same change.
94
94
  - Gap and top margin are public tokens. The label-width measurement no longer includes a
95
95
  selector for a class that doesn't exist. `fieldData` is a typed required model.
96
+
97
+ **Stress pass 2026-10-16**:
98
+
99
+ - Button-mode radio ids were `${name}-${item.value}`, so two groups sharing a `name` got duplicate
100
+ ids and a value with spaces made an invalid one (CLAUDE.md pitfall 30). `MultipleRadiobuttons`
101
+ no longer passes an `id`; `InputCheckboxRadioButton` generates one with `useId()`.
102
+ - The root fieldset sets `min-inline-size: 0` and `overflow-wrap: anywhere`, and equal-width
103
+ columns are capped at `min(100%, ...)`, so long legends, labels and errors wrap inside a narrow
104
+ column (same fix as `MultipleCheckboxes`). No line-clamp tokens: the legend, option labels and
105
+ error must stay readable.
106
+ - `styleClassPassthrough` is watched instead of read once at mount.
107
+ - The spec unmounts every wrapper in `afterEach`; the story moved to `StoryObj` with Docs
108
+ descriptions, `CanvasSwitcher` and a `StressTest` story, and its gap control is labelled as a
109
+ story-only CSS token.
@@ -1,15 +1,20 @@
1
- # InputRangeCore Component
1
+ # InputRange Component
2
+
3
+ > **Renamed 2026-10-16**: `InputRangeCore` → `InputRange`, `InputRangeDefault` → `InputRangeField`
4
+ > (DOM classes `.input-range-core` → `.input-range`, `.input-range-with-label` →
5
+ > `.input-range-field`; full class list in CONSUMER-STYLING.md). Skill doc renamed from
6
+ > `input-range-core.md`. No consumer app used either old name.
2
7
 
3
8
  ## Overview
4
9
 
5
- `InputRangeCore` is the low-level native `<input type="range">` primitive for the `05.forms`
10
+ `InputRange` is the low-level native `<input type="range">` primitive for the `05.forms`
6
11
  range-slider family. It renders a single native range input with left/right button slots (for
7
12
  step-down/step-up controls), an optional `markers` slot for custom tick marks overlaid on the
8
13
  track, and an optional `datalist` slot for native tick-mark snapping. It has no label,
9
- description, or error-message rendering of its own — that's composed by the `InputRangeDefault`
14
+ description, or error-message rendering of its own — that's composed by the `InputRangeField`
10
15
  variant below.
11
16
 
12
- Most consumers should reach for **InputRangeDefault** rather than `InputRangeCore` directly.
17
+ Most consumers should reach for **InputRangeField** rather than `InputRange` directly.
13
18
 
14
19
  ---
15
20
 
@@ -20,7 +25,7 @@ Most consumers should reach for **InputRangeDefault** rather than `InputRangeCor
20
25
  | Prop (template form) | Type | Default | Notes |
21
26
  |------|------|---------|-------|
22
27
  | `:id` | `string` | (required) | Applied to the native input. |
23
- | `:name` | `string` | (required) | Applied to the native input, and used to derive the `list` id when the `datalist` slot is used (`${name}-datalist`). |
28
+ | `:name` | `string` | (required) | Applied to the native input. (Until 2026-10-16 it also built the datalist id, `${name}-datalist`; that id now comes from `useId()`.) |
24
29
  | `:min` | `number` | (required) | Native `min`. |
25
30
  | `:max` | `number` | (required) | Native `max`. |
26
31
  | `:step` | `number` | `1` | Native `step`. |
@@ -28,8 +33,8 @@ Most consumers should reach for **InputRangeDefault** rather than `InputRangeCor
28
33
  | `:placeholder` | `string` | `""` | Declared but not rendered — range inputs have no placeholder concept. Kept for prop-shape consistency with sibling `05.forms` components. |
29
34
  | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
30
35
  | `:weight` | `FormWeight` | `"normal"` | Adds an `input-range--{weight}` class to the input. **No CSS in the library currently styles this class — it's a no-op today**, kept for prop-shape consistency with sibling components. |
31
- | `:field-has-error` | `boolean` | `false` | Declared, but doesn't drive any visible state inside `InputRangeCore` itself — `InputRangeDefault` handles the actual error styling (`data-invalid`/`.error` class) on its own wrapper. |
32
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. |
36
+ | `:field-has-error` | `boolean` | `false` | `data-invalid` on the wrapper and `aria-invalid="true"` on the input (it did nothing here until 2026-10-16). |
37
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. Reacts to changes. |
33
38
  | `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
34
39
 
35
40
  ### v-model
@@ -50,8 +55,8 @@ disable, not a fake default).
50
55
  |------|-------|
51
56
  | `left` | Rendered before the track (e.g. a step-down button). |
52
57
  | `right` | Rendered after the track (e.g. a step-up button). |
53
- | `markers` | Rendered as an overlay on top of the track (`grid-area: element-stack`, same cell as the input). Adds a `has-markers` class to the input, which shrinks its visible track to a thin `0.2rem` strip and hides the native thumb, so your marker content becomes the primary visual. Expected shape: a `.input-range-markers` wrapper containing `.marker` elements (each optionally containing a `.marker-icon`) — see the component's own scoped CSS for the classes it pre-styles. |
54
- | `datalist` | Rendered after the input. Must contain a real `<datalist>` element whose `id` matches `${name}-datalist` for the browser to link it via the input's `list` attribute — the component computes that id string for the `list` attribute itself, but does not set the `id` on your slotted `<datalist>` for you. |
58
+ | `markers` | Rendered as an overlay on top of the track (`grid-area: element-stack`, same cell as the input). Sets `data-has-markers` on the wrapper: a line joining the markers is drawn behind them, the input becomes a transparent layer over the whole marker row (so the row is draggable and clickable), and the thumb is restyled as a ring the size of one marker that circles the selected one. That lines up because markers are spaced `space-between` and given an explicit size (icon size + 2 × padding), the same as the thumb. Use one marker per step (`(max - min) / step + 1`). Expected shape: a `.input-range-markers` wrapper containing `.input-range-marker` elements (each optionally containing an `.input-range-marker-icon`); those classes are pre-styled (they were `.marker`/`.marker-icon` until 2026-10-16). |
59
+ | `datalist` | Rendered after the input, with a `datalistId` slot prop: put it on your `<datalist>` (`<template #datalist="{ datalistId }"><datalist :id="datalistId">`) and the input's `list` points at it. The id comes from `useId()`, so two ranges sharing a `name` don't collide. `list` is omitted when the slot is empty. |
55
60
 
56
61
  ---
57
62
 
@@ -93,13 +98,13 @@ was itself commented out.
93
98
 
94
99
  ## Variants
95
100
 
96
- ### InputRangeDefault
101
+ ### InputRangeField
97
102
 
98
- `InputRangeDefault` (`InputRangeDefault.vue`) composes `InputRangeCore` with `InputLabel`,
103
+ `InputRangeField` (`InputRangeField.vue`) composes `InputRange` with `InputLabel`,
99
104
  `InputDescription`, and `InputError`, plus optional step-down/step-up buttons (`InputButton`)
100
105
  wired into the `left`/`right` slots.
101
106
 
102
- **Additional props over InputRangeCore:**
107
+ **Additional props over InputRange:**
103
108
 
104
109
  | Prop (template form) | Type | Default | Notes |
105
110
  |------|------|---------|-------|
@@ -110,11 +115,11 @@ wired into the `left`/`right` slots.
110
115
 
111
116
  **Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`), plus
112
117
  `left`/`right`/`markers`/`datalist` (forwarded straight through to the underlying
113
- `InputRangeCore` — the step-down/step-up buttons only render when you use the `left`/`right`
118
+ `InputRange`; `markers` wasn't forwarded until 2026-10-16, and `datalist` passes `datalistId` on — the step-down/step-up buttons only render when you use the `left`/`right`
114
119
  slots yourself, since the button icon content itself is consumer-supplied via `#iconOnly`).
115
120
 
116
121
  ```vue
117
- <InputRangeDefault
122
+ <InputRangeField
118
123
  name="priceRange"
119
124
  label="Price range"
120
125
  v-model="price"
@@ -126,17 +131,47 @@ slots yourself, since the button icon content itself is consumer-supplied via `#
126
131
  >
127
132
  <template #left><Icon name="mdi:minus" /></template>
128
133
  <template #right><Icon name="mdi:plus" /></template>
129
- </InputRangeDefault>
134
+ </InputRangeField>
130
135
  ```
131
136
 
132
- `v-model` is `number`, **required** — same as `InputRangeCore` above.
137
+ `v-model` is `number`, **required** — same as `InputRange` above.
138
+
139
+ Each step-button press adds or subtracts `step` (its absolute value, or 1 when it's 0), rounds away
140
+ floating-point drift (0.2 + 0.1 gives 0.3) and clamps to `min`/`max`; step-down goes read-only at or
141
+ below `min`, step-up at or above `max`. `:style-class-passthrough` lands on the root
142
+ `.input-range-field` only (it also reached the inner input until 2026-10-16) and reacts to changes.
143
+ The root carries `data-theme` and `data-invalid` (its old `error` class is gone), and the label
144
+ gets `input-range-label` (it was the number field's `input-number-label`, with a non-existent
145
+ `body-normal-bold` utility).
133
146
 
134
147
  **Fixed 2026-09-21**: removed a dead `deepCssClassPassthrough` prop (declared, never consumed
135
148
  anywhere in the component or referenced elsewhere in the codebase) and a dead `<style>` block
136
149
  targeting `.input-range-label`/`.label-description` — classes that nothing in this component (or
137
150
  `InputLabel`/`InputDescription`) actually renders, so the block had no effect. Also removed two
138
151
  dead commented-out lines (`// const id = useId();`, a duplicate no-op `FormUiTheme` computed), and
139
- (same day, follow-up pass) a `const props = withDefaults(...)` in both `InputRangeCore` and
140
- `InputRangeDefault` that had become entirely unused once the dead `FormUiTheme` computed was
152
+ (same day, follow-up pass) a `const props = withDefaults(...)` in both `InputRange` and
153
+ `InputRangeField` that had become entirely unused once the dead `FormUiTheme` computed was
141
154
  removed — the template's `:id`/`:name`/etc. shorthand bindings resolve straight from `defineProps`
142
155
  automatically in `<script setup>`, no destructuring or `props.` reference needed.
156
+
157
+ ---
158
+
159
+ ## Stress pass 2026-10-16
160
+
161
+ - **Stepping** (`InputRangeField`): 0.1 steps drifted (0.30000000000000004) and a step that didn't
162
+ divide the range overshot `min`/`max`; the read-only checks used `===`, so an overshot value
163
+ never disabled the button. Now rounded, clamped, and `<=`/`>=`. A zero or negative `step`
164
+ steps by its size, or 1.
165
+ - **Datalist id**: was `${name}-datalist`, so two ranges sharing a `name` linked to the same list
166
+ (rule `ids-from-useid.md`). Now `useId()`, handed to the slot as `datalistId`.
167
+ - **Error state**: `fieldHasError` did nothing on `InputRange`; it now sets `data-invalid` and
168
+ `aria-invalid`.
169
+ - **Slots**: the Field didn't forward `markers`.
170
+ - **Classes**: bare/generic classes became prefixed classes or data attributes, and the marker
171
+ defaults moved from dark-page colours to `--theme-*` tokens (CONSUMER-STYLING.md has the
172
+ mapping). The Field's passthrough no longer reaches the inner input as well as its root.
173
+ - **Overflow**: the slider container gets `min-inline-size: 0` and the step buttons don't
174
+ shrink; the Field wraps long label, description and error copy.
175
+ - Specs unmount in `afterEach`; stories moved to `StoryObj` with Docs descriptions, a
176
+ `DecimalSteps` story, `CanvasSwitcher` and a `StressTest` story on the Field. The bare
177
+ control's step-button demo uses `InputButton`, not raw buttons.
@@ -33,7 +33,7 @@ directly.
33
33
 
34
34
  - `v-model` — `string | number | readonly string[] | null | undefined`, **required**
35
35
  (`defineModel({ required: true })`, satisfying `vue/require-default-prop` the same way as
36
- `InputRangeCore`/`InputNumber` — see `project_ledger_eslint_issues_column` in memory).
36
+ `InputRange`/`InputNumber` — see `project_ledger_eslint_issues_column` in memory).
37
37
  "Required" here means a consumer must bind `v-model`, not that the value can't be an empty
38
38
  string — `""` is a perfectly valid textarea value.
39
39
  - `v-model:is-dirty` — `boolean`. Set once by `InputTextareaWithLabel` the first time the value
@@ -50,7 +50,7 @@ directly.
50
50
  | `left` | Rendered before the textarea (decorative — e.g. an icon). Adds `has-left-slot` to the wrapper. |
51
51
  | `right` | Rendered after the textarea (decorative). Adds `has-right-slot` to the wrapper. |
52
52
 
53
- Unlike `InputNumber`/`InputRangeCore`, these slots have no built-in `InputButton`
53
+ Unlike `InputNumber`/`InputRange`, these slots have no built-in `InputButton`
54
54
  divider/embedded-button styling — they're plain decorative content (see the Storybook stories,
55
55
  which use emoji), not a stepper control.
56
56
 
@@ -75,7 +75,7 @@ the full table with defaults.
75
75
  score-1/5 state (no public tokens, no CONSUMER-STYLING.md, no tests, no skill doc, no snippet).
76
76
 
77
77
  - **Silently broken styling from undefined CSS custom properties** (same bug class as pitfall #21
78
- and `InputRangeCore`'s dead `--theme-form-range-accent-color`): the wrapper's `padding-inline`
78
+ and `InputRange`'s dead `--theme-form-range-accent-color`): the wrapper's `padding-inline`
79
79
  read `--element-decorator-padding-inline`, and the placeholder's `font-size`/`font-style`/
80
80
  `line-height`/`font-weight` read `--input-placeholder-font-size`/`-font-style`/`-line-height`/
81
81
  `-font-weight` — **none of these five custom properties were declared anywhere in the
@@ -105,7 +105,7 @@ score-1/5 state (no public tokens, no CONSUMER-STYLING.md, no tests, no skill do
105
105
  `InputLabel` (which does have a real `label` concept) already renders it correctly. Covered by
106
106
  a regression test.
107
107
  - **`defineModel()` had no default**, tripping `vue/require-default-prop` in both files — fixed
108
- with `{ required: true }`, same pattern as `InputRangeCore`/`InputNumber` (see the v-model
108
+ with `{ required: true }`, same pattern as `InputRange`/`InputNumber` (see the v-model
109
109
  note above for why this is still correct even though an empty string is a valid value here).
110
110
  - **`InputTextareaWithLabel.stories.ts` used `reactive()` in two render functions
111
111
  (`AllVariants`/`FormExamples`) without importing it** — only `computed` was imported from
@@ -147,8 +147,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
147
147
  ├── pop-over.md — PopOver: generic anchor-positioned disclosure panel, consumer-supplied trigger/content slots, placement prop, CSS token API
148
148
  ├── input-text-core.md — InputTextCore: native text/date/number input primitive, min/max pass-through (date-picker range), CSS token API; Variants section covers InputTextWithLabel/InputPasswordWithLabel/InputTextAsNumberWithLabel
149
149
  ├── input-button.md — InputButton (renamed from InputButtonCore 2026-09-28): button/NuxtLink/a, variants, icon slots, readonly (aria-disabled, activation blocked), tokens
150
- ├── input-range-core.md — InputRangeCore: native range-slider primitive, markers/datalist slots, dead --theme-form-range-accent-color token fixed, CSS token API; Variants section covers InputRangeDefault
151
- ├── input-number.md — InputNumber: native number-input primitive, left/right step-button slots, missing base class/placeholder/mismatched slot-selector bugs fixed, CSS token API; Variants section covers InputNumberField
150
+ ├── input-range.md — InputRange: native range-slider primitive, markers/datalist slots, dead --theme-form-range-accent-color token fixed, CSS token API; Variants section covers InputRangeField; renamed from InputRangeCore/InputRangeDefault 2026-10-16, clamped stepping, useId datalist id, data-* hooks
151
+ ├── input-number.md — InputNumber: native number-input primitive, left/right step-button slots, missing base class/placeholder/mismatched slot-selector bugs fixed, CSS token API; Variants section covers InputNumberField; clamped, rounded stepping, data-* state hooks, min/step-aware inputmode
152
152
  ├── input-otp.md — InputOtp: one-time-code digit boxes (paste/autofill spreading, Backspace/arrow/Home/End nav, Enter submits, leading zeros kept, complete event, hidden input); Variants section covers InputOtpField
153
153
  ├── form-field.md — FormField: layout wrapper for one form control (width cap, gutter, field spacing), data-width/data-has-gutter/data-invalid hooks, dead .underline rule removed, CSS token API
154
154
  ├── form-fieldset.md — FormFieldset: shared fieldset+legend for checkbox/radio groups, groupRole prop (hardcoded radiogroup bug fixed), aria-required only on radiogroup, dead description slot removed, legend token API
@@ -159,9 +159,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
159
159
  ├── pending-effect.md — PendingEffect: animated dashed border InputButton draws while is-pending (has-pending-effect), tokens renamed to --pending-effect-* and read at point of use, animation moved off the host button, aria-hidden
160
160
  ├── input-textarea-core.md — InputTextareaCore: native textarea primitive, left/right decorative slots, undefined-token/dead-code/label-leak bugs fixed, CSS token API; Variants section covers InputTextareaWithLabel
161
161
  ├── input-select-core.md — InputSelectCore: native select primitive, data-driven options with icon decorator, dead required/styleClassPassthrough/isDirty-isActive functionality fixed, appearance:base-select browser-support note, CSS token API; Variants section covers InputSelectWithLabel
162
- ├── input-checkbox-radio.md — InputCheckboxRadio (bare control), InputCheckboxRadioField (labelled), InputCheckboxRadioButton (button/pill option): renamed from Core/WithLabel, state classes → data-* hooks, type-switch v-model fix, no aria-checked, icon font-size sizing, button spacing tokens
163
- ├── input-checkbox.md — MultipleCheckboxes/SingleCheckbox: fieldset-wrapped checkbox group (data-driven, button or labelled) and single checkbox, per-box native-required/styleClassPassthrough/data-testid bugs fixed, spacing token API
164
- ├── input-radio.md — MultipleRadiobuttons: fieldset radiogroup (data-driven, button or labelled), shared-name grouping bug fixed, dead label/placeholder/equalCols props removed, data-options-layout hook, spacing token API
162
+ ├── input-checkbox-radio.md — InputCheckboxRadio (bare control), InputCheckboxRadioField (labelled), InputCheckboxRadioButton (button/pill option): renamed from Core/WithLabel, state classes → data-* hooks, type-switch v-model fix, no aria-checked, icon font-size sizing, button spacing tokens, long labels wrap (inline nowrap removed), optional useId button id
163
+ ├── input-checkbox.md — MultipleCheckboxes/SingleCheckbox: fieldset-wrapped checkbox group (data-driven, button or labelled) and single checkbox, per-box native-required/styleClassPassthrough/data-testid bugs fixed, spacing token API, wraps hostile content (fieldset min-inline-size, useId option ids)
164
+ ├── input-radio.md — MultipleRadiobuttons: fieldset radiogroup (data-driven, button or labelled), shared-name grouping bug fixed, dead label/placeholder/equalCols props removed, data-options-layout hook, spacing token API, useId button ids, wraps hostile content
165
165
  ├── toggle-switch-core.md — ToggleSwitchCore: pill/square checkbox-backed toggle primitive, dead round-prop/data-theme-error functionality and story theme-options bugs fixed, CSS token API; Variants section covers ToggleSwitchWithLabel (descriptionText/descriptionHtml via InputDescription, `description` slot removed 2026-09-29)/ToggleSwitchWithLabelInline
166
166
  ├── triple-toggle-switch.md — TripleToggleSwitch (renamed from TripleToggleSwitchCore): three-option icon radio pill with sliding marker, radiogroup + ariaLabel, system/light/dark marker-gradient tokens, reduced-motion, CSS token API
167
167
  ├── entry-animation.md — EntryAnimation: scroll-driven entry animation wrapper (slide-in/zoom-reveal/exit-blur utility classes), skipAnimation for above-the-fold loop items, reduced-motion handled at the CSS layer
@@ -321,6 +321,23 @@ it("passes myProp to ChildComponent", async () => {
321
321
 
322
322
  Import the child component directly in the test file — it is not auto-imported there.
323
323
 
324
+ ## Testing `useId()` uniqueness across instances
325
+
326
+ Each `mountSuspended` call is its own Nuxt app, so `useId()` restarts its counter and two separately
327
+ mounted instances get the **same** ids. That's a test artefact, not a bug. To prove two instances
328
+ don't collide, render both inside one host component (`MultipleCheckboxes.spec.ts`):
329
+
330
+ ```ts
331
+ const Host = defineComponent({
332
+ components: { ComponentUnderTest },
333
+ setup: () => ({ props: initialPropsData }),
334
+ template: `<div><ComponentUnderTest v-bind="props" /><ComponentUnderTest v-bind="props" /></div>`,
335
+ });
336
+ const host = await mountSuspended(Host);
337
+ const ids = host.findAll("input").map((input) => input.attributes("id"));
338
+ expect(new Set(ids).size).toBe(ids.length);
339
+ ```
340
+
324
341
  ## Mocking browser APIs
325
342
 
326
343
  ### Global constructors (ResizeObserver, IntersectionObserver, etc.)
@@ -30,7 +30,6 @@
30
30
  "body": [
31
31
  "<InputCheckboxRadioButton",
32
32
  " v-for=\"option in ${1:options}\"",
33
- " :id=\"`${2:name}-\\${option.value}`\"",
34
33
  " :key=\"option.value\"",
35
34
  " v-model=\"${3:state.value}\"",
36
35
  " type=\"${4|checkbox,radio|}\"",