srcdev-nuxt-components 9.6.7 → 9.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +14 -0
  2. package/.claude/skills/component-compliance-checklist.md +18 -1
  3. package/.claude/skills/component-inline-action-button.md +1 -1
  4. package/.claude/skills/component-local-style-override.md +1 -1
  5. package/.claude/skills/component-naming.md +13 -13
  6. package/.claude/skills/components/container-glow.md +2 -2
  7. package/.claude/skills/components/form-field.md +2 -2
  8. package/.claude/skills/components/form-wrapper.md +1 -1
  9. package/.claude/skills/components/glowing-border.md +1 -1
  10. package/.claude/skills/components/input-checkbox-radio.md +19 -3
  11. package/.claude/skills/components/input-checkbox.md +14 -3
  12. package/.claude/skills/components/input-description.md +7 -7
  13. package/.claude/skills/components/input-error.md +3 -3
  14. package/.claude/skills/components/input-label.md +7 -8
  15. package/.claude/skills/components/input-number.md +60 -29
  16. package/.claude/skills/components/input-radio.md +15 -1
  17. package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
  18. package/.claude/skills/components/input-select.md +153 -0
  19. package/.claude/skills/components/input-text.md +220 -0
  20. package/.claude/skills/components/input-textarea.md +147 -0
  21. package/.claude/skills/components/select-menu.md +4 -4
  22. package/.claude/skills/components/toggle-switch.md +143 -0
  23. package/.claude/skills/composable-zod-validation.md +1 -1
  24. package/.claude/skills/icon-sets.md +1 -1
  25. package/.claude/skills/icon-sizing.md +1 -1
  26. package/.claude/skills/index.md +10 -10
  27. package/.claude/skills/storybook-add-story.md +1 -1
  28. package/.claude/skills/testing-add-unit-test.md +17 -0
  29. package/.claude/skills/theming-component-token-pattern.md +8 -8
  30. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  31. package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
  32. package/.vscode/srcdev-component-input-range.code-snippets +38 -18
  33. package/.vscode/srcdev-component-input-select.code-snippets +20 -14
  34. package/.vscode/srcdev-component-input-text.code-snippets +46 -40
  35. package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
  36. package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
  37. package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
  38. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
  39. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
  40. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
  41. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
  42. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
  43. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
  44. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
  45. package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
  46. package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
  47. package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
  48. package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
  49. package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
  50. package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
  51. package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
  52. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
  53. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
  54. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
  55. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
  56. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
  57. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
  58. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
  59. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
  60. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
  61. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
  62. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +7 -7
  63. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +6 -6
  64. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +50 -6
  65. package/app/components/05.forms/input-number/InputNumber.vue +39 -27
  66. package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
  67. package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
  68. package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
  69. package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
  70. package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
  71. package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
  72. package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
  73. package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
  74. package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
  75. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
  76. package/app/components/05.forms/input-range/InputRange.vue +247 -0
  77. package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
  78. package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
  79. package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
  80. package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
  81. package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
  82. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
  83. package/app/components/05.forms/input-select/InputSelect.vue +198 -0
  84. package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
  85. package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
  86. package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
  87. package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
  88. package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
  89. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
  90. package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
  91. package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
  92. package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
  93. package/app/components/05.forms/input-text/InputTextField.vue +168 -0
  94. package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
  95. package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
  96. package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
  97. package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
  98. package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
  99. package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
  100. package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
  101. package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
  102. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
  103. package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
  104. package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
  105. package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
  106. package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
  107. package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
  108. package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
  109. package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
  110. package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
  111. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +34 -36
  112. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
  113. package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
  114. package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
  115. package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
  116. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
  117. package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
  118. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
  119. package/package.json +1 -1
  120. package/.claude/skills/components/input-select-core.md +0 -151
  121. package/.claude/skills/components/input-text-core.md +0 -204
  122. package/.claude/skills/components/input-textarea-core.md +0 -154
  123. package/.claude/skills/components/toggle-switch-core.md +0 -169
  124. package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
  125. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
  126. package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
  127. package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
  128. package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
  129. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
  130. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
  131. package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
  132. package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
  133. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
  134. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
  135. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
  136. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
  137. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
  138. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
  139. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
  140. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
  141. package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
  142. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
  143. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
  144. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
  145. package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
  146. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
  147. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
  148. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
  149. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
  150. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
  151. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
  152. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
@@ -1,15 +1,20 @@
1
- # InputRangeCore Component
1
+ # InputRange Component
2
+
3
+ > **Renamed 2026-10-16**: `InputRangeCore` → `InputRange`, `InputRangeDefault` → `InputRangeField`
4
+ > (DOM classes `.input-range-core` → `.input-range`, `.input-range-with-label` →
5
+ > `.input-range-field`; full class list in CONSUMER-STYLING.md). Skill doc renamed from
6
+ > `input-range-core.md`. No consumer app used either old name.
2
7
 
3
8
  ## Overview
4
9
 
5
- `InputRangeCore` is the low-level native `<input type="range">` primitive for the `05.forms`
10
+ `InputRange` is the low-level native `<input type="range">` primitive for the `05.forms`
6
11
  range-slider family. It renders a single native range input with left/right button slots (for
7
12
  step-down/step-up controls), an optional `markers` slot for custom tick marks overlaid on the
8
13
  track, and an optional `datalist` slot for native tick-mark snapping. It has no label,
9
- description, or error-message rendering of its own — that's composed by the `InputRangeDefault`
14
+ description, or error-message rendering of its own — that's composed by the `InputRangeField`
10
15
  variant below.
11
16
 
12
- Most consumers should reach for **InputRangeDefault** rather than `InputRangeCore` directly.
17
+ Most consumers should reach for **InputRangeField** rather than `InputRange` directly.
13
18
 
14
19
  ---
15
20
 
@@ -20,7 +25,7 @@ Most consumers should reach for **InputRangeDefault** rather than `InputRangeCor
20
25
  | Prop (template form) | Type | Default | Notes |
21
26
  |------|------|---------|-------|
22
27
  | `:id` | `string` | (required) | Applied to the native input. |
23
- | `:name` | `string` | (required) | Applied to the native input, and used to derive the `list` id when the `datalist` slot is used (`${name}-datalist`). |
28
+ | `:name` | `string` | (required) | Applied to the native input. (Until 2026-10-16 it also built the datalist id, `${name}-datalist`; that id now comes from `useId()`.) |
24
29
  | `:min` | `number` | (required) | Native `min`. |
25
30
  | `:max` | `number` | (required) | Native `max`. |
26
31
  | `:step` | `number` | `1` | Native `step`. |
@@ -28,8 +33,8 @@ Most consumers should reach for **InputRangeDefault** rather than `InputRangeCor
28
33
  | `:placeholder` | `string` | `""` | Declared but not rendered — range inputs have no placeholder concept. Kept for prop-shape consistency with sibling `05.forms` components. |
29
34
  | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
30
35
  | `:weight` | `FormWeight` | `"normal"` | Adds an `input-range--{weight}` class to the input. **No CSS in the library currently styles this class — it's a no-op today**, kept for prop-shape consistency with sibling components. |
31
- | `:field-has-error` | `boolean` | `false` | Declared, but doesn't drive any visible state inside `InputRangeCore` itself — `InputRangeDefault` handles the actual error styling (`data-invalid`/`.error` class) on its own wrapper. |
32
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. |
36
+ | `:field-has-error` | `boolean` | `false` | `data-invalid` on the wrapper and `aria-invalid="true"` on the input (it did nothing here until 2026-10-16). |
37
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. Reacts to changes. |
33
38
  | `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
34
39
 
35
40
  ### v-model
@@ -50,8 +55,8 @@ disable, not a fake default).
50
55
  |------|-------|
51
56
  | `left` | Rendered before the track (e.g. a step-down button). |
52
57
  | `right` | Rendered after the track (e.g. a step-up button). |
53
- | `markers` | Rendered as an overlay on top of the track (`grid-area: element-stack`, same cell as the input). Adds a `has-markers` class to the input, which shrinks its visible track to a thin `0.2rem` strip and hides the native thumb, so your marker content becomes the primary visual. Expected shape: a `.input-range-markers` wrapper containing `.marker` elements (each optionally containing a `.marker-icon`) — see the component's own scoped CSS for the classes it pre-styles. |
54
- | `datalist` | Rendered after the input. Must contain a real `<datalist>` element whose `id` matches `${name}-datalist` for the browser to link it via the input's `list` attribute — the component computes that id string for the `list` attribute itself, but does not set the `id` on your slotted `<datalist>` for you. |
58
+ | `markers` | Rendered as an overlay on top of the track (`grid-area: element-stack`, same cell as the input). Sets `data-has-markers` on the wrapper: a line joining the markers is drawn behind them, the input becomes a transparent layer over the whole marker row (so the row is draggable and clickable), and the thumb is restyled as a ring the size of one marker that circles the selected one. That lines up because markers are spaced `space-between` and given an explicit size (icon size + 2 × padding), the same as the thumb. Use one marker per step (`(max - min) / step + 1`). Expected shape: a `.input-range-markers` wrapper containing `.input-range-marker` elements (each optionally containing an `.input-range-marker-icon`); those classes are pre-styled (they were `.marker`/`.marker-icon` until 2026-10-16). |
59
+ | `datalist` | Rendered after the input, with a `datalistId` slot prop: put it on your `<datalist>` (`<template #datalist="{ datalistId }"><datalist :id="datalistId">`) and the input's `list` points at it. The id comes from `useId()`, so two ranges sharing a `name` don't collide. `list` is omitted when the slot is empty. |
55
60
 
56
61
  ---
57
62
 
@@ -93,13 +98,13 @@ was itself commented out.
93
98
 
94
99
  ## Variants
95
100
 
96
- ### InputRangeDefault
101
+ ### InputRangeField
97
102
 
98
- `InputRangeDefault` (`InputRangeDefault.vue`) composes `InputRangeCore` with `InputLabel`,
103
+ `InputRangeField` (`InputRangeField.vue`) composes `InputRange` with `InputLabel`,
99
104
  `InputDescription`, and `InputError`, plus optional step-down/step-up buttons (`InputButton`)
100
105
  wired into the `left`/`right` slots.
101
106
 
102
- **Additional props over InputRangeCore:**
107
+ **Additional props over InputRange:**
103
108
 
104
109
  | Prop (template form) | Type | Default | Notes |
105
110
  |------|------|---------|-------|
@@ -110,11 +115,11 @@ wired into the `left`/`right` slots.
110
115
 
111
116
  **Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`), plus
112
117
  `left`/`right`/`markers`/`datalist` (forwarded straight through to the underlying
113
- `InputRangeCore` — the step-down/step-up buttons only render when you use the `left`/`right`
118
+ `InputRange`; `markers` wasn't forwarded until 2026-10-16, and `datalist` passes `datalistId` on — the step-down/step-up buttons only render when you use the `left`/`right`
114
119
  slots yourself, since the button icon content itself is consumer-supplied via `#iconOnly`).
115
120
 
116
121
  ```vue
117
- <InputRangeDefault
122
+ <InputRangeField
118
123
  name="priceRange"
119
124
  label="Price range"
120
125
  v-model="price"
@@ -126,17 +131,47 @@ slots yourself, since the button icon content itself is consumer-supplied via `#
126
131
  >
127
132
  <template #left><Icon name="mdi:minus" /></template>
128
133
  <template #right><Icon name="mdi:plus" /></template>
129
- </InputRangeDefault>
134
+ </InputRangeField>
130
135
  ```
131
136
 
132
- `v-model` is `number`, **required** — same as `InputRangeCore` above.
137
+ `v-model` is `number`, **required** — same as `InputRange` above.
138
+
139
+ Each step-button press adds or subtracts `step` (its absolute value, or 1 when it's 0), rounds away
140
+ floating-point drift (0.2 + 0.1 gives 0.3) and clamps to `min`/`max`; step-down goes read-only at or
141
+ below `min`, step-up at or above `max`. `:style-class-passthrough` lands on the root
142
+ `.input-range-field` only (it also reached the inner input until 2026-10-16) and reacts to changes.
143
+ The root carries `data-theme` and `data-invalid` (its old `error` class is gone), and the label
144
+ gets `input-range-label` (it was the number field's `input-number-label`, with a non-existent
145
+ `body-normal-bold` utility).
133
146
 
134
147
  **Fixed 2026-09-21**: removed a dead `deepCssClassPassthrough` prop (declared, never consumed
135
148
  anywhere in the component or referenced elsewhere in the codebase) and a dead `<style>` block
136
149
  targeting `.input-range-label`/`.label-description` — classes that nothing in this component (or
137
150
  `InputLabel`/`InputDescription`) actually renders, so the block had no effect. Also removed two
138
151
  dead commented-out lines (`// const id = useId();`, a duplicate no-op `FormUiTheme` computed), and
139
- (same day, follow-up pass) a `const props = withDefaults(...)` in both `InputRangeCore` and
140
- `InputRangeDefault` that had become entirely unused once the dead `FormUiTheme` computed was
152
+ (same day, follow-up pass) a `const props = withDefaults(...)` in both `InputRange` and
153
+ `InputRangeField` that had become entirely unused once the dead `FormUiTheme` computed was
141
154
  removed — the template's `:id`/`:name`/etc. shorthand bindings resolve straight from `defineProps`
142
155
  automatically in `<script setup>`, no destructuring or `props.` reference needed.
156
+
157
+ ---
158
+
159
+ ## Stress pass 2026-10-16
160
+
161
+ - **Stepping** (`InputRangeField`): 0.1 steps drifted (0.30000000000000004) and a step that didn't
162
+ divide the range overshot `min`/`max`; the read-only checks used `===`, so an overshot value
163
+ never disabled the button. Now rounded, clamped, and `<=`/`>=`. A zero or negative `step`
164
+ steps by its size, or 1.
165
+ - **Datalist id**: was `${name}-datalist`, so two ranges sharing a `name` linked to the same list
166
+ (rule `ids-from-useid.md`). Now `useId()`, handed to the slot as `datalistId`.
167
+ - **Error state**: `fieldHasError` did nothing on `InputRange`; it now sets `data-invalid` and
168
+ `aria-invalid`.
169
+ - **Slots**: the Field didn't forward `markers`.
170
+ - **Classes**: bare/generic classes became prefixed classes or data attributes, and the marker
171
+ defaults moved from dark-page colours to `--theme-*` tokens (CONSUMER-STYLING.md has the
172
+ mapping). The Field's passthrough no longer reaches the inner input as well as its root.
173
+ - **Overflow**: the slider container gets `min-inline-size: 0` and the step buttons don't
174
+ shrink; the Field wraps long label, description and error copy.
175
+ - Specs unmount in `afterEach`; stories moved to `StoryObj` with Docs descriptions, a
176
+ `DecimalSteps` story, `CanvasSwitcher` and a `StressTest` story on the Field. The bare
177
+ control's step-button demo uses `InputButton`, not raw buttons.
@@ -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.
@@ -0,0 +1,220 @@
1
+ # InputText Component
2
+
3
+ > **Renamed 2026-10-16**: `InputTextCore` → `InputText`, `InputTextWithLabel` → `InputTextField`,
4
+ > `InputPasswordWithLabel` → `InputPasswordField`, `InputTextAsNumberWithLabel` →
5
+ > `InputTextAsNumberField` (DOM classes `.input-text-core` → `.input-text`,
6
+ > `.input-text-with-label` → `.input-text-field`; full class list in CONSUMER-STYLING.md). Skill
7
+ > doc renamed from `input-text-core.md`. Consumer apps use the first three old names; list their
8
+ > edits privately and apply them when each upgrades.
9
+
10
+ ## Overview
11
+
12
+ `InputText` is the low-level native `<input>` primitive for the `05.forms` text-input family. It
13
+ renders one styled text-like input with left/right slots, focus/dirty state models, and digit-only
14
+ typing for `inputmode="numeric"`. It has no label, description or error rendering of its own:
15
+ those come from the Field wrappers under **Variants**, or pair it with your own `<label for>`.
16
+
17
+ Most consumers should reach for one of the Variants rather than `InputText` directly.
18
+
19
+ ---
20
+
21
+ ## Props reference
22
+
23
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:input-variant`, `:aria-describedby`.
24
+
25
+ | Prop (template form) | Type | Default | Notes |
26
+ |---|---|---|---|
27
+ | `:type` | `InputTypesText` (`"text" \| "email" \| "password" \| "number" \| "tel" \| "url" \| "date"`) | `"text"` | Native input type. |
28
+ | `:inputmode` | `InputMode` | `"text"` | Virtual keyboard hint. `"numeric"` also adds `pattern="[0-9]+"` and blocks typing anything but digits (a `beforeinput` handler; deletions and paste without data pass). Sets `data-inputmode` on the wrapper. |
29
+ | `:maxlength` | `number` | `255` | Native `maxlength`. |
30
+ | `:min` / `:max` | `string \| number` | (optional) | Passed straight through, e.g. an ISO date for `type="date"`. Inert on text-like types. |
31
+ | `:id` | `string` | (required) | Applied to the native input. |
32
+ | `:name` | `string` | (required) | Applied to the native input. |
33
+ | `:required` | `boolean` | `false` | Native `required`. |
34
+ | `:readonly` | `boolean` | `false` | Native `readonly`; sets `data-readonly` on the wrapper. Added 2026-10-16: a `readonly` attribute used to fall through onto the wrapper div, not the input. |
35
+ | `:autocomplete` | `string` | (unset) | Native `autocomplete` token (`email`, `name`, `new-password`...). Added 2026-10-16. |
36
+ | `:placeholder` | `string` | `""` | Native placeholder. |
37
+ | `:field-has-error` | `boolean` | `false` | `data-invalid` on the wrapper and `aria-invalid="true"` on the input. It does **not** change `theme`. |
38
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the native input. Reacts to changes. |
39
+ | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
40
+ | `:aria-describedby` | `string` | `""` | Forwarded to the native input; omitted when empty. |
41
+ | `:input-variant` | `InputUiVariant` | `"normal"` | Sets `data-input-variant`. `normal` (bordered box) and `underlined` have CSS; `outlined` doesn't. |
42
+
43
+ ### v-models
44
+
45
+ | Model | Type | Notes |
46
+ |---|---|---|
47
+ | `v-model` | `string` | **Required** (`defineModel({ required: true })`). |
48
+ | `v-model:is-dirty` | `boolean` | Read only: `InputText` never sets it. The Fields set it once the value is non-empty. Reflected as `data-dirty`. |
49
+ | `v-model:is-active` | `boolean` | Tracks focus (`focusin`/`focusout`). Reflected as `data-active`. |
50
+
51
+ ---
52
+
53
+ ## Slots
54
+
55
+ | Slot | Notes |
56
+ |---|---|
57
+ | `left` | Rendered before the input in `.input-text-slot-left`, inside the bordered wrapper (an icon, or `InputButton variant="inline"`, which the slot styles). Sets `data-has-left-slot`. |
58
+ | `right` | Same, after the input, in `.input-text-slot-right`. Sets `data-has-right-slot`; a right-slot `InputButton` removes the wrapper's end padding so it sits flush. |
59
+
60
+ ---
61
+
62
+ ## CSS token API
63
+
64
+ See [CONSUMER-STYLING.md](../../app/components/05.forms/input-text/CONSUMER-STYLING.md) for the
65
+ full table with defaults and the state hooks.
66
+
67
+ **Common tokens:**
68
+
69
+ - `--input-text-surface` / `-surface-hover` — wrapper background, slot button hover
70
+ - `--input-text-border` / `-border-hover` / `-border-focus` — border and outline colours
71
+ - `--input-text-color` — input text colour (also the browser-autofill text colour, global rule)
72
+ - `--input-text-placeholder-color` — placeholder colour
73
+
74
+ **State hooks** (on `.input-text-wrapper`): `data-input-variant`, `data-theme`, `data-invalid`,
75
+ `data-dirty`, `data-active`, `data-readonly`, `data-inputmode`, `data-has-left-slot`,
76
+ `data-has-right-slot`.
77
+
78
+ ---
79
+
80
+ ## Using `min`/`max` for a native date picker
81
+
82
+ ```vue
83
+ <InputText
84
+ id="appointment-date"
85
+ v-model="appointmentDate"
86
+ name="appointmentDate"
87
+ type="date"
88
+ :min="new Date().toISOString().split('T')[0]"
89
+ max="2027-12-31"
90
+ />
91
+ ```
92
+
93
+ The browser enforces the range in its picker; validate the submitted value server-side as well.
94
+
95
+ ---
96
+
97
+ ## Changed 2026-10-16: rename, state hooks, wrapper fixes
98
+
99
+ - Renamed to the Control/Field convention (note at the top).
100
+ - State classes became data attributes (`.normal`/`.underlined` → `[data-input-variant]`,
101
+ `.dirty`/`.active`/`.error` → `[data-dirty]`/`[data-active]`/`[data-invalid]`,
102
+ `.has-left-slot`/`.has-right-slot` → `[data-has-*-slot]`); `.slot.left-slot`/`.right-slot` →
103
+ `.input-text-slot-left`/`-right`. The input no longer carries `dirty`/`active` classes.
104
+ - Numeric filtering used a `beforeinput` listener added in `onMounted` that was never removed and
105
+ added a new `input` listener per keystroke (it reverted pattern mismatches after the fact). It's
106
+ now a template `@beforeinput` handler that cancels non-digit input up front, and it follows
107
+ `inputmode` changes after mount.
108
+ - `aria-describedby=""` is no longer rendered when empty; an unused `FormUiTheme` computed is gone.
109
+ - `InputTextField` passed `label` and `error-message` to `InputText`, which has no such props, so
110
+ they landed on `.input-text-wrapper` as HTML attributes. It also applied its passthrough to both
111
+ its root and the input; now its root only.
112
+ - `InputPasswordField` forwarded both description slots unconditionally, so every password field
113
+ rendered an empty `InputDescription` and pointed `aria-describedby` at it. Slots now forward
114
+ only when filled. Its toggle icon class is `.input-password-field-icon` (was `.icon`).
115
+ - `InputTextAsNumberField`: step buttons overshot `min`/`max` and drifted on fractional steps
116
+ (now round, clamp and start from 0 when empty, like `InputNumberField`); its own CSS read
117
+ `--theme-*` directly and now uses the `--input-text-*` tokens; the step buttons ignored `theme`;
118
+ `inputmode` follows `min`/`step` (a negative `min` or fractional `step` couldn't be typed); the
119
+ input's width is capped at `12em`; root class `.input-text-as-number-field` (it reused
120
+ `.input-text-with-label`), and `InputError` gets `input-variant`.
121
+ - The global autofill rule in `04.elements/forms/00.element-defaults.css` read an undefined
122
+ `--_input-text-core-color`, so autofilled text lost its colour; it now uses `--input-text-color`.
123
+
124
+ ---
125
+
126
+ ## Variants
127
+
128
+ Three wrappers compose `InputText` with `InputLabel`, `InputDescription` and `InputError`. Ids
129
+ come from `useAriaDescribedById`, so they take `name`, not `id`. Each applies
130
+ `style-class-passthrough` to its own root only.
131
+
132
+ ### InputTextField
133
+
134
+ The general-purpose labelled text field. Root `.input-text-field`, with `data-theme`,
135
+ `data-input-variant`, `data-invalid`, `data-dirty` (set once the value is non-empty) and
136
+ `data-active`.
137
+
138
+ ```vue
139
+ <InputTextField
140
+ v-model="email"
141
+ name="email"
142
+ type="email"
143
+ label="Email address"
144
+ autocomplete="email"
145
+ :error-message="errors.email"
146
+ :field-has-error="!!errors.email"
147
+ >
148
+ <template #descriptionText>We only use it to reply to you.</template>
149
+ </InputTextField>
150
+ ```
151
+
152
+ Props: `type` (required), `inputmode`, `maxlength`, `min`, `max`, `name`, `placeholder`,
153
+ `required`, `readonly`, `autocomplete`, `fieldHasError`, `theme`, `inputVariant`, plus `label`
154
+ (required), `errorMessage: object | string` (required, pass `""` if unused) and
155
+ `styleClassPassthrough`. Slots: `left`, `right`, `descriptionText`, `descriptionHtml`. The
156
+ description renders before the input, or after the input and error for `outlined`.
157
+
158
+ ### InputPasswordField
159
+
160
+ `InputTextField` (root `.input-text-field.input-password-field`) with a show/hide toggle
161
+ (`InputButton variant="inline"` in the right slot) that swaps `type` between `password` and
162
+ `text`.
163
+
164
+ ```vue
165
+ <InputPasswordField
166
+ v-model="password"
167
+ name="password"
168
+ label="Password"
169
+ autocomplete="new-password"
170
+ :error-message="errors.password"
171
+ :field-has-error="!!errors.password"
172
+ />
173
+ ```
174
+
175
+ Extra props: `showPasswordText`/`hidePasswordText` (defaults `"Show password"`/`"Hide password"`,
176
+ the toggle's accessible name; pass translated strings), `autocomplete` (default
177
+ `"current-password"`; use `"new-password"` on sign-up). A passed `type` is accepted and ignored.
178
+ Slots: `descriptionText`, `descriptionHtml`.
179
+
180
+ ### InputTextAsNumberField
181
+
182
+ A compact numeric stepper on a text input (no native spinner). Root
183
+ `.input-text-as-number-field`.
184
+
185
+ ```vue
186
+ <InputTextAsNumberField
187
+ v-model="quantity"
188
+ name="quantity"
189
+ label="Quantity"
190
+ :min="1"
191
+ :max="99"
192
+ step-down-text="Decrease quantity"
193
+ step-up-text="Increase quantity"
194
+ :error-message="errors.quantity"
195
+ >
196
+ <template #left><Icon name="mdi:minus" /></template>
197
+ <template #right><Icon name="mdi:plus" /></template>
198
+ </InputTextAsNumberField>
199
+ ```
200
+
201
+ - `v-model` is `number | undefined`: clearing the field sets `undefined`; text that isn't a
202
+ finite number is ignored.
203
+ - Step buttons appear when the `left`/`right` slots are filled (the slot content is the icon).
204
+ They step by `step`, round to its precision, clamp to `min`/`max` (pulling a typed
205
+ out-of-range value back in), start from 0 (clamped) when empty, and go read-only at the limits.
206
+ - `inputmode` is `numeric` (digits only) for a whole-number, non-negative range, `decimal` for a
207
+ fractional `step`, `text` for a negative `min`.
208
+ - Extra props: `min`, `max` (required), `step` (default `1`), `stepDownText`/`stepUpText`
209
+ (defaults `"Step down"`/`"Step up"`).
210
+
211
+ ---
212
+
213
+ ## Accessibility
214
+
215
+ - `field-has-error` sets `aria-invalid="true"`; the Fields link the error and description via
216
+ `aria-describedby`.
217
+ - Focus is shown on the wrapper with `:has(input:focus-visible, button:focus-visible)`.
218
+ - The password and stepper buttons get accessible names through `InputButton`'s `buttonText`
219
+ (visually hidden in icon-only mode).
220
+ - Use `autocomplete` on personal-data fields (WCAG 1.3.5).