srcdev-nuxt-components 9.4.9 → 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.
- package/.claude/commands/migrate-component.md +20 -6
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/build.mjs +26 -0
- package/.claude/component-ledger/output.html +8 -3
- package/.claude/component-ledger/template.html +7 -2
- package/.claude/skills/component-dynamic-slots.md +1 -1
- package/.claude/skills/components/alert-mask-core.md +84 -0
- package/.claude/skills/components/canvas-switcher.md +1 -1
- package/.claude/skills/components/carousel-flip.md +112 -168
- package/.claude/skills/components/container-glow.md +123 -0
- package/.claude/skills/components/dashboard-quad-grid.md +98 -0
- package/.claude/skills/components/dashboard-stats-grid.md +103 -0
- package/.claude/skills/components/display-theme-switch.md +60 -0
- package/.claude/skills/components/glowing-border.md +66 -0
- package/.claude/skills/components/input-range-core.md +142 -0
- package/.claude/skills/components/section-parallax.md +32 -10
- package/.claude/skills/components/ui-block-decorated.md +56 -0
- package/.claude/skills/components/wipe-away-vertical.md +98 -0
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/index.md +10 -1
- package/.vscode/css-custom-data.json +38 -0
- package/.vscode/settings.json +3 -0
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
- package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
- package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
- package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
- package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
- package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
- package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
- package/.vscode/srcdev-component-input-range.code-snippets +85 -0
- package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
- package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
- package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
- package/README.md +18 -37
- package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
- package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
- package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
- package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
- package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
- package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
- package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
- package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
- package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
- package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
- package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
- package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
- package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
- package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
- package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
- package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
- package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
- package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
- package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
- package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
- package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
- package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +96 -11
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
- package/app/components/05.forms/form-field/FormField.vue +1 -1
- package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
- package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
- package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
- package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
- package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
- package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +135 -0
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +16 -2
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +6 -4
- package/app/types/components/container-glow.d.ts +8 -0
- package/app/types/components/index.ts +1 -1
- package/package.json +3 -2
- package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
- package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
- package/app/components/carousels/CarouselBasic.vue +0 -317
- package/app/components/carousels/CarouselInfinite.vue +0 -358
- package/app/components/container-glow/ContainerGlowCore.vue +0 -296
- package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
- package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
- package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
- package/app/components/glowing-border/GlowingBorder.vue +0 -142
- package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
- package/app/components/layout-grids/LayoutGridB.vue +0 -134
- package/app/components/test-storybook/TestStorybook.vue +0 -49
- package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
- package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
- package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
- 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
|
-
|
|
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
|
|
65
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
.
|
|
120
|
-
|
|
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-
|
|
128
|
-
|
|
91
|
+
.input-range-container {
|
|
92
|
+
flex-grow: 1;
|
|
129
93
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
margin: 0;
|
|
133
|
-
width: 100%;
|
|
94
|
+
display: grid;
|
|
95
|
+
grid-template-areas: "element-stack";
|
|
134
96
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
z-index: 2;
|
|
205
|
-
translate: 0 13px;
|
|
133
|
+
&:focus-visible {
|
|
134
|
+
box-shadow: var(--form-focus-box-shadow);
|
|
135
|
+
}
|
|
206
136
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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="
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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>
|