srcdev-nuxt-components 9.4.8 → 9.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.claude/commands/migrate-component.md +20 -6
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +26 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/alert-mask-core.md +84 -0
  8. package/.claude/skills/components/canvas-switcher.md +1 -1
  9. package/.claude/skills/components/carousel-flip.md +112 -168
  10. package/.claude/skills/components/container-glow.md +123 -0
  11. package/.claude/skills/components/dashboard-quad-grid.md +98 -0
  12. package/.claude/skills/components/dashboard-stats-grid.md +103 -0
  13. package/.claude/skills/components/display-theme-switch.md +60 -0
  14. package/.claude/skills/components/glowing-border.md +66 -0
  15. package/.claude/skills/components/input-range-core.md +142 -0
  16. package/.claude/skills/components/section-parallax.md +32 -10
  17. package/.claude/skills/components/ui-block-decorated.md +56 -0
  18. package/.claude/skills/components/wipe-away-vertical.md +98 -0
  19. package/.claude/skills/icon-sets.md +1 -1
  20. package/.claude/skills/index.md +10 -1
  21. package/.vscode/css-custom-data.json +38 -0
  22. package/.vscode/settings.json +3 -0
  23. package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
  24. package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
  25. package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
  26. package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
  27. package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
  28. package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
  29. package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
  30. package/.vscode/srcdev-component-input-range.code-snippets +85 -0
  31. package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
  32. package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
  33. package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
  34. package/README.md +18 -37
  35. package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
  36. package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
  37. package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
  38. package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
  39. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
  40. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
  41. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
  42. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
  43. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
  44. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
  45. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
  46. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
  47. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
  48. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
  49. package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
  50. package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
  51. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
  52. package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
  53. package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
  54. package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
  55. package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
  56. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
  57. package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
  58. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +1 -1
  59. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
  60. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
  61. package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
  62. package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
  63. package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
  64. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
  65. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
  66. package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
  67. package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
  68. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +7 -1
  69. package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
  70. package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
  71. package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
  72. package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
  73. package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
  74. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
  75. package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
  76. package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
  77. package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
  78. package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
  79. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
  80. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
  81. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +96 -11
  82. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
  83. package/app/components/05.forms/form-field/FormField.vue +1 -1
  84. package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
  85. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
  86. package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
  87. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
  88. package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
  89. package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
  90. package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
  91. package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
  92. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +135 -0
  93. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +16 -2
  94. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +6 -4
  95. package/app/types/components/container-glow.d.ts +8 -0
  96. package/app/types/components/index.ts +1 -1
  97. package/package.json +3 -2
  98. package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
  99. package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
  100. package/app/components/carousels/CarouselBasic.vue +0 -317
  101. package/app/components/carousels/CarouselInfinite.vue +0 -358
  102. package/app/components/container-glow/ContainerGlowCore.vue +0 -296
  103. package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
  104. package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
  105. package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
  106. package/app/components/glowing-border/GlowingBorder.vue +0 -142
  107. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
  108. package/app/components/layout-grids/LayoutGridB.vue +0 -134
  109. package/app/components/test-storybook/TestStorybook.vue +0 -49
  110. package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
  111. package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
  112. package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
  113. package/app/types/components/carousel-basic.d.ts +0 -19
@@ -0,0 +1,45 @@
1
+ # InputRangeCore — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--input-range-accent-colour` | `var(--theme-accent)` | Native `accent-color` of the range input (track/thumb tint) |
8
+ | `--input-range-marker-background-colour` | `light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%))` | Background colour of each `markers` slot dot |
9
+ | `--input-range-marker-padding` | `0.5rem` | Padding of each marker dot |
10
+ | `--input-range-marker-border-radius` | `50%` | Corner radius of each marker dot |
11
+ | `--input-range-marker-outline-width` | `0.1rem` | Marker dot outline width |
12
+ | `--input-range-marker-outline-colour` | `light-dark(var(--slate-04), var(--slate-06))` | Marker dot outline colour |
13
+ | `--input-range-marker-icon-size` | `2rem` | Font size of the marker icon (`.marker-icon`) |
14
+ | `--input-range-marker-icon-colour` | `var(--input-range-accent-colour, var(--theme-accent))` | Marker icon colour |
15
+ | `--input-range-datalist-font-size` | `1.4rem` | Font size of the `datalist` slot's option labels |
16
+ | `--input-range-datalist-font-weight` | `500` | Font weight of the `datalist` slot's option labels |
17
+
18
+ Geometry (thumb/track height, border radius) comes from the shared global form-geometry tokens
19
+ (`--input-range-button-size`, `--form-input-border-radius`), not from this component's own
20
+ tokens — see `theming-form-geometry-tokens.md`.
21
+
22
+ ```css
23
+ .my-page {
24
+ --input-range-accent-colour: var(--brand-primary);
25
+ --input-range-marker-background-colour: var(--brand-primary);
26
+ }
27
+ ```
28
+
29
+ Or scope to a single instance via `styleClassPassthrough`:
30
+
31
+ ```vue
32
+ <InputRangeCore style-class-passthrough="price-range" ... />
33
+ ```
34
+
35
+ ## Notes
36
+
37
+ - `accent-color` was previously read from `--theme-form-range-accent-color`, a custom property
38
+ never declared anywhere in the codebase (no fallback either) — per CSS spec this made the whole
39
+ `accent-color` declaration invalid at computed-value time, so it silently fell back to the
40
+ browser's native default accent colour instead of the intended theme colour. Fixed 2026-09-21;
41
+ see the component's skill doc for detail.
42
+ - The `weight` prop (`FormWeight`) generates an `input-range--{weight}` class on the input, but no
43
+ CSS in this component (or elsewhere in the library) currently styles that class — it's a no-op
44
+ today. Left as-is rather than redesigned, since the original intent wasn't clear; flagged here
45
+ so you don't spend time debugging "why doesn't `weight` do anything."
@@ -48,7 +48,7 @@ interface Props {
48
48
  ariaDescribedby?: string;
49
49
  }
50
50
 
51
- const props = withDefaults(defineProps<Props>(), {
51
+ withDefaults(defineProps<Props>(), {
52
52
  step: 1,
53
53
  placeholder: "",
54
54
  required: false,
@@ -61,173 +61,109 @@ const props = withDefaults(defineProps<Props>(), {
61
61
 
62
62
  const slots = useSlots();
63
63
 
64
- const FormUiTheme = computed(() => {
65
- return props.fieldHasError ? "error" : props.theme;
64
+ const modelValue = defineModel<number>({
65
+ required: true,
66
66
  });
67
-
68
- const modelValue = defineModel<number | readonly number[]>();
69
67
  </script>
70
68
 
71
69
  <style lang="css">
72
70
  @layer components {
73
- .input-range-wrapper {
74
- display: flex;
75
- align-items: center;
76
- justify-content: space-between;
77
- gap: 1rem;
78
-
79
- .slot {
80
- align-self: flex-start;
81
-
82
- .input-button-core {
83
- border-radius: var(--form-input-border-radius);
84
- width: var(--input-range-button-size);
85
- height: var(--input-range-button-size);
86
-
87
- .btn-icon {
88
- margin: initial;
89
- }
90
- }
91
- }
92
-
93
- .input-range-container {
94
- flex-grow: 1;
95
-
96
- display: grid;
97
- grid-template-areas: "element-stack";
98
- /* margin-top: 2px; */
99
-
100
- .input-range-markers {
101
- grid-area: element-stack;
102
- display: flex;
103
- align-items: center;
104
- justify-content: space-between;
105
- width: 100%;
106
- z-index: 2;
107
-
108
- .marker {
109
- background-color: black;
110
- padding: 0.5rem;
111
- border-radius: 50%;
112
- overflow: hidden;
113
- outline: 1px solid gray;
114
-
115
- &:hover {
116
- cursor: pointer;
117
- }
71
+ .input-range-wrapper {
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: space-between;
75
+ gap: 1rem;
76
+
77
+ .slot {
78
+ align-self: flex-start;
79
+
80
+ .input-button-core {
81
+ border-radius: var(--form-input-border-radius);
82
+ width: var(--input-range-button-size);
83
+ height: var(--input-range-button-size);
118
84
 
119
- .marker-icon {
120
- font-size: 2rem;
121
- display: block;
122
- color: var(--theme-form-range-accent-color);
85
+ .btn-icon {
86
+ margin: initial;
123
87
  }
124
88
  }
125
89
  }
126
90
 
127
- .input-range-core {
128
- grid-area: element-stack;
91
+ .input-range-container {
92
+ flex-grow: 1;
129
93
 
130
- accent-color: var(--theme-form-range-accent-color);
131
- height: var(--input-range-button-size);
132
- margin: 0;
133
- width: 100%;
94
+ display: grid;
95
+ grid-template-areas: "element-stack";
134
96
 
135
- /*
136
- &:hover {
137
- cursor: -webkit-grab;
138
- outline-color: red;
139
- }
140
- &:active {
141
- cursor: -webkit-grabbing;
142
- outline-color: blue;
143
- }
144
- &:focus-visible {
145
- outline-offset: 0.25rem;
146
- outline-color: transparent;
147
- }
148
- */
149
-
150
- &::-webkit-slider-thumb {
151
- /* appearance: none; */
152
- /* -webkit-appearance: none; */
153
- accent-color: blue;
154
- color: blue;
155
- background-color: 0.1rem solid green;
156
- outline: 0.1rem solid blue;
157
- border-radius: 50%;
158
- }
97
+ .input-range-markers {
98
+ grid-area: element-stack;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: space-between;
102
+ width: 100%;
103
+ z-index: 2;
104
+
105
+ .marker {
106
+ background-color: var(--input-range-marker-background-colour, light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%)));
107
+ padding: var(--input-range-marker-padding, 0.5rem);
108
+ border-radius: var(--input-range-marker-border-radius, 50%);
109
+ overflow: hidden;
110
+ outline: var(--input-range-marker-outline-width, 0.1rem) solid
111
+ var(--input-range-marker-outline-colour, light-dark(var(--slate-04), var(--slate-06)));
112
+
113
+ &:hover {
114
+ cursor: pointer;
115
+ }
159
116
 
160
- &::-webkit-slider-runnable-track {
161
- appearance: none;
162
- -webkit-appearance: none;
163
- /* background: hsl(10 80% 50% / 0.5); */
164
- /* box-shadow: 0.1rem 0.1rem 0.1rem #fff, 0rem 0rem 0.1rem #fff; */
117
+ .marker-icon {
118
+ font-size: var(--input-range-marker-icon-size, 2rem);
119
+ display: block;
120
+ color: var(--input-range-marker-icon-colour, var(--input-range-accent-colour, var(--theme-accent)));
121
+ }
122
+ }
165
123
  }
166
124
 
167
- /* For Chrome, Safari, Opera, and Edge */
168
- /* &::-webkit-slider-runnable-track {
169
- background: var(--theme-form-range-accent-color);
170
- height: var(--input-range-button-size);
171
- } */
125
+ .input-range-core {
126
+ grid-area: element-stack;
172
127
 
173
- /* For Firefox */
174
- /* &::-moz-range-track {
175
- background: var(--theme-form-range-accent-color);
128
+ accent-color: var(--input-range-accent-colour, var(--theme-accent));
176
129
  height: var(--input-range-button-size);
177
- } */
178
-
179
- /* Stling the thumb */
180
- /* &::-webkit-slider-thumb {
181
- -webkit-appearance: none;
182
- background: #5cd5eb;
183
- height: 2rem;
184
- width: 1rem;
185
- border-radius: 1rem;
186
- } */
187
-
188
- /* For Firefox */
189
- /* &::-moz-range-thumb {
190
- background: #5cd5eb;
191
- height: 2rem;
192
- width: 1rem;
193
- border-radius: 1rem;
194
- border: none;
195
- } */
196
-
197
- &:focus-visible {
198
- box-shadow: var(--form-focus-box-shadow);
199
- }
130
+ margin: 0;
131
+ width: 100%;
200
132
 
201
- &.has-markers {
202
- accent-color: var(--theme-form-range-accent-color);
203
- height: 2px;
204
- z-index: 2;
205
- translate: 0 13px;
133
+ &:focus-visible {
134
+ box-shadow: var(--form-focus-box-shadow);
135
+ }
206
136
 
207
- &::-webkit-slider-thumb {
208
- opacity: 0;
209
- &:hover {
210
- cursor: pointer;
137
+ &.has-markers {
138
+ accent-color: var(--input-range-accent-colour, var(--theme-accent));
139
+ height: 0.2rem;
140
+ z-index: 2;
141
+ translate: 0 1.3rem;
142
+
143
+ &::-webkit-slider-thumb {
144
+ opacity: 0;
145
+ &:hover {
146
+ cursor: pointer;
147
+ }
211
148
  }
212
149
  }
213
150
  }
214
- }
215
151
 
216
- .input-range-datalist {
217
- display: flex;
218
- flex-direction: column;
219
- font-family: var(--font-family);
220
- font-size: 1.4rem;
221
- font-weight: 500;
222
- justify-content: space-between;
223
- writing-mode: vertical-lr;
224
- width: 100%;
225
-
226
- option {
227
- padding: 0;
152
+ .input-range-datalist {
153
+ display: flex;
154
+ flex-direction: column;
155
+ font-family: var(--font-family);
156
+ font-size: var(--input-range-datalist-font-size, 1.4rem);
157
+ font-weight: var(--input-range-datalist-font-weight, 500);
158
+ justify-content: space-between;
159
+ writing-mode: vertical-lr;
160
+ width: 100%;
161
+
162
+ option {
163
+ padding: 0;
164
+ }
228
165
  }
229
166
  }
230
167
  }
231
168
  }
232
- }
233
169
  </style>
@@ -0,0 +1,126 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { ref } from "vue";
3
+ import StorybookComponent from "../InputRangeCore.vue";
4
+
5
+ interface InputRangeCoreStoryArgs {
6
+ id: string;
7
+ name: string;
8
+ min: number;
9
+ max: number;
10
+ step: number;
11
+ required: boolean;
12
+ fieldHasError: boolean;
13
+ weight: string;
14
+ }
15
+
16
+ export default {
17
+ title: "Components/Forms/Input Range/InputRangeCore",
18
+ component: StorybookComponent,
19
+ argTypes: {
20
+ id: { control: "text", table: { category: "Basic" } },
21
+ name: { control: "text", table: { category: "Basic" } },
22
+ min: { control: "number", table: { category: "Basic" } },
23
+ max: { control: "number", table: { category: "Basic" } },
24
+ step: { control: "number", table: { category: "Basic" } },
25
+ required: { control: "boolean", table: { category: "Basic" } },
26
+ fieldHasError: { control: "boolean", table: { category: "States" } },
27
+ weight: { control: "text", table: { category: "Styling" } },
28
+ },
29
+ args: {
30
+ id: "range-default",
31
+ name: "rangeDefault",
32
+ min: 0,
33
+ max: 100,
34
+ step: 1,
35
+ required: false,
36
+ fieldHasError: false,
37
+ weight: "normal",
38
+ },
39
+ } as Meta<InputRangeCoreStoryArgs>;
40
+
41
+ const Template: StoryFn<InputRangeCoreStoryArgs> = (args) => ({
42
+ components: { StorybookComponent },
43
+ setup() {
44
+ const modelValue = ref(50);
45
+ return { args, modelValue };
46
+ },
47
+ template: `
48
+ <div style="margin: 36px; max-width: 320px;">
49
+ <StorybookComponent v-bind="args" v-model="modelValue" />
50
+ </div>
51
+ `,
52
+ });
53
+
54
+ export const Default = Template.bind({});
55
+
56
+ export const ErrorState = Template.bind({});
57
+ ErrorState.args = { fieldHasError: true };
58
+
59
+ const WithStepButtonsTemplate: StoryFn<InputRangeCoreStoryArgs> = (args) => ({
60
+ components: { StorybookComponent },
61
+ setup() {
62
+ const modelValue = ref(50);
63
+ return { args, modelValue };
64
+ },
65
+ template: `
66
+ <div style="margin: 36px; max-width: 320px;">
67
+ <StorybookComponent v-bind="args" v-model="modelValue">
68
+ <template #left>
69
+ <button type="button" @click="modelValue = Math.max(args.min, modelValue - args.step)">−</button>
70
+ </template>
71
+ <template #right>
72
+ <button type="button" @click="modelValue = Math.min(args.max, modelValue + args.step)">+</button>
73
+ </template>
74
+ </StorybookComponent>
75
+ </div>
76
+ `,
77
+ });
78
+
79
+ export const WithStepButtons = WithStepButtonsTemplate.bind({});
80
+
81
+ const WithMarkersTemplate: StoryFn<InputRangeCoreStoryArgs> = (args) => ({
82
+ components: { StorybookComponent },
83
+ setup() {
84
+ const modelValue = ref(50);
85
+ return { args, modelValue };
86
+ },
87
+ template: `
88
+ <div style="margin: 36px; max-width: 320px;">
89
+ <StorybookComponent v-bind="args" v-model="modelValue">
90
+ <template #markers>
91
+ <div class="input-range-markers">
92
+ <div class="marker"><span class="marker-icon">😀</span></div>
93
+ <div class="marker"><span class="marker-icon">🙂</span></div>
94
+ <div class="marker"><span class="marker-icon">😐</span></div>
95
+ </div>
96
+ </template>
97
+ </StorybookComponent>
98
+ </div>
99
+ `,
100
+ });
101
+
102
+ export const WithMarkers = WithMarkersTemplate.bind({});
103
+ WithMarkers.args = { min: 0, max: 2, step: 1 };
104
+
105
+ const WithDatalistTemplate: StoryFn<InputRangeCoreStoryArgs> = (args) => ({
106
+ components: { StorybookComponent },
107
+ setup() {
108
+ const modelValue = ref(50);
109
+ return { args, modelValue };
110
+ },
111
+ template: `
112
+ <div style="margin: 36px; max-width: 320px;">
113
+ <StorybookComponent v-bind="args" v-model="modelValue">
114
+ <template #datalist>
115
+ <datalist :id="\`\${args.name}-datalist\`" class="input-range-datalist">
116
+ <option value="0" label="Low"></option>
117
+ <option value="50" label="Medium"></option>
118
+ <option value="100" label="High"></option>
119
+ </datalist>
120
+ </template>
121
+ </StorybookComponent>
122
+ </div>
123
+ `,
124
+ });
125
+
126
+ export const WithDatalist = WithDatalistTemplate.bind({});
@@ -0,0 +1,124 @@
1
+ // https://nuxt.com/docs/getting-started/testing#unit-testing
2
+ import type { VueWrapper } from "@vue/test-utils";
3
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
4
+ import ComponentUnderTest from "../InputRangeCore.vue";
5
+
6
+ const initialPropsData = {
7
+ id: "test-range",
8
+ name: "testName",
9
+ min: 0,
10
+ max: 100,
11
+ modelValue: 50,
12
+ };
13
+
14
+ let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
15
+ const wrapperFactory = (propsData = {}, slots = {}) => {
16
+ const mockPropsData = { ...initialPropsData, ...propsData };
17
+
18
+ return mountSuspended(ComponentUnderTest, {
19
+ attachTo: document.body,
20
+ props: mockPropsData,
21
+ slots,
22
+ });
23
+ };
24
+
25
+ describe("InputRangeCore", () => {
26
+ it("mounts without error", async () => {
27
+ wrapper = await wrapperFactory();
28
+ expect(wrapper.vm).toBeTruthy();
29
+ });
30
+
31
+ it("renders a native range input with the given min/max/step", async () => {
32
+ wrapper = await wrapperFactory({ min: 10, max: 90, step: 5 });
33
+ const input = wrapper.find("input");
34
+
35
+ expect(input.attributes("type")).toBe("range");
36
+ expect(input.attributes("min")).toBe("10");
37
+ expect(input.attributes("max")).toBe("90");
38
+ expect(input.attributes("step")).toBe("5");
39
+ });
40
+
41
+ it("defaults step to 1", async () => {
42
+ wrapper = await wrapperFactory();
43
+ expect(wrapper.find("input").attributes("step")).toBe("1");
44
+ });
45
+
46
+ it("binds v-model to the input value", async () => {
47
+ wrapper = await wrapperFactory({ modelValue: 42 });
48
+ expect(wrapper.find("input").element.value).toBe("42");
49
+ });
50
+
51
+ it("forwards aria-describedby", async () => {
52
+ wrapper = await wrapperFactory({ ariaDescribedby: "some-description-id" });
53
+ expect(wrapper.find("input").attributes("aria-describedby")).toBe("some-description-id");
54
+ });
55
+
56
+ it("sets data-theme from the theme prop", async () => {
57
+ wrapper = await wrapperFactory({ theme: "error" });
58
+ expect(wrapper.find(".input-range-wrapper").attributes("data-theme")).toBe("error");
59
+ });
60
+
61
+ it("does not render the left/right slot wrappers when the slots are not used", async () => {
62
+ wrapper = await wrapperFactory();
63
+ expect(wrapper.find(".slot.left").exists()).toBe(false);
64
+ expect(wrapper.find(".slot.right").exists()).toBe(false);
65
+ });
66
+
67
+ it("renders the left/right slots when provided", async () => {
68
+ wrapper = await wrapperFactory(
69
+ {},
70
+ {
71
+ left: '<button data-testid="left-slot">-</button>',
72
+ right: '<button data-testid="right-slot">+</button>',
73
+ }
74
+ );
75
+
76
+ expect(wrapper.find(".slot.left [data-testid='left-slot']").exists()).toBe(true);
77
+ expect(wrapper.find(".slot.right [data-testid='right-slot']").exists()).toBe(true);
78
+ });
79
+
80
+ it("renders the markers slot and adds has-markers class to the input", async () => {
81
+ wrapper = await wrapperFactory(
82
+ {},
83
+ {
84
+ markers: '<div class="input-range-markers" data-testid="markers"></div>',
85
+ }
86
+ );
87
+
88
+ expect(wrapper.find('[data-testid="markers"]').exists()).toBe(true);
89
+ expect(wrapper.find("input").classes()).toContain("has-markers");
90
+ });
91
+
92
+ it("does not add has-markers class when the markers slot is not used", async () => {
93
+ wrapper = await wrapperFactory();
94
+ expect(wrapper.find("input").classes()).not.toContain("has-markers");
95
+ });
96
+
97
+ it("sets the list attribute to `{name}-datalist` only when the datalist slot is used", async () => {
98
+ wrapper = await wrapperFactory(
99
+ { name: "price" },
100
+ { datalist: '<datalist id="price-datalist"></datalist>' }
101
+ );
102
+ expect(wrapper.find("input").attributes("list")).toBe("price-datalist");
103
+ });
104
+
105
+ it("leaves the list attribute empty when the datalist slot is not used", async () => {
106
+ wrapper = await wrapperFactory();
107
+ expect(wrapper.find("input").attributes("list")).toBe("");
108
+ });
109
+
110
+ it("applies the weight class from the weight prop", async () => {
111
+ wrapper = await wrapperFactory({ weight: "wght-700" });
112
+ expect(wrapper.find("input").classes()).toContain("input-range--wght-700");
113
+ });
114
+
115
+ it("applies styleClassPassthrough to the input", async () => {
116
+ wrapper = await wrapperFactory({ styleClassPassthrough: ["custom-range-class"] });
117
+ expect(wrapper.find("input").classes()).toContain("custom-range-class");
118
+ });
119
+
120
+ it("sets required attribute when required is true", async () => {
121
+ wrapper = await wrapperFactory({ required: true });
122
+ expect(wrapper.find("input").attributes("required")).toBeDefined();
123
+ });
124
+ });
@@ -54,7 +54,7 @@
54
54
  type="button"
55
55
  :readonly="Number(modelValue) === min"
56
56
  :is-pending="false"
57
- button-text="Step down"
57
+ :button-text="stepDownLabel"
58
58
  :theme
59
59
  variant="secondary"
60
60
  @click.stop.prevent="updateRange(-step, Number(modelValue) > min)"
@@ -69,7 +69,7 @@
69
69
  type="button"
70
70
  :readonly="Number(modelValue) === max"
71
71
  :is-pending="false"
72
- button-text="Step up"
72
+ :button-text="stepUpLabel"
73
73
  :theme
74
74
  variant="secondary"
75
75
  @click.stop.prevent="updateRange(step, Number(modelValue) < max)"
@@ -106,7 +106,8 @@ interface Props {
106
106
  theme?: FormUiTheme;
107
107
  weight?: FormWeight;
108
108
  styleClassPassthrough?: string | string[];
109
- deepCssClassPassthrough?: string;
109
+ stepDownLabel?: string;
110
+ stepUpLabel?: string;
110
111
  }
111
112
 
112
113
  const props = withDefaults(defineProps<Props>(), {
@@ -117,25 +118,22 @@ const props = withDefaults(defineProps<Props>(), {
117
118
  theme: "default",
118
119
  weight: "wght-400",
119
120
  styleClassPassthrough: () => [],
120
- deepCssClassPassthrough: "",
121
+ stepDownLabel: "Step down",
122
+ stepUpLabel: "Step up",
121
123
  });
122
124
 
123
125
  const slots = useSlots();
124
126
  const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
125
127
 
126
- // const id = useId();
127
-
128
128
  const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
129
129
  props.name,
130
130
  toRef(props, "fieldHasError"),
131
131
  slots
132
132
  );
133
133
 
134
- // const FormUiTheme = computed(() => {
135
- // return props.fieldHasError ? "error" : props.theme;
136
- // });
137
-
138
- const modelValue = defineModel<number | readonly number[]>();
134
+ const modelValue = defineModel<number>({
135
+ required: true,
136
+ });
139
137
 
140
138
  const updateRange = (step: number, withinRangeLimit: boolean) => {
141
139
  if (withinRangeLimit) {
@@ -143,24 +141,3 @@ const updateRange = (step: number, withinRangeLimit: boolean) => {
143
141
  }
144
142
  };
145
143
  </script>
146
-
147
- <style lang="css">
148
- @layer components {
149
- .input-range-with-label {
150
- .input-range-label {
151
- display: block;
152
- margin-block: 0.8rem;
153
-
154
- &:hover {
155
- cursor: pointer;
156
- }
157
- }
158
-
159
- .label-description {
160
- font-family: var(--font-family);
161
- font-size: 1.6rem;
162
- margin-top: 1.2rem;
163
- }
164
- }
165
- }
166
- </style>