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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type { VueWrapper } from "@vue/test-utils";
|
|
2
3
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
4
|
import InputCheckboxRadioField from "../InputCheckboxRadioField.vue";
|
|
4
5
|
|
|
@@ -9,12 +10,22 @@ const defaultProps = {
|
|
|
9
10
|
modelValue: false,
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
|
|
13
|
+
const mounted: VueWrapper[] = [];
|
|
14
|
+
|
|
15
|
+
const createWrapper = async (props = {}, slots = {}) => {
|
|
16
|
+
const wrapper = await mountSuspended(InputCheckboxRadioField, {
|
|
14
17
|
attachTo: document.body,
|
|
15
18
|
props: { ...defaultProps, ...props },
|
|
16
19
|
slots,
|
|
17
20
|
});
|
|
21
|
+
mounted.push(wrapper);
|
|
22
|
+
return wrapper;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
18
29
|
|
|
19
30
|
describe("InputCheckboxRadioField", () => {
|
|
20
31
|
it("renders a label linked to the inner control", async () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# InputDescription — Consumer Styling Guide
|
|
2
2
|
|
|
3
3
|
`InputDescription` is the help text rendered between the label and the control by every `*Field`
|
|
4
|
-
wrapper (`
|
|
5
|
-
`
|
|
4
|
+
wrapper (`InputTextField`, `InputNumberField`, `InputSelectField`,
|
|
5
|
+
`InputTextareaField`, `InputRangeField`, `InputTextAsNumberField`, `SingleCheckbox`,
|
|
6
6
|
`MultipleCheckboxes`, `MultipleRadiobuttons`) when you pass it a `descriptionText` or
|
|
7
7
|
`descriptionHtml` slot. With neither slot it renders nothing.
|
|
8
8
|
|
|
@@ -53,7 +53,7 @@ No private tokens.
|
|
|
53
53
|
|
|
54
54
|
| Attribute on `.input-description` | When |
|
|
55
55
|
|---|---|
|
|
56
|
-
| `data-input-variant="normal" \| "outlined" \| "underlined"` | Always, mirrors the field's `input-variant` (not passed by `
|
|
56
|
+
| `data-input-variant="normal" \| "outlined" \| "underlined"` | Always, mirrors the field's `input-variant` (not passed by `InputRangeField`, which has no variants, so it stays `normal` there) |
|
|
57
57
|
| `data-invalid` | The field has an error |
|
|
58
58
|
|
|
59
59
|
Inner elements: `.input-description-html` (div), `.input-description-text` (p).
|
|
@@ -84,9 +84,9 @@ description. Keep these blocks **unlayered** (no `@layer ...` wrapper) so they b
|
|
|
84
84
|
```vue
|
|
85
85
|
<template>
|
|
86
86
|
<div class="contact-page-content">
|
|
87
|
-
<
|
|
87
|
+
<InputTextField v-model="email" name="email" label="Email">
|
|
88
88
|
<template #descriptionText>We'll only use this for your booking confirmation</template>
|
|
89
|
-
</
|
|
89
|
+
</InputTextField>
|
|
90
90
|
</div>
|
|
91
91
|
</template>
|
|
92
92
|
|
|
@@ -108,9 +108,9 @@ The wrappers don't forward `style-class-passthrough` to the description, but a p
|
|
|
108
108
|
the wrapper itself lands on its root element, which is an ancestor of the description:
|
|
109
109
|
|
|
110
110
|
```vue
|
|
111
|
-
<
|
|
111
|
+
<InputTextField class="email-field" v-model="email" name="email" label="Email">
|
|
112
112
|
<template #descriptionText>We'll only use this for your booking confirmation</template>
|
|
113
|
-
</
|
|
113
|
+
</InputTextField>
|
|
114
114
|
|
|
115
115
|
<style>
|
|
116
116
|
.email-field {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# InputLabel — Consumer Styling Guide
|
|
2
2
|
|
|
3
|
-
`InputLabel` is the `<label>` rendered by every labelled field wrapper (`
|
|
4
|
-
`
|
|
5
|
-
`
|
|
6
|
-
`
|
|
3
|
+
`InputLabel` is the `<label>` rendered by every labelled field wrapper (`InputTextField`,
|
|
4
|
+
`InputTextAsNumberField`, `InputNumberField`, `InputSelectField`,
|
|
5
|
+
`InputTextareaField`, `InputRangeField`,
|
|
6
|
+
`ToggleSwitchField`).
|
|
7
7
|
|
|
8
8
|
## Public token API
|
|
9
9
|
|
|
@@ -50,8 +50,8 @@ shared fallback so the per-state tokens don't repeat it.
|
|
|
50
50
|
|
|
51
51
|
| Attribute on `.input-label` | When |
|
|
52
52
|
|---|---|
|
|
53
|
-
| `data-input-variant="normal" \| "outlined" \| "underlined"` | Always, mirrors the field's `input-variant` (`
|
|
54
|
-
| `data-invalid` | The field has an error (
|
|
53
|
+
| `data-input-variant="normal" \| "outlined" \| "underlined"` | Always, mirrors the field's `input-variant` (`InputRangeField` and the toggle switches always pass `normal`) |
|
|
54
|
+
| `data-invalid` | The field has an error (passed by every Field) |
|
|
55
55
|
|
|
56
56
|
| Attribute on `.input-label-indicator` | When |
|
|
57
57
|
|---|---|
|
|
@@ -7,11 +7,11 @@ to override just this component; leave it unset and it inherits the shared `--th
|
|
|
7
7
|
`--theme-*` token every other themed component also falls back to. See
|
|
8
8
|
`theming-component-token-pattern.md` for why this two-tier shape exists.
|
|
9
9
|
|
|
10
|
-
This shape mirrors `
|
|
11
|
-
is a native `<input type="number">` sibling to the text-based `
|
|
10
|
+
This shape mirrors `InputText`'s token set (see its own CONSUMER-STYLING.md) — `InputNumber`
|
|
11
|
+
is a native `<input type="number">` sibling to the text-based `InputTextAsNumberField` variant,
|
|
12
12
|
and both are meant to look and behave the same way. Border colour also doubles as the divider
|
|
13
13
|
colour between the input and an embedded left/right `InputButton` — there's no separate divider
|
|
14
|
-
token, same as `
|
|
14
|
+
token, same as `InputText`.
|
|
15
15
|
|
|
16
16
|
| Token | Falls back to | Controls |
|
|
17
17
|
|---|---|---|
|
|
@@ -27,14 +27,47 @@ Geometry (`--input-padding-block`, `--input-padding-inline`, `--input-min-height
|
|
|
27
27
|
`--form-input-border-radius`, `--input-font-size`, etc.) is shared across every `05.forms`
|
|
28
28
|
component and untouched by this — see `theming-form-geometry-tokens.md`.
|
|
29
29
|
|
|
30
|
+
Private (not public API): `--_surface`, `--_border` and `--_border-focus` (each resolved once and
|
|
31
|
+
read in several places) and `--_min-width` (the input's minimum width, worked out from `min`, `max`
|
|
32
|
+
and `step` and capped at `12em`).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## State hooks
|
|
37
|
+
|
|
38
|
+
| Attribute | On | When |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `data-input-variant="normal" \| "outlined" \| "underlined"` | `.input-number-wrapper`, `.input-number-field` | Always, mirrors `input-variant` |
|
|
41
|
+
| `data-theme` | `.input-number-wrapper`, `.input-number-field` | Always, mirrors `theme` |
|
|
42
|
+
| `data-invalid` | `.input-number-wrapper`, `.input-number-field` | `field-has-error` |
|
|
43
|
+
| `data-has-left-slot` / `data-has-right-slot` | `.input-number-wrapper` | The `left` / `right` slot is filled |
|
|
44
|
+
|
|
45
|
+
Inner elements: `.input-number-slot` (both slot holders), `.input-number-slot-left`,
|
|
46
|
+
`.input-number-slot-right`, `.input-number-container`, `.input-number` (the native input),
|
|
47
|
+
`.input-number-label` (the Field's `InputLabel`).
|
|
48
|
+
|
|
49
|
+
> **Changed 2026-10-16**: state used to be bare classes, which collide with common consumer CSS
|
|
50
|
+
> (an unlayered `.error` or `.normal` rule would restyle the field). Select on the attributes
|
|
51
|
+
> above instead:
|
|
52
|
+
>
|
|
53
|
+
> - `.input-number-wrapper.normal` / `.underlined` → `[data-input-variant="…"]`
|
|
54
|
+
> - `.input-number-wrapper.has-left-slot` / `.has-right-slot` → `[data-has-left-slot]` /
|
|
55
|
+
> `[data-has-right-slot]`
|
|
56
|
+
> - `.slot.left-slot` / `.slot.right-slot` → `.input-number-slot-left` / `.input-number-slot-right`
|
|
57
|
+
> - `.input-number-field.error` → `[data-invalid]`; its `.normal`/`.underlined` and `theme-*`
|
|
58
|
+
> classes → `[data-input-variant]` / `[data-theme]`
|
|
59
|
+
>
|
|
60
|
+
> Also: long label, description and error copy now wraps (`overflow-wrap: anywhere` on
|
|
61
|
+
> `.input-number-field`), and the control's minimum width is capped at `12em` so a long `min`/`max`
|
|
62
|
+
> can't push it past a phone screen.
|
|
63
|
+
|
|
30
64
|
---
|
|
31
65
|
|
|
32
66
|
## Input variant
|
|
33
67
|
|
|
34
68
|
`:input-variant="'normal' | 'outlined' | 'underlined'"` (default `"normal"`) — same prop as
|
|
35
|
-
`
|
|
36
|
-
(bottom-border only) have CSS today; `outlined` has no styling in `
|
|
37
|
-
`&.outlined` rule exists there), so passing it renders with no border/outline at all — a
|
|
69
|
+
`InputText`. Only `normal` (bordered box, with hover/focus outline) and `underlined`
|
|
70
|
+
(bottom-border only) have CSS today; `outlined` has no styling in `InputText` either, so passing it renders with no border/outline at all — a
|
|
38
71
|
pre-existing gap in the type, not something specific to `InputNumber`. Declared for prop-shape
|
|
39
72
|
consistency with the rest of `05.forms`.
|
|
40
73
|
|
|
@@ -68,3 +101,14 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
68
101
|
<InputNumber id="quantity" name="quantity" :min="1" :max="10" style="--input-number-border: var(--gold-04);" />
|
|
69
102
|
```
|
|
70
103
|
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Class passthrough
|
|
108
|
+
|
|
109
|
+
`InputNumber`'s `:style-class-passthrough` lands on the native `<input class="input-number">`.
|
|
110
|
+
`InputNumberField`'s lands on its root `.input-number-field` only.
|
|
111
|
+
|
|
112
|
+
> **Changed 2026-10-16**: `InputNumberField` also forwarded its passthrough classes to the inner
|
|
113
|
+
> input, so a class meant for the field matched two elements. If you styled the input through the
|
|
114
|
+
> Field's passthrough, select `.your-class .input-number` instead.
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
class="input-number-wrapper"
|
|
4
4
|
:data-theme="theme"
|
|
5
5
|
:data-invalid="fieldHasError ? '' : null"
|
|
6
|
-
:
|
|
6
|
+
:data-input-variant="inputVariant"
|
|
7
|
+
:data-has-left-slot="slots.left ? '' : undefined"
|
|
8
|
+
:data-has-right-slot="slots.right ? '' : undefined"
|
|
7
9
|
>
|
|
8
|
-
<div v-if="slots.left" class="slot
|
|
10
|
+
<div v-if="slots.left" class="input-number-slot input-number-slot-left">
|
|
9
11
|
<slot name="left"></slot>
|
|
10
12
|
</div>
|
|
11
13
|
|
|
@@ -22,12 +24,11 @@
|
|
|
22
24
|
:step
|
|
23
25
|
:placeholder
|
|
24
26
|
:class="['input-number', `input-number--${weight}`, elementClasses]"
|
|
25
|
-
inputmode
|
|
26
|
-
pattern="[0-9]+"
|
|
27
|
+
:inputmode
|
|
27
28
|
:aria-describedby
|
|
28
29
|
/>
|
|
29
30
|
</div>
|
|
30
|
-
<div v-if="slots.right" class="slot
|
|
31
|
+
<div v-if="slots.right" class="input-number-slot input-number-slot-right">
|
|
31
32
|
<slot name="right"></slot>
|
|
32
33
|
</div>
|
|
33
34
|
</div>
|
|
@@ -70,8 +71,26 @@ const modelValue = defineModel<number | readonly number[]>({
|
|
|
70
71
|
required: true,
|
|
71
72
|
});
|
|
72
73
|
|
|
73
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
74
|
-
|
|
74
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
75
|
+
|
|
76
|
+
watch(
|
|
77
|
+
() => props.styleClassPassthrough,
|
|
78
|
+
() => {
|
|
79
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const decimalPlaces = (value: number) => (String(value).split(".")[1] ?? "").length;
|
|
84
|
+
|
|
85
|
+
const minLength = computed(() => {
|
|
86
|
+
const longest = Math.max(String(props.min).length, String(props.max).length) + decimalPlaces(props.step);
|
|
87
|
+
return `${Math.min(longest + 1, 12)}em`;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const inputmode = computed(() => {
|
|
91
|
+
if (props.min < 0) return undefined;
|
|
92
|
+
return Number.isInteger(props.step) ? "numeric" : "decimal";
|
|
93
|
+
});
|
|
75
94
|
</script>
|
|
76
95
|
|
|
77
96
|
<style lang="css">
|
|
@@ -81,8 +100,8 @@ const minLength = computed(() => `${props.max.toString().length + 1}em`);
|
|
|
81
100
|
|
|
82
101
|
/* Public --input-number-* tokens, inline-fallback to the shared --theme-* tokens (see
|
|
83
102
|
theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
|
|
84
|
-
input/control that also reads --theme-input-surface/--theme-border. Mirrors
|
|
85
|
-
token shape so a native number input and the text-based
|
|
103
|
+
input/control that also reads --theme-input-surface/--theme-border. Mirrors InputText's
|
|
104
|
+
token shape so a native number input and the text-based InputTextAsNumberField variant
|
|
86
105
|
look and behave the same way (see input-number.md's "Fixed 2026-09-22" note). */
|
|
87
106
|
--_surface: var(--input-number-surface, var(--theme-input-surface));
|
|
88
107
|
--_border: var(--input-number-border, var(--theme-border));
|
|
@@ -92,6 +111,7 @@ const minLength = computed(() => `${props.max.toString().length + 1}em`);
|
|
|
92
111
|
align-items: center;
|
|
93
112
|
|
|
94
113
|
width: fit-content;
|
|
114
|
+
max-inline-size: 100%;
|
|
95
115
|
|
|
96
116
|
background-color: var(--_surface);
|
|
97
117
|
overflow: hidden;
|
|
@@ -99,7 +119,7 @@ const minLength = computed(() => `${props.max.toString().length + 1}em`);
|
|
|
99
119
|
position: relative;
|
|
100
120
|
z-index: 2;
|
|
101
121
|
|
|
102
|
-
|
|
122
|
+
&[data-input-variant="normal"] {
|
|
103
123
|
border: var(--form-element-border-width) solid var(--_border);
|
|
104
124
|
border-radius: var(--form-input-border-radius);
|
|
105
125
|
outline: var(--form-element-outline-width) solid transparent;
|
|
@@ -116,12 +136,14 @@ const minLength = computed(() => `${props.max.toString().length + 1}em`);
|
|
|
116
136
|
}
|
|
117
137
|
}
|
|
118
138
|
|
|
119
|
-
|
|
139
|
+
&[data-input-variant="underlined"] {
|
|
120
140
|
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
.slot {
|
|
124
|
-
display:
|
|
143
|
+
.input-number-slot {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
flex-shrink: 0;
|
|
125
147
|
|
|
126
148
|
.icon {
|
|
127
149
|
font-weight: 900;
|
|
@@ -153,25 +175,15 @@ const minLength = computed(() => `${props.max.toString().length + 1}em`);
|
|
|
153
175
|
}
|
|
154
176
|
}
|
|
155
177
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
display: flex;
|
|
159
|
-
align-items: center;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
&.has-right-slot {
|
|
164
|
-
.right-slot {
|
|
165
|
-
display: flex;
|
|
166
|
-
align-items: center;
|
|
167
|
-
}
|
|
178
|
+
.input-number-container {
|
|
179
|
+
min-inline-size: 0;
|
|
168
180
|
}
|
|
169
181
|
|
|
170
|
-
|
|
182
|
+
.input-number-slot-left .input-button {
|
|
171
183
|
border-right: var(--form-element-border-width) solid var(--_border);
|
|
172
184
|
}
|
|
173
185
|
|
|
174
|
-
|
|
186
|
+
.input-number-slot-right .input-button {
|
|
175
187
|
border-left: var(--form-element-border-width) solid var(--_border);
|
|
176
188
|
}
|
|
177
189
|
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
class="input-number-field"
|
|
4
4
|
:data-theme="theme"
|
|
5
5
|
:data-invalid="fieldHasError ? '' : null"
|
|
6
|
-
:
|
|
6
|
+
:data-input-variant="inputVariant"
|
|
7
|
+
:class="elementClasses"
|
|
7
8
|
>
|
|
8
9
|
<InputLabel
|
|
9
10
|
:id
|
|
10
11
|
:required
|
|
11
12
|
:input-variant
|
|
12
13
|
:field-has-error
|
|
13
|
-
:style-class-passthrough="['input-number-label'
|
|
14
|
+
:style-class-passthrough="['input-number-label']"
|
|
14
15
|
>
|
|
15
16
|
<template #textLabel>{{ label }}</template>
|
|
16
17
|
</InputLabel>
|
|
@@ -42,7 +43,6 @@
|
|
|
42
43
|
:weight
|
|
43
44
|
:input-variant
|
|
44
45
|
:field-has-error
|
|
45
|
-
:style-class-passthrough
|
|
46
46
|
:aria-describedby="ariaDescribedby()"
|
|
47
47
|
>
|
|
48
48
|
<template v-if="slots.left" #left>
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
:button-text="stepDownLabel"
|
|
54
54
|
:theme="theme"
|
|
55
55
|
variant="inline"
|
|
56
|
-
@click.stop.prevent="
|
|
56
|
+
@click.stop.prevent="stepBy(-1)"
|
|
57
57
|
>
|
|
58
58
|
<template #iconOnly>
|
|
59
59
|
<slot name="left"></slot>
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
:button-text="stepUpLabel"
|
|
69
69
|
:theme="theme"
|
|
70
70
|
variant="inline"
|
|
71
|
-
@click.stop.prevent="
|
|
71
|
+
@click.stop.prevent="stepBy(1)"
|
|
72
72
|
>
|
|
73
73
|
<template #iconOnly>
|
|
74
74
|
<slot name="right"></slot>
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
</InputButton>
|
|
77
77
|
</template>
|
|
78
78
|
</InputNumber>
|
|
79
|
-
<InputError :id="errorId" :error-message :show-error="fieldHasError" :is-detached="true" />
|
|
79
|
+
<InputError :id="errorId" :error-message :show-error="fieldHasError" :is-detached="true" :input-variant />
|
|
80
80
|
</div>
|
|
81
81
|
</template>
|
|
82
82
|
|
|
@@ -115,7 +115,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
const slots = useSlots();
|
|
118
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
118
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
119
|
+
|
|
120
|
+
watch(
|
|
121
|
+
() => props.styleClassPassthrough,
|
|
122
|
+
() => {
|
|
123
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
124
|
+
}
|
|
125
|
+
);
|
|
119
126
|
|
|
120
127
|
const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
121
128
|
props.name,
|
|
@@ -126,16 +133,28 @@ const modelValue = defineModel<number | readonly number[]>({
|
|
|
126
133
|
required: true,
|
|
127
134
|
});
|
|
128
135
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
const decimalPlaces = (value: number) => (String(value).split(".")[1] ?? "").length;
|
|
137
|
+
const clamp = (value: number) => Math.min(Math.max(value, props.min), props.max);
|
|
138
|
+
|
|
139
|
+
const stepBy = (direction: 1 | -1) => {
|
|
140
|
+
const current = modelValue.value;
|
|
141
|
+
if (typeof current !== "number" || !Number.isFinite(current)) {
|
|
142
|
+
modelValue.value = clamp(0);
|
|
143
|
+
return;
|
|
132
144
|
}
|
|
145
|
+
const stepSize = Math.abs(props.step) || 1;
|
|
146
|
+
const precision = Math.max(decimalPlaces(stepSize), decimalPlaces(current), decimalPlaces(props.min));
|
|
147
|
+
const next = clamp(Number((current + direction * stepSize).toFixed(precision)));
|
|
148
|
+
if (next !== current) modelValue.value = next;
|
|
133
149
|
};
|
|
134
150
|
</script>
|
|
135
151
|
|
|
136
152
|
<style lang="css">
|
|
137
153
|
@layer components {
|
|
138
154
|
.input-number-field {
|
|
155
|
+
min-inline-size: 0;
|
|
156
|
+
overflow-wrap: anywhere;
|
|
157
|
+
|
|
139
158
|
.input-number-label {
|
|
140
159
|
display: block;
|
|
141
160
|
margin-block: 0.8rem;
|
|
@@ -144,12 +163,6 @@ const updateValue = (step: number, withinRangeLimit: boolean) => {
|
|
|
144
163
|
cursor: pointer;
|
|
145
164
|
}
|
|
146
165
|
}
|
|
147
|
-
|
|
148
|
-
.label-description {
|
|
149
|
-
font-family: var(--font-family);
|
|
150
|
-
font-size: 1.6rem;
|
|
151
|
-
margin-top: 1.2rem;
|
|
152
|
-
}
|
|
153
166
|
}
|
|
154
167
|
}
|
|
155
168
|
</style>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
1
|
import { ref } from "vue";
|
|
3
|
-
import
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import InputNumber from "../InputNumber.vue";
|
|
4
|
+
import InputButton from "../../input-button/InputButton.vue";
|
|
5
|
+
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
4
6
|
|
|
5
|
-
interface
|
|
7
|
+
interface StoryArgs {
|
|
6
8
|
id: string;
|
|
7
9
|
name: string;
|
|
8
10
|
min: number;
|
|
@@ -11,13 +13,23 @@ interface InputNumberStoryArgs {
|
|
|
11
13
|
placeholder: string;
|
|
12
14
|
required: boolean;
|
|
13
15
|
fieldHasError: boolean;
|
|
16
|
+
theme: FormUiTheme;
|
|
14
17
|
weight: string;
|
|
15
|
-
inputVariant:
|
|
18
|
+
inputVariant: InputUiVariant;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
22
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
23
|
+
const parentNote = `
|
|
24
|
+
<p style="${storyNoteStyle}">
|
|
25
|
+
With a label, description, error message and clamped step buttons wired up, this is the
|
|
26
|
+
<a href="/?path=/story/components-forms-input-number-inputnumberfield--with-step-buttons" target="_top">InputNumberField</a>.
|
|
27
|
+
</p>
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const meta: Meta<StoryArgs> = {
|
|
19
31
|
title: "Components/Forms/Input Number/InputNumber",
|
|
20
|
-
component:
|
|
32
|
+
component: InputNumber,
|
|
21
33
|
argTypes: {
|
|
22
34
|
id: { control: "text", table: { category: "Basic" } },
|
|
23
35
|
name: { control: "text", table: { category: "Basic" } },
|
|
@@ -27,7 +39,12 @@ export default {
|
|
|
27
39
|
placeholder: { control: "text", table: { category: "Basic" } },
|
|
28
40
|
required: { control: "boolean", table: { category: "Basic" } },
|
|
29
41
|
fieldHasError: { control: "boolean", table: { category: "States" } },
|
|
30
|
-
|
|
42
|
+
theme: {
|
|
43
|
+
control: { type: "select" },
|
|
44
|
+
options: ["default", "success", "error", "warning"],
|
|
45
|
+
table: { category: "Styling" },
|
|
46
|
+
},
|
|
47
|
+
weight: { control: "text", description: "Adds an `input-number--{weight}` class; nothing styles it yet", table: { category: "Styling" } },
|
|
31
48
|
inputVariant: {
|
|
32
49
|
control: { type: "select" },
|
|
33
50
|
options: ["normal", "outlined", "underlined"],
|
|
@@ -43,50 +60,84 @@ export default {
|
|
|
43
60
|
placeholder: "",
|
|
44
61
|
required: false,
|
|
45
62
|
fieldHasError: false,
|
|
63
|
+
theme: "default",
|
|
46
64
|
weight: "normal",
|
|
47
65
|
inputVariant: "normal",
|
|
48
66
|
},
|
|
49
|
-
|
|
67
|
+
parameters: {
|
|
68
|
+
docs: {
|
|
69
|
+
description: {
|
|
70
|
+
component:
|
|
71
|
+
"The bare native `<input type=\"number\">` with `left`/`right` slots for embedded step buttons. No label, description or error: that's `InputNumberField`. `inputmode` follows `min` and `step` (`numeric`, `decimal`, or none when negatives are allowed). Hooks: `data-input-variant`, `data-theme`, `data-invalid`, `data-has-left-slot`, `data-has-right-slot`.",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default meta;
|
|
78
|
+
type Story = StoryObj<StoryArgs>;
|
|
50
79
|
|
|
51
|
-
const
|
|
52
|
-
components: {
|
|
80
|
+
const render = (args: StoryArgs) => ({
|
|
81
|
+
components: { InputNumber },
|
|
53
82
|
setup() {
|
|
54
83
|
const modelValue = ref(50);
|
|
55
84
|
return { args, modelValue };
|
|
56
85
|
},
|
|
57
86
|
template: `
|
|
58
87
|
<div style="margin: 36px; max-width: 320px;">
|
|
59
|
-
|
|
88
|
+
${parentNote}
|
|
89
|
+
<InputNumber v-bind="args" v-model="modelValue" />
|
|
60
90
|
</div>
|
|
61
91
|
`,
|
|
62
92
|
});
|
|
63
93
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
export const ErrorState = Template.bind({});
|
|
67
|
-
ErrorState.args = { fieldHasError: true };
|
|
68
|
-
|
|
69
|
-
const WithStepButtonsTemplate: StoryFn<InputNumberStoryArgs> = (args) => ({
|
|
70
|
-
components: { StorybookComponent },
|
|
94
|
+
const renderWithSlots = (args: StoryArgs) => ({
|
|
95
|
+
components: { InputNumber, InputButton },
|
|
71
96
|
setup() {
|
|
72
97
|
const modelValue = ref(50);
|
|
73
98
|
return { args, modelValue };
|
|
74
99
|
},
|
|
75
100
|
template: `
|
|
76
101
|
<div style="margin: 36px; max-width: 320px;">
|
|
77
|
-
|
|
102
|
+
${parentNote}
|
|
103
|
+
<InputNumber v-bind="args" v-model="modelValue">
|
|
78
104
|
<template #left>
|
|
79
|
-
<
|
|
105
|
+
<InputButton type="button" button-text="Step down" variant="inline" @click="modelValue = Math.max(args.min, modelValue - args.step)">
|
|
106
|
+
<template #iconOnly><Icon name="mdi:minus" /></template>
|
|
107
|
+
</InputButton>
|
|
80
108
|
</template>
|
|
81
109
|
<template #right>
|
|
82
|
-
<
|
|
110
|
+
<InputButton type="button" button-text="Step up" variant="inline" @click="modelValue = Math.min(args.max, modelValue + args.step)">
|
|
111
|
+
<template #iconOnly><Icon name="mdi:plus" /></template>
|
|
112
|
+
</InputButton>
|
|
83
113
|
</template>
|
|
84
|
-
</
|
|
114
|
+
</InputNumber>
|
|
85
115
|
</div>
|
|
86
116
|
`,
|
|
87
117
|
});
|
|
88
118
|
|
|
89
|
-
export const
|
|
119
|
+
export const Default: Story = { render };
|
|
120
|
+
|
|
121
|
+
export const ErrorState: Story = {
|
|
122
|
+
args: { fieldHasError: true },
|
|
123
|
+
render,
|
|
124
|
+
parameters: { docs: { description: { story: "`fieldHasError` sets `data-invalid` on the wrapper." } } },
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const WithStepButtons: Story = {
|
|
128
|
+
render: renderWithSlots,
|
|
129
|
+
parameters: {
|
|
130
|
+
docs: {
|
|
131
|
+
description: {
|
|
132
|
+
story:
|
|
133
|
+
"Inline `InputButton`s in the `left`/`right` slots become flush segments of the control. The wiring here is deliberately minimal; `InputNumberField` does it properly (clamping, rounding, empty field).",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
90
138
|
|
|
91
|
-
export const Underlined =
|
|
92
|
-
|
|
139
|
+
export const Underlined: Story = {
|
|
140
|
+
args: { inputVariant: "underlined" },
|
|
141
|
+
render: renderWithSlots,
|
|
142
|
+
parameters: { docs: { description: { story: "`inputVariant=\"underlined\"` keeps only a bottom border." } } },
|
|
143
|
+
};
|