srcdev-nuxt-components 9.6.8 → 9.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/component-compliance-checklist.md +28 -1
- package/.claude/skills/component-inline-action-button.md +1 -1
- package/.claude/skills/component-local-style-override.md +1 -1
- package/.claude/skills/component-naming.md +17 -12
- package/.claude/skills/components/container-glow.md +2 -2
- package/.claude/skills/components/form-field.md +2 -2
- package/.claude/skills/components/form-wrapper.md +1 -1
- package/.claude/skills/components/glowing-border.md +1 -1
- package/.claude/skills/components/input-description.md +7 -7
- package/.claude/skills/components/input-error.md +3 -3
- package/.claude/skills/components/input-label.md +7 -8
- package/.claude/skills/components/input-number.md +17 -17
- package/.claude/skills/components/input-select.md +153 -0
- package/.claude/skills/components/input-text.md +220 -0
- package/.claude/skills/components/input-textarea.md +147 -0
- package/.claude/skills/components/select-menu.md +4 -4
- package/.claude/skills/components/toggle-switch.md +143 -0
- package/.claude/skills/composable-zod-validation.md +1 -1
- package/.claude/skills/css-nesting-conventions.md +22 -1
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/icon-sizing.md +1 -1
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +1 -1
- package/.claude/skills/theming-component-token-pattern.md +8 -8
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.vscode/srcdev-component-input-select.code-snippets +20 -14
- package/.vscode/srcdev-component-input-text.code-snippets +46 -40
- package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
- package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
- package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +7 -7
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
- package/app/components/05.forms/input-number/InputNumber.vue +2 -2
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
- package/app/components/05.forms/input-select/InputSelect.vue +198 -0
- package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
- package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
- package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
- package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
- package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
- package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
- package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
- package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
- package/app/components/05.forms/input-text/InputTextField.vue +168 -0
- package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
- package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
- package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
- package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
- package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
- package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
- package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
- package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
- package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
- package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
- package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
- package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
- package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
- package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
- package/package.json +1 -1
- package/.claude/skills/components/input-select-core.md +0 -151
- package/.claude/skills/components/input-text-core.md +0 -204
- package/.claude/skills/components/input-textarea-core.md +0 -154
- package/.claude/skills/components/toggle-switch-core.md +0 -169
- package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
- package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
- package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
- package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
- package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
- package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
|
@@ -0,0 +1,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).
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# InputTextarea Component
|
|
2
|
+
|
|
3
|
+
> **Renamed 2026-10-16**: `InputTextareaCore` → `InputTextarea`, `InputTextareaWithLabel` →
|
|
4
|
+
> `InputTextareaField` (DOM classes `.input-textarea-core` → `.input-textarea`,
|
|
5
|
+
> `.input-textarea-with-label` → `.input-textarea-field`; full class list in
|
|
6
|
+
> CONSUMER-STYLING.md). Skill doc renamed from `input-textarea-core.md`. Consumer apps use the
|
|
7
|
+
> Field's old name; list their edits privately and apply them when each upgrades.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`InputTextarea` is the low-level native `<textarea>` primitive for the `05.forms` multi-line text
|
|
12
|
+
family. It renders one native textarea with left/right decorative slots and grows with its
|
|
13
|
+
content (`field-sizing: content`) between a minimum and an optional maximum height. It has no
|
|
14
|
+
label, description or error rendering of its own; that's composed by `InputTextareaField` below.
|
|
15
|
+
|
|
16
|
+
Most consumers should reach for **InputTextareaField** rather than `InputTextarea` directly.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Props reference
|
|
21
|
+
|
|
22
|
+
> **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`.
|
|
23
|
+
|
|
24
|
+
| Prop (template form) | Type | Default | Notes |
|
|
25
|
+
|------|------|---------|-------|
|
|
26
|
+
| `:id` | `string` | (required) | Applied to the native textarea. |
|
|
27
|
+
| `:name` | `string` | (required) | Applied to the native textarea. |
|
|
28
|
+
| `:maxlength` | `number` | `255` | Native `maxlength`. Raise it for free-text messages. |
|
|
29
|
+
| `:placeholder` | `string` | `""` | Native `placeholder`. |
|
|
30
|
+
| `:required` | `boolean` | `false` | Native `required`. |
|
|
31
|
+
| `:readonly` | `boolean` | `false` | Native `readonly`. Added 2026-10-16. |
|
|
32
|
+
| `:autocomplete` | `string` | (unset) | Native `autocomplete` token (e.g. `street-address`). Added 2026-10-16. |
|
|
33
|
+
| `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
|
|
34
|
+
| `:input-variant` | `InputUiVariant` | `"normal"` | Sets `data-input-variant`. `normal` (bordered box) and `underlined` have CSS; `outlined` doesn't. |
|
|
35
|
+
| `:field-has-error` | `boolean` | `false` | `data-invalid` on the wrapper and `aria-invalid="true"` on the textarea (omitted otherwise). |
|
|
36
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the textarea. Reacts to changes. |
|
|
37
|
+
| `:aria-describedby` | `string` | `""` | Forwarded to the native textarea; omitted when empty. |
|
|
38
|
+
|
|
39
|
+
### v-models
|
|
40
|
+
|
|
41
|
+
- `v-model` — `string | number | readonly string[] | null | undefined`, **required**
|
|
42
|
+
(`defineModel({ required: true })`; `""` is a valid value, "required" means it must be bound).
|
|
43
|
+
- `v-model:is-dirty` — `boolean`. Read only here: `InputTextareaField` sets it the first time the
|
|
44
|
+
value is non-empty. Reflected as `data-dirty`.
|
|
45
|
+
- `v-model:is-active` — `boolean`. Tracks focus (`focusin`/`focusout`). Reflected as
|
|
46
|
+
`data-active`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Slots
|
|
51
|
+
|
|
52
|
+
| Slot | Notes |
|
|
53
|
+
|------|-------|
|
|
54
|
+
| `left` | Rendered before the textarea in `.input-textarea-slot-left` (decorative, e.g. an icon). Sets `data-has-left-slot`. |
|
|
55
|
+
| `right` | Same, after the textarea, in `.input-textarea-slot-right`. Sets `data-has-right-slot`. |
|
|
56
|
+
|
|
57
|
+
The slots have no built-in `InputButton` styling, unlike `InputText`'s.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## CSS token API
|
|
62
|
+
|
|
63
|
+
See [CONSUMER-STYLING.md](../../app/components/05.forms/input-textarea/CONSUMER-STYLING.md) for
|
|
64
|
+
the full table with defaults and the state hooks.
|
|
65
|
+
|
|
66
|
+
**Common tokens:**
|
|
67
|
+
|
|
68
|
+
- `--input-textarea-surface` — wrapper background
|
|
69
|
+
- `--input-textarea-border` / `-border-hover` / `-border-focus` — border and outline colours
|
|
70
|
+
- `--input-textarea-text-color` / `-placeholder-color` — text colours
|
|
71
|
+
- `--input-textarea-min-height` (default `6lh`) / `-max-height` (default `none`) — growth range
|
|
72
|
+
- `--input-textarea-slot-gap` (default `1.2rem`) — gap between slots and textarea
|
|
73
|
+
- `--input-textarea-slot-align` (default `center`) / `-slot-padding-block` (default `0`) — where
|
|
74
|
+
the slots sit against a tall textarea; `start` plus
|
|
75
|
+
`var(--form-textarea-padding-block)` lines an icon up with the first line
|
|
76
|
+
|
|
77
|
+
**State hooks** (on `.input-textarea-wrapper`): `data-input-variant`, `data-theme`,
|
|
78
|
+
`data-invalid`, `data-dirty`, `data-active`, `data-has-left-slot`, `data-has-right-slot`.
|
|
79
|
+
|
|
80
|
+
`field-sizing: content` isn't supported in Safari 16 or Firefox; there the textarea keeps its
|
|
81
|
+
minimum height and scrolls. That's degraded, not broken.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Changed 2026-10-16: rename, state hooks, line breaks
|
|
86
|
+
|
|
87
|
+
- Renamed to the Control/Field convention (note at the top).
|
|
88
|
+
- State classes became data attributes (`.normal`/`.underlined` → `[data-input-variant]`,
|
|
89
|
+
`.dirty`/`.active`/`.error` → `[data-dirty]`/`[data-active]`/`[data-invalid]`,
|
|
90
|
+
`.has-left-slot`/`.has-right-slot` → `[data-has-*-slot]`); `.slot.left-slot`/`.right-slot` →
|
|
91
|
+
`.input-textarea-slot-left`/`-right`. The textarea no longer carries `dirty`/`active` classes.
|
|
92
|
+
- `all: unset` on the textarea also reset the browser's `white-space: pre-wrap`, so it inherited
|
|
93
|
+
`normal` from the page. It now sets `white-space: pre-wrap` and `overflow-wrap: anywhere`
|
|
94
|
+
explicitly, and `min-inline-size: 0` so a long unbroken string can't widen the field.
|
|
95
|
+
- `aria-invalid="false"` was rendered on every field without an error; it's now omitted.
|
|
96
|
+
`aria-describedby=""` likewise.
|
|
97
|
+
- `InputTextareaField` applied its passthrough to both its root and the textarea; now its root
|
|
98
|
+
only, which is `.input-textarea-field` itself (the unclassed outer `<div>` is gone).
|
|
99
|
+
- The hardcoded `gap: 12px` and `min-height: 6lh` became `--input-textarea-slot-gap` and
|
|
100
|
+
`--input-textarea-min-height`; `--input-textarea-max-height` is new.
|
|
101
|
+
- Removed an unused `inputField` template ref.
|
|
102
|
+
|
|
103
|
+
## Fixed 2026-09-22: undefined tokens and dead code
|
|
104
|
+
|
|
105
|
+
Migrated from a score-1/5 state. Five undefined custom properties (wrapper padding and
|
|
106
|
+
placeholder font) were replaced with the tokens `InputText` uses; public `--input-textarea-*`
|
|
107
|
+
colour tokens were added (pitfall #14); a dead `FormUiTheme` computed, commented-out code and a
|
|
108
|
+
`label` pass-through to the control were removed; `defineModel` got `{ required: true }`; and
|
|
109
|
+
the Field story imported `reactive`, which it used without importing.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Variants
|
|
114
|
+
|
|
115
|
+
### InputTextareaField
|
|
116
|
+
|
|
117
|
+
`InputTextareaField` composes `InputTextarea` with `InputLabel`, `InputDescription` (before the
|
|
118
|
+
control, or after the control and error when `inputVariant === "outlined"`) and `InputError`. Ids
|
|
119
|
+
come from `useAriaDescribedById`, so it takes `name`, not `id`. Root `.input-textarea-field`,
|
|
120
|
+
with `data-theme`, `data-input-variant`, `data-invalid`, `data-dirty` and `data-active`.
|
|
121
|
+
|
|
122
|
+
**Additional props over InputTextarea:**
|
|
123
|
+
|
|
124
|
+
| Prop (template form) | Type | Default | Notes |
|
|
125
|
+
|------|------|---------|-------|
|
|
126
|
+
| `:label` | `string` | (required) | Rendered via `InputLabel`. |
|
|
127
|
+
| `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
|
|
128
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Applied to the `.input-textarea-field` root only. |
|
|
129
|
+
|
|
130
|
+
`maxlength`, `name`, `placeholder`, `required`, `readonly`, `autocomplete`, `field-has-error`,
|
|
131
|
+
`theme` and `input-variant` are forwarded.
|
|
132
|
+
|
|
133
|
+
**Slots**: `descriptionHtml`, `descriptionText` (forwarded to `InputDescription`), plus
|
|
134
|
+
`left`/`right` (forwarded to `InputTextarea`).
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<InputTextareaField
|
|
138
|
+
v-model="message"
|
|
139
|
+
name="message"
|
|
140
|
+
label="Message"
|
|
141
|
+
:maxlength="1000"
|
|
142
|
+
:error-message="errors.message"
|
|
143
|
+
:field-has-error="!!errors.message"
|
|
144
|
+
>
|
|
145
|
+
<template #descriptionText>Please describe your enquiry in detail</template>
|
|
146
|
+
</InputTextareaField>
|
|
147
|
+
```
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
5
|
`SelectMenu` is a trigger-and-popover component for selecting from a list of options — a custom,
|
|
6
|
-
fully token-styled alternative to a native `<select>` or `
|
|
6
|
+
fully token-styled alternative to a native `<select>` or `InputSelect`, built for non-form
|
|
7
7
|
contexts (language switchers, category filters, faceted filter bars, multi-pick treatment lists).
|
|
8
8
|
It reuses the same Popover API + CSS anchor-positioning mechanism as `ActionMenu` (see that skill
|
|
9
9
|
doc), but its items carry a `value`/`label`/optional `icon`, and the whole thing is driven by
|
|
@@ -43,7 +43,7 @@ selected value(s). In single-select mode (default) it's the one selected option'
|
|
|
43
43
|
`undefined` if nothing is selected — a valid, expected state that renders the placeholder. With
|
|
44
44
|
`multiple`, it's an array of selected values (`[]` when nothing is selected). `{ default: undefined }`
|
|
45
45
|
is this repo's fix for the `vue/require-default-prop` false positive on `defineModel`-declared
|
|
46
|
-
props that are legitimately optional — unlike `
|
|
46
|
+
props that are legitimately optional — unlike `InputSelect`'s `modelValue`, which uses
|
|
47
47
|
`{ required: true }` because a native form field genuinely can't be meaningfully empty, this model
|
|
48
48
|
is optional by design (see that rule's `defineModel` gap noted in the repo's ledger `eslint_issues`
|
|
49
49
|
docs). For several independent single-select categories, place multiple `SelectMenu` instances
|
|
@@ -257,10 +257,10 @@ ellipsis. The `StressTest` story covers this at every canvas width.
|
|
|
257
257
|
- **Chevron flip** (`scaleY(-1)`, was `rotate(180deg)` until 2026-10-07) keys off the trigger's `aria-expanded` (from the composable's `isOpen`), not
|
|
258
258
|
`:has(...:popover-open)`, so it also works without the Popover API (changed 2026-10-05).
|
|
259
259
|
- **No native `<select>`/`appearance: base-select` involved** — this is a from-scratch popover
|
|
260
|
-
listbox, not a styled native select, so it doesn't inherit `
|
|
260
|
+
listbox, not a styled native select, so it doesn't inherit `InputSelect`'s WebKit
|
|
261
261
|
`appearance: base-select` constraints.
|
|
262
262
|
- **Not a form field** — `SelectMenu` has no `fieldHasError`/`theme`/`inputVariant` props like the
|
|
263
|
-
`05.forms` inputs. Use `
|
|
263
|
+
`05.forms` inputs. Use `InputSelect` instead for an actual form field that needs validation
|
|
264
264
|
state and native `<select>` semantics.
|
|
265
265
|
- **2026-10-08 stress-test pass**: root `max-inline-size: 100%` (a long trigger label overflowed
|
|
266
266
|
narrow containers instead of truncating), popover `--select-menu-popover-max-width` and
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# ToggleSwitch Component
|
|
2
|
+
|
|
3
|
+
> **Renamed 2026-10-16**: `ToggleSwitchCore` → `ToggleSwitch`, `ToggleSwitchWithLabel` →
|
|
4
|
+
> `ToggleSwitchField`; `ToggleSwitchWithLabelInline` was removed in favour of
|
|
5
|
+
> `layout="inline"` on `ToggleSwitchField`. DOM classes `.toggle-switch-core` → `.toggle-switch`,
|
|
6
|
+
> `.toggle-switch-with-label` → `.toggle-switch-field` (full list in CONSUMER-STYLING.md). Skill
|
|
7
|
+
> doc renamed from `toggle-switch-core.md`. Consumer apps use `ToggleSwitchWithLabel`; list their
|
|
8
|
+
> edits privately and apply them when each upgrades.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
`ToggleSwitch` is the low-level on/off switch for the `05.forms` toggle family: a visually hidden
|
|
13
|
+
native `<input type="checkbox" role="switch">` (real keyboard and form semantics) plus a
|
|
14
|
+
decorative track and thumb (`aria-hidden`), with optional icon slots inside the thumb. It has no
|
|
15
|
+
label, description or error rendering of its own; that's `ToggleSwitchField` below.
|
|
16
|
+
|
|
17
|
+
Most consumers should reach for **ToggleSwitchField**.
|
|
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: `:field-has-error`, `:style-class-passthrough`, `:aria-describedby`, `:true-value`, `:false-value`.
|
|
24
|
+
|
|
25
|
+
| Prop (template form) | Type | Default | Notes |
|
|
26
|
+
|------|------|---------|-------|
|
|
27
|
+
| `:id` | `string` | (required) | Builds the native input's id: `toggle-switch-${id}`. Point your own `<label for>` at that. |
|
|
28
|
+
| `:name` | `string` | (required) | Applied to the native checkbox. |
|
|
29
|
+
| `:required` | `boolean` | `false` | Native `required`. |
|
|
30
|
+
| `:field-has-error` | `boolean` | `false` | `aria-invalid="true"` on the checkbox (omitted otherwise), `data-invalid` on the root, and `data-theme="error"` whatever `theme` says. |
|
|
31
|
+
| `:true-value` / `:false-value` | `string \| number \| boolean` | `true` / `false` | The two values `v-model` toggles between. A model value equal to neither shows as off. |
|
|
32
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root. Reacts to changes. |
|
|
33
|
+
| `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the root (unless `field-has-error`). |
|
|
34
|
+
| `:round` | `boolean` | `true` | Pill (`100vw` radius) or rounded square (`0.4rem`); `--toggle-switch-border-radius` overrides both. |
|
|
35
|
+
| `:aria-describedby` | `string` | `""` | Forwarded to the checkbox; omitted when empty. |
|
|
36
|
+
|
|
37
|
+
### v-model
|
|
38
|
+
|
|
39
|
+
- `v-model` — `string | number | boolean`, **required** (`defineModel({ required: true })`).
|
|
40
|
+
|
|
41
|
+
### Slots
|
|
42
|
+
|
|
43
|
+
- `iconOn` / `iconOff` — replace the default `material-symbols:circle-outline` icon shown inside
|
|
44
|
+
the thumb in each state. Decorative: the whole track is `aria-hidden`.
|
|
45
|
+
|
|
46
|
+
### Interaction
|
|
47
|
+
|
|
48
|
+
Clicking the track toggles the value. Clicks that land on the input itself (a label click, Space,
|
|
49
|
+
or a click on the input) are left to `v-model`, so each action toggles exactly once.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## CSS token API
|
|
54
|
+
|
|
55
|
+
See [CONSUMER-STYLING.md](../../app/components/05.forms/toggle-switch/CONSUMER-STYLING.md) for the
|
|
56
|
+
full table with defaults and the state hooks.
|
|
57
|
+
|
|
58
|
+
**Common tokens:**
|
|
59
|
+
|
|
60
|
+
- `--toggle-switch-surface` / `-surface-hover` — track background resting/hover
|
|
61
|
+
- `--toggle-switch-border` / `-border-focus` — track border, and the `:focus-visible` outline
|
|
62
|
+
- `--toggle-switch-symbol-surface-off` / `-symbol-surface-on` — thumb background per state
|
|
63
|
+
- `--toggle-switch-border-radius`, `--toggle-switch-track-width` — shape and width overrides
|
|
64
|
+
- `--toggle-switch-field-inline-gap` — space between label and switch in the inline layout
|
|
65
|
+
|
|
66
|
+
**State hooks** (on `.toggle-switch`): `data-checked`, `data-invalid`, `data-theme`. On
|
|
67
|
+
`.toggle-switch-field`: `data-layout`, `data-invalid`, `data-theme`.
|
|
68
|
+
|
|
69
|
+
The thumb's slide respects `prefers-reduced-motion`.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Changed 2026-10-16: rename, switch role, inline layout
|
|
74
|
+
|
|
75
|
+
- Renamed to the Control/Field convention (note at the top). `ToggleSwitchWithLabelInline` was
|
|
76
|
+
folded into `ToggleSwitchField` as `layout="inline"`, which also gives the inline row
|
|
77
|
+
description, error and `required` support it never had.
|
|
78
|
+
- The checkbox now has `role="switch"`, so screen readers announce "switch, on/off" rather than
|
|
79
|
+
a checkbox. The decorative track is `aria-hidden`.
|
|
80
|
+
- `aria-invalid="false"` was rendered on every switch without an error; now omitted.
|
|
81
|
+
- Generic inner classes (`.symbol-wrapper`, `.symbol`, `.symbol-icon`, `.icon-on`/`.icon-off`,
|
|
82
|
+
state classes `.checked`/`.active`, the unused `.use-default-icons`) became
|
|
83
|
+
`.toggle-switch-track`, `-thumb`, `-icon`, `-icon-on`/`-icon-off` and `[data-checked]`
|
|
84
|
+
(pitfall #16). The input got `.toggle-switch-input`; the default icon lost its `.icon` class.
|
|
85
|
+
- The Fields' label passthrough used `input-switch-label`, so the `.toggle-switch-label` rules
|
|
86
|
+
never matched; the label now gets `.toggle-switch-label`. The inline variant's `nowrap` (dead
|
|
87
|
+
for the same reason) is gone: long labels wrap.
|
|
88
|
+
- Clicking the input bubbled to the wrapper's click handler as well as updating `v-model`; it
|
|
89
|
+
happened to land on the right value. The wrapper handler now ignores clicks on the input.
|
|
90
|
+
- Removed a redundant `:checked` binding next to `v-model`, and a duplicate `outline` in the
|
|
91
|
+
track's transition list.
|
|
92
|
+
- `styleClassPassthrough` reacts to prop changes on both components.
|
|
93
|
+
|
|
94
|
+
## Fixed 2026-09-23
|
|
95
|
+
|
|
96
|
+
Migrated from a score-3/5 state: the `round` prop was never declared on the control (added, with
|
|
97
|
+
`--toggle-switch-border-radius`); `fieldHasError` didn't switch `data-theme`; an invalid `for` on
|
|
98
|
+
a `<div>` was removed; `defineModel` got `{ required: true }`; dead `--_transition-duration`
|
|
99
|
+
tokens were removed; `line-height: 0` on the icon holder keeps slotted icons square; story theme
|
|
100
|
+
options were corrected to `FormUiTheme`.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Variants
|
|
105
|
+
|
|
106
|
+
### ToggleSwitchField
|
|
107
|
+
|
|
108
|
+
`ToggleSwitchField` composes `ToggleSwitch` with `InputLabel` (no required/optional indicator), an
|
|
109
|
+
optional description (`InputDescription`, class `.toggle-switch-description`) and `InputError`.
|
|
110
|
+
DOM order is label, description, switch, error in both layouts. Root `.toggle-switch-field`.
|
|
111
|
+
|
|
112
|
+
**Additional props over ToggleSwitch** (it takes no `id`; ids come from `useId()`):
|
|
113
|
+
|
|
114
|
+
| Prop (template form) | Type | Default | Notes |
|
|
115
|
+
|------|------|---------|-------|
|
|
116
|
+
| `:label` | `string` | (required) | Rendered via `InputLabel` (class `.toggle-switch-label`). |
|
|
117
|
+
| `:layout` | `"stacked" \| "inline"` | `"stacked"` | `stacked`: label, description, switch, error in one column. `inline`: label and switch on one row (a grid, `data-layout="inline"`), description and error below. |
|
|
118
|
+
| `:label-weight` | `LabelWeight` | bold when stacked, normal when inline | `body-normal` / `body-normal-semibold` / `body-normal-bold` class on the label. |
|
|
119
|
+
| `:error-message` | `object \| string` | `""` | Rendered via `InputError` when `field-has-error` is true. |
|
|
120
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Applied to the root only. |
|
|
121
|
+
|
|
122
|
+
`name`, `required`, `field-has-error`, `true-value`, `false-value`, `theme` and `round` are
|
|
123
|
+
forwarded. The root's `data-theme` switches to `error` with `field-has-error`, like the control's.
|
|
124
|
+
|
|
125
|
+
**Slots**: `descriptionText` / `descriptionHtml` (via `InputDescription`), `iconOn`, `iconOff`.
|
|
126
|
+
The switch's `aria-describedby` lists the description id when a description slot is filled and the
|
|
127
|
+
error id when `field-has-error` is true.
|
|
128
|
+
|
|
129
|
+
```vue
|
|
130
|
+
<ToggleSwitchField
|
|
131
|
+
v-model="notificationsEnabled"
|
|
132
|
+
name="notifications"
|
|
133
|
+
label="Enable notifications"
|
|
134
|
+
:error-message="errors.notifications"
|
|
135
|
+
:field-has-error="!!errors.notifications"
|
|
136
|
+
>
|
|
137
|
+
<template #descriptionText>Sends at most one email a week.</template>
|
|
138
|
+
</ToggleSwitchField>
|
|
139
|
+
|
|
140
|
+
<ToggleSwitchField v-model="darkMode" name="darkMode" label="Dark mode" layout="inline" />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The `description` slot was removed on 2026-09-29; use `descriptionText` or `descriptionHtml`.
|
|
@@ -28,7 +28,8 @@ This project uses **native CSS nesting** (W3C CSS Nesting spec), not Sass/SCSS.
|
|
|
28
28
|
**Why it silently fails**: esbuild converts `&__heading` to `:is(__heading)`, which tries to match an HTML element named `__heading`. No such element exists, so the styles are never applied. There's no error — just missing styles.
|
|
29
29
|
|
|
30
30
|
The build will show this warning:
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
```text
|
|
32
33
|
▲ [WARNING] Cannot use type selector "__heading" directly after nesting selector "&" [css-syntax-error]
|
|
33
34
|
CSS nesting syntax does not allow the "&" selector to come before a type selector.
|
|
34
35
|
```
|
|
@@ -119,3 +120,23 @@ If you see `&__` or `&-` in a `.vue` `<style lang="css">` block, it's Sass synta
|
|
|
119
120
|
|
|
120
121
|
- `CLAUDE.md` → Styling Methodology section
|
|
121
122
|
- `css-grid-max-width-gutters.md` — example of correct native nesting in a grid utility
|
|
123
|
+
|
|
124
|
+
## State rules and specificity
|
|
125
|
+
|
|
126
|
+
Nesting multiplies specificity: `.toggle-switch { .toggle-switch-wrapper { .toggle-switch-track
|
|
127
|
+
{ .toggle-switch-thumb {} } } }` compiles to a four-class selector. A state rule written once at
|
|
128
|
+
the root level, `.toggle-switch[data-checked] .toggle-switch-thumb`, has only three, so the base
|
|
129
|
+
rule wins and the state never shows. Nest the state rule inside the element's own block instead:
|
|
130
|
+
|
|
131
|
+
```css
|
|
132
|
+
.toggle-switch-thumb {
|
|
133
|
+
translate: 0 0;
|
|
134
|
+
|
|
135
|
+
.toggle-switch[data-checked] & {
|
|
136
|
+
translate: 100% 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`.toggle-switch[data-checked] &` adds the root's class and attribute on top of the full nested
|
|
142
|
+
selector, so it always outranks the base rule.
|
|
@@ -25,7 +25,7 @@ The layer's `modules/icon-sets.ts` runs at dev/build time and logs an info messa
|
|
|
25
25
|
| `lucide` | `@iconify-json/lucide` | ActionMenu, InputCopy, PopOver, SelectMenu |
|
|
26
26
|
| `material-symbols` | `@iconify-json/material-symbols` | form components |
|
|
27
27
|
| `mdi` | `@iconify-json/mdi` | Form components, ServicesCard |
|
|
28
|
-
| `radix-icons` | `@iconify-json/radix-icons` |
|
|
28
|
+
| `radix-icons` | `@iconify-json/radix-icons` | InputPasswordField, InputError, DisplayThemeSwitch |
|
|
29
29
|
|
|
30
30
|
## Fix: install missing packages
|
|
31
31
|
|
|
@@ -48,7 +48,7 @@ beat every layered one regardless of specificity, so any `width`/`height` in `@l
|
|
|
48
48
|
on an icon showed struck through in DevTools and did nothing, and the rule was "use `font-size`".
|
|
49
49
|
When the layer was added, the `width`/`height` declarations written against icons at the time
|
|
50
50
|
(CanvasSwitcher, DisplayTooltip, PopOver, InputCopy, SelectMenu, CarouselFlip, ResponsiveHeader,
|
|
51
|
-
ServiceDetail,
|
|
51
|
+
ServiceDetail, InputSelect) started applying for the first time. If an icon in one of those
|
|
52
52
|
looks a different size than it used to, that's the cause.
|
|
53
53
|
|
|
54
54
|
## Related notes
|
package/.claude/skills/index.md
CHANGED
|
@@ -126,7 +126,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
126
126
|
├── display-theme-switch.md — DisplayThemeSwitch: system/light/dark picker wrapping TripleToggleSwitch, wired to useSettingsStore, labels/icons props, small sizing variant
|
|
127
127
|
├── card-core.md — CardCore: generic card container, dynamic named slots as rows, 4 variants, blurred backdrop layer, full CSS token API
|
|
128
128
|
├── action-menu.md — ActionMenu + ActionMenuItem: ellipsis trigger + anchored popover menu, indexed item-{n} slots, link/button items, full CSS token API
|
|
129
|
-
├── select-menu.md — SelectMenu: v-model single-select listbox popover (ActionMenu's popover mechanics,
|
|
129
|
+
├── select-menu.md — SelectMenu: v-model single-select listbox popover (ActionMenu's popover mechanics, InputSelect's selected-option semantics), icon/text/chevron trigger toggles, checkmark on selected option, options-array driven, full CSS token API
|
|
130
130
|
├── display-dialog.md — DisplayDialog: native <dialog> overlay, 5 variants (dialog/modal/confirm/alert/fullscreen), useDialogControls integration, CSS token API
|
|
131
131
|
├── display-chip.md — DisplayChip: status indicator chip overlay, CSS trig positioning, circle/square shapes, status colours, icon/label content
|
|
132
132
|
├── display-pill.md — DisplayPill: pill/badge label with icon slot, 6 variants, 3 sizes, reversible order, --display-pill-* tokens (variants fall back to base colour/border/ring tokens); data-size/data-variant/data-reversed hooks (2026-10-08, were bare classes), label line-clamp token, -02 ring and outlined-monochrome recipes, StressTest story
|
|
@@ -145,7 +145,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
145
145
|
├── display-tooltip.md — DisplayTooltip: anchor-positioned popover trigger, browser support caveat, CSS token API
|
|
146
146
|
├── display-tooltip-defined.md — DisplayTooltipDefined: structured title/body/action tooltip content with close button, composes DisplayTooltip
|
|
147
147
|
├── pop-over.md — PopOver: generic anchor-positioned disclosure panel, consumer-supplied trigger/content slots, placement prop, CSS token API
|
|
148
|
-
├── input-text
|
|
148
|
+
├── input-text.md — InputText (bare control), InputTextField, InputPasswordField, InputTextAsNumberField: renamed from Core/WithLabel, data-* state hooks, readonly/autocomplete props, leak-free digit filtering, wrapper attr-leak/empty-description/passthrough fixes, rounded+clamped stepping, min/max pass-through, CSS token API, stress stories
|
|
149
149
|
├── input-button.md — InputButton (renamed from InputButtonCore 2026-09-28): button/NuxtLink/a, variants, icon slots, readonly (aria-disabled, activation blocked), tokens
|
|
150
150
|
├── input-range.md — InputRange: native range-slider primitive, markers/datalist slots, dead --theme-form-range-accent-color token fixed, CSS token API; Variants section covers InputRangeField; renamed from InputRangeCore/InputRangeDefault 2026-10-16, clamped stepping, useId datalist id, data-* hooks
|
|
151
151
|
├── input-number.md — InputNumber: native number-input primitive, left/right step-button slots, missing base class/placeholder/mismatched slot-selector bugs fixed, CSS token API; Variants section covers InputNumberField; clamped, rounded stepping, data-* state hooks, min/step-aware inputmode
|
|
@@ -157,12 +157,12 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
157
157
|
├── input-label.md — InputLabel: the <label> every labelled *Field wrapper renders (textLabel/htmlLabel slots), id is the control's id rendered as for, required/optional indicator (app.config srcdev.inputLabel, text or icon), dead name/theme props and fallthrough :for removed, data-input-variant/data-invalid hooks, CSS token API
|
|
158
158
|
├── form-wrapper.md — FormWrapper: width-capped outer container for a whole form, data-width hook, named inline-size container, not centred by default (--form-wrapper-margin-inline), CSS token API
|
|
159
159
|
├── pending-effect.md — PendingEffect: animated dashed border InputButton draws while is-pending (has-pending-effect), tokens renamed to --pending-effect-* and read at point of use, animation moved off the host button, aria-hidden
|
|
160
|
-
├── input-textarea
|
|
161
|
-
├── input-select
|
|
160
|
+
├── input-textarea.md — InputTextarea (bare control), InputTextareaField (labelled): renamed from Core/WithLabel, data-* state hooks, line breaks kept (all:unset pre-wrap fix), readonly/autocomplete props, min/max-height + slot-gap tokens, aria-invalid=false fix, Field passthrough on root only, CSS token API, stress stories
|
|
161
|
+
├── input-select.md — InputSelect (bare control), InputSelectField (labelled): renamed from Core/WithLabel, data-input-variant/data-invalid/data-dirty/data-active hooks, data-driven options with icon decorator, placeholder-vs-0 fix, Field passthrough on root only, appearance:base-select browser-support note, CSS token API, stress stories
|
|
162
162
|
├── input-checkbox-radio.md — InputCheckboxRadio (bare control), InputCheckboxRadioField (labelled), InputCheckboxRadioButton (button/pill option): renamed from Core/WithLabel, state classes → data-* hooks, type-switch v-model fix, no aria-checked, icon font-size sizing, button spacing tokens, long labels wrap (inline nowrap removed), optional useId button id
|
|
163
163
|
├── input-checkbox.md — MultipleCheckboxes/SingleCheckbox: fieldset-wrapped checkbox group (data-driven, button or labelled) and single checkbox, per-box native-required/styleClassPassthrough/data-testid bugs fixed, spacing token API, wraps hostile content (fieldset min-inline-size, useId option ids)
|
|
164
164
|
├── input-radio.md — MultipleRadiobuttons: fieldset radiogroup (data-driven, button or labelled), shared-name grouping bug fixed, dead label/placeholder/equalCols props removed, data-options-layout hook, spacing token API, useId button ids, wraps hostile content
|
|
165
|
-
├── toggle-switch
|
|
165
|
+
├── toggle-switch.md — ToggleSwitch (bare control), ToggleSwitchField (labelled, layout="stacked"|"inline", replaces ToggleSwitchWithLabelInline): renamed from Core/WithLabel, role="switch", prefixed inner classes + data-checked, single-toggle click handling, reduced motion, round prop + border-radius token, track width from geometry, CSS token API, stress stories
|
|
166
166
|
├── triple-toggle-switch.md — TripleToggleSwitch (renamed from TripleToggleSwitchCore): three-option icon radio pill with sliding marker, radiogroup + ariaLabel, system/light/dark marker-gradient tokens, reduced-motion, CSS token API
|
|
167
167
|
├── entry-animation.md — EntryAnimation: scroll-driven entry animation wrapper (slide-in/zoom-reveal/exit-blur utility classes), skipAnimation for above-the-fold loop items, reduced-motion handled at the CSS layer
|
|
168
168
|
├── column-flow-grid.md — ColumnFlowGrid (renamed from MasonryGrid): CSS multi-column text-flow layout, named dynamic slots (no count/data prop), itemMinWidth/gap/unit sizing, CSS token API; not a true masonry, see "which one do I want?"
|