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
@@ -1,204 +0,0 @@
1
- # InputTextCore Component
2
-
3
- ## Overview
4
-
5
- `InputTextCore` is the low-level native `<input>` primitive for the `05.forms` text-input family.
6
- It renders a single styled text-like input with left/right icon slots, focus/dirty state models,
7
- and numeric-pattern validation for `inputmode="numeric"`. It has no label, description, or error
8
- message rendering of its own — those are composed by the wrapper variants below.
9
-
10
- Most consumers should reach for one of the **Variants** rather than `InputTextCore` directly.
11
-
12
- ---
13
-
14
- ## Props reference
15
-
16
- > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:input-variant`, `:aria-describedby`.
17
-
18
- | Prop (template form) | Type | Default | Notes |
19
- | --------------------------- | ------------------------------------------------------------ | ----------- | ---------------------------------------------------------------------- |
20
- | `:type` | `"text" \| "email" \| "password" \| "number" \| "tel" \| "url" \| "date"` | `"text"` | Native input type. |
21
- | `:inputmode` | `"text" \| "email" \| "tel" \| "url" \| "search" \| "numeric" \| "none" \| "decimal"` | `"text"` | Sets virtual keyboard hint; `"numeric"` also enables pattern validation. |
22
- | `:maxlength` | `number` | `255` | Native `maxlength`. |
23
- | `:min` | `string \| number` | (optional) | Passed straight through to the native input — e.g. earliest allowed date for `type="date"`, or lowest number for `type="number"`. Use an ISO date string (`"2026-01-01"`) for `type="date"`. |
24
- | `:max` | `string \| number` | (optional) | Same as `min`, for the upper bound. |
25
- | `:id` | `string` | (required) | Applied to the native input. |
26
- | `:name` | `string` | (required) | Applied to the native input. |
27
- | `:required` | `boolean` | `false` | Native `required`. |
28
- | `:placeholder` | `string` | `""` | Native placeholder. |
29
- | `:field-has-error` | `boolean` | `false` | Drives `.error` class, `aria-invalid`, and forces `theme` to `"error"`. |
30
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the wrapper. |
31
- | `:theme` | `FormUiTheme` | `"default"` | Overridden to `"error"` internally when `fieldHasError` is true. |
32
- | `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
33
- | `:input-variant` | `"normal" \| "outlined" \| "underlined"` | `"normal"` | Visual style — border box vs. underline. |
34
-
35
- ### v-models
36
-
37
- | Model | Type | Notes |
38
- | ----------------- | --------- | ---------------------------------------------------------- |
39
- | `v-model` | `string` | The input value. **Required** (`defineModel({ required: true })`, 2026-09-25), same for `InputTextWithLabel` and `InputPasswordWithLabel`. |
40
- | `v-model:is-dirty` | `boolean` | Set to `true` once the wrapper variant detects a non-empty value at mount or on change. |
41
- | `v-model:is-active`| `boolean` | Tracks focus state (`focusin`/`focusout`). |
42
-
43
- ---
44
-
45
- ## Slots
46
-
47
- | Slot | Notes |
48
- | ------- | ---------------------------------------------------------------------- |
49
- | `left` | Rendered before the input, inside the bordered wrapper (e.g. an icon or `InputButton variant="inline"`). |
50
- | `right` | Rendered after the input, inside the bordered wrapper. |
51
-
52
- ---
53
-
54
- ## CSS Token Customization
55
-
56
- All `--input-text-*` tokens can be overridden at global, page, or instance scope, each falling back
57
- to the shared `--theme-*`/`--theme-input-*` tokens every other themed form component also uses. See
58
- `CONSUMER-STYLING.md` in the component directory for the full token table.
59
-
60
- **Common tokens:**
61
- - `--input-text-surface` — wrapper background
62
- - `--input-text-border` / `--input-text-border-hover` / `--input-text-border-focus` — border/outline colour
63
- - `--input-text-color` — input text colour
64
- - `--input-text-placeholder-color` — placeholder colour
65
-
66
- ---
67
-
68
- ## Using `min`/`max` for a native date picker
69
-
70
- `type="date"` plus `min`/`max` is the standard way to stop the browser's native date picker from
71
- offering dates outside an allowed range — e.g. preventing a booking date in the past:
72
-
73
- ```vue
74
- <InputTextCore
75
- id="appointment-date"
76
- name="appointmentDate"
77
- type="date"
78
- v-model="appointmentDate"
79
- :min="new Date().toISOString().split('T')[0]"
80
- max="2027-12-31"
81
- />
82
- ```
83
-
84
- `min`/`max` are passed straight through with no validation of their own — the browser enforces the
85
- range in its native picker/typed-entry UI. Pair with server-side validation; a native `min`/`max`
86
- constraint is a UX affordance, not a substitute for validating the submitted value.
87
-
88
- ---
89
-
90
- ## Variants
91
-
92
- Three sibling wrappers compose `InputTextCore` with `InputLabel`, `InputDescription`, and
93
- `InputError` for the common "labelled field" case. Each forwards `min`/`max` straight through if
94
- you pass them.
95
-
96
- ### InputTextWithLabel
97
-
98
- The general-purpose labelled text field — supports every `InputTypesText` value including
99
- `"date"`.
100
-
101
- ```vue
102
- <InputTextWithLabel
103
- v-model="appointmentDate"
104
- id="appointment-date"
105
- name="appointmentDate"
106
- type="date"
107
- label="Appointment date"
108
- :min="new Date().toISOString().split('T')[0]"
109
- max="2027-12-31"
110
- :error-message="errors.appointmentDate"
111
- :field-has-error="!!errors.appointmentDate"
112
- />
113
- ```
114
-
115
- Props: same `type`/`inputmode`/`maxlength`/`min`/`max`/`name`/`placeholder`/`label`/`errorMessage`/
116
- `fieldHasError`/`required`/`styleClassPassthrough`/`theme`/`inputVariant` as `InputTextCore`, plus
117
- `label: string` (required) and `errorMessage: object | string` (required — pass `""` if unused).
118
- Slots: `left`, `right`, `descriptionHtml`, `descriptionText`.
119
-
120
- ### InputPasswordWithLabel
121
-
122
- Adds a show/hide toggle button (`InputButton variant="inline"` in the `right` slot) that swaps
123
- `type` between `"password"` and `"text"`.
124
-
125
- ```vue
126
- <InputPasswordWithLabel
127
- v-model="password"
128
- name="password"
129
- label="Password"
130
- :error-message="errors.password"
131
- :field-has-error="!!errors.password"
132
- show-password-text="Show password"
133
- hide-password-text="Hide password"
134
- />
135
- ```
136
-
137
- Extra props: `showPasswordText` (default `"Show password"`) and `hidePasswordText` (default
138
- `"Hide password"`) — the toggle button's accessible name in each state. Pass translated strings
139
- here for a non-English consumer app; there is no built-in i18n framework dependency.
140
-
141
- ### InputTextAsNumberWithLabel
142
-
143
- A numeric stepper: left/right `InputButton variant="inline"` buttons decrement/increment the
144
- value by `step`, disabled once `min`/`max` is reached.
145
-
146
- ```vue
147
- <InputTextAsNumberWithLabel
148
- v-model="quantity"
149
- name="quantity"
150
- label="Quantity"
151
- :min="1"
152
- :max="99"
153
- :step="1"
154
- step-down-text="Decrease quantity"
155
- step-up-text="Increase quantity"
156
- >
157
- <template #left>−</template>
158
- <template #right>+</template>
159
- </InputTextAsNumberWithLabel>
160
- ```
161
-
162
- `v-model` is `number | undefined` (`defineModel({ default: undefined })`): clearing the field sets it to `undefined`, so bind a `ref<number | undefined>` rather than assuming a number.
163
-
164
- Extra props: `min: number` (required), `max: number` (required), `step?: number` (default `1`),
165
- `stepDownText`/`stepUpText` (default `"Step down"`/`"Step up"`) — the decrement/increment buttons'
166
- accessible names. Requires `left`/`right` slot content for the step icons (no default icon).
167
-
168
- **Fixed 2026-09-22**: `updateElementClasses(["input-text-as-number", "has-left-button",
169
- "has-right-button"])` computes marker classes onto this component's own `elementClasses`, which is
170
- bound to the outer `.input-text-with-label` wrapper — but the nested `<InputTextCore>` was passed
171
- `:style-class-passthrough` as a bare shorthand, which resolves to the raw incoming
172
- `styleClassPassthrough` **prop**, not the local `elementClasses`. So the marker classes never
173
- reached the actual `<input>`, the scoped `.input-text-core.input-text-as-number { width:
174
- fit-content; text-align: center; ... }` CSS never matched anything, and the input silently fell
175
- back to `InputTextCore`'s default full-width, left-aligned styling instead of the intended
176
- compact, centered numeric stepper box. Fixed by changing that binding to
177
- `:style-class-passthrough="elementClasses"` — `elementClasses` still carries any consumer-supplied
178
- `styleClassPassthrough` too, since the composable seeds it from the prop before the marker classes
179
- are toggled on top. When a component computes its own marker/state classes via
180
- `updateElementClasses()` and needs them on a *child* component rather than its own root element,
181
- always pass the computed `elementClasses` explicitly — the `:x` shorthand only ever resolves to
182
- the prop of that exact name, never a same-named local composable output.
183
-
184
- ---
185
-
186
- ## Accessibility
187
-
188
- - `field-has-error` sets `aria-invalid="true"` and forces the error theme, independent of what
189
- `theme` was passed.
190
- - `inputmode="numeric"` adds a `pattern="[0-9]+"` and blocks non-numeric `beforeinput` edits that
191
- would fail it.
192
- - Focus is tracked via native `focusin`/`focusout`, not a custom keyboard handler — `:focus-visible`
193
- styling is preserved.
194
- - The password/number variants give their icon-only toggle/step buttons a real accessible name via
195
- `InputButton`'s `buttonText` (visually hidden via `.sr-only` when an icon-only slot is used).
196
-
197
- ---
198
-
199
- ## Notes
200
-
201
- - **No built-in label/error rendering**: `InputTextCore` alone renders no `<label>` or error text —
202
- use a variant, or compose your own wrapper the same way they do.
203
- - **`min`/`max` need matching `type`**: they're inert on `type="text"`/`"email"`/etc. — only
204
- `"date"` and `"number"` (native range-constrained types) act on them.
@@ -1,154 +0,0 @@
1
- # InputTextareaCore Component
2
-
3
- ## Overview
4
-
5
- `InputTextareaCore` is the low-level native `<textarea>` primitive for the `05.forms` multi-line
6
- text family. It renders a single native textarea with left/right decorative slots. It has no
7
- label, description, or error-message rendering of its own — that's composed by the
8
- `InputTextareaWithLabel` variant below.
9
-
10
- Most consumers should reach for **InputTextareaWithLabel** rather than `InputTextareaCore`
11
- directly.
12
-
13
- ---
14
-
15
- ## Props reference
16
-
17
- > **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`.
18
-
19
- | Prop (template form) | Type | Default | Notes |
20
- |------|------|---------|-------|
21
- | `:id` | `string` | (required) | Applied to the native textarea. |
22
- | `:name` | `string` | (required) | Applied to the native textarea. |
23
- | `:maxlength` | `number` | `255` | Native `maxlength`. |
24
- | `:placeholder` | `string` | `""` | Native `placeholder`. |
25
- | `:required` | `boolean` | `false` | Native `required`. |
26
- | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
27
- | `:input-variant` | `InputUiVariant` | `"normal"` | Adds the variant name as a class on the wrapper — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
28
- | `:field-has-error` | `boolean` | `false` | Drives `data-invalid` on the wrapper and `aria-invalid` on the textarea. |
29
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the textarea element. |
30
- | `:aria-describedby` | `string` | `""` | Forwarded to the native textarea. |
31
-
32
- ### v-models
33
-
34
- - `v-model` — `string | number | readonly string[] | null | undefined`, **required**
35
- (`defineModel({ required: true })`, satisfying `vue/require-default-prop` the same way as
36
- `InputRange`/`InputNumber` — see `project_ledger_eslint_issues_column` in memory).
37
- "Required" here means a consumer must bind `v-model`, not that the value can't be an empty
38
- string — `""` is a perfectly valid textarea value.
39
- - `v-model:is-dirty` — `boolean`. Set once by `InputTextareaWithLabel` the first time the value
40
- becomes non-empty (never reset back to `false` by this component).
41
- - `v-model:is-active` — `boolean`. Tracks native focus/blur (`focusin`/`focusout`), toggling
42
- `.active` on the wrapper and textarea.
43
-
44
- ---
45
-
46
- ## Slots
47
-
48
- | Slot | Notes |
49
- |------|-------|
50
- | `left` | Rendered before the textarea (decorative — e.g. an icon). Adds `has-left-slot` to the wrapper. |
51
- | `right` | Rendered after the textarea (decorative). Adds `has-right-slot` to the wrapper. |
52
-
53
- Unlike `InputNumber`/`InputRange`, these slots have no built-in `InputButton`
54
- divider/embedded-button styling — they're plain decorative content (see the Storybook stories,
55
- which use emoji), not a stepper control.
56
-
57
- ---
58
-
59
- ## CSS token API
60
-
61
- See [CONSUMER-STYLING.md](../../app/components/05.forms/input-textarea/CONSUMER-STYLING.md) for
62
- the full table with defaults.
63
-
64
- **Common tokens:**
65
- - `--input-textarea-surface` — wrapper background, falls back to `--theme-input-surface`
66
- - `--input-textarea-border` / `-border-hover` / `-border-focus` — border colour resting, mouse-hover outline, and `:focus-visible` outline, fall back to `--theme-border`/`--theme-border-focus`
67
- - `--input-textarea-text-color` — textarea text colour, falls back to `--theme-input-text-color-normal`
68
- - `--input-textarea-placeholder-color` — placeholder text colour, falls back to `--theme-input-placeholder`
69
-
70
- ---
71
-
72
- ## Fixed 2026-09-22: undefined tokens and dead code
73
-
74
- `InputTextareaCore` migrated to full compliance from a `variants:true`, `eslint_issues:true`,
75
- score-1/5 state (no public tokens, no CONSUMER-STYLING.md, no tests, no skill doc, no snippet).
76
-
77
- - **Silently broken styling from undefined CSS custom properties** (same bug class as pitfall #21
78
- and `InputRange`'s dead `--theme-form-range-accent-color`): the wrapper's `padding-inline`
79
- read `--element-decorator-padding-inline`, and the placeholder's `font-size`/`font-style`/
80
- `line-height`/`font-weight` read `--input-placeholder-font-size`/`-font-style`/`-line-height`/
81
- `-font-weight` — **none of these five custom properties were declared anywhere in the
82
- codebase**. Per the CSS spec, an undefined `var()` with no fallback makes the whole declaration
83
- invalid, so all five silently fell back to the browser/inherited default instead of the intended
84
- design. Fixed by switching to the tokens `InputTextCore` actually uses for the same purpose:
85
- `--input-padding-inline` for the wrapper padding, and `--theme-input-placeholder-font-size` +
86
- hardcoded `italic`/`1`/`normal` for the placeholder (matching `InputTextCore`'s placeholder rule
87
- exactly — those three placeholder sub-properties aren't tokenized there either).
88
- - **Zero public `--input-textarea-*` tokens existed at all** — every colour value read a
89
- `--theme-*` global directly, with no component-level override point (the exact gap pitfall #14
90
- describes for early `05.forms` components). Added the full set matching `InputTextCore`'s shape:
91
- `--input-textarea-surface`, `-border`, `-border-hover`, `-border-focus`, `-text-color`,
92
- `-placeholder-color`.
93
- - **Dead code**: both `InputTextareaCore` and `InputTextareaWithLabel` declared
94
- `const FormUiTheme = computed(() => props.fieldHasError ? "error" : props.theme);` — a value
95
- binding that shadows the imported `FormUiTheme` *type* import of the same name, and is never
96
- referenced anywhere in either file (the template's `:theme` shorthand resolves straight from
97
- `defineProps`). Removed both. `InputTextareaWithLabel` also had ~5 lines of commented-out dead
98
- code (an old, hand-rolled `id`/`errorId`/`ariaDescribedby` implementation, superseded by
99
- `useAriaDescribedById` right below it) — removed. (The identical dead `FormUiTheme` computed
100
- still exists in `InputTextCore`/`InputTextWithLabel` themselves — out of scope for this pass
101
- since those weren't being touched, but worth cleaning up next time either is migrated.)
102
- - **`InputTextareaWithLabel` passed `:label` down to `InputTextareaCore`**, which has no `label`
103
- prop (matching `InputTextCore`, which doesn't have one either) — it fell through as a
104
- meaningless raw `label="..."` HTML attribute on the wrapper `<div>`. Removed the pass-through;
105
- `InputLabel` (which does have a real `label` concept) already renders it correctly. Covered by
106
- a regression test.
107
- - **`defineModel()` had no default**, tripping `vue/require-default-prop` in both files — fixed
108
- with `{ required: true }`, same pattern as `InputRange`/`InputNumber` (see the v-model
109
- note above for why this is still correct even though an empty string is a valid value here).
110
- - **`InputTextareaWithLabel.stories.ts` used `reactive()` in two render functions
111
- (`AllVariants`/`FormExamples`) without importing it** — only `computed` was imported from
112
- `"vue"`. This library's `.ts` Storybook files need Vue reactivity APIs imported explicitly (no
113
- Nuxt auto-import outside `.vue` files) — a real runtime `ReferenceError` waiting to happen the
114
- first time either story rendered. Fixed by adding `reactive` to the existing `computed` import.
115
-
116
- ---
117
-
118
- ## Variants
119
-
120
- ### InputTextareaWithLabel
121
-
122
- `InputTextareaWithLabel` (`InputTextareaWithLabel.vue`) composes `InputTextareaCore` with
123
- `InputLabel`, `InputDescription` (positioned before or after the field depending on
124
- `inputVariant === "outlined"`, matching `InputTextWithLabel`'s exact placement rule), and
125
- `InputError`.
126
-
127
- **Additional props over InputTextareaCore:**
128
-
129
- | Prop (template form) | Type | Default | Notes |
130
- |------|------|---------|-------|
131
- | `:label` | `string` | (required) | Rendered via `InputLabel`. |
132
- | `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
133
-
134
- **Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`), plus
135
- `left`/`right` (forwarded straight through to the underlying `InputTextareaCore`).
136
-
137
- ```vue
138
- <InputTextareaWithLabel
139
- name="message"
140
- label="Message"
141
- v-model="message"
142
- placeholder="How can we help you?"
143
- :maxlength="500"
144
- :error-message="errors.message"
145
- :field-has-error="!!errors.message"
146
- >
147
- <template #descriptionText>Please describe your inquiry in detail</template>
148
- </InputTextareaWithLabel>
149
- ```
150
-
151
- `v-model` is `string | number | readonly string[] | null | undefined`, **required** — same as
152
- `InputTextareaCore` above. `isDirty`/`isActive` are managed internally (not exposed as props) —
153
- `isDirty` flips to `true` the first time the value becomes a non-empty string (checked on mount
154
- and on every change), and never resets.
@@ -1,169 +0,0 @@
1
- # ToggleSwitchCore Component
2
-
3
- ## Overview
4
-
5
- `ToggleSwitchCore` is the low-level pill-shaped (or square, via `round`) toggle switch primitive
6
- for the `05.forms` toggle family. It renders a visually-hidden native `<input type="checkbox">`
7
- (kept in the DOM for real keyboard/checkbox semantics) plus a styled track/thumb visual, with
8
- optional icon slots shown inside the thumb for the on/off states. It has no label, description, or
9
- error-message rendering of its own — that's composed by the `ToggleSwitchWithLabel` and
10
- `ToggleSwitchWithLabelInline` variants below.
11
-
12
- Most consumers should reach for **ToggleSwitchWithLabel** or **ToggleSwitchWithLabelInline** rather
13
- than `ToggleSwitchCore` directly.
14
-
15
- ---
16
-
17
- ## Props reference
18
-
19
- > **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`, `:true-value`, `:false-value`.
20
-
21
- | Prop (template form) | Type | Default | Notes |
22
- |------|------|---------|-------|
23
- | `:id` | `string` | (required) | Combined with a fixed prefix to build the native input's `id` (`toggle-switch-${id}`). |
24
- | `:name` | `string` | (required) | Applied to the native checkbox. |
25
- | `:required` | `boolean` | `false` | Native `required`. |
26
- | `:field-has-error` | `boolean` | `false` | Drives `aria-invalid` on the checkbox, and switches `data-theme` to `"error"` regardless of the `theme` prop's own value. |
27
- | `:true-value` / `:false-value` | `string \| number \| boolean` | `true` / `false` | The two values `v-model` toggles between. |
28
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root element. |
29
- | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the root (superseded by `"error"` when `field-has-error` is true — see above). |
30
- | `:round` | `boolean` | `true` | `true` renders a pill-shaped track/thumb (`border-radius: 100vw`); `false` renders a rounded-square shape (`border-radius: 0.4rem`). See the `--toggle-switch-border-radius` token in CONSUMER-STYLING.md to override the exact value either state produces. |
31
- | `:aria-describedby` | `string` | `""` | Forwarded to the native checkbox (omitted when empty). Changed 2026-09-27: it was ignored before, and the checkbox always pointed at `<id>-description` whether or not that element existed, so `ToggleSwitchWithLabel`'s error-message id never reached it. |
32
-
33
- ### v-model
34
-
35
- - `v-model` — `string | number | boolean`, **required** (`defineModel({ required: true })`, satisfying `vue/require-default-prop` — see `project_ledger_eslint_issues_column` in memory).
36
-
37
- ### Slots
38
-
39
- - `iconOn` / `iconOff` — replace the default `material-symbols:circle-outline` icon shown inside the thumb for each state. Providing either slot switches off the `use-default-icons` wrapper class (checked via `useSlots()`, so this only reacts to slot *presence*, not conditional slot content).
40
-
41
- ---
42
-
43
- ## CSS token API
44
-
45
- See [CONSUMER-STYLING.md](../../app/components/05.forms/toggle-switch/CONSUMER-STYLING.md) for the
46
- full table with defaults.
47
-
48
- **Common tokens:**
49
- - `--toggle-switch-surface` / `-surface-hover` — track background resting/hover, fall back to `--theme-checkbox-symbol-surface`/`--theme-surface-subtle`
50
- - `--toggle-switch-border` / `-border-focus` — track border colour, and `:focus-visible` outline colour, fall back to `--theme-border`/`--theme-border-focus`
51
- - `--toggle-switch-symbol-surface-off` / `-symbol-surface-on` — thumb background unchecked/checked
52
- - `--toggle-switch-border-radius` — track and thumb shape; see the `round` prop above
53
-
54
- ---
55
-
56
- ## Fixed 2026-09-23: dead functionality and story bugs
57
-
58
- `ToggleSwitchCore` (and its two variants) migrated to full compliance from a `variants:true`,
59
- `eslint_issues:true`, score-3/5 state (no tests, no skill doc).
60
-
61
- - **`round` prop was completely dead** — both variants declared and forwarded it to
62
- `ToggleSwitchCore`, and Storybook's `SquareShape` story existed specifically to demonstrate
63
- `round: false`, but `ToggleSwitchCore` never declared the prop at all, so it silently fell
64
- through as an inert `round` DOM attribute on the root `<div>` and the track/thumb stayed pill-shaped
65
- regardless. Fixed by adding `round` to `ToggleSwitchCore` and wiring it to a new
66
- `--toggle-switch-border-radius` token (100vw when round, 0.4rem when square).
67
- - **`fieldHasError` didn't switch the component's theme** — `ToggleSwitchCore` and
68
- `ToggleSwitchWithLabel` both computed an error-aware theme value but bound `data-theme` to the
69
- raw `theme` prop instead of that computed, so this library's `[data-theme="error"]` global
70
- token overrides never applied when a consumer set `fieldHasError`. Fixed by binding `data-theme`
71
- to the computed value in both files.
72
- - **An invalid `for` attribute on a `<div>`** (`.toggle-switch-wrapper`) was removed — `for` is
73
- only a valid HTML attribute on `<label>`/`<output>`, so it was inert dead markup; the real
74
- label association is via `InputLabel`'s own `for`/`id` wiring in the variants.
75
- - **`defineModel` had no default**, tripping `vue/require-default-prop` (a known false-positive
76
- for `defineModel` — see `project_ledger_eslint_issues_column` in memory) — fixed with
77
- `{ required: true }` across `ToggleSwitchCore` and both variants, since a checkbox genuinely
78
- can't be meaningfully empty.
79
- - **Two dead, unread `--_transition-duration` private tokens** (one per variant) were removed —
80
- nothing in either variant's own CSS or `ToggleSwitchCore`'s ever read them; the actual
81
- transition durations are hardcoded `0.4s` literals inside `ToggleSwitchCore`.
82
- - **Custom `iconOn`/`iconOff` slot content could render as a non-square box, throwing off
83
- centering** — `.symbol-icon` is `display: grid; place-content: center;`, and its single
84
- auto-sized track shrink-wraps to the slotted content's own box. A plain inline element (e.g. a
85
- `<span>` wrapping a 16×16 SVG, as in the `WithCustomIcons` stories) sizes to its line box's
86
- leading rather than its intrinsic height, rendering as ~16×20 instead of 16×16. Fixed by adding
87
- `line-height: 0` to `.symbol-icon`, which collapses that leading for whatever markup a consumer
88
- slots in. The default icon (`@nuxt/icon`'s `Icon` component) wasn't affected, since iconify sets
89
- its own sizing/`vertical-align` internally.
90
- - **All three story files' `theme` control listed the wrong option set**
91
- (`["primary", "secondary", "tertiary", "ghost", "error", "success", "warning"]`, none of which
92
- match `FormUiTheme`) — fixed to the real `["default", "success", "error", "warning"]`.
93
- `ToggleSwitchWithLabelInline.stories.ts`'s `InlineFormExample` story also passed the now-invalid
94
- `theme="secondary"` directly — fixed to `"default"`.
95
-
96
- ---
97
-
98
- ## Variants
99
-
100
- ### ToggleSwitchWithLabel
101
-
102
- `ToggleSwitchWithLabel` (`ToggleSwitchWithLabel.vue`) composes `ToggleSwitchCore` with
103
- `InputLabel`, an optional description (rendered by `InputDescription`, the same as every other labelled form field), and `InputError`.
104
-
105
- **Additional props over ToggleSwitchCore:**
106
-
107
- | Prop (template form) | Type | Default | Notes |
108
- |------|------|---------|-------|
109
- | `:label` | `string` | (required) | Rendered via `InputLabel`. |
110
- | `:error-message` | `object \| string` | `""` | Rendered via `InputError` when `field-has-error` is true. |
111
-
112
- Note this wrapper passes `id`/`name`/`required`/`field-has-error`/`true-value`/`false-value`/
113
- `theme`/`round`/`aria-describedby` straight through to `ToggleSwitchCore`; `id` is a self-generated
114
- `useId()` value, not a prop — consumers don't set it directly.
115
-
116
- **Slots**: `descriptionText` / `descriptionHtml` (help text between the label and the switch, via `InputDescription`), `iconOn`, `iconOff` (the latter two forwarded to `ToggleSwitchCore`).
117
-
118
- The switch's `aria-describedby` lists the description id when a description slot is present and the error id when `field-has-error` is true, both together when both apply.
119
-
120
- ```vue
121
- <ToggleSwitchWithLabel
122
- name="notifications"
123
- label="Enable notifications"
124
- v-model="notificationsEnabled"
125
- :error-message="errors.notifications"
126
- :field-has-error="!!errors.notifications"
127
- >
128
- <template #descriptionText>Turn this on to receive notifications</template>
129
- </ToggleSwitchWithLabel>
130
- ```
131
-
132
- #### Breaking change 2026-09-29: description slot
133
-
134
- The `description` slot is removed. Use `descriptionText` (plain text, wrapped in a `<p>`) or
135
- `descriptionHtml` (rich markup) instead. The old slot rendered into a bare `<div>` with no class,
136
- spacing or tokens, so its content sat flush against the switch; it now goes through
137
- `InputDescription` like every other labelled form field, styled by the `--input-description-*`
138
- tokens (see `input-description` CONSUMER-STYLING.md) and targetable as
139
- `.toggle-switch-description`. `aria-describedby` also used to drop the description id whenever
140
- the field had an error; it now lists both.
141
-
142
- ```vue
143
- <!-- before -->
144
- <template #description>Sends at most one email a week.</template>
145
- <!-- after -->
146
- <template #descriptionText>Sends at most one email a week.</template>
147
- ```
148
-
149
- ### ToggleSwitchWithLabelInline
150
-
151
- `ToggleSwitchWithLabelInline` (`ToggleSwitchWithLabelInline.vue`) composes
152
- `ToggleSwitchCore` with `InputLabel` laid out inline (label and switch side by side, no
153
- description/error-message support — this variant is for compact settings-row usage).
154
-
155
- **Additional props over ToggleSwitchCore:**
156
-
157
- | Prop (template form) | Type | Default | Notes |
158
- |------|------|---------|-------|
159
- | `:label` | `string` | (required) | Rendered via `InputLabel`. |
160
- | `:label-weight` | `LabelWeight` (`"normal" \| "semi-bold" \| "bold"`) | `"normal"` | Maps to a `body-normal`/`body-normal-semibold`/`body-normal-bold` class on the label. |
161
-
162
- This variant has no `required`/`field-has-error`/`error-message` props — it never claimed error-state
163
- support, unlike `ToggleSwitchWithLabel`.
164
-
165
- **Slots**: `iconOn`, `iconOff` (forwarded to `ToggleSwitchCore`).
166
-
167
- ```vue
168
- <ToggleSwitchWithLabelInline name="darkMode" label="Dark mode" v-model="darkModeEnabled" />
169
- ```