srcdev-nuxt-components 9.6.7 → 9.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/consumer/ids-from-useid.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +18 -1
- package/.claude/skills/component-inline-action-button.md +1 -1
- package/.claude/skills/component-local-style-override.md +1 -1
- package/.claude/skills/component-naming.md +13 -13
- package/.claude/skills/components/container-glow.md +2 -2
- package/.claude/skills/components/form-field.md +2 -2
- package/.claude/skills/components/form-wrapper.md +1 -1
- package/.claude/skills/components/glowing-border.md +1 -1
- package/.claude/skills/components/input-checkbox-radio.md +19 -3
- package/.claude/skills/components/input-checkbox.md +14 -3
- package/.claude/skills/components/input-description.md +7 -7
- package/.claude/skills/components/input-error.md +3 -3
- package/.claude/skills/components/input-label.md +7 -8
- package/.claude/skills/components/input-number.md +60 -29
- package/.claude/skills/components/input-radio.md +15 -1
- package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
- package/.claude/skills/components/input-select.md +153 -0
- package/.claude/skills/components/input-text.md +220 -0
- package/.claude/skills/components/input-textarea.md +147 -0
- package/.claude/skills/components/select-menu.md +4 -4
- package/.claude/skills/components/toggle-switch.md +143 -0
- package/.claude/skills/composable-zod-validation.md +1 -1
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/icon-sizing.md +1 -1
- package/.claude/skills/index.md +10 -10
- package/.claude/skills/storybook-add-story.md +1 -1
- package/.claude/skills/testing-add-unit-test.md +17 -0
- package/.claude/skills/theming-component-token-pattern.md +8 -8
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
- package/.vscode/srcdev-component-input-range.code-snippets +38 -18
- package/.vscode/srcdev-component-input-select.code-snippets +20 -14
- package/.vscode/srcdev-component-input-text.code-snippets +46 -40
- package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
- package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
- package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
- package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
- package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
- package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
- package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
- package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
- package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
- package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +7 -7
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +6 -6
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +50 -6
- package/app/components/05.forms/input-number/InputNumber.vue +39 -27
- package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
- package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
- package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
- package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
- package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
- package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
- package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
- package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
- package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
- package/app/components/05.forms/input-range/InputRange.vue +247 -0
- package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
- package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
- package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
- package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
- package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
- package/app/components/05.forms/input-select/InputSelect.vue +198 -0
- package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
- package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
- package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
- package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
- package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
- package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
- package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
- package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
- package/app/components/05.forms/input-text/InputTextField.vue +168 -0
- package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
- package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
- package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
- package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
- package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
- package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
- package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
- package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
- package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
- package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
- package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
- package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +34 -36
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
- package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
- package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
- package/package.json +1 -1
- package/.claude/skills/components/input-select-core.md +0 -151
- package/.claude/skills/components/input-text-core.md +0 -204
- package/.claude/skills/components/input-textarea-core.md +0 -154
- package/.claude/skills/components/toggle-switch-core.md +0 -169
- package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
- package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
- package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
- package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
- package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
- package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
- package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
- package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
|
@@ -0,0 +1,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`.
|
|
@@ -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,10 +145,10 @@ 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
|
-
├── input-range
|
|
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
|
|
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
|
+
├── 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
|
|
152
152
|
├── input-otp.md — InputOtp: one-time-code digit boxes (paste/autofill spreading, Backspace/arrow/Home/End nav, Enter submits, leading zeros kept, complete event, hidden input); Variants section covers InputOtpField
|
|
153
153
|
├── form-field.md — FormField: layout wrapper for one form control (width cap, gutter, field spacing), data-width/data-has-gutter/data-invalid hooks, dead .underline rule removed, CSS token API
|
|
154
154
|
├── form-fieldset.md — FormFieldset: shared fieldset+legend for checkbox/radio groups, groupRole prop (hardcoded radiogroup bug fixed), aria-required only on radiogroup, dead description slot removed, legend token API
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
165
|
-
├── toggle-switch
|
|
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
|
+
├── 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
|
+
├── 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
|
+
├── 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.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?"
|
|
@@ -181,7 +181,7 @@ Playwright `STORY_BASE` slug. The slug is derived by lowercasing and replacing s
|
|
|
181
181
|
| Title | STORY_BASE slug |
|
|
182
182
|
| --------------------------------------------- | ------------------------------------------- |
|
|
183
183
|
| `"Atoms/Text Blocks/HeroText"` | `atoms-text-blocks-herotext` |
|
|
184
|
-
| `"Components/Forms/Input Text/
|
|
184
|
+
| `"Components/Forms/Input Text/InputText"` | `components-forms-input-text-inputtext` |
|
|
185
185
|
|
|
186
186
|
## argTypes control types
|
|
187
187
|
|
|
@@ -321,6 +321,23 @@ it("passes myProp to ChildComponent", async () => {
|
|
|
321
321
|
|
|
322
322
|
Import the child component directly in the test file — it is not auto-imported there.
|
|
323
323
|
|
|
324
|
+
## Testing `useId()` uniqueness across instances
|
|
325
|
+
|
|
326
|
+
Each `mountSuspended` call is its own Nuxt app, so `useId()` restarts its counter and two separately
|
|
327
|
+
mounted instances get the **same** ids. That's a test artefact, not a bug. To prove two instances
|
|
328
|
+
don't collide, render both inside one host component (`MultipleCheckboxes.spec.ts`):
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
const Host = defineComponent({
|
|
332
|
+
components: { ComponentUnderTest },
|
|
333
|
+
setup: () => ({ props: initialPropsData }),
|
|
334
|
+
template: `<div><ComponentUnderTest v-bind="props" /><ComponentUnderTest v-bind="props" /></div>`,
|
|
335
|
+
});
|
|
336
|
+
const host = await mountSuspended(Host);
|
|
337
|
+
const ids = host.findAll("input").map((input) => input.attributes("id"));
|
|
338
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
339
|
+
```
|
|
340
|
+
|
|
324
341
|
## Mocking browser APIs
|
|
325
342
|
|
|
326
343
|
### Global constructors (ResizeObserver, IntersectionObserver, etc.)
|
|
@@ -25,7 +25,7 @@ overrides (2026-08-25):
|
|
|
25
25
|
signalled that to the consumer — the app's declaration became permanently dead, silently. A
|
|
26
26
|
`var()` with an undefined custom property and no fallback just resolves to the property's
|
|
27
27
|
initial value; there is no error, no warning, ever.
|
|
28
|
-
2. **Tokens that were "shared" only by accident.** `
|
|
28
|
+
2. **Tokens that were "shared" only by accident.** `InputText`, `InputSelect`,
|
|
29
29
|
`InputNumber` and the checkbox button all read the exact same `--theme-input-surface`, so a
|
|
30
30
|
consumer wanting to restyle just text inputs had no way to do that without also restyling every
|
|
31
31
|
other input type. The private `--_input-text-surface: var(--theme-input-surface);` indirection
|
|
@@ -208,27 +208,27 @@ consuming app having declared anything.
|
|
|
208
208
|
Pure token-rename pass, no visual/default changes (unlike `InputButton`'s redesign above).
|
|
209
209
|
Added Storybook stories for both (`InputCheckboxRadio`, `InputCheckboxRadioButton`) —
|
|
210
210
|
neither had one before.
|
|
211
|
-
- ✅ `
|
|
212
|
-
`CONSUMER-STYLING.md`. `
|
|
211
|
+
- ✅ `InputText`, `InputSelect`, `InputNumber` — migrated 2026-08-25, see their
|
|
212
|
+
`CONSUMER-STYLING.md`. `InputText`/`InputSelect` also had this exact problem's original
|
|
213
213
|
form: a private `--_input-text-*`/`--_input-select-*` naming scheme documented as a second
|
|
214
214
|
"local override path" alongside the global tokens, added 2026-08-22 (three days before this
|
|
215
215
|
pattern was formalised) as a workaround for a global-override-not-landing report that was never
|
|
216
216
|
root-caused. That private-var-as-override-point shape is exactly what this pattern replaces —
|
|
217
217
|
renamed public with real fallbacks, old two-path docs removed. `InputNumber` had no
|
|
218
218
|
component-scoped tokens at all before this (bare `--theme-input-*` throughout). Added a
|
|
219
|
-
Storybook story for `
|
|
220
|
-
`
|
|
219
|
+
Storybook story for `InputSelect` (didn't have one); `InputNumber` still doesn't.
|
|
220
|
+
`InputText`/`InputSelect`'s `.normal` variant also had hover and `:focus-visible` share
|
|
221
221
|
one token (literally the same rule content duplicated under two selectors) — split into
|
|
222
222
|
`--input-text-border-hover`/`--input-select-border-hover` alongside the existing `-border-focus`
|
|
223
223
|
pair, both still defaulting to `--theme-border-focus` so default rendering is unchanged, but
|
|
224
224
|
mouse and assistive-tech interaction can now be themed independently. `InputNumber` has no
|
|
225
225
|
hover state on its border at all (only a `:focus-visible` box-shadow toggle), so there was
|
|
226
226
|
nothing to split there.
|
|
227
|
-
- ✅ `
|
|
227
|
+
- ✅ `ToggleSwitch`, `TripleToggleSwitch`, `DisplayThemeSwitch` — migrated 2026-08-25, see
|
|
228
228
|
the first two's `CONSUMER-STYLING.md`. Correction to the two lines this replaces: they claimed
|
|
229
|
-
`
|
|
229
|
+
`ToggleSwitch` read a bare `--theme-toggle-symbol-color-*` — that token doesn't exist
|
|
230
230
|
anywhere in this codebase and never did; false memory from conflating it with the checkbox
|
|
231
|
-
fix. `
|
|
231
|
+
fix. `ToggleSwitch` itself turned out to be a plain Tier 2 case (real, live tokens, just no
|
|
232
232
|
component-scoped override point) plus five confirmed-dead private locals (declared, never read
|
|
233
233
|
anywhere in the file) removed outright. The real dead-token bug was in `TripleToggleSwitch`
|
|
234
234
|
(and duplicated in `DisplayThemeSwitch`'s own override): `--theme-form-radio-border`,
|
|
@@ -51,7 +51,7 @@ From `00.element-defaults.css` through `09.animation-motion.css`:
|
|
|
51
51
|
|
|
52
52
|
`00.element-defaults.css` is the one exception — it's not tokens, it's a real selector-based rule
|
|
53
53
|
(browser autofill background/text-colour reset via `input:autofill` etc.). It reads
|
|
54
|
-
`var(--theme-surface)` and the component-private `--_input-text-
|
|
54
|
+
`var(--theme-surface)` and the component-private `--_input-text-color`, so changing
|
|
55
55
|
`--theme-surface` (per `theming-partial-override.md`) changes autofill colours too — no separate
|
|
56
56
|
override needed there. If you need to change that rule's *behaviour* itself (not just its colour
|
|
57
57
|
inputs), you'd need a competing unlayered selector of your own targeting the same pseudo-classes,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"SRCDEV
|
|
3
|
-
"description": "Basic
|
|
2
|
+
"SRCDEV InputRange Basic": {
|
|
3
|
+
"description": "Basic InputRange native range input (no label/error wrapper)",
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
|
-
"<
|
|
6
|
+
"<InputRange",
|
|
7
7
|
" id=\"$1fieldId\"",
|
|
8
8
|
" name=\"$2fieldName\"",
|
|
9
9
|
" v-model=\"$3fieldValue\"",
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
"/>"
|
|
14
14
|
]
|
|
15
15
|
},
|
|
16
|
-
"SRCDEV
|
|
17
|
-
"description": "
|
|
16
|
+
"SRCDEV InputRange With Markers": {
|
|
17
|
+
"description": "InputRange with custom tick-mark markers overlaid on the track",
|
|
18
18
|
"scope": "vue,html",
|
|
19
19
|
"body": [
|
|
20
|
-
"<
|
|
20
|
+
"<InputRange",
|
|
21
21
|
" id=\"$1fieldId\"",
|
|
22
22
|
" name=\"$2fieldName\"",
|
|
23
23
|
" v-model=\"$3fieldValue\"",
|
|
@@ -27,19 +27,39 @@
|
|
|
27
27
|
">",
|
|
28
28
|
" <template #markers>",
|
|
29
29
|
" <div class=\"input-range-markers\">",
|
|
30
|
-
" <div class=\"marker\"><span class=\"marker-icon\">$7\\ud83d\\ude00</span></div>",
|
|
31
|
-
" <div class=\"marker\"><span class=\"marker-icon\">$8\\ud83d\\ude10</span></div>",
|
|
32
|
-
" <div class=\"marker\"><span class=\"marker-icon\">$9\\ud83d\\ude1e</span></div>",
|
|
30
|
+
" <div class=\"input-range-marker\"><span class=\"input-range-marker-icon\">$7\\ud83d\\ude00</span></div>",
|
|
31
|
+
" <div class=\"input-range-marker\"><span class=\"input-range-marker-icon\">$8\\ud83d\\ude10</span></div>",
|
|
32
|
+
" <div class=\"input-range-marker\"><span class=\"input-range-marker-icon\">$9\\ud83d\\ude1e</span></div>",
|
|
33
33
|
" </div>",
|
|
34
34
|
" </template>",
|
|
35
|
-
"</
|
|
35
|
+
"</InputRange>"
|
|
36
36
|
]
|
|
37
37
|
},
|
|
38
|
-
"SRCDEV
|
|
39
|
-
"description": "
|
|
38
|
+
"SRCDEV InputRange With Datalist": {
|
|
39
|
+
"description": "InputRange with a datalist; the slot passes the generated datalistId",
|
|
40
40
|
"scope": "vue,html",
|
|
41
41
|
"body": [
|
|
42
|
-
"<
|
|
42
|
+
"<InputRange",
|
|
43
|
+
" id=\"$1fieldId\"",
|
|
44
|
+
" name=\"$2fieldName\"",
|
|
45
|
+
" v-model=\"$3fieldValue\"",
|
|
46
|
+
" :min=\"${4:0}\"",
|
|
47
|
+
" :max=\"${5:100}\"",
|
|
48
|
+
">",
|
|
49
|
+
" <template #datalist=\"{ datalistId }\">",
|
|
50
|
+
" <datalist :id=\"datalistId\" class=\"input-range-datalist\">",
|
|
51
|
+
" <option value=\"${4:0}\" label=\"${6:Low}\"></option>",
|
|
52
|
+
" <option value=\"${5:100}\" label=\"${7:High}\"></option>",
|
|
53
|
+
" </datalist>",
|
|
54
|
+
" </template>",
|
|
55
|
+
"</InputRange>"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"SRCDEV InputRangeField Basic": {
|
|
59
|
+
"description": "InputRangeField: labelled range slider with error/description support",
|
|
60
|
+
"scope": "vue,html",
|
|
61
|
+
"body": [
|
|
62
|
+
"<InputRangeField",
|
|
43
63
|
" v-model=\"$1fieldValue\"",
|
|
44
64
|
" name=\"$2fieldName\"",
|
|
45
65
|
" label=\"$3Field label\"",
|
|
@@ -51,11 +71,11 @@
|
|
|
51
71
|
"/>"
|
|
52
72
|
]
|
|
53
73
|
},
|
|
54
|
-
"SRCDEV
|
|
55
|
-
"description": "
|
|
74
|
+
"SRCDEV InputRangeField With Step Buttons": {
|
|
75
|
+
"description": "InputRangeField with step-down/step-up buttons wired into the left/right slots",
|
|
56
76
|
"scope": "vue,html",
|
|
57
77
|
"body": [
|
|
58
|
-
"<
|
|
78
|
+
"<InputRangeField",
|
|
59
79
|
" v-model=\"$1fieldValue\"",
|
|
60
80
|
" name=\"$2fieldName\"",
|
|
61
81
|
" label=\"$3Field label\"",
|
|
@@ -67,11 +87,11 @@
|
|
|
67
87
|
">",
|
|
68
88
|
" <template #left>\\u2212</template>",
|
|
69
89
|
" <template #right>+</template>",
|
|
70
|
-
"</
|
|
90
|
+
"</InputRangeField>"
|
|
71
91
|
]
|
|
72
92
|
},
|
|
73
93
|
"SRCDEV InputRange CSS Override": {
|
|
74
|
-
"description": "CSS override scaffold for
|
|
94
|
+
"description": "CSS override scaffold for InputRange tokens",
|
|
75
95
|
"scope": "css",
|
|
76
96
|
"body": [
|
|
77
97
|
".$1my-page {",
|