srcdev-nuxt-components 9.4.10 → 9.4.12
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/commands/migrate-component.md +18 -0
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/output.html +1 -1
- package/.claude/skills/components/input-number-core.md +214 -0
- package/.claude/skills/components/input-select-core.md +151 -0
- package/.claude/skills/components/input-text-core.md +16 -0
- package/.claude/skills/components/input-textarea-core.md +154 -0
- package/.claude/skills/components/select-menu.md +195 -0
- package/.claude/skills/components/toggle-switch-core.md +150 -0
- package/.claude/skills/index.md +5 -0
- package/.vscode/srcdev-component-input-number.code-snippets +65 -0
- package/.vscode/srcdev-component-input-select.code-snippets +42 -0
- package/.vscode/srcdev-component-input-textarea.code-snippets +40 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +65 -0
- package/.vscode/srcdev-component-toggle-switch.code-snippets +49 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +1 -1
- package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/animations/section-parallax/SectionParallax.vue +1 -1
- package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +2 -2
- package/app/components/01.atoms/card/CardCore.vue +4 -4
- package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +2 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +2 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +1 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +1 -1
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +8 -8
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +8 -8
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +6 -6
- package/app/components/01.atoms/pop-over/PopOver.vue +6 -6
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +1 -1
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +11 -0
- package/app/components/02.molecules/pricing-card/PricingCard.vue +3 -1
- package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +2 -0
- package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +13 -0
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +168 -0
- package/app/components/02.molecules/select-menu/SelectMenu.vue +384 -0
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +359 -0
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +476 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +42 -0
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +3 -3
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +3 -3
- package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +18 -18
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +2 -7
- package/app/components/05.forms/input-button/InputButtonCore.vue +1 -5
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +26 -6
- package/app/components/05.forms/input-number/InputNumberCore.vue +125 -85
- package/app/components/05.forms/input-number/stories/InputNumberCore.stories.ts +92 -0
- package/app/components/05.forms/input-number/tests/InputNumberCore.spec.ts +119 -0
- package/app/components/05.forms/input-number/variants/InputNumberDefault.vue +30 -22
- package/app/components/05.forms/input-number/variants/stories/InputNumberDefault.stories.ts +123 -0
- package/app/components/05.forms/input-number/variants/tests/InputNumberDefault.spec.ts +143 -0
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-range/InputRangeCore.vue +2 -2
- package/app/components/05.forms/input-select/InputSelectCore.vue +23 -30
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +2 -2
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +115 -0
- package/app/components/05.forms/input-select/tests/InputSelectCore.spec.ts +129 -0
- package/app/components/05.forms/input-select/variants/InputSelectWithLabel.vue +1 -1
- package/app/components/05.forms/input-select/variants/tests/InputSelectWithLabel.spec.ts +87 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +14 -0
- package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +1 -1
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +55 -0
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +24 -16
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +1 -1
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +119 -0
- package/app/components/05.forms/input-textarea/variants/InputTextareaWithLabel.vue +3 -13
- package/app/components/05.forms/input-textarea/variants/tests/InputTextareaWithLabel.spec.ts +93 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +95 -10
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +29 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +120 -119
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +1 -1
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +1 -1
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +2 -2
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +101 -0
- package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +3 -5
- package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +1 -2
- package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabel.spec.ts +77 -0
- package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabelInline.spec.ts +66 -0
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +4 -4
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +6 -6
- package/app/types/components/index.ts +1 -0
- package/app/types/components/select-menu.d.ts +5 -0
- package/package.json +1 -1
- package/.vscode/srcdev-component-toggle-switch-core.code-snippets +0 -13
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# InputNumberCore Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`InputNumberCore` is the low-level native `<input type="number">` primitive for the `05.forms`
|
|
6
|
+
number-input family. It renders a single native number input with left/right button slots (for
|
|
7
|
+
step-down/step-up controls). It has no label, description, or error-message rendering of its
|
|
8
|
+
own — that's composed by the `InputNumberDefault` variant below.
|
|
9
|
+
|
|
10
|
+
Most consumers should reach for **InputNumberDefault** rather than `InputNumberCore` directly.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Props reference
|
|
15
|
+
|
|
16
|
+
> **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:field-has-error`, `:style-class-passthrough`, `:aria-describedby`.
|
|
17
|
+
|
|
18
|
+
| Prop (template form) | Type | Default | Notes |
|
|
19
|
+
|------|------|---------|-------|
|
|
20
|
+
| `:id` | `string` | (required) | Applied to the native input. |
|
|
21
|
+
| `:name` | `string` | (required) | Applied to the native input. |
|
|
22
|
+
| `:min` | `number` | (required) | Native `min`. |
|
|
23
|
+
| `:max` | `number` | (required) | Native `max`. |
|
|
24
|
+
| `:step` | `number` | `1` | Native `step`. |
|
|
25
|
+
| `:placeholder` | `string` | `""` | Native `placeholder`. |
|
|
26
|
+
| `:required` | `boolean` | `false` | Native `required`. |
|
|
27
|
+
| `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
|
|
28
|
+
| `:weight` | `FormWeight` | `"normal"` | Adds an `input-number--{weight}` class to the input, for prop-shape consistency with sibling `05.forms` components. **No CSS in the library currently styles this class — it's a no-op today**, same convention as `InputRangeCore`'s `weight` prop. |
|
|
29
|
+
| `:input-variant` | `InputUiVariant` | `"normal"` | Adds the variant name as a class on the wrapper — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
|
|
30
|
+
| `:field-has-error` | `boolean` | `false` | Drives `data-invalid` on the wrapper. |
|
|
31
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. |
|
|
32
|
+
| `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
|
|
33
|
+
|
|
34
|
+
### v-model
|
|
35
|
+
|
|
36
|
+
`v-model` — `number | readonly number[]`, **required** (`defineModel({ required: true })`). Bound
|
|
37
|
+
directly to the native number input. Required rather than defaulted deliberately — same reasoning
|
|
38
|
+
as `InputRangeCore`'s `v-model` (see that skill doc) — this also satisfies `vue/require-default-prop`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Slots
|
|
43
|
+
|
|
44
|
+
| Slot | Notes |
|
|
45
|
+
|------|-------|
|
|
46
|
+
| `left` | Rendered before the input (e.g. a step-down button). Adds `has-left-slot` to the wrapper. |
|
|
47
|
+
| `right` | Rendered after the input (e.g. a step-up button). Adds `has-right-slot` to the wrapper. |
|
|
48
|
+
|
|
49
|
+
Any `.input-button-core` inside `.slot` gets its own border/outline stripped and is sized to
|
|
50
|
+
match the input (`aspect-ratio: 1`, `width: var(--input-min-height)`) regardless of which slot
|
|
51
|
+
it's in. The divider border is then added directly to that button element specifically when its
|
|
52
|
+
slot is present — `&.has-left-slot .left-slot .input-button-core { border-right: ... }` and the
|
|
53
|
+
right-hand mirror — so it sits flush against the button's own edge rather than around the
|
|
54
|
+
containing `.slot` div. This makes step buttons read as embedded segments of the same control
|
|
55
|
+
rather than separate floating buttons. The divider reuses the wrapper's own border colour token
|
|
56
|
+
(no separate divider token), same as `InputTextCore`/`InputTextAsNumberWithLabel`.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## CSS token API
|
|
61
|
+
|
|
62
|
+
See [CONSUMER-STYLING.md](../../app/components/05.forms/input-number/CONSUMER-STYLING.md) for the
|
|
63
|
+
full table with defaults.
|
|
64
|
+
|
|
65
|
+
**Common tokens:**
|
|
66
|
+
- `--input-number-surface` / `-surface-hover` — wrapper/input/embedded-button background, resting and hover, falls back to `--theme-input-surface`/`-surface-hover`
|
|
67
|
+
- `--input-number-border` / `-border-hover` / `-border-focus` — border colour resting, mouse-hover outline, and `:focus-visible` outline (also the divider colour and the embedded button's own focus outline), falls back to `--theme-border`/`--theme-border-focus`
|
|
68
|
+
- `--input-number-text-color` — input text colour, falls back to `--theme-input-text-color-normal`
|
|
69
|
+
- `--input-number-placeholder-color` — placeholder text colour, falls back to `--theme-input-placeholder`
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Fixed 2026-09-22: dead CSS from a missing base class and mismatched selectors
|
|
74
|
+
|
|
75
|
+
`InputNumberCore` migrated to full compliance from a `variants:true`, `eslint_issues:true`,
|
|
76
|
+
score-1/5 state. Several latent bugs were fixed alongside the standard checklist:
|
|
77
|
+
|
|
78
|
+
- The native `<input>` had no static `input-number-core` class — only the dynamic
|
|
79
|
+
`elementClasses` passthrough — so the entire `.input-number-core { ... }` style block (font,
|
|
80
|
+
padding, colour, focus box-shadow) never matched anything. Fixed by adding the class directly.
|
|
81
|
+
- `placeholder` was declared as a prop but never bound on the `<input>` — unlike `InputRangeCore`
|
|
82
|
+
(where a range input genuinely has no placeholder concept and the no-op is intentional and
|
|
83
|
+
documented), a number input *does* support `placeholder` meaningfully, so this was a real gap,
|
|
84
|
+
not a deliberate no-op. Fixed by binding `:placeholder`.
|
|
85
|
+
- The left/right slot divs rendered with classes `slot left`/`slot right`, but the CSS targeted
|
|
86
|
+
`.left-slot`/`.right-slot` and `.has-left-slot`/`.has-right-slot` — neither the slot divs nor
|
|
87
|
+
the wrapper ever carried those exact class names, so the divider-border and flex-alignment
|
|
88
|
+
rules for embedded step buttons never applied. Fixed by renaming the slot divs to
|
|
89
|
+
`slot left-slot`/`slot right-slot` and adding `has-left-slot`/`has-right-slot` to the wrapper
|
|
90
|
+
directly from `slots.left`/`slots.right` (Vue's own `useSlots()`), rather than a fragile,
|
|
91
|
+
cross-component class-toggling relay.
|
|
92
|
+
- Relatedly, `InputNumberDefault` called `updateElementClasses(["has-left-button",
|
|
93
|
+
"has-right-button"])` unconditionally on mount, which permanently added those two classes to
|
|
94
|
+
its own outer `.input-number-with-label` wrapper — a different element than
|
|
95
|
+
`InputNumberCore`'s `.input-number-wrapper`, so the `:has()` selector that was meant to key off
|
|
96
|
+
those classes could never match (the class lived on an ancestor, not a descendant). Replaced
|
|
97
|
+
entirely: `InputNumberCore` now detects an embedded button directly via
|
|
98
|
+
`:has(.left-slot .input-button-core)` / `:has(.right-slot .input-button-core)`, so no
|
|
99
|
+
JS-driven class relay is needed at all.
|
|
100
|
+
- `theme` was typed as an inline `"default" | "success" | "error" | "warning"` union in both
|
|
101
|
+
`InputNumberCore` and `InputNumberDefault` instead of importing the shared `FormUiTheme` type —
|
|
102
|
+
aligned with the rest of `05.forms`.
|
|
103
|
+
- `weight` was passed from `InputNumberDefault` to `InputNumberCore` but `InputNumberCore` had no
|
|
104
|
+
matching prop, so it fell through as a raw, meaningless `weight="normal"` HTML attribute on the
|
|
105
|
+
wrapper `<div>`. Fixed by declaring the prop on `InputNumberCore` too, matching `InputRangeCore`.
|
|
106
|
+
- `defineModel()` had no default, tripping `vue/require-default-prop` — fixed the same way as
|
|
107
|
+
`InputRangeCore`: `defineModel({ required: true })`, not a rule disable or an artificial default.
|
|
108
|
+
- A single-use private token, `--_border`, was inlined to the public
|
|
109
|
+
`var(--input-number-border, var(--theme-border))` directly at its point of use — it had no
|
|
110
|
+
composition or state-swap behind it, just a bare pass-through (see pitfall #20). `--_surface`
|
|
111
|
+
(used twice) and `--_min-width` (composed from a `v-bind()` value) correctly stayed private.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Fixed 2026-09-22 (follow-up): rewritten to visually mirror InputTextCore
|
|
116
|
+
|
|
117
|
+
The first migration pass above only made `InputNumberCore`'s *own* (pre-existing, previously dead)
|
|
118
|
+
CSS active — it didn't reconsider whether that CSS was actually the right design. Once live, it
|
|
119
|
+
was visually inconsistent with the rest of `05.forms`: no border-radius, no hover/focus outline
|
|
120
|
+
state, wrong padding-token family (`--form-textarea-padding-*` instead of the shared input
|
|
121
|
+
padding tokens), and a box-shadow-toggle focus mechanism instead of the `:has()`-based outline
|
|
122
|
+
approach every other input in this library uses. `InputNumberCore` is meant to be a native
|
|
123
|
+
`<input type="number">` sibling to the text-based `InputTextAsNumberWithLabel` variant (see
|
|
124
|
+
`input-text-core.md`'s Variants section) — both should look and behave the same. The CSS was rewritten
|
|
125
|
+
to mirror `InputTextCore` directly:
|
|
126
|
+
|
|
127
|
+
- Added `:input-variant` (`"normal"` / `"outlined"` / `"underlined"`), matching `InputTextCore`'s
|
|
128
|
+
prop and wrapper-class shape (`InputNumberDefault` forwards it the same way
|
|
129
|
+
`InputTextWithLabel` forwards it to `InputTextCore`, including to `InputLabel`).
|
|
130
|
+
- Replaced the flat, unconditional border/radius/box-shadow with `InputTextCore`'s `.normal`
|
|
131
|
+
variant shape: border + `border-radius: var(--form-input-border-radius)` (previously this read
|
|
132
|
+
`var(--form-element-border-width)` — a pre-existing bug that used the *border-width* token as
|
|
133
|
+
the *radius*, coincidentally invisible while the block was dead), plus `:has(input:hover)` /
|
|
134
|
+
`:has(input:focus-visible)` outline states instead of the old `--_focus-box-shadow` custom
|
|
135
|
+
property toggle.
|
|
136
|
+
- `--input-number-border-hover` and `--input-number-border-focus` are new public tokens for
|
|
137
|
+
these two states (both fall back to `--theme-border-focus`, same as `InputTextCore`).
|
|
138
|
+
- Padding moved from `--form-textarea-padding-*` (the wrong token family — textarea, not input)
|
|
139
|
+
to the shared `--input-padding-block`/`--input-padding-inline`/`--input-min-height` tokens every
|
|
140
|
+
other `05.forms` text-like input uses.
|
|
141
|
+
- The embedded step button now gets a hover background (`--input-number-surface-hover`, new
|
|
142
|
+
public token) and a visible `:focus-visible` outline (reusing `--input-number-border-focus`) —
|
|
143
|
+
previously it had neither, an accessibility gap beyond the visual mismatch (checklist item 9).
|
|
144
|
+
- The divider between the input and an embedded button now reuses the border colour
|
|
145
|
+
(`--_border`) directly instead of a separate `--input-number-divider-color` token — removed,
|
|
146
|
+
since `InputTextCore` doesn't have an equivalent separate token either (the border colour
|
|
147
|
+
*is* the divider colour there too).
|
|
148
|
+
- `--_border-hover` and `--_surface-hover` were each used only once with no composition or
|
|
149
|
+
state-swap, so per the public-token rule they were inlined directly
|
|
150
|
+
(`var(--input-number-border-hover, var(--theme-border-focus))` /
|
|
151
|
+
`var(--input-number-surface-hover, var(--theme-input-surface-hover))`) rather than kept as
|
|
152
|
+
private wrappers. `--_border-focus` stayed private since it's now genuinely reused (the
|
|
153
|
+
wrapper's own focus outline *and* the embedded button's focus outline both read it).
|
|
154
|
+
|
|
155
|
+
**Separately found in the same investigation** (not an `InputNumberCore` bug, but what the visual
|
|
156
|
+
comparison was originally checking against): `InputTextAsNumberWithLabel.vue` had its own,
|
|
157
|
+
unrelated pre-existing bug — see `input-text-core.md`'s Variants section for the fix.
|
|
158
|
+
|
|
159
|
+
**Correction, same day**: the first version of this rewrite still didn't visually match — the
|
|
160
|
+
divider border and button sizing (`aspect-ratio: 1`, `width: var(--input-min-height)`) had been
|
|
161
|
+
put on the `.left-slot`/`.right-slot` *wrapper divs*, not on `.input-button-core` itself, unlike
|
|
162
|
+
`InputTextCore`'s equivalent block. That left an unstyled gap between the button and the border
|
|
163
|
+
line instead of a flush edge, and the buttons weren't sized to match the input's height. Fixed by
|
|
164
|
+
moving the divider border and all button sizing/colour rules onto `.input-button-core` directly
|
|
165
|
+
(`&.has-left-slot .left-slot .input-button-core { border-right: ... }`, matching
|
|
166
|
+
`InputTextAsNumberWithLabel`'s `.left-slot .input-button-core { border-right: ... }` exactly)
|
|
167
|
+
rather than the containing `.slot` div. When mirroring another component's CSS structure, check
|
|
168
|
+
*which element* a rule targets, not just that an equivalent rule exists somewhere in the block —
|
|
169
|
+
a border/sizing rule one level off in the DOM tree produces a visually-close-but-not-matching
|
|
170
|
+
result that's easy to miss without a side-by-side screenshot comparison.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Variants
|
|
175
|
+
|
|
176
|
+
### InputNumberDefault
|
|
177
|
+
|
|
178
|
+
`InputNumberDefault` (`variants/InputNumberDefault.vue`) composes `InputNumberCore` with
|
|
179
|
+
`InputLabel`, `InputDescription`, and `InputError`, plus optional step-down/step-up buttons
|
|
180
|
+
(`InputButtonCore`) wired into the `left`/`right` slots.
|
|
181
|
+
|
|
182
|
+
**Additional props over InputNumberCore:**
|
|
183
|
+
|
|
184
|
+
| Prop (template form) | Type | Default | Notes |
|
|
185
|
+
|------|------|---------|-------|
|
|
186
|
+
| `:label` | `string` | (required) | Rendered via `InputLabel`. |
|
|
187
|
+
| `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
|
|
188
|
+
| `:step-down-label` | `string` | `"Step down"` | `button-text` on the step-down `InputButtonCore` — override for localisation. |
|
|
189
|
+
| `:step-up-label` | `string` | `"Step up"` | `button-text` on the step-up `InputButtonCore` — override for localisation. |
|
|
190
|
+
|
|
191
|
+
**Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`), plus
|
|
192
|
+
`left`/`right` (forwarded straight through to the underlying `InputNumberCore` — the
|
|
193
|
+
step-down/step-up buttons only render when you use the `left`/`right` slots yourself, since the
|
|
194
|
+
button icon content itself is consumer-supplied).
|
|
195
|
+
|
|
196
|
+
```vue
|
|
197
|
+
<InputNumberDefault
|
|
198
|
+
name="quantity"
|
|
199
|
+
label="Quantity"
|
|
200
|
+
v-model="quantity"
|
|
201
|
+
:min="1"
|
|
202
|
+
:max="10"
|
|
203
|
+
error-message="Please choose a value between 1 and 10"
|
|
204
|
+
:field-has-error="hasError"
|
|
205
|
+
>
|
|
206
|
+
<template #left><Icon name="mdi:minus" /></template>
|
|
207
|
+
<template #right><Icon name="mdi:plus" /></template>
|
|
208
|
+
</InputNumberDefault>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`v-model` is `number | readonly number[]`, **required** — same as `InputNumberCore` above.
|
|
212
|
+
|
|
213
|
+
Step-down is disabled (`readonly` on its `InputButtonCore`) once the value reaches `min`, and
|
|
214
|
+
step-up once it reaches `max`; clicking either button while at that boundary is a no-op.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# InputSelectCore Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`InputSelectCore` is the low-level native `<select>` primitive for the `05.forms` select family.
|
|
6
|
+
It renders a native select with a data-driven list of `<option>`s (each optionally decorated with
|
|
7
|
+
an icon), an optional placeholder option, and adopts the modern CSS Customizable Select API
|
|
8
|
+
(`appearance: base-select`) for a fully styleable open-picker state where supported, with a
|
|
9
|
+
graceful CSS fallback (see "Browser support" below). It has no label, description, or
|
|
10
|
+
error-message rendering of its own — that's composed by the `InputSelectWithLabel` variant below.
|
|
11
|
+
|
|
12
|
+
Most consumers should reach for **InputSelectWithLabel** rather than `InputSelectCore` directly.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Props reference
|
|
17
|
+
|
|
18
|
+
> **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`.
|
|
19
|
+
|
|
20
|
+
| Prop (template form) | Type | Default | Notes |
|
|
21
|
+
|------|------|---------|-------|
|
|
22
|
+
| `:id` | `string` | (required) | Applied to the native select. |
|
|
23
|
+
| `:name` | `string` | (required) | Applied to the native select. |
|
|
24
|
+
| `:placeholder` | `string` | `""` | Renders a disabled placeholder `<option>` (selected when `modelValue` is empty). No option rendered at all when unset. |
|
|
25
|
+
| `:title` | `string` | `"Please select an option"` | Native `title` attribute (tooltip). |
|
|
26
|
+
| `:required` | `boolean` | `false` | Native `required`. |
|
|
27
|
+
| `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
|
|
28
|
+
| `:input-variant` | `InputUiVariant` | `"normal"` | Adds the variant name as a class on the wrapper — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
|
|
29
|
+
| `:field-has-error` | `boolean` | `false` | Drives `data-invalid` on the wrapper and `aria-invalid` on the select. |
|
|
30
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the select element. |
|
|
31
|
+
| `:aria-describedby` | `string` | `""` | Forwarded to the native select. |
|
|
32
|
+
|
|
33
|
+
### v-models
|
|
34
|
+
|
|
35
|
+
- `v-model` — `string | number | readonly number[]`, **required** (`defineModel({ required: true })`, satisfying `vue/require-default-prop` — see `project_ledger_eslint_issues_column` in memory).
|
|
36
|
+
- `v-model:field-data` — `IFormMultipleOptions` (`{ data: IOptionsConfig[]; total; skip; limit }`), **required**. The `data` array drives the rendered `<option>`s. The template dereferences `fieldData.data` unconditionally, so a consumer that doesn't bind `v-model:field-data` will crash on render — `required: true` documents that constraint (it's a dev-time warning only, not a runtime guard).
|
|
37
|
+
- `v-model:is-dirty` — `boolean`. Set to `true` on the native `change` event (never reset to `false` by this component).
|
|
38
|
+
- `v-model:is-active` — `boolean`. Tracks native focus/blur (`focusin`/`focusout`), toggling `.active` on the wrapper.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## CSS token API
|
|
43
|
+
|
|
44
|
+
See [CONSUMER-STYLING.md](../../app/components/05.forms/input-select/CONSUMER-STYLING.md) for the
|
|
45
|
+
full table with defaults.
|
|
46
|
+
|
|
47
|
+
**Common tokens:**
|
|
48
|
+
- `--input-select-surface` / `-surface-hover` — wrapper/select background and option-row hover background, fall back to `--theme-input-surface`/`-surface-hover`
|
|
49
|
+
- `--input-select-border` / `-border-hover` / `-border-focus` — border colour resting, mouse-hover outline, and `:focus-visible` outline (also the open-picker's own border/outline), fall back to `--theme-border`/`--theme-border-focus`
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Browser support: `appearance: base-select`
|
|
54
|
+
|
|
55
|
+
The open-picker styling (`&::picker(select)`, `:open`/`:not(:open)` states, the transition/opacity
|
|
56
|
+
`@starting-style` reveal) relies on the CSS Customizable Select API
|
|
57
|
+
(`appearance: base-select`/`::picker(select)`), a very new feature not yet supported everywhere.
|
|
58
|
+
The component degrades gracefully in non-supporting browsers rather than breaking: `appearance:
|
|
59
|
+
none` is set first (unconditionally), then overridden by `appearance: base-select` only where the
|
|
60
|
+
browser recognizes that value — an unsupported value is simply ignored by the CSS parser, so the
|
|
61
|
+
select falls back to the browser's native (unstyled) dropdown instead of losing functionality.
|
|
62
|
+
Per pitfall #19's general lesson (checking WebKit support specifically for load-bearing use of a
|
|
63
|
+
very new CSS feature, since Chrome's device-emulation mode can't catch a WebKit-only gap): this
|
|
64
|
+
fallback is CSS-only degradation, not a functional break like `ExpandingPanel`'s `::details-content`
|
|
65
|
+
case was, so it's lower risk — but the open-picker's custom border/outline/transition styling
|
|
66
|
+
should still be spot-checked on a real Safari session before treating it as visually final.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Fixed 2026-09-22: dead functionality and prop-shape bugs
|
|
71
|
+
|
|
72
|
+
`InputSelectCore` migrated to full compliance from a `variants:true`, `eslint_issues:true`,
|
|
73
|
+
score-2/5 state (no tests, no skill doc, no snippet).
|
|
74
|
+
|
|
75
|
+
- **`required` prop was declared but never bound to the native `<select>`** — native HTML
|
|
76
|
+
`required` validation was completely inert regardless of the prop's value. Fixed by adding
|
|
77
|
+
`:required` to the select.
|
|
78
|
+
- **`styleClassPassthrough` prop was declared but never consumed** — no `useStyleClassPassthrough`
|
|
79
|
+
call existed anywhere in the script, and the select's `class` was a hardcoded static string.
|
|
80
|
+
The prop was entirely dead; a consumer passing it had no effect. Fixed by wiring up
|
|
81
|
+
`useStyleClassPassthrough` and binding `elementClasses` alongside the static class.
|
|
82
|
+
- **`isDirty`/`isActive` models were declared and read in the wrapper's CSS class bindings
|
|
83
|
+
(`{ dirty: isDirty }`, `{ active: isActive }`), but nothing ever set them** — no `focusin`/
|
|
84
|
+
`focusout`/`change` handlers existed at all, unlike `InputTextareaCore`'s `updateFocus`
|
|
85
|
+
pattern. The `.dirty`/`.active` CSS states could never trigger. Fixed by adding
|
|
86
|
+
`@focusin`/`@focusout` (toggling `isActive`) and `@change` (setting `isDirty = true`) directly
|
|
87
|
+
on the select.
|
|
88
|
+
- **The placeholder `<option>` used `readonly`**, which is not a valid/supported HTML attribute
|
|
89
|
+
on `<option>` — a silent no-op, leaving the placeholder fully selectable as a real answer
|
|
90
|
+
(defeating its purpose). Fixed by switching to `disabled`, the correct native pattern for a
|
|
91
|
+
non-selectable placeholder option.
|
|
92
|
+
- **`theme`/`inputVariant` were typed as inline unions** (`"default" | "success" | "error" |
|
|
93
|
+
"warning"` / `"normal" | "outlined" | "underlined"`) instead of importing the shared
|
|
94
|
+
`FormUiTheme`/`InputUiVariant` types — aligned with the rest of `05.forms`.
|
|
95
|
+
- **`fieldData`'s `defineModel` used an untyped `defineModel("fieldData") as Ref<T>` cast**
|
|
96
|
+
instead of the proper generic `defineModel<T>("fieldData")` call, and had no default — the
|
|
97
|
+
`as` cast also hid the prop from ESLint's `vue/require-default-prop` detection entirely (it
|
|
98
|
+
didn't fire on `InputSelectCore.vue`, only on `InputSelectWithLabel.vue`'s correctly-typed
|
|
99
|
+
equivalent). Fixed by using the proper generic form with `{ required: true }` on both files —
|
|
100
|
+
see the `v-model:field-data` note above for why `required` is the meaningful fix here (the
|
|
101
|
+
template would otherwise crash on `fieldData.data` with no data bound).
|
|
102
|
+
- **Dead commented-out CSS** (~8 lines, a "legacy support - eg, Safari" `::after` arrow
|
|
103
|
+
fallback that was never active) and a **redundant duplicate declaration**
|
|
104
|
+
(`&.underlined { background-color: var(--_surface); }`, re-setting a value the unscoped base
|
|
105
|
+
rule already applied unconditionally) were removed.
|
|
106
|
+
- **CSS token cleanup**: `--_surface`, `--_surface-hover`, `--_border-hover`, and
|
|
107
|
+
`--_outline-color` were each used at exactly one point with no composition or state-swap
|
|
108
|
+
behind them (`--_outline-color` was a bare alias for `--_border-focus`, per pitfall #20) — all
|
|
109
|
+
four inlined directly to their public `var(--input-select-*, {default})` form at their point of
|
|
110
|
+
use. `--_border` (reused across `.normal`/`.underlined`/open-picker) and `--_border-focus`
|
|
111
|
+
(used directly once, plus now the open-picker's outline too) correctly stayed private.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Variants
|
|
116
|
+
|
|
117
|
+
### InputSelectWithLabel
|
|
118
|
+
|
|
119
|
+
`InputSelectWithLabel` (`variants/InputSelectWithLabel.vue`) composes `InputSelectCore` with
|
|
120
|
+
`InputLabel`, `InputDescription` (positioned before or after the field depending on
|
|
121
|
+
`inputVariant === "outlined"`, matching `InputTextWithLabel`'s exact placement rule), and
|
|
122
|
+
`InputError`.
|
|
123
|
+
|
|
124
|
+
**Additional props over InputSelectCore:**
|
|
125
|
+
|
|
126
|
+
| Prop (template form) | Type | Default | Notes |
|
|
127
|
+
|------|------|---------|-------|
|
|
128
|
+
| `:data-testid` | `string` | `"input-select-with-label"` | Sets `data-testid` on the wrapper — used for the wrapper element specifically (`:data-testid` shorthand resolves to the camelCase `dataTestid` prop, same convention as `:aria-describedby`). |
|
|
129
|
+
| `:label` | `string` | (required) | Rendered via `InputLabel`. |
|
|
130
|
+
| `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
|
|
131
|
+
|
|
132
|
+
**Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`).
|
|
133
|
+
|
|
134
|
+
```vue
|
|
135
|
+
<InputSelectWithLabel
|
|
136
|
+
name="colour"
|
|
137
|
+
label="Favourite colour"
|
|
138
|
+
v-model="colour"
|
|
139
|
+
v-model:field-data="colourOptions"
|
|
140
|
+
placeholder="Choose a colour"
|
|
141
|
+
:error-message="errors.colour"
|
|
142
|
+
:field-has-error="!!errors.colour"
|
|
143
|
+
>
|
|
144
|
+
<template #descriptionText>Pick the colour you like best</template>
|
|
145
|
+
</InputSelectWithLabel>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`v-model` and `v-model:field-data` are both **required** — same as `InputSelectCore` above. Note
|
|
149
|
+
this wrapper passes `:theme="FormUiTheme"` (a local `computed`, not the raw `theme` prop) down to
|
|
150
|
+
`InputSelectCore`, so the theme automatically switches to `"error"` whenever `fieldHasError` is
|
|
151
|
+
true, regardless of the `theme` prop's own value.
|
|
@@ -163,6 +163,22 @@ Extra props: `min: number` (required), `max: number` (required), `step?: number`
|
|
|
163
163
|
`stepDownText`/`stepUpText` (default `"Step down"`/`"Step up"`) — the decrement/increment buttons'
|
|
164
164
|
accessible names. Requires `left`/`right` slot content for the step icons (no default icon).
|
|
165
165
|
|
|
166
|
+
**Fixed 2026-09-22**: `updateElementClasses(["input-text-as-number", "has-left-button",
|
|
167
|
+
"has-right-button"])` computes marker classes onto this component's own `elementClasses`, which is
|
|
168
|
+
bound to the outer `.input-text-with-label` wrapper — but the nested `<InputTextCore>` was passed
|
|
169
|
+
`:style-class-passthrough` as a bare shorthand, which resolves to the raw incoming
|
|
170
|
+
`styleClassPassthrough` **prop**, not the local `elementClasses`. So the marker classes never
|
|
171
|
+
reached the actual `<input>`, the scoped `.input-text-core.input-text-as-number { width:
|
|
172
|
+
fit-content; text-align: center; ... }` CSS never matched anything, and the input silently fell
|
|
173
|
+
back to `InputTextCore`'s default full-width, left-aligned styling instead of the intended
|
|
174
|
+
compact, centered numeric stepper box. Fixed by changing that binding to
|
|
175
|
+
`:style-class-passthrough="elementClasses"` — `elementClasses` still carries any consumer-supplied
|
|
176
|
+
`styleClassPassthrough` too, since the composable seeds it from the prop before the marker classes
|
|
177
|
+
are toggled on top. When a component computes its own marker/state classes via
|
|
178
|
+
`updateElementClasses()` and needs them on a *child* component rather than its own root element,
|
|
179
|
+
always pass the computed `elementClasses` explicitly — the `:x` shorthand only ever resolves to
|
|
180
|
+
the prop of that exact name, never a same-named local composable output.
|
|
181
|
+
|
|
166
182
|
---
|
|
167
183
|
|
|
168
184
|
## Accessibility
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# InputTextareaCore Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`InputTextareaCore` is the low-level native `<textarea>` primitive for the `05.forms` multi-line
|
|
6
|
+
text family. It renders a single native textarea with left/right decorative slots. It has no
|
|
7
|
+
label, description, or error-message rendering of its own — that's composed by the
|
|
8
|
+
`InputTextareaWithLabel` variant below.
|
|
9
|
+
|
|
10
|
+
Most consumers should reach for **InputTextareaWithLabel** rather than `InputTextareaCore`
|
|
11
|
+
directly.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Props reference
|
|
16
|
+
|
|
17
|
+
> **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:field-has-error`, `:style-class-passthrough`, `:aria-describedby`.
|
|
18
|
+
|
|
19
|
+
| Prop (template form) | Type | Default | Notes |
|
|
20
|
+
|------|------|---------|-------|
|
|
21
|
+
| `:id` | `string` | (required) | Applied to the native textarea. |
|
|
22
|
+
| `:name` | `string` | (required) | Applied to the native textarea. |
|
|
23
|
+
| `:maxlength` | `number` | `255` | Native `maxlength`. |
|
|
24
|
+
| `:placeholder` | `string` | `""` | Native `placeholder`. |
|
|
25
|
+
| `:required` | `boolean` | `false` | Native `required`. |
|
|
26
|
+
| `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
|
|
27
|
+
| `:input-variant` | `InputUiVariant` | `"normal"` | Adds the variant name as a class on the wrapper — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
|
|
28
|
+
| `:field-has-error` | `boolean` | `false` | Drives `data-invalid` on the wrapper and `aria-invalid` on the textarea. |
|
|
29
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the textarea element. |
|
|
30
|
+
| `:aria-describedby` | `string` | `""` | Forwarded to the native textarea. |
|
|
31
|
+
|
|
32
|
+
### v-models
|
|
33
|
+
|
|
34
|
+
- `v-model` — `string | number | readonly string[] | null | undefined`, **required**
|
|
35
|
+
(`defineModel({ required: true })`, satisfying `vue/require-default-prop` the same way as
|
|
36
|
+
`InputRangeCore`/`InputNumberCore` — see `project_ledger_eslint_issues_column` in memory).
|
|
37
|
+
"Required" here means a consumer must bind `v-model`, not that the value can't be an empty
|
|
38
|
+
string — `""` is a perfectly valid textarea value.
|
|
39
|
+
- `v-model:is-dirty` — `boolean`. Set once by `InputTextareaWithLabel` the first time the value
|
|
40
|
+
becomes non-empty (never reset back to `false` by this component).
|
|
41
|
+
- `v-model:is-active` — `boolean`. Tracks native focus/blur (`focusin`/`focusout`), toggling
|
|
42
|
+
`.active` on the wrapper and textarea.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Slots
|
|
47
|
+
|
|
48
|
+
| Slot | Notes |
|
|
49
|
+
|------|-------|
|
|
50
|
+
| `left` | Rendered before the textarea (decorative — e.g. an icon). Adds `has-left-slot` to the wrapper. |
|
|
51
|
+
| `right` | Rendered after the textarea (decorative). Adds `has-right-slot` to the wrapper. |
|
|
52
|
+
|
|
53
|
+
Unlike `InputNumberCore`/`InputRangeCore`, these slots have no built-in `InputButtonCore`
|
|
54
|
+
divider/embedded-button styling — they're plain decorative content (see the Storybook stories,
|
|
55
|
+
which use emoji), not a stepper control.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## CSS token API
|
|
60
|
+
|
|
61
|
+
See [CONSUMER-STYLING.md](../../app/components/05.forms/input-textarea/CONSUMER-STYLING.md) for
|
|
62
|
+
the full table with defaults.
|
|
63
|
+
|
|
64
|
+
**Common tokens:**
|
|
65
|
+
- `--input-textarea-surface` — wrapper background, falls back to `--theme-input-surface`
|
|
66
|
+
- `--input-textarea-border` / `-border-hover` / `-border-focus` — border colour resting, mouse-hover outline, and `:focus-visible` outline, fall back to `--theme-border`/`--theme-border-focus`
|
|
67
|
+
- `--input-textarea-text-color` — textarea text colour, falls back to `--theme-input-text-color-normal`
|
|
68
|
+
- `--input-textarea-placeholder-color` — placeholder text colour, falls back to `--theme-input-placeholder`
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Fixed 2026-09-22: undefined tokens and dead code
|
|
73
|
+
|
|
74
|
+
`InputTextareaCore` migrated to full compliance from a `variants:true`, `eslint_issues:true`,
|
|
75
|
+
score-1/5 state (no public tokens, no CONSUMER-STYLING.md, no tests, no skill doc, no snippet).
|
|
76
|
+
|
|
77
|
+
- **Silently broken styling from undefined CSS custom properties** (same bug class as pitfall #21
|
|
78
|
+
and `InputRangeCore`'s dead `--theme-form-range-accent-color`): the wrapper's `padding-inline`
|
|
79
|
+
read `--element-decorator-padding-inline`, and the placeholder's `font-size`/`font-style`/
|
|
80
|
+
`line-height`/`font-weight` read `--input-placeholder-font-size`/`-font-style`/`-line-height`/
|
|
81
|
+
`-font-weight` — **none of these five custom properties were declared anywhere in the
|
|
82
|
+
codebase**. Per the CSS spec, an undefined `var()` with no fallback makes the whole declaration
|
|
83
|
+
invalid, so all five silently fell back to the browser/inherited default instead of the intended
|
|
84
|
+
design. Fixed by switching to the tokens `InputTextCore` actually uses for the same purpose:
|
|
85
|
+
`--input-padding-inline` for the wrapper padding, and `--theme-input-placeholder-font-size` +
|
|
86
|
+
hardcoded `italic`/`1`/`normal` for the placeholder (matching `InputTextCore`'s placeholder rule
|
|
87
|
+
exactly — those three placeholder sub-properties aren't tokenized there either).
|
|
88
|
+
- **Zero public `--input-textarea-*` tokens existed at all** — every colour value read a
|
|
89
|
+
`--theme-*` global directly, with no component-level override point (the exact gap pitfall #14
|
|
90
|
+
describes for early `05.forms` components). Added the full set matching `InputTextCore`'s shape:
|
|
91
|
+
`--input-textarea-surface`, `-border`, `-border-hover`, `-border-focus`, `-text-color`,
|
|
92
|
+
`-placeholder-color`.
|
|
93
|
+
- **Dead code**: both `InputTextareaCore` and `InputTextareaWithLabel` declared
|
|
94
|
+
`const FormUiTheme = computed(() => props.fieldHasError ? "error" : props.theme);` — a value
|
|
95
|
+
binding that shadows the imported `FormUiTheme` *type* import of the same name, and is never
|
|
96
|
+
referenced anywhere in either file (the template's `:theme` shorthand resolves straight from
|
|
97
|
+
`defineProps`). Removed both. `InputTextareaWithLabel` also had ~5 lines of commented-out dead
|
|
98
|
+
code (an old, hand-rolled `id`/`errorId`/`ariaDescribedby` implementation, superseded by
|
|
99
|
+
`useAriaDescribedById` right below it) — removed. (The identical dead `FormUiTheme` computed
|
|
100
|
+
still exists in `InputTextCore`/`InputTextWithLabel` themselves — out of scope for this pass
|
|
101
|
+
since those weren't being touched, but worth cleaning up next time either is migrated.)
|
|
102
|
+
- **`InputTextareaWithLabel` passed `:label` down to `InputTextareaCore`**, which has no `label`
|
|
103
|
+
prop (matching `InputTextCore`, which doesn't have one either) — it fell through as a
|
|
104
|
+
meaningless raw `label="..."` HTML attribute on the wrapper `<div>`. Removed the pass-through;
|
|
105
|
+
`InputLabel` (which does have a real `label` concept) already renders it correctly. Covered by
|
|
106
|
+
a regression test.
|
|
107
|
+
- **`defineModel()` had no default**, tripping `vue/require-default-prop` in both files — fixed
|
|
108
|
+
with `{ required: true }`, same pattern as `InputRangeCore`/`InputNumberCore` (see the v-model
|
|
109
|
+
note above for why this is still correct even though an empty string is a valid value here).
|
|
110
|
+
- **`InputTextareaWithLabel.stories.ts` used `reactive()` in two render functions
|
|
111
|
+
(`AllVariants`/`FormExamples`) without importing it** — only `computed` was imported from
|
|
112
|
+
`"vue"`. This library's `.ts` Storybook files need Vue reactivity APIs imported explicitly (no
|
|
113
|
+
Nuxt auto-import outside `.vue` files) — a real runtime `ReferenceError` waiting to happen the
|
|
114
|
+
first time either story rendered. Fixed by adding `reactive` to the existing `computed` import.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Variants
|
|
119
|
+
|
|
120
|
+
### InputTextareaWithLabel
|
|
121
|
+
|
|
122
|
+
`InputTextareaWithLabel` (`variants/InputTextareaWithLabel.vue`) composes `InputTextareaCore` with
|
|
123
|
+
`InputLabel`, `InputDescription` (positioned before or after the field depending on
|
|
124
|
+
`inputVariant === "outlined"`, matching `InputTextWithLabel`'s exact placement rule), and
|
|
125
|
+
`InputError`.
|
|
126
|
+
|
|
127
|
+
**Additional props over InputTextareaCore:**
|
|
128
|
+
|
|
129
|
+
| Prop (template form) | Type | Default | Notes |
|
|
130
|
+
|------|------|---------|-------|
|
|
131
|
+
| `:label` | `string` | (required) | Rendered via `InputLabel`. |
|
|
132
|
+
| `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
|
|
133
|
+
|
|
134
|
+
**Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`), plus
|
|
135
|
+
`left`/`right` (forwarded straight through to the underlying `InputTextareaCore`).
|
|
136
|
+
|
|
137
|
+
```vue
|
|
138
|
+
<InputTextareaWithLabel
|
|
139
|
+
name="message"
|
|
140
|
+
label="Message"
|
|
141
|
+
v-model="message"
|
|
142
|
+
placeholder="How can we help you?"
|
|
143
|
+
:maxlength="500"
|
|
144
|
+
:error-message="errors.message"
|
|
145
|
+
:field-has-error="!!errors.message"
|
|
146
|
+
>
|
|
147
|
+
<template #descriptionText>Please describe your inquiry in detail</template>
|
|
148
|
+
</InputTextareaWithLabel>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`v-model` is `string | number | readonly string[] | null | undefined`, **required** — same as
|
|
152
|
+
`InputTextareaCore` above. `isDirty`/`isActive` are managed internally (not exposed as props) —
|
|
153
|
+
`isDirty` flips to `true` the first time the value becomes a non-empty string (checked on mount
|
|
154
|
+
and on every change), and never resets.
|