srcdev-nuxt-components 9.6.6 → 9.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +14 -0
  2. package/.claude/skills/component-compliance-checklist.md +6 -0
  3. package/.claude/skills/component-naming.md +2 -2
  4. package/.claude/skills/components/input-checkbox-radio.md +19 -3
  5. package/.claude/skills/components/input-checkbox.md +14 -3
  6. package/.claude/skills/components/input-description.md +1 -1
  7. package/.claude/skills/components/input-error.md +1 -1
  8. package/.claude/skills/components/input-label.md +2 -2
  9. package/.claude/skills/components/input-number.md +44 -13
  10. package/.claude/skills/components/input-radio.md +15 -1
  11. package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
  12. package/.claude/skills/components/input-textarea-core.md +4 -4
  13. package/.claude/skills/components/page-hero-highlights.md +22 -18
  14. package/.claude/skills/index.md +6 -6
  15. package/.claude/skills/testing-add-unit-test.md +17 -0
  16. package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
  17. package/.vscode/srcdev-component-input-range.code-snippets +38 -18
  18. package/.vscode/srcdev-component-page-hero-highlights.code-snippets +2 -1
  19. package/app/components/04.templates/page-hero-highlights/CONSUMER-STYLING.md +32 -12
  20. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +56 -47
  21. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +158 -307
  22. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +10 -0
  23. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +49 -27
  24. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +42 -7
  25. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +12 -10
  26. package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
  27. package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
  28. package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
  29. package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
  30. package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
  31. package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
  32. package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
  33. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
  34. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
  35. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
  36. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
  37. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
  38. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
  39. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
  40. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
  41. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
  42. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
  43. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +2 -2
  44. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +2 -2
  45. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +42 -2
  46. package/app/components/05.forms/input-number/InputNumber.vue +37 -25
  47. package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
  48. package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
  49. package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
  50. package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
  51. package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
  52. package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
  53. package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
  54. package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
  55. package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
  56. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
  57. package/app/components/05.forms/input-range/InputRange.vue +247 -0
  58. package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
  59. package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
  60. package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
  61. package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
  62. package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
  63. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +5 -5
  64. package/package.json +1 -1
  65. package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
  66. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
  67. package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
@@ -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 createWrapper = (props = {}, slots = {}) =>
13
- mountSuspended(InputCheckboxRadioField, {
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 () => {
@@ -2,7 +2,7 @@
2
2
 
3
3
  `InputDescription` is the help text rendered between the label and the control by every `*Field`
4
4
  wrapper (`InputTextWithLabel`, `InputNumberField`, `InputSelectWithLabel`,
5
- `InputTextareaWithLabel`, `InputRangeDefault`, `InputTextAsNumberWithLabel`, `SingleCheckbox`,
5
+ `InputTextareaWithLabel`, `InputRangeField`, `InputTextAsNumberWithLabel`, `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 `InputRangeDefault`, which has no variants, so it stays `normal` there) |
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).
@@ -2,7 +2,7 @@
2
2
 
3
3
  `InputLabel` is the `<label>` rendered by every labelled field wrapper (`InputTextWithLabel`,
4
4
  `InputTextAsNumberWithLabel`, `InputNumberField`, `InputSelectWithLabel`,
5
- `InputTextareaWithLabel`, `InputRangeDefault`, `ToggleSwitchWithLabel`,
5
+ `InputTextareaWithLabel`, `InputRangeField`, `ToggleSwitchWithLabel`,
6
6
  `ToggleSwitchWithLabelInline`).
7
7
 
8
8
  ## Public token API
@@ -50,7 +50,7 @@ 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` (`InputRangeDefault` and the toggle switches always pass `normal`) |
53
+ | `data-input-variant="normal" \| "outlined" \| "underlined"` | Always, mirrors the field's `input-variant` (`InputRangeField` and the toggle switches always pass `normal`) |
54
54
  | `data-invalid` | The field has an error (not passed by `ToggleSwitchWithLabelInline`) |
55
55
 
56
56
  | Attribute on `.input-label-indicator` | When |
@@ -29,12 +29,41 @@ component and untouched by this — see `theming-form-geometry-tokens.md`.
29
29
 
30
30
  ---
31
31
 
32
+ ## State hooks
33
+
34
+ | Attribute | On | When |
35
+ |---|---|---|
36
+ | `data-input-variant="normal" \| "outlined" \| "underlined"` | `.input-number-wrapper`, `.input-number-field` | Always, mirrors `input-variant` |
37
+ | `data-theme` | `.input-number-wrapper`, `.input-number-field` | Always, mirrors `theme` |
38
+ | `data-invalid` | `.input-number-wrapper`, `.input-number-field` | `field-has-error` |
39
+ | `data-has-left-slot` / `data-has-right-slot` | `.input-number-wrapper` | The `left` / `right` slot is filled |
40
+
41
+ Inner elements: `.input-number-slot` (both slot holders), `.input-number-slot-left`,
42
+ `.input-number-slot-right`, `.input-number-container`, `.input-number` (the native input),
43
+ `.input-number-label` (the Field's `InputLabel`).
44
+
45
+ > **Changed 2026-10-16**: state used to be bare classes, which collide with common consumer CSS
46
+ > (an unlayered `.error` or `.normal` rule would restyle the field). Select on the attributes
47
+ > above instead:
48
+ >
49
+ > - `.input-number-wrapper.normal` / `.underlined` → `[data-input-variant="…"]`
50
+ > - `.input-number-wrapper.has-left-slot` / `.has-right-slot` → `[data-has-left-slot]` /
51
+ > `[data-has-right-slot]`
52
+ > - `.slot.left-slot` / `.slot.right-slot` → `.input-number-slot-left` / `.input-number-slot-right`
53
+ > - `.input-number-field.error` → `[data-invalid]`; its `.normal`/`.underlined` and `theme-*`
54
+ > classes → `[data-input-variant]` / `[data-theme]`
55
+ >
56
+ > Also: long label, description and error copy now wraps (`overflow-wrap: anywhere` on
57
+ > `.input-number-field`), and the control's minimum width is capped at `12em` so a long `min`/`max`
58
+ > can't push it past a phone screen.
59
+
60
+ ---
61
+
32
62
  ## Input variant
33
63
 
34
64
  `:input-variant="'normal' | 'outlined' | 'underlined'"` (default `"normal"`) — same prop as
35
65
  `InputTextCore`. Only `normal` (bordered box, with hover/focus outline) and `underlined`
36
- (bottom-border only) have CSS today; `outlined` has no styling in `InputTextCore` either (no
37
- `&.outlined` rule exists there), so passing it renders with no border/outline at all — a
66
+ (bottom-border only) have CSS today; `outlined` has no styling in `InputTextCore` either, so passing it renders with no border/outline at all — a
38
67
  pre-existing gap in the type, not something specific to `InputNumber`. Declared for prop-shape
39
68
  consistency with the rest of `05.forms`.
40
69
 
@@ -68,3 +97,14 @@ Set the tokens above on an element you own (a page or section class, or a class
68
97
  <InputNumber id="quantity" name="quantity" :min="1" :max="10" style="--input-number-border: var(--gold-04);" />
69
98
  ```
70
99
 
100
+
101
+ ---
102
+
103
+ ## Class passthrough
104
+
105
+ `InputNumber`'s `:style-class-passthrough` lands on the native `<input class="input-number">`.
106
+ `InputNumberField`'s lands on its root `.input-number-field` only.
107
+
108
+ > **Changed 2026-10-16**: `InputNumberField` also forwarded its passthrough classes to the inner
109
+ > input, so a class meant for the field matched two elements. If you styled the input through the
110
+ > 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
- :class="[inputVariant, { 'has-left-slot': slots.left, 'has-right-slot': slots.right }]"
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 left-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="numeric"
26
- pattern="[0-9]+"
27
+ :inputmode
27
28
  :aria-describedby
28
29
  />
29
30
  </div>
30
- <div v-if="slots.right" class="slot right-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
- const minLength = computed(() => `${props.max.toString().length + 1}em`);
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">
@@ -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
- &.normal {
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
- &.underlined {
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: inline-block;
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
- &.has-left-slot {
157
- .left-slot {
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
- &.has-left-slot .left-slot .input-button {
182
+ .input-number-slot-left .input-button {
171
183
  border-right: var(--form-element-border-width) solid var(--_border);
172
184
  }
173
185
 
174
- &.has-right-slot .right-slot .input-button {
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
- :class="[elementClasses, inputVariant, `theme-${theme}`, { error: fieldHasError }]"
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', 'body-normal-bold']"
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="updateValue(-step, Number(modelValue) > min)"
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="updateValue(step, Number(modelValue) < max)"
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 updateValue = (step: number, withinRangeLimit: boolean) => {
130
- if (withinRangeLimit) {
131
- modelValue.value = (Number(modelValue.value) + step) as number;
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 StorybookComponent from "../InputNumber.vue";
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 InputNumberStoryArgs {
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: string;
18
+ inputVariant: InputUiVariant;
16
19
  }
17
20
 
18
- export default {
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: StorybookComponent,
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
- weight: { control: "text", table: { category: "Styling" } },
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
- } as Meta<InputNumberStoryArgs>;
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 Template: StoryFn<InputNumberStoryArgs> = (args) => ({
52
- components: { StorybookComponent },
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
- <StorybookComponent v-bind="args" v-model="modelValue" />
88
+ ${parentNote}
89
+ <InputNumber v-bind="args" v-model="modelValue" />
60
90
  </div>
61
91
  `,
62
92
  });
63
93
 
64
- export const Default = Template.bind({});
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
- <StorybookComponent v-bind="args" v-model="modelValue">
102
+ ${parentNote}
103
+ <InputNumber v-bind="args" v-model="modelValue">
78
104
  <template #left>
79
- <button type="button" @click="modelValue = Math.max(args.min, modelValue - args.step)">−</button>
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
- <button type="button" @click="modelValue = Math.min(args.max, modelValue + args.step)">+</button>
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
- </StorybookComponent>
114
+ </InputNumber>
85
115
  </div>
86
116
  `,
87
117
  });
88
118
 
89
- export const WithStepButtons = WithStepButtonsTemplate.bind({});
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 = WithStepButtonsTemplate.bind({});
92
- Underlined.args = { inputVariant: "underlined" };
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
+ };