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.
- package/.claude/rules/consumer/ids-from-useid.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +18 -1
- package/.claude/skills/component-inline-action-button.md +1 -1
- package/.claude/skills/component-local-style-override.md +1 -1
- package/.claude/skills/component-naming.md +13 -13
- package/.claude/skills/components/container-glow.md +2 -2
- package/.claude/skills/components/form-field.md +2 -2
- package/.claude/skills/components/form-wrapper.md +1 -1
- package/.claude/skills/components/glowing-border.md +1 -1
- package/.claude/skills/components/input-checkbox-radio.md +19 -3
- package/.claude/skills/components/input-checkbox.md +14 -3
- package/.claude/skills/components/input-description.md +7 -7
- package/.claude/skills/components/input-error.md +3 -3
- package/.claude/skills/components/input-label.md +7 -8
- package/.claude/skills/components/input-number.md +60 -29
- package/.claude/skills/components/input-radio.md +15 -1
- package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
- package/.claude/skills/components/input-select.md +153 -0
- package/.claude/skills/components/input-text.md +220 -0
- package/.claude/skills/components/input-textarea.md +147 -0
- package/.claude/skills/components/select-menu.md +4 -4
- package/.claude/skills/components/toggle-switch.md +143 -0
- package/.claude/skills/composable-zod-validation.md +1 -1
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/icon-sizing.md +1 -1
- package/.claude/skills/index.md +10 -10
- package/.claude/skills/storybook-add-story.md +1 -1
- package/.claude/skills/testing-add-unit-test.md +17 -0
- package/.claude/skills/theming-component-token-pattern.md +8 -8
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
- package/.vscode/srcdev-component-input-range.code-snippets +38 -18
- package/.vscode/srcdev-component-input-select.code-snippets +20 -14
- package/.vscode/srcdev-component-input-text.code-snippets +46 -40
- package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
- package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
- package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
- package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
- package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
- package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
- package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
- package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
- package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
- package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +7 -7
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +6 -6
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +50 -6
- package/app/components/05.forms/input-number/InputNumber.vue +39 -27
- package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
- package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
- package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
- package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
- package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
- package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
- package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
- package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
- package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
- package/app/components/05.forms/input-range/InputRange.vue +247 -0
- package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
- package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
- package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
- package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
- package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
- package/app/components/05.forms/input-select/InputSelect.vue +198 -0
- package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
- package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
- package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
- package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
- package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
- package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
- package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
- package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
- package/app/components/05.forms/input-text/InputTextField.vue +168 -0
- package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
- package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
- package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
- package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
- package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
- package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
- package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
- package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
- package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
- package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
- package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
- package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +34 -36
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
- package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
- package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
- package/package.json +1 -1
- package/.claude/skills/components/input-select-core.md +0 -151
- package/.claude/skills/components/input-text-core.md +0 -204
- package/.claude/skills/components/input-textarea-core.md +0 -154
- package/.claude/skills/components/toggle-switch-core.md +0 -169
- package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
- package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
- package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
- package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
- package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
- package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
- package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
- package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
- 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()
|
|
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
|
-
- `
|
|
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 `
|
|
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`
|
|
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
|
-
`
|
|
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 (`
|
|
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
|
-
<
|
|
40
|
+
<InputTextField
|
|
41
41
|
v-model="state.email"
|
|
42
42
|
name="email"
|
|
43
43
|
label="Email"
|
|
@@ -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 `
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
(`
|
|
7
|
-
`
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
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 `
|
|
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` (`
|
|
7
|
-
`
|
|
8
|
-
`MultipleRadiobuttons`, `
|
|
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
|
-
(`
|
|
7
|
-
`
|
|
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. `
|
|
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
|
-
- `
|
|
65
|
-
`
|
|
66
|
-
`
|
|
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
|
-
`
|
|
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 `
|
|
33
|
-
| `:input-variant` | `InputUiVariant` | `"normal"` |
|
|
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 `
|
|
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)
|
|
51
|
-
| `right` | Rendered after the input (e.g. a step-up button)
|
|
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
|
|
57
|
-
right-hand mirror
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
-
`
|
|
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
|
|
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 `
|
|
129
|
-
`input-text
|
|
130
|
-
to mirror `
|
|
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 `
|
|
132
|
+
- Added `:input-variant` (`"normal"` / `"outlined"` / `"underlined"`), matching `InputText`'s
|
|
133
133
|
prop and wrapper-class shape (`InputNumberField` forwards it the same way
|
|
134
|
-
`
|
|
135
|
-
- Replaced the flat, unconditional border/radius/box-shadow with `
|
|
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 `
|
|
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 `
|
|
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
|
-
`
|
|
162
|
-
selectors are now `:has(input:…, button:…)`, mirroring `
|
|
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): `
|
|
166
|
-
unrelated pre-existing bug — see `input-text
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
|
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.
|