srcdev-nuxt-components 9.6.7 → 9.6.9

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 (152) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +14 -0
  2. package/.claude/skills/component-compliance-checklist.md +18 -1
  3. package/.claude/skills/component-inline-action-button.md +1 -1
  4. package/.claude/skills/component-local-style-override.md +1 -1
  5. package/.claude/skills/component-naming.md +13 -13
  6. package/.claude/skills/components/container-glow.md +2 -2
  7. package/.claude/skills/components/form-field.md +2 -2
  8. package/.claude/skills/components/form-wrapper.md +1 -1
  9. package/.claude/skills/components/glowing-border.md +1 -1
  10. package/.claude/skills/components/input-checkbox-radio.md +19 -3
  11. package/.claude/skills/components/input-checkbox.md +14 -3
  12. package/.claude/skills/components/input-description.md +7 -7
  13. package/.claude/skills/components/input-error.md +3 -3
  14. package/.claude/skills/components/input-label.md +7 -8
  15. package/.claude/skills/components/input-number.md +60 -29
  16. package/.claude/skills/components/input-radio.md +15 -1
  17. package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
  18. package/.claude/skills/components/input-select.md +153 -0
  19. package/.claude/skills/components/input-text.md +220 -0
  20. package/.claude/skills/components/input-textarea.md +147 -0
  21. package/.claude/skills/components/select-menu.md +4 -4
  22. package/.claude/skills/components/toggle-switch.md +143 -0
  23. package/.claude/skills/composable-zod-validation.md +1 -1
  24. package/.claude/skills/icon-sets.md +1 -1
  25. package/.claude/skills/icon-sizing.md +1 -1
  26. package/.claude/skills/index.md +10 -10
  27. package/.claude/skills/storybook-add-story.md +1 -1
  28. package/.claude/skills/testing-add-unit-test.md +17 -0
  29. package/.claude/skills/theming-component-token-pattern.md +8 -8
  30. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  31. package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
  32. package/.vscode/srcdev-component-input-range.code-snippets +38 -18
  33. package/.vscode/srcdev-component-input-select.code-snippets +20 -14
  34. package/.vscode/srcdev-component-input-text.code-snippets +46 -40
  35. package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
  36. package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
  37. package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
  38. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
  39. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
  40. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
  41. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
  42. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
  43. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
  44. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
  45. package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
  46. package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
  47. package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
  48. package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
  49. package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
  50. package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
  51. package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
  52. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
  53. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
  54. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
  55. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
  56. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
  57. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
  58. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
  59. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
  60. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
  61. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
  62. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +7 -7
  63. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +6 -6
  64. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +50 -6
  65. package/app/components/05.forms/input-number/InputNumber.vue +39 -27
  66. package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
  67. package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
  68. package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
  69. package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
  70. package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
  71. package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
  72. package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
  73. package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
  74. package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
  75. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
  76. package/app/components/05.forms/input-range/InputRange.vue +247 -0
  77. package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
  78. package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
  79. package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
  80. package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
  81. package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
  82. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
  83. package/app/components/05.forms/input-select/InputSelect.vue +198 -0
  84. package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
  85. package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
  86. package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
  87. package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
  88. package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
  89. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
  90. package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
  91. package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
  92. package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
  93. package/app/components/05.forms/input-text/InputTextField.vue +168 -0
  94. package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
  95. package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
  96. package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
  97. package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
  98. package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
  99. package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
  100. package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
  101. package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
  102. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
  103. package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
  104. package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
  105. package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
  106. package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
  107. package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
  108. package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
  109. package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
  110. package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
  111. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +34 -36
  112. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
  113. package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
  114. package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
  115. package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
  116. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
  117. package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
  118. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
  119. package/package.json +1 -1
  120. package/.claude/skills/components/input-select-core.md +0 -151
  121. package/.claude/skills/components/input-text-core.md +0 -204
  122. package/.claude/skills/components/input-textarea-core.md +0 -154
  123. package/.claude/skills/components/toggle-switch-core.md +0 -169
  124. package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
  125. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
  126. package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
  127. package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
  128. package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
  129. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
  130. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
  131. package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
  132. package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
  133. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
  134. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
  135. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
  136. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
  137. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
  138. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
  139. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
  140. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
  141. package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
  142. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
  143. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
  144. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
  145. package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
  146. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
  147. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
  148. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
  149. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
  150. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
  151. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
  152. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
@@ -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).
@@ -25,7 +25,10 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
25
25
  **Existing component:** if it lives outside a tier, move it with `git mv`.
26
26
  2. **Props pattern** — `interface Props` + `withDefaults(defineProps<Props>(), {...})`, never
27
27
  options-style `defineProps({...})`. Include `styleClassPassthrough` and wire it through
28
- `useStyleClassPassthrough()`. Consumer-facing data-shape types go in
28
+ `useStyleClassPassthrough()`, with a `watch` that calls `resetElementClasses` so it reacts to prop
29
+ changes. A Field wrapper applies its passthrough to its own root only: forwarding the same prop
30
+ to the inner control as well puts one consumer class on two elements (`InputNumberField`,
31
+ `InputRangeField`, fixed 2026-10-16). Consumer-facing data-shape types go in
29
32
  `app/types/components/<component-name>.d.ts` and are exported from `app/types/components/index.ts`
30
33
  (see `component-export-types.md`), not left inline in the `.vue` file.
31
34
  3. **CSS tokens** — audit every custom property in the component's `<style>` block against the
@@ -196,6 +199,10 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
196
199
  - Live-updating or auto-advancing content (carousels, marquees, auto-dismissing toasts) has a
197
200
  way to pause it, per WCAG 2.2.2 — a visible control, or pause-on-hover/focus at minimum.
198
201
  - `prefers-reduced-motion` is respected for any animation that isn't purely decorative.
202
+ - A mode that hides a native control's own indicator (a range thumb, a checkbox box, a caret)
203
+ draws its own visible state for the current value, and its interactive area isn't shrunk to a
204
+ sliver. `InputRange`'s markers mode hid the thumb with nothing in its place and left only a
205
+ 2px line clickable, from its first commit until 2026-10-16; its 5/5 score didn't show it.
199
206
  - A component with a `tag` prop that can render a landmark uses `useAriaLabelledById`
200
207
  (`component-aria-landmark.md`), unless it falls back to `aria-label` when no heading is
201
208
  given: the composable can't do that and warns whenever a landmark has no heading. Then label
@@ -262,6 +269,16 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
262
269
  the edits in private memory. Note that an inline-size container can't size itself from its
263
270
  content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
264
271
  - Grid tracks that hold text are `minmax(0, …)` so an unbroken string can't widen the grid.
272
+ - An `auto-fit` column minimum taken from a token or a measured width is wrapped in
273
+ `min(100%, …)`, or one long item makes the column wider than its container.
274
+ - A component whose root is a `<fieldset>` sets `min-inline-size: 0` on it: a native fieldset
275
+ defaults to its min-content width and overflows a narrow column (`MultipleCheckboxes`,
276
+ 2026-10-16).
277
+ - Element ids come from `useId()`, never from data values (rule `consumer/ids-from-useid.md`).
278
+ - Step buttons and any other script that changes a number round to the step's precision
279
+ (`toFixed`), clamp to `min`/`max`, and compare limits with `<=`/`>=`, never `===` (0.1 steps
280
+ drift to 0.30000000000000004, and an overshoot past an `===` check never disables the button).
281
+ Reference: `stepBy()` in `InputNumberField`.
265
282
  - Layout measured in script (connector lines, indicators) re-measures when any measured item
266
283
  resizes or changes class, not only when the container resizes (`measured-layout-observers.md`,
267
284
  pitfall #28).
@@ -105,6 +105,6 @@ changed visible label ("Copied!") isn't announced, because the aria-label wins;
105
105
 
106
106
  ## Real examples
107
107
 
108
- - `InputPasswordWithLabel`: `app/components/05.forms/input-text/InputPasswordWithLabel.vue` (show/hide toggle attached to the input)
108
+ - `InputPasswordField`: `app/components/05.forms/input-text/InputPasswordField.vue` (show/hide toggle attached to the input)
109
109
  - `CookieConsentBanner`: `app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue`
110
110
  (standalone Accept/Reject actions, not attached to an input)
@@ -165,7 +165,7 @@ Use when the same component appears multiple times on a page and you need to tar
165
165
  instance.
166
166
 
167
167
  If the component you're rendering is a **wrapper** that doesn't forward `styleClassPassthrough` to
168
- the inner component you want to style (e.g. `InputDescription` inside `InputTextWithLabel`), put a
168
+ the inner component you want to style (e.g. `InputDescription` inside `InputTextField`), put a
169
169
  plain `class` on the wrapper instead. Vue falls it through to the wrapper's root element, which is
170
170
  an ancestor of the inner component, so tokens set there inherit down. This needs the wrapper to
171
171
  have a single root element and not set `inheritAttrs: false`.
@@ -79,19 +79,19 @@ external consumer, free to rename. Run `/check-component-usage` for the current
79
79
  |---------|----------|-----------|
80
80
  | ~~InputNumberCore~~ | ✅ `InputNumber` (2026-09-25) | - |
81
81
  | ~~InputNumberDefault~~ | ✅ `InputNumberField` (2026-09-25) | - |
82
- | InputTextCore | `InputText` | yes |
83
- | InputTextWithLabel | `InputTextField` | yes |
84
- | InputPasswordWithLabel | `InputPasswordField` | yes |
85
- | InputTextAsNumberWithLabel | `InputTextAsNumberField` | - |
86
- | InputRangeCore | `InputRange` | - |
87
- | InputRangeDefault | `InputRangeField` | - |
88
- | InputTextareaCore | `InputTextarea` | - |
89
- | InputTextareaWithLabel | `InputTextareaField` | yes |
90
- | InputSelectCore | `InputSelect` | yes |
91
- | InputSelectWithLabel | `InputSelectField` | yes |
92
- | ToggleSwitchCore | `ToggleSwitch` | yes |
93
- | ToggleSwitchWithLabel | `ToggleSwitchField` | yes |
94
- | ToggleSwitchWithLabelInline | `layout` prop on `ToggleSwitchField`, or `ToggleSwitchFieldInline` | - |
82
+ | ~~InputTextCore~~ | ✅ `InputText` (2026-10-16) | yes |
83
+ | ~~InputTextWithLabel~~ | ✅ `InputTextField` (2026-10-16) | yes |
84
+ | ~~InputPasswordWithLabel~~ | ✅ `InputPasswordField` (2026-10-16) | yes |
85
+ | ~~InputTextAsNumberWithLabel~~ | ✅ `InputTextAsNumberField` (2026-10-16) | - |
86
+ | ~~InputRangeCore~~ | ✅ `InputRange` (2026-10-16) | - |
87
+ | ~~InputRangeDefault~~ | ✅ `InputRangeField` (2026-10-16) | - |
88
+ | ~~InputTextareaCore~~ | ✅ `InputTextarea` (2026-10-16) | - |
89
+ | ~~InputTextareaWithLabel~~ | ✅ `InputTextareaField` (2026-10-16) | yes |
90
+ | ~~InputSelectCore~~ | ✅ `InputSelect` (2026-10-16) | yes |
91
+ | ~~InputSelectWithLabel~~ | ✅ `InputSelectField` (2026-10-16) | yes |
92
+ | ~~ToggleSwitchCore~~ | ✅ `ToggleSwitch` (2026-10-16) | yes |
93
+ | ~~ToggleSwitchWithLabel~~ | ✅ `ToggleSwitchField` (2026-10-16) | yes |
94
+ | ~~ToggleSwitchWithLabelInline~~ | ✅ `layout="inline"` on `ToggleSwitchField` (2026-10-16) | - |
95
95
  | ~~TripleToggleSwitchCore~~ | ✅ `TripleToggleSwitch` (2026-09-27) | - |
96
96
  | ~~InputButtonCore~~ | ✅ `InputButton` (2026-09-28) | yes |
97
97
  | ~~InputCheckboxRadioCore~~ | ✅ `InputCheckboxRadio` (2026-09-27) | - |
@@ -96,8 +96,8 @@ apply instantly instead of fading).
96
96
  ## Notes
97
97
 
98
98
  - 2026-09-20 migration: renamed from `ContainerGlowCore` to `ContainerGlow` — the `Core` suffix in
99
- this library denotes a primitive with a non-Core wrapper built on top of it (`InputTextCore` →
100
- `InputTextWithLabel`, `AlertMaskCore` → `AlertMaskedContent`), and this component has no such
99
+ this library denotes a primitive with a non-Core wrapper built on top of it (`InputTextCore` → `InputTextWithLabel`, since renamed;
100
+ `AlertMaskCore` → `AlertMaskedContent`), and this component has no such
101
101
  sibling. Checked all consumer repos first; found one real usage in a defunct
102
102
  demo app, left as a follow-up for that repo rather than edited here.
103
103
  - Moved from `app/components/container-glow/` (unplaced) into
@@ -6,7 +6,7 @@
6
6
  field's width (`narrow`/`medium`/`wide`), centres it with `margin-inline: auto`, optionally insets
7
7
  it from its container edges with a gutter, and applies the shared vertical field spacing
8
8
  (`--field-margin-block`). It renders no label, input or error text itself: put an `*Field`
9
- component (`InputTextWithLabel`, `InputNumberField`, `InputSelectWithLabel`, ...) in its default
9
+ component (`InputTextField`, `InputNumberField`, `InputSelectField`, ...) in its default
10
10
  slot.
11
11
 
12
12
  Typical use inside `FormWrapper` / a pattern form: `<FormField width="wide" :has-gutter="false">`
@@ -37,7 +37,7 @@ around each control, so the form's own container controls the width.
37
37
 
38
38
  ```vue
39
39
  <FormField width="wide" :has-gutter="false" :field-has-error="!!errors.email">
40
- <InputTextWithLabel
40
+ <InputTextField
41
41
  v-model="state.email"
42
42
  name="email"
43
43
  label="Email"
@@ -36,7 +36,7 @@ centre it.
36
36
  <FormWrapper width="medium">
37
37
  <form @submit.prevent="submitForm">
38
38
  <FormField width="wide" :has-gutter="false">
39
- <InputTextWithLabel ... />
39
+ <InputTextField ... />
40
40
  </FormField>
41
41
  </form>
42
42
  </FormWrapper>
@@ -50,7 +50,7 @@ stops are public CSS custom properties — see `CONSUMER-STYLING.md` for the ful
50
50
 
51
51
  ## Highlighting a single control
52
52
 
53
- Wrap just an `InputButton` or `InputTextCore` so the glow becomes the control's border (a design
53
+ Wrap just an `InputButton` or `InputText` so the glow becomes the control's border (a design
54
54
  call-out for something "special"). Set `--glowing-border-radius` to the control's radius plus the
55
55
  glow width, `--glowing-border-surface: transparent`, `--glowing-border-overflow: visible` (or the
56
56
  control's focus ring is clipped), shrink-to-fit display, and `--input-text-border: transparent` for
@@ -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:
@@ -3,15 +3,15 @@
3
3
  ## Overview
4
4
 
5
5
  `InputDescription` renders a form field's help text. Every `*Field` wrapper in `05.forms`
6
- (`InputTextWithLabel`, `InputNumberField`, `InputSelectWithLabel`, `InputTextareaWithLabel`,
7
- `InputRangeDefault`, `InputTextAsNumberWithLabel`, `SingleCheckbox`, `MultipleCheckboxes`,
6
+ (`InputTextField`, `InputNumberField`, `InputSelectField`, `InputTextareaField`,
7
+ `InputRangeField`, `InputTextAsNumberField`, `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
 
11
11
  ```vue
12
- <InputTextWithLabel v-model="email" name="email" label="Email" :field-has-error :error-message>
12
+ <InputTextField v-model="email" name="email" label="Email" :field-has-error :error-message>
13
13
  <template #descriptionText>We'll only use this for your booking confirmation</template>
14
- </InputTextWithLabel>
14
+ </InputTextField>
15
15
  ```
16
16
 
17
17
  Use `InputDescription` directly only when building a custom field wrapper.
@@ -54,7 +54,7 @@ otherwise `InputDescription` sees an (empty) slot and renders an empty element:
54
54
 
55
55
  - It's a plain description, not a live region: link it with `aria-describedby` on the control
56
56
  (`useAriaDescribedById` does this in every wrapper).
57
- - `InputTextWithLabel`, `InputSelectWithLabel` and `InputTextareaWithLabel` render it **after**
57
+ - `InputTextField`, `InputSelectField` and `InputTextareaField` render it **after**
58
58
  the control for the `outlined` variant and before it otherwise; both positions carry the same id.
59
59
 
60
60
  ---
@@ -81,9 +81,9 @@ look, zero the slot margins and space it with the root margins (example in CONSU
81
81
  - Wrappers forwarded both description slots unconditionally, so every field rendered an empty
82
82
  description element (with its id) even when no description was given. Each wrapper now forwards
83
83
  a slot only when its own consumer supplied it.
84
- - `InputTextWithLabel` and `InputSelectWithLabel` rendered the description **twice** for the
84
+ - `InputTextField` and `InputSelectField` rendered the description **twice** for the
85
85
  `outlined` variant (before and after the control); the first copy now has
86
- `v-if="inputVariant !== 'outlined'"`, matching `InputTextareaWithLabel`.
86
+ `v-if="inputVariant !== 'outlined'"`, matching `InputTextareaField`.
87
87
  - The outlined-variant copies had no `description-id`, so the control's `aria-describedby`
88
88
  pointed at an id that didn't exist. They now pass it.
89
89
  - Removed props that were never read: `id` (a leftover from a commented-out id computation) and
@@ -3,9 +3,9 @@
3
3
  ## Overview
4
4
 
5
5
  `InputError` renders the red error message strip under a form control. Every `*Field` wrapper in
6
- `05.forms` (`InputTextWithLabel`, `InputNumberField`, `InputSelectWithLabel`, `InputTextareaWithLabel`,
7
- `InputRangeDefault`, `ToggleSwitchWithLabel`, `SingleCheckbox`, `MultipleCheckboxes`,
8
- `MultipleRadiobuttons`, `InputTextAsNumberWithLabel`) already renders one, so consumers rarely use
6
+ `05.forms` (`InputTextField`, `InputNumberField`, `InputSelectField`, `InputTextareaField`,
7
+ `InputRangeField`, `ToggleSwitchField`, `SingleCheckbox`, `MultipleCheckboxes`,
8
+ `MultipleRadiobuttons`, `InputTextAsNumberField`) already renders one, so consumers rarely use
9
9
  it directly. Use it yourself only when building a custom field wrapper.
10
10
 
11
11
  It is designed to sit in **grid row 2** of the wrapper, directly under the control in row 1: it is
@@ -3,9 +3,8 @@
3
3
  ## Overview
4
4
 
5
5
  `InputLabel` renders a form field's `<label>`. Every labelled wrapper in `05.forms`
6
- (`InputTextWithLabel`, `InputTextAsNumberWithLabel`, `InputNumberField`, `InputSelectWithLabel`,
7
- `InputTextareaWithLabel`, `InputRangeDefault`, `ToggleSwitchWithLabel`,
8
- `ToggleSwitchWithLabelInline`) already renders one from its `label` prop, so consumers normally
6
+ (`InputTextField`, `InputTextAsNumberField`, `InputNumberField`, `InputSelectField`,
7
+ `InputTextareaField`, `InputRangeField`, `ToggleSwitchField`) already renders one from its `label` prop, so consumers normally
9
8
  never use it directly.
10
9
 
11
10
  Use `InputLabel` directly only when building a custom field wrapper:
@@ -24,7 +23,7 @@ Use `InputLabel` directly only when building a custom field wrapper:
24
23
 
25
24
  | Prop (template form) | Type | Default | Notes |
26
25
  |------|------|---------|-------|
27
- | `:id` | `string` | required | The **control's** id, rendered as the label's `for`. The label itself gets no id. Must match the id actually on the `<input>`/`<select>`/`<textarea>` (e.g. `ToggleSwitchCore` prefixes its input id with `toggle-switch-`, so its wrappers pass `toggleSwitchId`). |
26
+ | `:id` | `string` | required | The **control's** id, rendered as the label's `for`. The label itself gets no id. Must match the id actually on the `<input>`/`<select>`/`<textarea>` (e.g. `ToggleSwitch` prefixes its input id with `toggle-switch-`, so its wrappers pass `toggleSwitchId`). |
28
27
  | `:required` | `boolean` | `false` | Whether the field is required. Only decides which fields the indicator marks; it doesn't set `required` on the control. Wrappers forward their own `required`. |
29
28
  | `:indicator` | `"none" \| "required" \| "optional"` | `undefined` → app.config → `"none"` | Which fields get a marker. |
30
29
  | `:required-text` | `string` | `undefined` → app.config → `"*"` | Required marker text. |
@@ -61,9 +60,9 @@ export default defineAppConfig({
61
60
 
62
61
  - `"required"` mode: a marker on fields with `required`; `"optional"` mode: a marker on fields
63
62
  without it; `"none"`: nothing (default, so existing sites are unchanged).
64
- - `InputTextWithLabel`, `InputTextAsNumberWithLabel`, `InputNumberField`, `InputSelectWithLabel`,
65
- `InputTextareaWithLabel` and `InputRangeDefault` forward `:required`.
66
- `ToggleSwitchWithLabel`/`ToggleSwitchWithLabelInline` pass `indicator="none"`: a switch always
63
+ - `InputTextField`, `InputTextAsNumberField`, `InputNumberField`, `InputSelectField`,
64
+ `InputTextareaField` and `InputRangeField` forward `:required`.
65
+ `ToggleSwitchField` pass `indicator="none"`: a switch always
67
66
  has a value, so neither marker means anything there.
68
67
  - The wrappers don't expose the indicator props per field; use `app.config`.
69
68
  - Text props are the i18n hook: pass translated `optionalText`/`requiredText` (or set them in
@@ -101,7 +100,7 @@ Hooks: `data-input-variant`, `data-invalid`, `.input-label-indicator[data-indica
101
100
  - Removed props that were never read: `name` and `theme`. Wrappers stopped passing them.
102
101
  - Wrappers also passed a non-prop `:for`, which fell through and overrode the `for` built from
103
102
  `id`. For the text/number/select/textarea/range wrappers it was the same value (dropped). For
104
- `ToggleSwitchWithLabel`/`ToggleSwitchWithLabelInline` it was the only thing pointing the label
103
+ `ToggleSwitchField` it was the only thing pointing the label
105
104
  at the real input (`toggle-switch-<id>`); they now pass `:id="toggleSwitchId"` explicitly.
106
105
  - `inputVariant` moved from a bare class (`.normal`/`.underlined`, whose rules were duplicates of
107
106
  the base rule) to `data-input-variant`. `fieldHasError` (accepted but unused) now renders
@@ -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 `InputText`. |
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,17 +47,17 @@ 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
- (no separate divider token), same as `InputTextCore`/`InputTextAsNumberWithLabel`.
60
+ (no separate divider token), same as `InputText`/`InputTextAsNumberField`.
61
61
 
62
62
  ---
63
63
 
@@ -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`
@@ -117,7 +117,7 @@ score-1/5 state. Several latent bugs were fixed alongside the standard checklist
117
117
 
118
118
  ---
119
119
 
120
- ## Fixed 2026-09-22 (follow-up): rewritten to visually mirror InputTextCore
120
+ ## Fixed 2026-09-22 (follow-up): rewritten to visually mirror InputText
121
121
 
122
122
  The first migration pass above only made `InputNumber`'s *own* (pre-existing, previously dead)
123
123
  CSS active — it didn't reconsider whether that CSS was actually the right design. Once live, it
@@ -125,21 +125,21 @@ was visually inconsistent with the rest of `05.forms`: no border-radius, no hove
125
125
  state, wrong padding-token family (`--form-textarea-padding-*` instead of the shared input
126
126
  padding tokens), and a box-shadow-toggle focus mechanism instead of the `:has()`-based outline
127
127
  approach every other input in this library uses. `InputNumber` is meant to be a native
128
- `<input type="number">` sibling to the text-based `InputTextAsNumberWithLabel` variant (see
129
- `input-text-core.md`'s Variants section) — both should look and behave the same. The CSS was rewritten
130
- to mirror `InputTextCore` directly:
128
+ `<input type="number">` sibling to the text-based `InputTextAsNumberField` variant (see
129
+ `input-text.md`'s Variants section) — both should look and behave the same. The CSS was rewritten
130
+ to mirror `InputText` directly:
131
131
 
132
- - Added `:input-variant` (`"normal"` / `"outlined"` / `"underlined"`), matching `InputTextCore`'s
132
+ - Added `:input-variant` (`"normal"` / `"outlined"` / `"underlined"`), matching `InputText`'s
133
133
  prop and wrapper-class shape (`InputNumberField` forwards it the same way
134
- `InputTextWithLabel` forwards it to `InputTextCore`, including to `InputLabel`).
135
- - Replaced the flat, unconditional border/radius/box-shadow with `InputTextCore`'s `.normal`
134
+ `InputTextField` forwards it to `InputText`, including to `InputLabel`).
135
+ - Replaced the flat, unconditional border/radius/box-shadow with `InputText`'s `.normal`
136
136
  variant shape: border + `border-radius: var(--form-input-border-radius)` (previously this read
137
137
  `var(--form-element-border-width)` — a pre-existing bug that used the *border-width* token as
138
138
  the *radius*, coincidentally invisible while the block was dead), plus `:has(input:hover)` /
139
139
  `:has(input:focus-visible)` outline states instead of the old `--_focus-box-shadow` custom
140
140
  property toggle.
141
141
  - `--input-number-border-hover` and `--input-number-border-focus` are new public tokens for
142
- these two states (both fall back to `--theme-border-focus`, same as `InputTextCore`).
142
+ these two states (both fall back to `--theme-border-focus`, same as `InputText`).
143
143
  - Padding moved from `--form-textarea-padding-*` (the wrong token family — textarea, not input)
144
144
  to the shared `--input-padding-block`/`--input-padding-inline`/`--input-min-height` tokens every
145
145
  other `05.forms` text-like input uses.
@@ -148,7 +148,7 @@ to mirror `InputTextCore` directly:
148
148
  previously it had neither, an accessibility gap beyond the visual mismatch (checklist item 9).
149
149
  - The divider between the input and an embedded button now reuses the border colour
150
150
  (`--_border`) directly instead of a separate `--input-number-divider-color` token — removed,
151
- since `InputTextCore` doesn't have an equivalent separate token either (the border colour
151
+ since `InputText` doesn't have an equivalent separate token either (the border colour
152
152
  *is* the divider colour there too).
153
153
  - `--_border-hover` and `--_surface-hover` were each used only once with no composition or
154
154
  state-swap, so per the public-token rule they were inlined directly
@@ -158,21 +158,21 @@ to mirror `InputTextCore` directly:
158
158
  wrapper's own focus outline *and* the embedded button's focus outline both read it).
159
159
  - Fixed 2026-10-04: the wrapper's hover and focus outlines only matched `input:hover` /
160
160
  `input:focus-visible`, so hovering or tabbing to a step button showed no outer ring, unlike
161
- `InputTextCore` (and so `InputTextAsNumberWithLabel`), which matches `button` too. Both
162
- selectors are now `:has(input:…, button:…)`, mirroring `InputTextCore`.
161
+ `InputText` (and so `InputTextAsNumberField`), which matches `button` too. Both
162
+ selectors are now `:has(input:…, button:…)`, mirroring `InputText`.
163
163
 
164
164
  **Separately found in the same investigation** (not an `InputNumber` bug, but what the visual
165
- comparison was originally checking against): `InputTextAsNumberWithLabel.vue` had its own,
166
- unrelated pre-existing bug — see `input-text-core.md`'s Variants section for the fix.
165
+ comparison was originally checking against): `InputTextAsNumberField.vue` had its own,
166
+ unrelated pre-existing bug — see `input-text.md`'s Variants section for the fix.
167
167
 
168
168
  **Correction, same day**: the first version of this rewrite still didn't visually match — the
169
169
  divider border and button sizing (`aspect-ratio: 1`, `width: var(--input-min-height)`) had been
170
170
  put on the `.left-slot`/`.right-slot` *wrapper divs*, not on `.input-button` itself, unlike
171
- `InputTextCore`'s equivalent block. That left an unstyled gap between the button and the border
171
+ `InputText`'s equivalent block. That left an unstyled gap between the button and the border
172
172
  line instead of a flush edge, and the buttons weren't sized to match the input's height. Fixed by
173
173
  moving the divider border and all button sizing/colour rules onto `.input-button` directly
174
174
  (`&.has-left-slot .left-slot .input-button { border-right: ... }`, matching
175
- `InputTextAsNumberWithLabel`'s `.left-slot .input-button { border-right: ... }` exactly)
175
+ `InputTextAsNumberField`'s `.left-slot .input-button { border-right: ... }` exactly)
176
176
  rather than the containing `.slot` div. When mirroring another component's CSS structure, check
177
177
  *which element* a rule targets, not just that an equivalent rule exists somewhere in the block —
178
178
  a border/sizing rule one level off in the DOM tree produces a visually-close-but-not-matching
@@ -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.