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.
Files changed (58) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +14 -0
  2. package/.claude/skills/component-compliance-checklist.md +6 -0
  3. package/.claude/skills/component-naming.md +2 -2
  4. package/.claude/skills/components/input-checkbox-radio.md +19 -3
  5. package/.claude/skills/components/input-checkbox.md +14 -3
  6. package/.claude/skills/components/input-description.md +1 -1
  7. package/.claude/skills/components/input-error.md +1 -1
  8. package/.claude/skills/components/input-label.md +2 -2
  9. package/.claude/skills/components/input-number.md +44 -13
  10. package/.claude/skills/components/input-radio.md +15 -1
  11. package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
  12. package/.claude/skills/components/input-textarea-core.md +4 -4
  13. package/.claude/skills/index.md +5 -5
  14. package/.claude/skills/testing-add-unit-test.md +17 -0
  15. package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
  16. package/.vscode/srcdev-component-input-range.code-snippets +38 -18
  17. package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
  18. package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
  19. package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
  20. package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
  21. package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
  22. package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
  23. package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
  24. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
  25. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
  26. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
  27. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
  28. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
  29. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
  30. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
  31. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
  32. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
  33. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
  34. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +2 -2
  35. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +2 -2
  36. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +42 -2
  37. package/app/components/05.forms/input-number/InputNumber.vue +37 -25
  38. package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
  39. package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
  40. package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
  41. package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
  42. package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
  43. package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
  44. package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
  45. package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
  46. package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
  47. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
  48. package/app/components/05.forms/input-range/InputRange.vue +247 -0
  49. package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
  50. package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
  51. package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
  52. package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
  53. package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
  54. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +5 -5
  55. package/package.json +1 -1
  56. package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
  57. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
  58. package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
@@ -1,9 +1,9 @@
1
1
  {
2
- "SRCDEV InputRangeCore Basic": {
3
- "description": "Basic InputRangeCore native range input (no label/error wrapper)",
2
+ "SRCDEV InputRange Basic": {
3
+ "description": "Basic InputRange native range input (no label/error wrapper)",
4
4
  "scope": "vue,html",
5
5
  "body": [
6
- "<InputRangeCore",
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 InputRangeCore With Markers": {
17
- "description": "InputRangeCore with custom tick-mark markers overlaid on the track",
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
- "<InputRangeCore",
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
- "</InputRangeCore>"
35
+ "</InputRange>"
36
36
  ]
37
37
  },
38
- "SRCDEV InputRangeDefault Basic": {
39
- "description": "InputRangeDefault: labelled range slider with error/description support",
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
- "<InputRangeDefault",
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 InputRangeDefault With Step Buttons": {
55
- "description": "InputRangeDefault with step-down/step-up buttons wired into the left/right slots",
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
- "<InputRangeDefault",
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
- "</InputRangeDefault>"
90
+ "</InputRangeField>"
71
91
  ]
72
92
  },
73
93
  "SRCDEV InputRange CSS Override": {
74
- "description": "CSS override scaffold for InputRangeCore tokens",
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="`${name}-${item.value}`"
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 type { Meta, StoryFn } from "@nuxtjs/storybook";
2
- import { ref } from "vue";
3
- import StorybookComponent from "../MultipleCheckboxes.vue";
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 MultipleCheckboxesStoryArgs {
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
- export default {
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: StorybookComponent,
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
- } as Meta<MultipleCheckboxesStoryArgs>;
112
+ };
113
+
114
+ export default meta;
115
+ type Story = StoryObj<StoryArgs>;
63
116
 
64
- const Template: StoryFn<MultipleCheckboxesStoryArgs> = (args) => ({
65
- components: { StorybookComponent },
117
+ const render = (args: StoryArgs) => ({
118
+ components: { MultipleCheckboxes },
66
119
  setup() {
67
- const modelValue = ref<string[]>([]);
68
- const fieldData = ref<IFormMultipleOptions>({
69
- data: [
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
- return { args, modelValue, fieldData };
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
- <div style="margin: 36px; max-width: 560px;">
84
- <StorybookComponent v-bind="args" v-model="modelValue" v-model:field-data="fieldData">
85
- <template #descriptionText>Select all that apply.</template>
86
- </StorybookComponent>
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 = Template.bind({});
141
+ export const Default: Story = { render };
93
142
 
94
- export const Buttons = Template.bind({});
95
- Buttons.args = { isButton: true, optionsLayout: "inline" };
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 = Template.bind({});
98
- Pills.args = { isButton: true, isPill: true, optionsLayout: "inline" };
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 = Template.bind({});
101
- Block.args = { optionsLayout: "block" };
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 ErrorState = Template.bind({});
104
- ErrorState.args = { fieldHasError: true };
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 type { Meta, StoryFn } from "@nuxtjs/storybook";
2
- import { ref } from "vue";
3
- import StorybookComponent from "../SingleCheckbox.vue";
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 SingleCheckboxStoryArgs {
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
- export default {
19
+ const meta: Meta<StoryArgs> = {
17
20
  title: "Components/Forms/Input Checkbox/SingleCheckbox",
18
- component: StorybookComponent,
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
- } as Meta<SingleCheckboxStoryArgs>;
76
+ };
42
77
 
43
- const Template: StoryFn<SingleCheckboxStoryArgs> = (args) => ({
44
- components: { StorybookComponent },
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
- <div style="margin: 36px; max-width: 480px;">
51
- <StorybookComponent v-bind="args" v-model="modelValue" />
52
- <p style="margin-top: 2rem; font-family: monospace;">modelValue: {{ modelValue }}</p>
53
- </div>
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 = Template.bind({});
58
-
59
- export const ErrorState = Template.bind({});
60
- ErrorState.args = { fieldHasError: true };
99
+ export const Default: Story = { render };
61
100
 
62
- export const RichLabel: StoryFn<SingleCheckboxStoryArgs> = (args) => ({
63
- components: { StorybookComponent },
64
- setup() {
65
- const modelValue = ref(false);
66
- return { args, modelValue };
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
- template: `
69
- <div style="margin: 36px; max-width: 480px;">
70
- <StorybookComponent v-bind="args" v-model="modelValue">
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
- </StorybookComponent>
73
- </div>
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
+ };