srcdev-nuxt-components 9.6.7 → 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.
- package/.claude/rules/consumer/ids-from-useid.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +6 -0
- package/.claude/skills/component-naming.md +2 -2
- 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 +1 -1
- package/.claude/skills/components/input-error.md +1 -1
- package/.claude/skills/components/input-label.md +2 -2
- package/.claude/skills/components/input-number.md +44 -13
- 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-textarea-core.md +4 -4
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/testing-add-unit-test.md +17 -0
- package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
- package/.vscode/srcdev-component-input-range.code-snippets +38 -18
- 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 +2 -2
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +42 -2
- package/app/components/05.forms/input-number/InputNumber.vue +37 -25
- 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/patterns/stories/MigratedFieldsForm.stories.ts +5 -5
- package/package.json +1 -1
- 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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"SRCDEV
|
|
3
|
-
"description": "Basic
|
|
2
|
+
"SRCDEV InputRange Basic": {
|
|
3
|
+
"description": "Basic InputRange native range input (no label/error wrapper)",
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
|
-
"<
|
|
6
|
+
"<InputRange",
|
|
7
7
|
" id=\"$1fieldId\"",
|
|
8
8
|
" name=\"$2fieldName\"",
|
|
9
9
|
" v-model=\"$3fieldValue\"",
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
"/>"
|
|
14
14
|
]
|
|
15
15
|
},
|
|
16
|
-
"SRCDEV
|
|
17
|
-
"description": "
|
|
16
|
+
"SRCDEV InputRange With Markers": {
|
|
17
|
+
"description": "InputRange with custom tick-mark markers overlaid on the track",
|
|
18
18
|
"scope": "vue,html",
|
|
19
19
|
"body": [
|
|
20
|
-
"<
|
|
20
|
+
"<InputRange",
|
|
21
21
|
" id=\"$1fieldId\"",
|
|
22
22
|
" name=\"$2fieldName\"",
|
|
23
23
|
" v-model=\"$3fieldValue\"",
|
|
@@ -27,19 +27,39 @@
|
|
|
27
27
|
">",
|
|
28
28
|
" <template #markers>",
|
|
29
29
|
" <div class=\"input-range-markers\">",
|
|
30
|
-
" <div class=\"marker\"><span class=\"marker-icon\">$7\\ud83d\\ude00</span></div>",
|
|
31
|
-
" <div class=\"marker\"><span class=\"marker-icon\">$8\\ud83d\\ude10</span></div>",
|
|
32
|
-
" <div class=\"marker\"><span class=\"marker-icon\">$9\\ud83d\\ude1e</span></div>",
|
|
30
|
+
" <div class=\"input-range-marker\"><span class=\"input-range-marker-icon\">$7\\ud83d\\ude00</span></div>",
|
|
31
|
+
" <div class=\"input-range-marker\"><span class=\"input-range-marker-icon\">$8\\ud83d\\ude10</span></div>",
|
|
32
|
+
" <div class=\"input-range-marker\"><span class=\"input-range-marker-icon\">$9\\ud83d\\ude1e</span></div>",
|
|
33
33
|
" </div>",
|
|
34
34
|
" </template>",
|
|
35
|
-
"</
|
|
35
|
+
"</InputRange>"
|
|
36
36
|
]
|
|
37
37
|
},
|
|
38
|
-
"SRCDEV
|
|
39
|
-
"description": "
|
|
38
|
+
"SRCDEV InputRange With Datalist": {
|
|
39
|
+
"description": "InputRange with a datalist; the slot passes the generated datalistId",
|
|
40
40
|
"scope": "vue,html",
|
|
41
41
|
"body": [
|
|
42
|
-
"<
|
|
42
|
+
"<InputRange",
|
|
43
|
+
" id=\"$1fieldId\"",
|
|
44
|
+
" name=\"$2fieldName\"",
|
|
45
|
+
" v-model=\"$3fieldValue\"",
|
|
46
|
+
" :min=\"${4:0}\"",
|
|
47
|
+
" :max=\"${5:100}\"",
|
|
48
|
+
">",
|
|
49
|
+
" <template #datalist=\"{ datalistId }\">",
|
|
50
|
+
" <datalist :id=\"datalistId\" class=\"input-range-datalist\">",
|
|
51
|
+
" <option value=\"${4:0}\" label=\"${6:Low}\"></option>",
|
|
52
|
+
" <option value=\"${5:100}\" label=\"${7:High}\"></option>",
|
|
53
|
+
" </datalist>",
|
|
54
|
+
" </template>",
|
|
55
|
+
"</InputRange>"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"SRCDEV InputRangeField Basic": {
|
|
59
|
+
"description": "InputRangeField: labelled range slider with error/description support",
|
|
60
|
+
"scope": "vue,html",
|
|
61
|
+
"body": [
|
|
62
|
+
"<InputRangeField",
|
|
43
63
|
" v-model=\"$1fieldValue\"",
|
|
44
64
|
" name=\"$2fieldName\"",
|
|
45
65
|
" label=\"$3Field label\"",
|
|
@@ -51,11 +71,11 @@
|
|
|
51
71
|
"/>"
|
|
52
72
|
]
|
|
53
73
|
},
|
|
54
|
-
"SRCDEV
|
|
55
|
-
"description": "
|
|
74
|
+
"SRCDEV InputRangeField With Step Buttons": {
|
|
75
|
+
"description": "InputRangeField with step-down/step-up buttons wired into the left/right slots",
|
|
56
76
|
"scope": "vue,html",
|
|
57
77
|
"body": [
|
|
58
|
-
"<
|
|
78
|
+
"<InputRangeField",
|
|
59
79
|
" v-model=\"$1fieldValue\"",
|
|
60
80
|
" name=\"$2fieldName\"",
|
|
61
81
|
" label=\"$3Field label\"",
|
|
@@ -67,11 +87,11 @@
|
|
|
67
87
|
">",
|
|
68
88
|
" <template #left>\\u2212</template>",
|
|
69
89
|
" <template #right>+</template>",
|
|
70
|
-
"</
|
|
90
|
+
"</InputRangeField>"
|
|
71
91
|
]
|
|
72
92
|
},
|
|
73
93
|
"SRCDEV InputRange CSS Override": {
|
|
74
|
-
"description": "CSS override scaffold for
|
|
94
|
+
"description": "CSS override scaffold for InputRange tokens",
|
|
75
95
|
"scope": "css",
|
|
76
96
|
"body": [
|
|
77
97
|
".$1my-page {",
|
|
@@ -29,6 +29,16 @@ columns need more room.
|
|
|
29
29
|
| `--single-checkbox-margin-block-start` | `1.2rem` | Space between the legend/description and the checkbox |
|
|
30
30
|
| `--single-checkbox-column-min-width` | `100px` | Grid column minimum width with `options-layout="equal-widths"` |
|
|
31
31
|
|
|
32
|
+
> **Changed 2026-10-16**: the equal-width column minimum is capped at `min(100%, ...)` so one long
|
|
33
|
+
> label can no longer push the grid wider than its container.
|
|
34
|
+
|
|
35
|
+
### Root fieldset
|
|
36
|
+
|
|
37
|
+
`.multiple-checkboxes-fieldset` and `.single-checkbox-fieldset` set `min-inline-size: 0` and
|
|
38
|
+
`overflow-wrap: anywhere` (added 2026-10-16). The second inherits into the legend, description,
|
|
39
|
+
option labels and error, so long unbroken words and URLs wrap instead of overflowing. Neither is a
|
|
40
|
+
token; override `overflow-wrap` on your passthrough class if you need the old behaviour.
|
|
41
|
+
|
|
32
42
|
---
|
|
33
43
|
|
|
34
44
|
## State hooks
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
</template>
|
|
24
24
|
</InputDescription>
|
|
25
25
|
<div ref="itemsContainer" class="multiple-checkboxes-items" :data-options-layout="optionsLayout">
|
|
26
|
-
<template v-for="item in fieldData.data" :key="item.id">
|
|
26
|
+
<template v-for="(item, index) in fieldData.data" :key="item.id">
|
|
27
27
|
<InputCheckboxRadioButton
|
|
28
28
|
v-if="isButton"
|
|
29
|
-
:id="`${
|
|
29
|
+
:id="`${optionIdPrefix}-${index}`"
|
|
30
30
|
v-model="modelValue"
|
|
31
31
|
type="checkbox"
|
|
32
32
|
:name
|
|
@@ -119,7 +119,14 @@ const slots = useSlots();
|
|
|
119
119
|
const modelValue = defineModel<(string | number | boolean)[] | string | number | boolean | undefined>({ required: true });
|
|
120
120
|
const fieldData = defineModel<IFormMultipleOptions>("fieldData", { required: true });
|
|
121
121
|
|
|
122
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
122
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
123
|
+
|
|
124
|
+
watch(
|
|
125
|
+
() => props.styleClassPassthrough,
|
|
126
|
+
() => {
|
|
127
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
128
|
+
}
|
|
129
|
+
);
|
|
123
130
|
|
|
124
131
|
const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
125
132
|
props.name,
|
|
@@ -127,6 +134,8 @@ const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
|
127
134
|
slots
|
|
128
135
|
);
|
|
129
136
|
|
|
137
|
+
const optionIdPrefix = useId();
|
|
138
|
+
|
|
130
139
|
const { maxChildWidth, updateMaxChildWidth } = useMaxChildWidth(
|
|
131
140
|
".input-checkbox-radio-field-label, .input-checkbox-radio-field",
|
|
132
141
|
"100px"
|
|
@@ -146,6 +155,11 @@ watch(
|
|
|
146
155
|
|
|
147
156
|
<style lang="css">
|
|
148
157
|
@layer components {
|
|
158
|
+
.multiple-checkboxes-fieldset {
|
|
159
|
+
min-inline-size: 0;
|
|
160
|
+
overflow-wrap: anywhere;
|
|
161
|
+
}
|
|
162
|
+
|
|
149
163
|
.multiple-checkboxes-items {
|
|
150
164
|
display: flex;
|
|
151
165
|
gap: var(--multiple-checkboxes-gap, 1.2rem);
|
|
@@ -165,7 +179,7 @@ watch(
|
|
|
165
179
|
grid-template-columns: repeat(
|
|
166
180
|
auto-fit,
|
|
167
181
|
minmax(
|
|
168
|
-
calc(var(--input-checked-icon-gap) + (2 * var(--input-checkbox-label-padding-inline)) + v-bind(maxChildWidth)),
|
|
182
|
+
min(100%, calc(var(--input-checked-icon-gap) + (2 * var(--input-checkbox-label-padding-inline)) + v-bind(maxChildWidth))),
|
|
169
183
|
1fr
|
|
170
184
|
)
|
|
171
185
|
);
|
|
@@ -84,7 +84,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
84
84
|
|
|
85
85
|
const slots = useSlots();
|
|
86
86
|
const modelValue = defineModel<(string | number | boolean)[] | string | number | boolean | undefined>({ required: true });
|
|
87
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
87
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
88
|
+
|
|
89
|
+
watch(
|
|
90
|
+
() => props.styleClassPassthrough,
|
|
91
|
+
() => {
|
|
92
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
88
95
|
|
|
89
96
|
const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
90
97
|
props.name,
|
|
@@ -95,6 +102,11 @@ const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
|
95
102
|
|
|
96
103
|
<style lang="css">
|
|
97
104
|
@layer components {
|
|
105
|
+
.single-checkbox-fieldset {
|
|
106
|
+
min-inline-size: 0;
|
|
107
|
+
overflow-wrap: anywhere;
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
.single-checkbox-items {
|
|
99
111
|
display: flex;
|
|
100
112
|
gap: var(--single-checkbox-gap, 1.2rem);
|
|
@@ -111,7 +123,7 @@ const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
|
111
123
|
|
|
112
124
|
&[data-options-layout="equal-widths"] {
|
|
113
125
|
display: grid;
|
|
114
|
-
grid-template-columns: repeat(auto-fit, minmax(var(--single-checkbox-column-min-width, 100px), 1fr));
|
|
126
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--single-checkbox-column-min-width, 100px)), 1fr));
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import MultipleCheckboxes from "../MultipleCheckboxes.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { IFormMultipleOptions, InputUiVariant, OptionsLayout } from "~/types/forms/types.forms.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
|
-
interface
|
|
8
|
+
interface StoryArgs {
|
|
7
9
|
name: string;
|
|
8
10
|
legend: string;
|
|
9
11
|
errorMessage: string;
|
|
@@ -15,11 +17,21 @@ interface MultipleCheckboxesStoryArgs {
|
|
|
15
17
|
direction: "row" | "row-reverse";
|
|
16
18
|
displayAsDisc: boolean;
|
|
17
19
|
inputVariant: InputUiVariant;
|
|
20
|
+
descriptionText: string;
|
|
21
|
+
options: IFormMultipleOptions["data"];
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
const serviceOptions: IFormMultipleOptions["data"] = [
|
|
25
|
+
{ id: "1", name: "services", value: "cut", label: "Cut" },
|
|
26
|
+
{ id: "2", name: "services", value: "colour", label: "Colour" },
|
|
27
|
+
{ id: "3", name: "services", value: "balayage", label: "Balayage" },
|
|
28
|
+
{ id: "4", name: "services", value: "styling", label: "Styling" },
|
|
29
|
+
{ id: "5", name: "services", value: "treatment", label: "Conditioning treatment" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const meta: Meta<StoryArgs> = {
|
|
21
33
|
title: "Components/Forms/Input Checkbox/MultipleCheckboxes",
|
|
22
|
-
component:
|
|
34
|
+
component: MultipleCheckboxes,
|
|
23
35
|
argTypes: {
|
|
24
36
|
name: { control: "text", table: { category: "Basic" } },
|
|
25
37
|
legend: { control: "text", table: { category: "Basic" } },
|
|
@@ -45,6 +57,16 @@ export default {
|
|
|
45
57
|
options: ["normal", "outlined", "underlined"],
|
|
46
58
|
table: { category: "Styling" },
|
|
47
59
|
},
|
|
60
|
+
descriptionText: {
|
|
61
|
+
control: "text",
|
|
62
|
+
description: "Story-only: fills the descriptionText slot",
|
|
63
|
+
table: { category: "Slots" },
|
|
64
|
+
},
|
|
65
|
+
options: {
|
|
66
|
+
control: "object",
|
|
67
|
+
description: "Story-only: the `data` array of the `fieldData` model",
|
|
68
|
+
table: { category: "Model" },
|
|
69
|
+
},
|
|
48
70
|
},
|
|
49
71
|
args: {
|
|
50
72
|
name: "services",
|
|
@@ -58,47 +80,134 @@ export default {
|
|
|
58
80
|
direction: "row",
|
|
59
81
|
displayAsDisc: false,
|
|
60
82
|
inputVariant: "normal",
|
|
83
|
+
descriptionText: "Select all that apply.",
|
|
84
|
+
options: serviceOptions,
|
|
85
|
+
},
|
|
86
|
+
decorators: [
|
|
87
|
+
(story, context) => ({
|
|
88
|
+
components: { story, CanvasSwitcher },
|
|
89
|
+
setup() {
|
|
90
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
91
|
+
return { canvasName };
|
|
92
|
+
},
|
|
93
|
+
template: `
|
|
94
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
95
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
96
|
+
</div>
|
|
97
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
98
|
+
<story />
|
|
99
|
+
</div>
|
|
100
|
+
`,
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
parameters: {
|
|
104
|
+
layout: "fullscreen",
|
|
105
|
+
docs: {
|
|
106
|
+
description: {
|
|
107
|
+
component:
|
|
108
|
+
"A fieldset of checkboxes built from the `fieldData` model, bound to an array `v-model`. `isButton` swaps the labelled checkboxes for button tiles (`isPill` rounds them), `optionsLayout` lays them out as equal-width columns, an inline wrap or a block column, and `errorMessage` with `fieldHasError` shows the error strip. Slots: `descriptionText`, `descriptionHtml`, `checkedIcon`, `itemIcon`.",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
61
111
|
},
|
|
62
|
-
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default meta;
|
|
115
|
+
type Story = StoryObj<StoryArgs>;
|
|
63
116
|
|
|
64
|
-
const
|
|
65
|
-
components: {
|
|
117
|
+
const render = (args: StoryArgs) => ({
|
|
118
|
+
components: { MultipleCheckboxes },
|
|
66
119
|
setup() {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{ id: "1", name: "services", value: "cut", label: "Cut" },
|
|
71
|
-
{ id: "2", name: "services", value: "colour", label: "Colour" },
|
|
72
|
-
{ id: "3", name: "services", value: "balayage", label: "Balayage" },
|
|
73
|
-
{ id: "4", name: "services", value: "styling", label: "Styling" },
|
|
74
|
-
{ id: "5", name: "services", value: "treatment", label: "Conditioning treatment" },
|
|
75
|
-
],
|
|
76
|
-
total: 5,
|
|
77
|
-
skip: 0,
|
|
78
|
-
limit: 10,
|
|
120
|
+
const componentArgs = computed(() => {
|
|
121
|
+
const { descriptionText: _descriptionText, options: _options, ...rest } = args;
|
|
122
|
+
return rest;
|
|
79
123
|
});
|
|
80
|
-
|
|
124
|
+
const fieldData = computed<IFormMultipleOptions>(() => ({
|
|
125
|
+
data: args.options,
|
|
126
|
+
total: args.options.length,
|
|
127
|
+
skip: 0,
|
|
128
|
+
limit: args.options.length,
|
|
129
|
+
}));
|
|
130
|
+
const modelValue = ref<string[]>([]);
|
|
131
|
+
return { args, componentArgs, fieldData, modelValue };
|
|
81
132
|
},
|
|
82
133
|
template: `
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<p style="margin-top: 2rem; font-family: monospace;">modelValue: {{ JSON.stringify(modelValue) }}</p>
|
|
88
|
-
</div>
|
|
134
|
+
<MultipleCheckboxes v-bind="componentArgs" v-model="modelValue" :field-data="fieldData">
|
|
135
|
+
<template v-if="args.descriptionText" #descriptionText>{{ args.descriptionText }}</template>
|
|
136
|
+
</MultipleCheckboxes>
|
|
137
|
+
<p style="margin-block-start: 2rem; font-family: monospace; overflow-wrap: anywhere;">modelValue: {{ JSON.stringify(modelValue) }}</p>
|
|
89
138
|
`,
|
|
90
139
|
});
|
|
91
140
|
|
|
92
|
-
export const Default =
|
|
141
|
+
export const Default: Story = { render };
|
|
93
142
|
|
|
94
|
-
export const Buttons =
|
|
95
|
-
|
|
143
|
+
export const Buttons: Story = {
|
|
144
|
+
args: { isButton: true, optionsLayout: "inline" },
|
|
145
|
+
render,
|
|
146
|
+
parameters: { docs: { description: { story: "Button tiles in an inline wrap, each with the `itemIcon` decorator." } } },
|
|
147
|
+
};
|
|
96
148
|
|
|
97
|
-
export const Pills =
|
|
98
|
-
|
|
149
|
+
export const Pills: Story = {
|
|
150
|
+
args: { isButton: true, isPill: true, optionsLayout: "inline" },
|
|
151
|
+
render,
|
|
152
|
+
parameters: { docs: { description: { story: "Button tiles with `isPill` for fully rounded ends." } } },
|
|
153
|
+
};
|
|
99
154
|
|
|
100
|
-
export const Block =
|
|
101
|
-
|
|
155
|
+
export const Block: Story = {
|
|
156
|
+
args: { optionsLayout: "block" },
|
|
157
|
+
render,
|
|
158
|
+
parameters: { docs: { description: { story: "`optionsLayout=\"block\"` stacks the options in one column." } } },
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const ErrorState: Story = {
|
|
162
|
+
args: { fieldHasError: true },
|
|
163
|
+
render,
|
|
164
|
+
parameters: {
|
|
165
|
+
docs: { description: { story: "`fieldHasError` shows the error message and links it to every checkbox with `aria-describedby`." } },
|
|
166
|
+
},
|
|
167
|
+
};
|
|
102
168
|
|
|
103
|
-
export const
|
|
104
|
-
|
|
169
|
+
export const StressTest: Story = {
|
|
170
|
+
name: "Stress Test (Worst-Case Data)",
|
|
171
|
+
args: {
|
|
172
|
+
name: "dienstleistungen auswahl",
|
|
173
|
+
legend:
|
|
174
|
+
"🎨 Gewünschte Dienstleistungsauswahlmöglichkeitenbeschreibungsüberschrift https://example.com/a/very/long/unbroken/url/that/should/wrap/somewhere <script>alert(1)</script>",
|
|
175
|
+
descriptionText:
|
|
176
|
+
"اختر الخدمات Wählen Sie alle zutreffenden Dienstleistungen aus, die Sie bei Ihrem nächsten Besuch in Anspruch nehmen möchten. <b>not bold</b>",
|
|
177
|
+
errorMessage: "BittewählenSiemindestenseineDienstleistungausbevorSiedasFormularabsenden",
|
|
178
|
+
fieldHasError: true,
|
|
179
|
+
required: true,
|
|
180
|
+
options: [
|
|
181
|
+
{ id: "1", name: "s", value: "a", label: "A" },
|
|
182
|
+
{ id: "2", name: "s", value: "", label: "" },
|
|
183
|
+
{ id: "3", name: "s", value: "long", label: "Haarverlängerungsberatungstermindienstleistungspaket" },
|
|
184
|
+
{ id: "4", name: "s", value: "url", label: "https://example.com/a/very/long/unbroken/url/that/should/wrap/somewhere" },
|
|
185
|
+
{ id: "5", name: "s", value: "has spaces <b>", label: "<b>Not bold</b> & <i>not italic</i>" },
|
|
186
|
+
{ id: "6", name: "s", value: "emoji", label: "💇♀️ Colour + cut 🌈" },
|
|
187
|
+
{ id: "7", name: "s", value: "rtl", label: "قص الشعر وتصفيفه" },
|
|
188
|
+
{
|
|
189
|
+
id: "8",
|
|
190
|
+
name: "s",
|
|
191
|
+
value: "sentence",
|
|
192
|
+
label: "A conditioning treatment with a scalp massage, a hot towel and a blow-dry finish, booked as one appointment",
|
|
193
|
+
},
|
|
194
|
+
...Array.from({ length: 22 }, (_, index) => ({
|
|
195
|
+
id: `extra-${index}`,
|
|
196
|
+
name: "s",
|
|
197
|
+
value: `extra-${index}`,
|
|
198
|
+
label: `Option ${index + 9}`,
|
|
199
|
+
})),
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
render,
|
|
203
|
+
parameters: {
|
|
204
|
+
initialCanvas: "mobileCanvas",
|
|
205
|
+
docs: {
|
|
206
|
+
story: { inline: false, height: "900px" },
|
|
207
|
+
description: {
|
|
208
|
+
story:
|
|
209
|
+
"30 options including an empty label, a single character, long unbroken German and URL labels, emoji, right-to-left text, HTML-like text and a value with spaces. Check at every canvas width: the legend, description, labels and error wrap inside the dashed outline, markup renders as text, the equal-width columns never get wider than the container, and clicking any label (including the odd ones) toggles its own checkbox. Switch `optionsLayout` to `inline` and `block`, and turn on `isButton`..",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import SingleCheckbox from "../SingleCheckbox.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { InputUiVariant } from "~/types/forms/types.forms.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
|
-
interface
|
|
8
|
+
interface StoryArgs {
|
|
7
9
|
name: string;
|
|
8
10
|
legend: string;
|
|
9
11
|
label: string;
|
|
@@ -11,11 +13,12 @@ interface SingleCheckboxStoryArgs {
|
|
|
11
13
|
required: boolean;
|
|
12
14
|
fieldHasError: boolean;
|
|
13
15
|
inputVariant: InputUiVariant;
|
|
16
|
+
descriptionText: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
const meta: Meta<StoryArgs> = {
|
|
17
20
|
title: "Components/Forms/Input Checkbox/SingleCheckbox",
|
|
18
|
-
component:
|
|
21
|
+
component: SingleCheckbox,
|
|
19
22
|
argTypes: {
|
|
20
23
|
name: { control: "text", table: { category: "Basic" } },
|
|
21
24
|
legend: { control: "text", table: { category: "Basic" } },
|
|
@@ -28,6 +31,11 @@ export default {
|
|
|
28
31
|
options: ["normal", "outlined", "underlined"],
|
|
29
32
|
table: { category: "Styling" },
|
|
30
33
|
},
|
|
34
|
+
descriptionText: {
|
|
35
|
+
control: "text",
|
|
36
|
+
description: "Story-only: fills the descriptionText slot",
|
|
37
|
+
table: { category: "Slots" },
|
|
38
|
+
},
|
|
31
39
|
},
|
|
32
40
|
args: {
|
|
33
41
|
name: "terms",
|
|
@@ -37,39 +45,108 @@ export default {
|
|
|
37
45
|
required: true,
|
|
38
46
|
fieldHasError: false,
|
|
39
47
|
inputVariant: "normal",
|
|
48
|
+
descriptionText: "",
|
|
49
|
+
},
|
|
50
|
+
decorators: [
|
|
51
|
+
(story, context) => ({
|
|
52
|
+
components: { story, CanvasSwitcher },
|
|
53
|
+
setup() {
|
|
54
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
55
|
+
return { canvasName };
|
|
56
|
+
},
|
|
57
|
+
template: `
|
|
58
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
59
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
60
|
+
</div>
|
|
61
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
62
|
+
<story />
|
|
63
|
+
</div>
|
|
64
|
+
`,
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
parameters: {
|
|
68
|
+
layout: "fullscreen",
|
|
69
|
+
docs: {
|
|
70
|
+
description: {
|
|
71
|
+
component:
|
|
72
|
+
"A fieldset holding one checkbox, for consent or opt-in fields. `v-model` toggles between `trueValue` and `falseValue` (default `true`/`false`); `required` puts native `required` on the checkbox. Slots: `labelContent` (rich label, e.g. a link), `descriptionText`, `descriptionHtml`, `checkedIcon`.",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
40
75
|
},
|
|
41
|
-
}
|
|
76
|
+
};
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
export default meta;
|
|
79
|
+
type Story = StoryObj<StoryArgs>;
|
|
80
|
+
|
|
81
|
+
const render = (args: StoryArgs) => ({
|
|
82
|
+
components: { SingleCheckbox },
|
|
45
83
|
setup() {
|
|
84
|
+
const componentArgs = computed(() => {
|
|
85
|
+
const { descriptionText: _descriptionText, ...rest } = args;
|
|
86
|
+
return rest;
|
|
87
|
+
});
|
|
46
88
|
const modelValue = ref(false);
|
|
47
|
-
return { args, modelValue };
|
|
89
|
+
return { args, componentArgs, modelValue };
|
|
48
90
|
},
|
|
49
91
|
template: `
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
</
|
|
92
|
+
<SingleCheckbox v-bind="componentArgs" v-model="modelValue">
|
|
93
|
+
<template v-if="args.descriptionText" #descriptionText>{{ args.descriptionText }}</template>
|
|
94
|
+
</SingleCheckbox>
|
|
95
|
+
<p style="margin-block-start: 2rem; font-family: monospace;">modelValue: {{ modelValue }}</p>
|
|
54
96
|
`,
|
|
55
97
|
});
|
|
56
98
|
|
|
57
|
-
export const Default =
|
|
58
|
-
|
|
59
|
-
export const ErrorState = Template.bind({});
|
|
60
|
-
ErrorState.args = { fieldHasError: true };
|
|
99
|
+
export const Default: Story = { render };
|
|
61
100
|
|
|
62
|
-
export const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
export const ErrorState: Story = {
|
|
102
|
+
args: { fieldHasError: true },
|
|
103
|
+
render,
|
|
104
|
+
parameters: {
|
|
105
|
+
docs: { description: { story: "`fieldHasError` shows the error message and links it to the checkbox with `aria-describedby`." } },
|
|
67
106
|
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const RichLabel: Story = {
|
|
110
|
+
render: (args: StoryArgs) => ({
|
|
111
|
+
components: { SingleCheckbox },
|
|
112
|
+
setup() {
|
|
113
|
+
const componentArgs = computed(() => {
|
|
114
|
+
const { descriptionText: _descriptionText, ...rest } = args;
|
|
115
|
+
return rest;
|
|
116
|
+
});
|
|
117
|
+
const modelValue = ref(false);
|
|
118
|
+
return { componentArgs, modelValue };
|
|
119
|
+
},
|
|
120
|
+
template: `
|
|
121
|
+
<SingleCheckbox v-bind="componentArgs" v-model="modelValue">
|
|
71
122
|
<template #labelContent>I agree to the <a href="#">terms and conditions</a></template>
|
|
72
|
-
</
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
123
|
+
</SingleCheckbox>
|
|
124
|
+
`,
|
|
125
|
+
}),
|
|
126
|
+
parameters: { docs: { description: { story: "The `labelContent` slot replaces the `label` prop, here with a link." } } },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const StressTest: Story = {
|
|
130
|
+
name: "Stress Test (Worst-Case Data)",
|
|
131
|
+
args: {
|
|
132
|
+
name: "marketing einwilligung",
|
|
133
|
+
legend:
|
|
134
|
+
"📨 Einwilligungserklärungzurdatenschutzkonformenverarbeitungpersonenbezogenerdaten https://example.com/a/very/long/unbroken/url <script>alert(1)</script>",
|
|
135
|
+
label:
|
|
136
|
+
"Ich stimme zu: Datenschutzgrundverordnungseinwilligungsbestätigung https://example.com/privacy/policy/with/a/long/path <b>not bold</b> قص",
|
|
137
|
+
descriptionText:
|
|
138
|
+
"سياسة الخصوصية Sie können Ihre Einwilligung jederzeit widerrufen, indem Sie uns eine kurze Nachricht schicken. 🙂",
|
|
139
|
+
errorMessage: "BitteakzeptierenSiedieDatenschutzbestimmungenbevorSiefortfahren",
|
|
140
|
+
fieldHasError: true,
|
|
141
|
+
},
|
|
142
|
+
render,
|
|
143
|
+
parameters: {
|
|
144
|
+
initialCanvas: "mobileCanvas",
|
|
145
|
+
docs: {
|
|
146
|
+
description: {
|
|
147
|
+
story:
|
|
148
|
+
"Long unbroken German and URL text in the legend, label and error, emoji, right-to-left text and HTML-like text. Check at every canvas width: everything wraps inside the dashed outline, markup renders as text, and clicking anywhere on the label toggles the checkbox. Also try an empty label and a single character.",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|