srcdev-nuxt-components 9.6.9 → 9.6.11

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 (109) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +4 -2
  2. package/.claude/skills/component-compliance-checklist.md +24 -2
  3. package/.claude/skills/component-inline-action-button.md +5 -4
  4. package/.claude/skills/component-naming.md +8 -2
  5. package/.claude/skills/components/{alert-mask-core.md → alert-mask.md} +38 -23
  6. package/.claude/skills/components/alert-masked-content.md +3 -3
  7. package/.claude/skills/components/container-glow.md +1 -1
  8. package/.claude/skills/components/display-theme-switch.md +8 -8
  9. package/.claude/skills/components/input-button.md +26 -7
  10. package/.claude/skills/components/input-checkbox-radio.md +2 -0
  11. package/.claude/skills/components/input-description.md +5 -0
  12. package/.claude/skills/components/input-error.md +9 -2
  13. package/.claude/skills/components/input-label.md +8 -2
  14. package/.claude/skills/components/input-number.md +2 -0
  15. package/.claude/skills/components/input-otp.md +2 -0
  16. package/.claude/skills/components/input-range.md +2 -0
  17. package/.claude/skills/components/input-select.md +2 -0
  18. package/.claude/skills/components/input-text.md +2 -0
  19. package/.claude/skills/components/input-textarea.md +2 -0
  20. package/.claude/skills/components/pending-effect.md +5 -4
  21. package/.claude/skills/components/{triple-toggle-switch.md → segmented-control.md} +34 -19
  22. package/.claude/skills/components/services-card.md +1 -1
  23. package/.claude/skills/components/tabbed-content.md +1 -1
  24. package/.claude/skills/components/toggle-switch.md +2 -0
  25. package/.claude/skills/css-nesting-conventions.md +22 -1
  26. package/.claude/skills/index.md +5 -5
  27. package/.claude/skills/storybook-add-story.md +3 -1
  28. package/.claude/skills/theming-component-token-pattern.md +24 -0
  29. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  30. package/.claude/skills/theming-partial-override.md +1 -1
  31. package/.vscode/srcdev-component-alert-mask.code-snippets +41 -0
  32. package/.vscode/{srcdev-component-triple-toggle-switch.code-snippets → srcdev-component-segmented-control.code-snippets} +6 -7
  33. package/app/assets/styles/setup/03.theming/_theme-slots.css +1 -1
  34. package/app/assets/styles/setup/04.elements/forms/03.generic-input-geometry.css +4 -0
  35. package/app/components/01.atoms/alert-mask/AlertMask.vue +194 -0
  36. package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +57 -42
  37. package/app/components/01.atoms/alert-mask/stories/AlertMask.stories.ts +198 -0
  38. package/app/components/01.atoms/alert-mask/tests/AlertMask.spec.ts +233 -0
  39. package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +2 -2
  40. package/app/components/02.molecules/alert-masked-content/CONSUMER-STYLING.md +4 -4
  41. package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +60 -0
  42. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +12 -12
  43. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +3 -3
  44. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +1 -1
  45. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +1 -1
  46. package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +9 -9
  47. package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +16 -16
  48. package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +53 -1
  49. package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +8 -8
  50. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +4 -4
  51. package/app/components/02.molecules/input-copy/InputCopy.vue +2 -2
  52. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +2 -2
  53. package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +4 -4
  54. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +2 -2
  55. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +2 -2
  56. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +6 -6
  57. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +8 -0
  58. package/app/components/05.forms/form-errors/InputError.vue +43 -37
  59. package/app/components/05.forms/form-errors/stories/InputError.stories.ts +79 -1
  60. package/app/components/05.forms/form-errors/tests/InputError.spec.ts +29 -1
  61. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +80 -47
  62. package/app/components/05.forms/input-button/InputButton.vue +52 -100
  63. package/app/components/05.forms/input-button/playwright/input-button.playwright.ts +1 -1
  64. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +145 -8
  65. package/app/components/05.forms/input-button/tests/InputButton.spec.ts +76 -46
  66. package/app/components/05.forms/input-button/tests/__snapshots__/InputButton.spec.ts.snap +17 -17
  67. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +9 -1
  68. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +8 -0
  69. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +2 -2
  70. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +14 -2
  71. package/app/components/05.forms/input-description/InputDescription.vue +2 -1
  72. package/app/components/05.forms/input-description/stories/InputDescription.stories.ts +70 -4
  73. package/app/components/05.forms/input-description/tests/InputDescription.spec.ts +2 -1
  74. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +10 -2
  75. package/app/components/05.forms/input-label/InputLabel.vue +1 -0
  76. package/app/components/05.forms/input-label/stories/InputLabel.stories.ts +68 -2
  77. package/app/components/05.forms/input-label/tests/InputLabel.spec.ts +13 -3
  78. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +7 -1
  79. package/app/components/05.forms/input-number/InputNumber.vue +4 -5
  80. package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +7 -1
  81. package/app/components/05.forms/input-otp/InputOtp.vue +2 -2
  82. package/app/components/05.forms/input-range/InputRange.vue +1 -1
  83. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +7 -1
  84. package/app/components/05.forms/input-select/InputSelect.vue +14 -5
  85. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +7 -1
  86. package/app/components/05.forms/input-text/InputText.vue +4 -4
  87. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +7 -1
  88. package/app/components/05.forms/input-textarea/InputTextarea.vue +2 -3
  89. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +4 -4
  90. package/app/components/05.forms/pending-effect/CONSUMER-STYLING.md +7 -3
  91. package/app/components/05.forms/pending-effect/PendingEffect.vue +3 -3
  92. package/app/components/05.forms/pending-effect/tests/PendingEffect.spec.ts +1 -1
  93. package/app/components/05.forms/segmented-control/CONSUMER-STYLING.md +125 -0
  94. package/app/components/05.forms/segmented-control/SegmentedControl.vue +231 -0
  95. package/app/components/05.forms/segmented-control/stories/SegmentedControl.stories.ts +215 -0
  96. package/app/components/05.forms/segmented-control/tests/SegmentedControl.spec.ts +202 -0
  97. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +7 -0
  98. package/app/components/05.forms/toggle-switch/ToggleSwitch.vue +2 -0
  99. package/app/types/components/index.ts +1 -1
  100. package/package.json +1 -1
  101. package/.vscode/srcdev-component-alert-mask-core.code-snippets +0 -31
  102. package/app/components/01.atoms/alert-mask/AlertMaskCore.vue +0 -166
  103. package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +0 -105
  104. package/app/components/01.atoms/alert-mask/tests/AlertMaskCore.spec.ts +0 -342
  105. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +0 -99
  106. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +0 -251
  107. package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitch.stories.ts +0 -104
  108. package/app/components/05.forms/triple-toggle-switch/tests/TripleToggleSwitch.spec.ts +0 -138
  109. /package/app/types/components/{alert-mask-core.d.ts → alert-mask.d.ts} +0 -0
@@ -4,23 +4,29 @@
4
4
  :type="!isLink ? props.type : undefined"
5
5
  :href="isLink ? props.href : undefined"
6
6
  :readonly="!isLink ? props.readonly : undefined"
7
- :aria-disabled="!isLink ? props.readonly : undefined"
7
+ :aria-disabled="!isLink && props.readonly ? 'true' : undefined"
8
8
  data-testid="input-button"
9
9
  :data-theme="theme"
10
+ :data-variant="variant"
11
+ :data-pill="isPill ? '' : undefined"
12
+ :data-pending="isPending ? '' : undefined"
13
+ :data-pending-effect="hasPendingEffect ? '' : undefined"
14
+ :data-link="isLink ? '' : undefined"
15
+ :data-icon-only="hasIconOnlySlot() ? '' : undefined"
10
16
  class="input-button"
11
- :class="buttonClasses()"
17
+ :class="elementClasses"
12
18
  @click.capture="guardReadonly"
13
19
  >
14
20
  <PendingEffect v-if="hasPendingEffect" :theme="theme" />
15
21
 
16
- <span v-if="hasLeftSlot()" class="btn-icon left">
22
+ <span v-if="hasLeftSlot()" class="input-button-icon" data-slot="left">
17
23
  <slot name="left"></slot>
18
24
  </span>
19
- <span class="button-text" :class="[{ 'sr-only': hasIconOnlySlot() }]">{{ buttonText }}</span>
20
- <span v-if="hasRightSlot()" class="btn-icon right">
25
+ <span class="input-button-text" :class="{ 'sr-only': hasIconOnlySlot() }">{{ buttonText }}</span>
26
+ <span v-if="hasRightSlot()" class="input-button-icon" data-slot="right">
21
27
  <slot name="right"></slot>
22
28
  </span>
23
- <span v-if="hasIconOnlySlot()" class="btn-icon icon-only">
29
+ <span v-if="hasIconOnlySlot()" class="input-button-icon" data-slot="icon-only">
24
30
  <slot name="iconOnly"></slot>
25
31
  </span>
26
32
  </component>
@@ -77,22 +83,10 @@ const tag = computed(() => {
77
83
  return "button";
78
84
  });
79
85
 
80
- // Cache slot computations for better performance
81
86
  const hasLeftSlot = () => Boolean(slots.left && !slots.iconOnly);
82
87
  const hasRightSlot = () => Boolean(slots.right && !slots.iconOnly);
83
88
  const hasIconOnlySlot = () => Boolean(slots.iconOnly);
84
89
 
85
- // Combine all button classes into a single computed
86
- const buttonClasses = () => [
87
- props.variant,
88
- elementClasses.value,
89
- { "icon-only": hasIconOnlySlot() },
90
- { "pending-effect": props.hasPendingEffect },
91
- { "is-pending": props.isPending },
92
- { pill: props.isPill },
93
- { "is-link": isLink.value },
94
- ];
95
-
96
90
  const guardReadonly = (event: MouseEvent) => {
97
91
  if (!props.readonly) return;
98
92
  event.preventDefault();
@@ -128,47 +122,32 @@ watch(
128
122
  overflow: hidden;
129
123
  transition: all var(--control-transition-duration) var(--control-transition-ease);
130
124
 
131
- &.is-link {
125
+ &[data-link] {
132
126
  display: inline-grid;
133
127
  }
134
128
 
135
- &.pending-effect {
129
+ &[data-pending-effect] {
136
130
  overflow: visible;
137
131
  }
138
132
 
139
- /*
140
- * Theming
141
- * Every colour is a public --input-button-{variant}-{property} token, inline-fallback to
142
- * the shared theme slot (see theming-component-token-pattern.md for why: overriding one
143
- * variant here never touches the --theme-* slots other components read).
144
- *
145
- * Flat by default: border defaults to the variant's own surface colour (so the resting state
146
- * shows no visible edge) and outline defaults to fully transparent — outline is reserved for
147
- * the focus-visible ring, not used to reinforce the flat look. Hover darkens the surface via
148
- * color-mix (always darker, regardless of which direction the underlying theme ramp runs in
149
- * light vs dark mode — no more "hover looks lighter" surprises) and carries the border along
150
- * with it, still flat; outline stays transparent through hover too.
151
- * focus-visible is deliberately its own block, not grouped with :hover — it's the one state
152
- * that gets a real, separately-themeable a11y indicator, and its outline-width/-offset (set
153
- * below, in Shared States) give it a visible ring regardless of the border/outline being
154
- * invisible everywhere else.
155
- * --_surface is a private local (not a public token) — it exists purely so -hover/-border/-ring
156
- * can color-mix() off the SAME resolved value a consumer's --input-button-{variant}-surface
157
- * override produces, without every derived token repeating that fallback chain itself.
158
- **/
159
- &.primary {
133
+ /* Primary is filled, secondary outlined, tertiary text-only; inline has no colours (wrappers style it).
134
+ Primary and secondary carry a light shadow ring; hover draws the outline over its inner edge. */
135
+ &[data-variant="primary"] {
160
136
  --_surface: var(--input-button-primary-surface, var(--theme-surface));
161
137
  --_surface-hover: var(--input-button-primary-surface-hover, color-mix(in oklab, var(--_surface) 85%, black));
138
+ --_primary-border-hover: var(--input-button-primary-border-hover, var(--_surface-hover));
162
139
 
163
140
  background-color: var(--_surface);
164
141
  color: var(--input-button-primary-text, var(--theme-on-surface));
165
142
  border: var(--button-border-width) solid var(--input-button-primary-border, var(--_surface));
166
143
  outline: var(--button-outline-width) solid var(--input-button-primary-ring, transparent);
144
+ box-shadow: 0 0 0 var(--input-button-shadow-width, var(--form-element-shadow-width))
145
+ var(--input-button-primary-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
167
146
 
168
147
  &:hover {
169
148
  background-color: var(--_surface-hover);
170
- border-color: var(--input-button-primary-border-hover, var(--_surface-hover));
171
- outline-color: var(--input-button-primary-ring-hover, transparent);
149
+ border-color: var(--_primary-border-hover);
150
+ outline-color: var(--input-button-primary-ring-hover, var(--_primary-border-hover));
172
151
  }
173
152
 
174
153
  &:focus-visible {
@@ -177,9 +156,7 @@ watch(
177
156
  }
178
157
  }
179
158
 
180
- &.is-pending {
181
- /* Always the primary surface regardless of variant — pre-existing behaviour, not
182
- something this migration changes. */
159
+ &[data-pending] {
183
160
  background-color: color-mix(
184
161
  in oklab,
185
162
  var(--input-button-primary-surface, var(--theme-surface)) 50%,
@@ -187,22 +164,21 @@ watch(
187
164
  );
188
165
  }
189
166
 
190
- &.secondary {
191
- --_surface: var(--input-button-secondary-surface, var(--theme-surface-inverted));
192
- --_surface-hover: var(
193
- --input-button-secondary-surface-hover,
194
- color-mix(in oklab, var(--_surface) 85%, black)
195
- );
167
+ &[data-variant="secondary"] {
168
+ --_secondary-border: var(--input-button-secondary-border, var(--theme-surface));
169
+ --_secondary-border-hover: var(--input-button-secondary-border-hover, var(--_secondary-border));
196
170
 
197
- background-color: var(--_surface);
198
- color: var(--input-button-secondary-text, var(--theme-text-inverted));
199
- border: var(--button-border-width) solid var(--input-button-secondary-border, var(--_surface));
171
+ background-color: var(--input-button-secondary-surface, transparent);
172
+ color: var(--input-button-secondary-text, var(--theme-surface));
173
+ border: var(--button-border-width) solid var(--_secondary-border);
200
174
  outline: var(--button-outline-width) solid var(--input-button-secondary-ring, transparent);
175
+ box-shadow: 0 0 0 var(--input-button-shadow-width, var(--form-element-shadow-width))
176
+ var(--input-button-secondary-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
201
177
 
202
178
  &:hover {
203
- background-color: var(--_surface-hover);
204
- border-color: var(--input-button-secondary-border-hover, var(--_surface-hover));
205
- outline-color: var(--input-button-secondary-ring-hover, transparent);
179
+ background-color: var(--input-button-secondary-surface-hover, var(--theme-surface-subtle));
180
+ border-color: var(--_secondary-border-hover);
181
+ outline-color: var(--input-button-secondary-ring-hover, var(--_secondary-border-hover));
206
182
  }
207
183
 
208
184
  &:focus-visible {
@@ -211,22 +187,15 @@ watch(
211
187
  }
212
188
  }
213
189
 
214
- &.tertiary {
215
- --_surface: var(--input-button-tertiary-surface, var(--slate-01));
216
- --_surface-hover: var(
217
- --input-button-tertiary-surface-hover,
218
- color-mix(in oklab, var(--_surface) 85%, black)
219
- );
220
-
221
- background-color: var(--_surface);
222
- color: var(--input-button-tertiary-text, var(--theme-text));
223
- border: var(--button-border-width) solid var(--input-button-tertiary-border, var(--_surface));
190
+ &[data-variant="tertiary"] {
191
+ background-color: var(--input-button-tertiary-surface, transparent);
192
+ color: var(--input-button-tertiary-text, var(--theme-surface));
193
+ border: var(--button-border-width) solid var(--input-button-tertiary-border, transparent);
224
194
  outline: var(--button-outline-width) solid var(--input-button-tertiary-ring, transparent);
225
- text-decoration: underline;
226
195
 
227
196
  &:hover {
228
- background-color: var(--_surface-hover);
229
- border-color: var(--input-button-tertiary-border-hover, var(--_surface-hover));
197
+ background-color: var(--input-button-tertiary-surface-hover, var(--theme-surface-subtle));
198
+ border-color: var(--input-button-tertiary-border-hover, transparent);
230
199
  outline-color: var(--input-button-tertiary-ring-hover, transparent);
231
200
  }
232
201
 
@@ -236,38 +205,21 @@ watch(
236
205
  }
237
206
  }
238
207
 
239
- /* Trial (2026-09-27): where supported, text picks black or white from its own surface, per
240
- state. An explicit --input-button-{variant}-text still wins. Remove this block to revert. */
208
+ /* Trial (2026-09-27): primary text picks black or white from its own surface. Remove to revert. */
241
209
  @supports (color: contrast-color(red)) {
242
- &.primary {
210
+ &[data-variant="primary"] {
243
211
  color: var(--input-button-primary-text, contrast-color(var(--_surface)));
244
212
 
245
213
  &:hover {
246
214
  color: var(--input-button-primary-text, contrast-color(var(--_surface-hover)));
247
215
  }
248
216
  }
249
-
250
- &.secondary {
251
- color: var(--input-button-secondary-text, contrast-color(var(--_surface)));
252
-
253
- &:hover {
254
- color: var(--input-button-secondary-text, contrast-color(var(--_surface-hover)));
255
- }
256
- }
257
-
258
- &.tertiary {
259
- color: var(--input-button-tertiary-text, contrast-color(var(--_surface)));
260
-
261
- &:hover {
262
- color: var(--input-button-tertiary-text, contrast-color(var(--_surface-hover)));
263
- }
264
- }
265
217
  }
266
218
 
267
- &.primary,
268
- &.secondary,
269
- &.tertiary {
270
- &.pill {
219
+ &[data-variant="primary"],
220
+ &[data-variant="secondary"],
221
+ &[data-variant="tertiary"] {
222
+ &[data-pill] {
271
223
  border-radius: 100vw;
272
224
  }
273
225
  }
@@ -290,10 +242,10 @@ watch(
290
242
  pointer-events: none;
291
243
  }
292
244
 
293
- .button-text {
245
+ & .input-button-text {
294
246
  display: inline-block;
295
247
  min-inline-size: 0;
296
- white-space: var(--input-button-text-white-space, nowrap);
248
+ white-space: var(--input-button-text-white-space, normal);
297
249
  overflow-wrap: anywhere;
298
250
  font-size: var(--button-font-size);
299
251
  line-height: var(--button-line-height);
@@ -301,22 +253,22 @@ watch(
301
253
  text-transform: var(--button-text-transform);
302
254
  }
303
255
 
304
- .btn-icon {
256
+ & .input-button-icon {
305
257
  display: flex;
306
258
 
307
- .icon {
259
+ & .icon {
308
260
  display: inline-block;
309
261
  font-size: var(--input-icon-size);
310
262
  }
311
263
  }
312
264
 
313
- &.icon-only {
265
+ &[data-icon-only] {
314
266
  aspect-ratio: 1;
315
267
  border-radius: var(--button-border-radius-icon-only);
316
268
  margin: 0;
317
269
  padding: 0;
318
270
 
319
- .btn-icon {
271
+ & .input-button-icon {
320
272
  margin: 1.2rem;
321
273
  }
322
274
  }
@@ -2,7 +2,7 @@ import { test, expect, type Page } from "@playwright/test";
2
2
 
3
3
  // --- Config ---
4
4
  const STORYBOOK_URL = "http://127.0.0.1:6006";
5
- const STORY_BASE = "components-forms-input-button-inputbuttoncore";
5
+ const STORY_BASE = "components-forms-input-button-inputbutton";
6
6
  const BUTTON_TIMEOUT = 15000;
7
7
 
8
8
  // --- Helpers ---
@@ -1,13 +1,16 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
- import { ref } from "vue";
2
+ import { computed, ref } from "vue";
3
3
  import StorybookComponent from "../InputButton.vue";
4
4
  import InputText from "../../input-text/InputText.vue";
5
+ import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
6
+ import type { MediaCanvas } from "~/types/components";
5
7
 
6
8
  interface InputButtonStoryArgs {
7
9
  type: "submit" | "button" | "reset";
8
10
  theme: "default" | "success" | "error" | "warning";
9
11
  variant: "primary" | "secondary" | "tertiary" | "inline";
10
12
  buttonText: string;
13
+ isPill: boolean;
11
14
  styleClassPassthrough: string[];
12
15
  isPending: boolean;
13
16
  hasPendingEffect: boolean;
@@ -75,7 +78,7 @@ export default {
75
78
  variant: {
76
79
  control: { type: "select" },
77
80
  options: ["primary", "secondary", "tertiary", "inline"],
78
- description: "Button variant/style",
81
+ description: "primary: filled. secondary: outlined. tertiary: text-only. inline: no colours, for wrapper components that style the button themselves",
79
82
  table: {
80
83
  category: "Styling",
81
84
  },
@@ -201,7 +204,7 @@ export default {
201
204
  isPending: false,
202
205
  hasPendingEffect: false,
203
206
  readonly: false,
204
- isPill: false, // Ensure isPill is present and defaulted
207
+ isPill: false,
205
208
  useLeftSlot: false,
206
209
  useRightSlot: false,
207
210
  useIconOnlySlot: false,
@@ -215,7 +218,45 @@ export default {
215
218
  rightIconName: "mdi:arrow-right",
216
219
  iconOnlyName: "mdi:flash",
217
220
  },
218
- } as Meta<typeof StorybookComponent>;
221
+ parameters: {
222
+ docs: {
223
+ description: {
224
+ component:
225
+ "The library's button. Renders a `<button>`, or a link when `href` is set (`NuxtLink` for a `/` path unless `external`). `variant` (primary, secondary, tertiary, inline) and `theme` pick the colours, `is-pill` rounds it, `readonly` dims it and blocks clicks, and `is-pending` with `has-pending-effect` animates a border while work runs. Label copy is `button-text`; the `left`/`right` slots add icons beside it, and `iconOnly` replaces it (the text stays as screen-reader text). The Slots and Icons controls are story-only. It's also the action button inside [InputCopy](?path=/story/molecules-input-copy--default), [CookieConsentBanner](?path=/story/molecules-cookieconsentbanner--default) and the step buttons of [InputNumberField](?path=/story/components-forms-input-number-inputnumberfield--default).",
226
+ },
227
+ },
228
+ },
229
+ } as Meta<InputButtonStoryArgs>;
230
+
231
+ // Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
232
+ const storyNoteStyle = "margin: 0; font-size: 1.4rem;";
233
+ const usedByNote = `
234
+ <p style="${storyNoteStyle}">
235
+ Also the action button inside
236
+ <a href="/?path=/story/molecules-input-copy--default" target="_top">InputCopy</a>,
237
+ <a href="/?path=/story/molecules-cookieconsentbanner--default" target="_top">CookieConsentBanner</a>
238
+ (variant inline, styled by the banner's tokens) and the step buttons of
239
+ <a href="/?path=/story/components-forms-input-number-inputnumberfield--default" target="_top">InputNumberField</a>.
240
+ </p>
241
+ `;
242
+
243
+ const storyOnlyArgs = [
244
+ "useLeftSlot",
245
+ "useRightSlot",
246
+ "useIconOnlySlot",
247
+ "leftSlotContent",
248
+ "rightSlotContent",
249
+ "iconOnlyContent",
250
+ "leftIconName",
251
+ "rightIconName",
252
+ "iconOnlyName",
253
+ "useLeftIcon",
254
+ "useRightIcon",
255
+ "useIconOnly",
256
+ ] as const;
257
+
258
+ const stripStoryArgs = (args: InputButtonStoryArgs) =>
259
+ Object.fromEntries(Object.entries(args).filter(([key]) => !(storyOnlyArgs as readonly string[]).includes(key)));
219
260
 
220
261
  const Template: StoryFn<InputButtonStoryArgs> = (args) => ({
221
262
  components: { StorybookComponent },
@@ -228,14 +269,18 @@ const Template: StoryFn<InputButtonStoryArgs> = (args) => ({
228
269
  }
229
270
  };
230
271
 
272
+ const componentArgs = computed(() => stripStoryArgs(args));
273
+
231
274
  return {
232
275
  args,
276
+ componentArgs,
233
277
  clickCount,
234
278
  handleClick,
235
279
  };
236
280
  },
237
281
  template: `
238
- <div style="display: flex; flex-direction: column; gap: 1rem; align-items: flex-start;margin: 36px;">
282
+ <div style="display: flex; flex-direction: column; gap: 1rem; align-items: flex-start; margin: 3.6rem;">
283
+ ${usedByNote}
239
284
  <!-- :key forces a remount when any slot-visibility arg changes. Verified the args
240
285
  themselves ARE reactive here (a plain {{ args.useLeftSlot }} interpolation updates
241
286
  live on toggle) — the bug is narrower: Vue's compiled-at-runtime string template
@@ -244,10 +289,11 @@ const Template: StoryFn<InputButtonStoryArgs> = (args) => ({
244
289
  Toggling a control changed the fallthrough attribute (confirmed via useleftslot="true"
245
290
  in the rendered DOM) but never actually added/removed the slot content until a hard
246
291
  refresh. Remounting sidesteps whatever the underlying slot-stability quirk is, rather
247
- than chasing it further. -->
292
+ than chasing it further. The story-only args are stripped before v-bind, so they no
293
+ longer land on the button as attributes. -->
248
294
  <StorybookComponent
249
295
  :key="[args.useLeftSlot, args.useLeftIcon, args.useRightSlot, args.useRightIcon, args.useIconOnlySlot, args.useIconOnly].join(',')"
250
- v-bind="args"
296
+ v-bind="componentArgs"
251
297
  @click="handleClick"
252
298
  >
253
299
  <template v-if="args.useLeftSlot || args.useLeftIcon" #left>
@@ -276,6 +322,9 @@ Default.args = {
276
322
  useLeftSlot: false,
277
323
  useRightSlot: false,
278
324
  };
325
+ Default.parameters = {
326
+ docs: { description: { story: "Text only. Every prop is live in Controls, including the pending, pill and readonly states." } },
327
+ };
279
328
 
280
329
  export const WithBothEmojiIcons = Template.bind({});
281
330
  WithBothEmojiIcons.args = {
@@ -285,6 +334,9 @@ WithBothEmojiIcons.args = {
285
334
  leftSlotContent: "💸",
286
335
  rightSlotContent: "✅",
287
336
  };
337
+ WithBothEmojiIcons.parameters = {
338
+ docs: { description: { story: "Plain content in the `left` and `right` slots." } },
339
+ };
288
340
 
289
341
  export const WithBothNuxtIconComponents = Template.bind({});
290
342
  WithBothNuxtIconComponents.args = {
@@ -294,6 +346,9 @@ WithBothNuxtIconComponents.args = {
294
346
  leftIconName: "mdi:arrow-left",
295
347
  rightIconName: "mdi:arrow-right",
296
348
  };
349
+ WithBothNuxtIconComponents.parameters = {
350
+ docs: { description: { story: "`<Icon class=\"icon\">` in both slots, sized by `--input-icon-size`." } },
351
+ };
297
352
 
298
353
  export const EmojiIconOnly = Template.bind({});
299
354
  EmojiIconOnly.args = {
@@ -301,6 +356,9 @@ EmojiIconOnly.args = {
301
356
  useIconOnlySlot: true,
302
357
  iconOnlyContent: "⚡",
303
358
  };
359
+ EmojiIconOnly.parameters = {
360
+ docs: { description: { story: "The `iconOnly` slot makes the button square (`data-icon-only`) and keeps `button-text` as screen-reader text." } },
361
+ };
304
362
 
305
363
  export const NuxtIconOnlyComponent = Template.bind({});
306
364
  NuxtIconOnlyComponent.args = {
@@ -308,6 +366,9 @@ NuxtIconOnlyComponent.args = {
308
366
  useIconOnly: true,
309
367
  iconOnlyName: "mdi:chevron-right-circle-outline",
310
368
  };
369
+ NuxtIconOnlyComponent.parameters = {
370
+ docs: { description: { story: "Icon-only with an `<Icon>`; the accessible name still comes from `button-text`." } },
371
+ };
311
372
 
312
373
  // Composition check, not a props demo: confirms InputButton's --button-min-height (defaults
313
374
  // to var(--input-min-height)) actually keeps it height-aligned with an inline text input, the
@@ -334,4 +395,80 @@ const InlineTemplate: StoryFn = () => ({
334
395
  export const InlineWithTextInput = InlineTemplate.bind({});
335
396
 
336
397
  // Fixed showcase: hardcoded values, so Controls would do nothing here.
337
- InlineWithTextInput.parameters = { controls: { disable: true } };
398
+ InlineWithTextInput.parameters = {
399
+ controls: { disable: true },
400
+ docs: {
401
+ description: {
402
+ story: "Composition check: `--button-min-height` defaults to `--input-min-height`, so the button lines up with an inline text input. Controls do nothing here.",
403
+ },
404
+ },
405
+ };
406
+
407
+ const stressLabels = [
408
+ "Jetzt unverbindlich einen kostenlosen Beratungstermin vereinbaren",
409
+ "Datenschutzgrundverordnungskonformitätsbestätigung",
410
+ "https://example.com/a/very/long/unbroken/url/for/the/button/label",
411
+ "<b>not bold</b>",
412
+ "🎉 Book now",
413
+ "احجز الآن",
414
+ "x",
415
+ "",
416
+ ];
417
+
418
+ const StressTemplate: StoryFn<InputButtonStoryArgs> = (args) => ({
419
+ components: { StorybookComponent },
420
+ setup() {
421
+ const componentArgs = computed(() => {
422
+ const { buttonText: _buttonText, ...rest } = stripStoryArgs(args);
423
+ return rest;
424
+ });
425
+ return { args, componentArgs, stressLabels };
426
+ },
427
+ template: `
428
+ <div style="display: grid; gap: 2.4rem;">
429
+ <div style="display: flex; flex-direction: column; align-items: flex-start; gap: 1.2rem;">
430
+ <StorybookComponent v-for="(label, index) in stressLabels" :key="index" v-bind="componentArgs" :button-text="label" />
431
+ <StorybookComponent v-bind="componentArgs" :button-text="stressLabels[0]">
432
+ <template #left><Icon name="mdi:calendar" class="icon" /></template>
433
+ <template #right><Icon name="mdi:arrow-right" class="icon" /></template>
434
+ </StorybookComponent>
435
+ </div>
436
+ <div style="display: flex; flex-wrap: wrap; gap: 1.2rem;">
437
+ <StorybookComponent v-bind="componentArgs" variant="secondary" button-text="Zurück zur vorherigen Seite" />
438
+ <StorybookComponent v-bind="componentArgs" :button-text="stressLabels[0]" />
439
+ </div>
440
+ </div>
441
+ `,
442
+ });
443
+
444
+ export const StressTest = StressTemplate.bind({});
445
+ StressTest.storyName = "Stress Test (Worst-Case Data)";
446
+ StressTest.args = { isPill: true };
447
+ StressTest.decorators = [
448
+ (story, context) => ({
449
+ components: { story, CanvasSwitcher },
450
+ setup() {
451
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
452
+ return { canvasName };
453
+ },
454
+ template: `
455
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
456
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
457
+ </div>
458
+ <div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
459
+ <story />
460
+ </div>
461
+ `,
462
+ }),
463
+ ];
464
+ StressTest.parameters = {
465
+ layout: "fullscreen",
466
+ initialCanvas: "mobileCanvas",
467
+ docs: {
468
+ story: { inline: false, height: "900px" },
469
+ description: {
470
+ story:
471
+ "Long German copy, an unbroken compound word, a long URL, HTML-like text, emoji, right-to-left text, a single character and an empty label, then a long label with icons on both sides and a wrapping row of two buttons. Check at every canvas width: no button pushes past the dashed outline, long labels wrap inside the button (`--input-button-text-white-space` defaults to `normal`), icons keep their size, markup renders as text, and the pill shape still reads with several lines. Toggle variant, pill and pending in Controls. Button text is never line-clamped: a cut-off action label can't be trusted.",
472
+ },
473
+ },
474
+ };