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.
- package/.claude/rules/consumer/ids-from-useid.md +4 -2
- package/.claude/skills/component-compliance-checklist.md +24 -2
- package/.claude/skills/component-inline-action-button.md +5 -4
- package/.claude/skills/component-naming.md +8 -2
- package/.claude/skills/components/{alert-mask-core.md → alert-mask.md} +38 -23
- package/.claude/skills/components/alert-masked-content.md +3 -3
- package/.claude/skills/components/container-glow.md +1 -1
- package/.claude/skills/components/display-theme-switch.md +8 -8
- package/.claude/skills/components/input-button.md +26 -7
- package/.claude/skills/components/input-checkbox-radio.md +2 -0
- package/.claude/skills/components/input-description.md +5 -0
- package/.claude/skills/components/input-error.md +9 -2
- package/.claude/skills/components/input-label.md +8 -2
- package/.claude/skills/components/input-number.md +2 -0
- package/.claude/skills/components/input-otp.md +2 -0
- package/.claude/skills/components/input-range.md +2 -0
- package/.claude/skills/components/input-select.md +2 -0
- package/.claude/skills/components/input-text.md +2 -0
- package/.claude/skills/components/input-textarea.md +2 -0
- package/.claude/skills/components/pending-effect.md +5 -4
- package/.claude/skills/components/{triple-toggle-switch.md → segmented-control.md} +34 -19
- package/.claude/skills/components/services-card.md +1 -1
- package/.claude/skills/components/tabbed-content.md +1 -1
- package/.claude/skills/components/toggle-switch.md +2 -0
- package/.claude/skills/css-nesting-conventions.md +22 -1
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +3 -1
- package/.claude/skills/theming-component-token-pattern.md +24 -0
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.claude/skills/theming-partial-override.md +1 -1
- package/.vscode/srcdev-component-alert-mask.code-snippets +41 -0
- package/.vscode/{srcdev-component-triple-toggle-switch.code-snippets → srcdev-component-segmented-control.code-snippets} +6 -7
- package/app/assets/styles/setup/03.theming/_theme-slots.css +1 -1
- package/app/assets/styles/setup/04.elements/forms/03.generic-input-geometry.css +4 -0
- package/app/components/01.atoms/alert-mask/AlertMask.vue +194 -0
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +57 -42
- package/app/components/01.atoms/alert-mask/stories/AlertMask.stories.ts +198 -0
- package/app/components/01.atoms/alert-mask/tests/AlertMask.spec.ts +233 -0
- package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +2 -2
- package/app/components/02.molecules/alert-masked-content/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +60 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +12 -12
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +1 -1
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +1 -1
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +9 -9
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +16 -16
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +53 -1
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +8 -8
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/input-copy/InputCopy.vue +2 -2
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +2 -2
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +4 -4
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +2 -2
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +2 -2
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +6 -6
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/form-errors/InputError.vue +43 -37
- package/app/components/05.forms/form-errors/stories/InputError.stories.ts +79 -1
- package/app/components/05.forms/form-errors/tests/InputError.spec.ts +29 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +80 -47
- package/app/components/05.forms/input-button/InputButton.vue +52 -100
- package/app/components/05.forms/input-button/playwright/input-button.playwright.ts +1 -1
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +145 -8
- package/app/components/05.forms/input-button/tests/InputButton.spec.ts +76 -46
- package/app/components/05.forms/input-button/tests/__snapshots__/InputButton.spec.ts.snap +17 -17
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +9 -1
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +8 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +2 -2
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +14 -2
- package/app/components/05.forms/input-description/InputDescription.vue +2 -1
- package/app/components/05.forms/input-description/stories/InputDescription.stories.ts +70 -4
- package/app/components/05.forms/input-description/tests/InputDescription.spec.ts +2 -1
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +10 -2
- package/app/components/05.forms/input-label/InputLabel.vue +1 -0
- package/app/components/05.forms/input-label/stories/InputLabel.stories.ts +68 -2
- package/app/components/05.forms/input-label/tests/InputLabel.spec.ts +13 -3
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-number/InputNumber.vue +4 -5
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-otp/InputOtp.vue +2 -2
- package/app/components/05.forms/input-range/InputRange.vue +1 -1
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-select/InputSelect.vue +14 -5
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-text/InputText.vue +4 -4
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-textarea/InputTextarea.vue +2 -3
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +4 -4
- package/app/components/05.forms/pending-effect/CONSUMER-STYLING.md +7 -3
- package/app/components/05.forms/pending-effect/PendingEffect.vue +3 -3
- package/app/components/05.forms/pending-effect/tests/PendingEffect.spec.ts +1 -1
- package/app/components/05.forms/segmented-control/CONSUMER-STYLING.md +125 -0
- package/app/components/05.forms/segmented-control/SegmentedControl.vue +231 -0
- package/app/components/05.forms/segmented-control/stories/SegmentedControl.stories.ts +215 -0
- package/app/components/05.forms/segmented-control/tests/SegmentedControl.spec.ts +202 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitch.vue +2 -0
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +0 -31
- package/app/components/01.atoms/alert-mask/AlertMaskCore.vue +0 -166
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +0 -105
- package/app/components/01.atoms/alert-mask/tests/AlertMaskCore.spec.ts +0 -342
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +0 -99
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +0 -251
- package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitch.stories.ts +0 -104
- package/app/components/05.forms/triple-toggle-switch/tests/TripleToggleSwitch.spec.ts +0 -138
- /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
|
|
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="
|
|
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="
|
|
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="
|
|
20
|
-
<span v-if="hasRightSlot()" class="
|
|
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="
|
|
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
|
-
|
|
125
|
+
&[data-link] {
|
|
132
126
|
display: inline-grid;
|
|
133
127
|
}
|
|
134
128
|
|
|
135
|
-
|
|
129
|
+
&[data-pending-effect] {
|
|
136
130
|
overflow: visible;
|
|
137
131
|
}
|
|
138
132
|
|
|
139
|
-
/*
|
|
140
|
-
|
|
141
|
-
|
|
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(--
|
|
171
|
-
outline-color: var(--input-button-primary-ring-hover,
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
--
|
|
192
|
-
--
|
|
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(--
|
|
198
|
-
color: var(--input-button-secondary-text, var(--theme-
|
|
199
|
-
border: var(--button-border-width) solid var(--
|
|
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(--
|
|
204
|
-
border-color: var(--
|
|
205
|
-
outline-color: var(--input-button-secondary-ring-hover,
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
--
|
|
217
|
-
|
|
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(--
|
|
229
|
-
border-color: var(--input-button-tertiary-border-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):
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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,
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
.
|
|
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-
|
|
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: "
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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="
|
|
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 = {
|
|
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
|
+
};
|