srcdev-nuxt-components 9.6.8 → 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 (108) hide show
  1. package/.claude/skills/component-compliance-checklist.md +12 -1
  2. package/.claude/skills/component-inline-action-button.md +1 -1
  3. package/.claude/skills/component-local-style-override.md +1 -1
  4. package/.claude/skills/component-naming.md +11 -11
  5. package/.claude/skills/components/container-glow.md +2 -2
  6. package/.claude/skills/components/form-field.md +2 -2
  7. package/.claude/skills/components/form-wrapper.md +1 -1
  8. package/.claude/skills/components/glowing-border.md +1 -1
  9. package/.claude/skills/components/input-description.md +7 -7
  10. package/.claude/skills/components/input-error.md +3 -3
  11. package/.claude/skills/components/input-label.md +7 -8
  12. package/.claude/skills/components/input-number.md +17 -17
  13. package/.claude/skills/components/input-select.md +153 -0
  14. package/.claude/skills/components/input-text.md +220 -0
  15. package/.claude/skills/components/input-textarea.md +147 -0
  16. package/.claude/skills/components/select-menu.md +4 -4
  17. package/.claude/skills/components/toggle-switch.md +143 -0
  18. package/.claude/skills/composable-zod-validation.md +1 -1
  19. package/.claude/skills/icon-sets.md +1 -1
  20. package/.claude/skills/icon-sizing.md +1 -1
  21. package/.claude/skills/index.md +5 -5
  22. package/.claude/skills/storybook-add-story.md +1 -1
  23. package/.claude/skills/theming-component-token-pattern.md +8 -8
  24. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  25. package/.vscode/srcdev-component-input-select.code-snippets +20 -14
  26. package/.vscode/srcdev-component-input-text.code-snippets +46 -40
  27. package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
  28. package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
  29. package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
  30. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
  31. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
  32. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
  33. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
  34. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
  35. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
  36. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
  37. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
  38. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +5 -5
  39. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
  40. package/app/components/05.forms/input-number/InputNumber.vue +2 -2
  41. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
  42. package/app/components/05.forms/input-select/InputSelect.vue +198 -0
  43. package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
  44. package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
  45. package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
  46. package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
  47. package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
  48. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
  49. package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
  50. package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
  51. package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
  52. package/app/components/05.forms/input-text/InputTextField.vue +168 -0
  53. package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
  54. package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
  55. package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
  56. package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
  57. package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
  58. package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
  59. package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
  60. package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
  61. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
  62. package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
  63. package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
  64. package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
  65. package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
  66. package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
  67. package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
  68. package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
  69. package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
  70. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
  71. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
  72. package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
  73. package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
  74. package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
  75. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
  76. package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
  77. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
  78. package/package.json +1 -1
  79. package/.claude/skills/components/input-select-core.md +0 -151
  80. package/.claude/skills/components/input-text-core.md +0 -204
  81. package/.claude/skills/components/input-textarea-core.md +0 -154
  82. package/.claude/skills/components/toggle-switch-core.md +0 -169
  83. package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
  84. package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
  85. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
  86. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
  87. package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
  88. package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
  89. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
  90. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
  91. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
  92. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
  93. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
  94. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
  95. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
  96. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
  97. package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
  98. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
  99. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
  100. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
  101. package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
  102. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
  103. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
  104. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
  105. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
  106. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
  107. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
  108. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
@@ -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
@@ -268,6 +275,10 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
268
275
  defaults to its min-content width and overflows a narrow column (`MultipleCheckboxes`,
269
276
  2026-10-16).
270
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`.
271
282
  - Layout measured in script (connector lines, indicators) re-measures when any measured item
272
283
  resizes or changes class, not only when the container resizes (`measured-layout-observers.md`,
273
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` | - |
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
86
  | ~~InputRangeCore~~ | ✅ `InputRange` (2026-10-16) | - |
87
87
  | ~~InputRangeDefault~~ | ✅ `InputRangeField` (2026-10-16) | - |
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` | - |
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
@@ -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
- `InputRangeField`, `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
- `InputRangeField`, `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`, `InputRangeField`, `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 `InputRangeField` 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
@@ -30,7 +30,7 @@ Most consumers should reach for **InputNumberField** rather than `InputNumber` d
30
30
  | `:required` | `boolean` | `false` | Native `required`. |
31
31
  | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
32
32
  | `:weight` | `FormWeight` | `"normal"` | Adds an `input-number--{weight}` class to the input, for prop-shape consistency with sibling `05.forms` components. **No CSS in the library currently styles this class — it's a no-op today**, same convention as `InputRange`'s `weight` prop. |
33
- | `:input-variant` | `InputUiVariant` | `"normal"` | Rendered as `data-input-variant` on the wrapper (a bare class until 2026-10-16) — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
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
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. |
@@ -57,7 +57,7 @@ slot is present (`.input-number-slot-left .input-button { border-right: ... }` a
57
57
  right-hand mirror), so it sits flush against the button's own edge rather than around the
58
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
 
@@ -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
@@ -0,0 +1,153 @@
1
+ # InputSelect Component
2
+
3
+ > **Renamed 2026-10-16**: `InputSelectCore` → `InputSelect`, `InputSelectWithLabel` →
4
+ > `InputSelectField` (DOM classes `.input-select-core` → `.input-select`,
5
+ > `.input-select-with-label` → `.input-select-field`; full class list in CONSUMER-STYLING.md).
6
+ > Skill doc renamed from `input-select-core.md`. A consumer app uses both old names; list its edits
7
+ > privately and apply them when it upgrades.
8
+
9
+ ## Overview
10
+
11
+ `InputSelect` is the low-level native `<select>` primitive for the `05.forms` select family.
12
+ It renders a native select with a data-driven list of `<option>`s (each optionally decorated with
13
+ an icon), an optional placeholder option, and adopts the modern CSS Customizable Select API
14
+ (`appearance: base-select`) for a fully styleable open-picker state where supported, with a
15
+ graceful CSS fallback (see "Browser support" below). It has no label, description, or
16
+ error-message rendering of its own — that's composed by the `InputSelectField` variant below.
17
+
18
+ Most consumers should reach for **InputSelectField** rather than `InputSelect` directly.
19
+
20
+ ---
21
+
22
+ ## Props reference
23
+
24
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:field-has-error`, `:style-class-passthrough`, `:aria-describedby`.
25
+
26
+ | Prop (template form) | Type | Default | Notes |
27
+ |------|------|---------|-------|
28
+ | `:id` | `string` | (required) | Applied to the native select. |
29
+ | `:name` | `string` | (required) | Applied to the native select. |
30
+ | `:placeholder` | `string` | `""` | Renders a disabled placeholder `<option>`, selected while `modelValue` is `""`, `null` or `undefined` (not `0`). No option rendered at all when unset. |
31
+ | `:title` | `string` | `"Please select an option"` | Native `title` attribute (tooltip). Visitor-facing copy: pass a translated string. |
32
+ | `:required` | `boolean` | `false` | Native `required`. |
33
+ | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
34
+ | `:input-variant` | `InputUiVariant` | `"normal"` | Sets `data-input-variant` on the wrapper. `"normal"` (bordered box) and `"underlined"` (bottom border only) have CSS; `"outlined"` doesn't. |
35
+ | `:field-has-error` | `boolean` | `false` | `data-invalid` on the wrapper and `aria-invalid="true"` on the select. |
36
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the select element. Reacts to changes. |
37
+ | `:aria-describedby` | `string` | `""` | Forwarded to the native select; omitted when empty. |
38
+
39
+ ### v-models
40
+
41
+ - `v-model` — `string | number | readonly number[]`, **required** (`defineModel({ required: true })`, satisfying `vue/require-default-prop` — see `project_ledger_eslint_issues_column` in memory).
42
+ - `v-model:field-data` — `IFormMultipleOptions` (`{ data: IOptionsConfig[]; total; skip; limit }`), **required**. The `data` array drives the rendered `<option>`s; only `data` is read. The template dereferences `fieldData.data` unconditionally, so a consumer that doesn't bind it crashes on render. A one-way `:field-data` works too: the component never writes it.
43
+ - `v-model:is-dirty` — `boolean`. Set to `true` on the native `change` event (never reset to `false` by this component). Reflected as `data-dirty` on the wrapper.
44
+ - `v-model:is-active` — `boolean`. Tracks native focus/blur (`focusin`/`focusout`). Reflected as `data-active` on the wrapper.
45
+
46
+ ### Option icons
47
+
48
+ An `IOptionsConfig` item with `icon` renders an `<Icon class="input-select-option-icon">`
49
+ (`aria-hidden`) before its label. It only shows where `appearance: base-select` is supported; a
50
+ native dropdown renders option text only. Labels render as text, never as markup.
51
+
52
+ ---
53
+
54
+ ## CSS token API
55
+
56
+ See [CONSUMER-STYLING.md](../../app/components/05.forms/input-select/CONSUMER-STYLING.md) for the
57
+ full table with defaults and the state hooks.
58
+
59
+ **Common tokens:**
60
+
61
+ - `--input-select-surface` / `-surface-hover` — wrapper/select background and option-row hover background
62
+ - `--input-select-text-color` — select text colour
63
+ - `--input-select-border` / `-border-hover` / `-border-focus` — border colour resting, mouse-hover outline, and `:focus-visible` outline (also the open picker's border/outline)
64
+ - `--input-select-option-gap` / `-option-icon-size` — option icon spacing and size
65
+
66
+ **State hooks** (on `.input-select-wrapper`): `data-input-variant`, `data-theme`, `data-invalid`,
67
+ `data-dirty`, `data-active`.
68
+
69
+ ---
70
+
71
+ ## Browser support: `appearance: base-select`
72
+
73
+ The open-picker styling (`&::picker(select)`, `:open`/`:not(:open)` states, the transition/opacity
74
+ `@starting-style` reveal) relies on the CSS Customizable Select API
75
+ (`appearance: base-select`/`::picker(select)`), a very new feature not yet supported everywhere.
76
+ `appearance: none` is set first, then overridden by `appearance: base-select` only where the
77
+ browser recognises that value, so elsewhere (including Safari 16) the select falls back to the
78
+ browser's native dropdown with full functionality. This is decorative degradation, not a
79
+ functional break like `ExpandingPanel`'s `::details-content` case (pitfall #19), but the
80
+ open-picker styling should still be spot-checked on a real Safari session before treating it as
81
+ visually final.
82
+
83
+ ---
84
+
85
+ ## Changed 2026-10-16: rename, state hooks, stress fixes
86
+
87
+ - Renamed to the Control/Field convention (see the note at the top).
88
+ - State classes (`.normal`/`.underlined`/`.outlined`, `.dirty`, `.active`, `.error`) became data
89
+ attributes, so a consumer's unlayered `.error` rule can't restyle the field.
90
+ - The placeholder option used `:selected="!modelValue"`, so a model value of `0` also marked the
91
+ placeholder selected. It now checks for an empty value only.
92
+ - `aria-describedby=""` was rendered when no description id was passed; it's now omitted.
93
+ - `styleClassPassthrough` now reacts to prop changes (it was read once at setup).
94
+ - `InputSelectField` applied its passthrough to the inner select; it now applies it to its own
95
+ root only. It also gained a `title` prop (forwarded), and its root is `.input-select-field`
96
+ itself rather than an unclassed outer `<div>` around it.
97
+ - New public tokens: `--input-select-text-color`, `--input-select-option-gap`,
98
+ `--input-select-option-icon-size`.
99
+ - Long option labels wrap in the open picker, and the Field wraps long label, description and
100
+ error copy. `StressTest` stories on both components.
101
+
102
+ ## Fixed 2026-09-22: dead functionality and prop-shape bugs
103
+
104
+ Migrated from a `variants:true`, `eslint_issues:true`, score-2/5 state.
105
+
106
+ - `required` and `styleClassPassthrough` were declared but never bound.
107
+ - `isDirty`/`isActive` were read but nothing set them; `@focusin`/`@focusout`/`@change` added.
108
+ - The placeholder option used `readonly` (not valid on `<option>`); switched to `disabled`.
109
+ - `theme`/`inputVariant` moved to the shared `FormUiTheme`/`InputUiVariant` types; `fieldData`
110
+ moved from an `as Ref<T>` cast to `defineModel<T>("fieldData", { required: true })`.
111
+ - Dead commented-out CSS and single-use private tokens removed (pitfall #20).
112
+
113
+ ---
114
+
115
+ ## Variants
116
+
117
+ ### InputSelectField
118
+
119
+ `InputSelectField` (`InputSelectField.vue`) composes `InputSelect` with `InputLabel`,
120
+ `InputDescription` (before the control, or after the control and error when
121
+ `inputVariant === "outlined"`, matching `InputTextField`'s placement rule), and `InputError`.
122
+ Ids for the label, description and error come from `useAriaDescribedById`.
123
+
124
+ **Additional props over InputSelect** (it takes no `id`; the id is generated):
125
+
126
+ | Prop (template form) | Type | Default | Notes |
127
+ |------|------|---------|-------|
128
+ | `:data-testid` | `string` | `"input-select-field"` | Sets `data-testid` on the root. (Was `"input-select-with-label"` until 2026-10-16.) |
129
+ | `:label` | `string` | (required) | Rendered via `InputLabel`. |
130
+ | `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
131
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Applied to the `.input-select-field` root only, not the inner select. |
132
+
133
+ `name`, `placeholder`, `title`, `required`, `field-has-error`, `theme` and `input-variant` are
134
+ forwarded. While `field-has-error` is set, the inner `InputSelect` gets `theme="error"` whatever
135
+ the `theme` prop says; the Field root's own `data-theme` keeps the prop value.
136
+
137
+ **Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`).
138
+
139
+ ```vue
140
+ <InputSelectField
141
+ v-model="colour"
142
+ v-model:field-data="colourOptions"
143
+ name="colour"
144
+ label="Favourite colour"
145
+ placeholder="Choose a colour"
146
+ :error-message="errors.colour"
147
+ :field-has-error="!!errors.colour"
148
+ >
149
+ <template #descriptionText>Pick the colour you like best</template>
150
+ </InputSelectField>
151
+ ```
152
+
153
+ `v-model` and `v-model:field-data` are both **required**, same as `InputSelect` above.