srcdev-nuxt-components 9.6.10 → 9.6.12
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 +14 -3
- package/.claude/skills/component-inline-action-button.md +5 -4
- package/.claude/skills/component-naming.md +2 -1
- 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/form-field.md +15 -0
- package/.claude/skills/components/form-fieldset.md +18 -2
- 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/marquee-scroller.md +51 -14
- 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/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-form-field.code-snippets +7 -6
- package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
- 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/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
- 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/form-field/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
- package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
- package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
- package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
- package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
- 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 +8 -0
- 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
|
@@ -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
|
+
};
|
|
@@ -9,7 +9,6 @@ interface InputButtonInstance {
|
|
|
9
9
|
hasLeftSlot: () => boolean;
|
|
10
10
|
hasRightSlot: () => boolean;
|
|
11
11
|
hasIconOnlySlot: () => boolean;
|
|
12
|
-
buttonClasses: () => unknown[];
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
// --- Helpers ---
|
|
@@ -92,12 +91,12 @@ describe("InputButton", () => {
|
|
|
92
91
|
|
|
93
92
|
it("renders button text", async () => {
|
|
94
93
|
wrapper = await createWrapper({ buttonText: "Hello" });
|
|
95
|
-
expect(wrapper.find(".button-text").text()).toBe("Hello");
|
|
94
|
+
expect(wrapper.find(".input-button-text").text()).toBe("Hello");
|
|
96
95
|
});
|
|
97
96
|
|
|
98
97
|
it("handles empty buttonText gracefully", async () => {
|
|
99
98
|
wrapper = await createWrapper({ buttonText: "" });
|
|
100
|
-
const buttonText = wrapper.find(".button-text");
|
|
99
|
+
const buttonText = wrapper.find(".input-button-text");
|
|
101
100
|
expect(buttonText.exists()).toBe(true);
|
|
102
101
|
expect(buttonText.text()).toBe("");
|
|
103
102
|
});
|
|
@@ -107,31 +106,45 @@ describe("InputButton", () => {
|
|
|
107
106
|
// Props
|
|
108
107
|
// -------------------------
|
|
109
108
|
describe("Props", () => {
|
|
110
|
-
it("
|
|
111
|
-
for (const variant of ["primary", "secondary", "tertiary"] as const) {
|
|
109
|
+
it("reflects variant as data-variant", async () => {
|
|
110
|
+
for (const variant of ["primary", "secondary", "tertiary", "inline"] as const) {
|
|
112
111
|
const w = await mountSuspended(InputButton, {
|
|
113
112
|
props: { buttonText: "Test", variant },
|
|
114
113
|
});
|
|
115
|
-
expect(w.find("button").
|
|
114
|
+
expect(w.find("button").attributes("data-variant")).toBe(variant);
|
|
115
|
+
expect(w.find("button").classes()).not.toContain(variant);
|
|
116
116
|
w.unmount();
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it("
|
|
120
|
+
it("sets data-pill when isPill is true", async () => {
|
|
121
121
|
wrapper = await createWrapper({ isPill: true });
|
|
122
|
-
expect(wrapper.find("button").
|
|
122
|
+
expect(wrapper.find("button").attributes("data-pill")).toBe("");
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
it("does not
|
|
125
|
+
it("does not set data-pill by default", async () => {
|
|
126
126
|
wrapper = await createWrapper();
|
|
127
|
-
expect(wrapper.find("button").
|
|
127
|
+
expect(wrapper.find("button").attributes("data-pill")).toBeUndefined();
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
it("
|
|
130
|
+
it("sets data-pending and data-pending-effect", async () => {
|
|
131
131
|
wrapper = await createWrapper({ isPending: true, hasPendingEffect: true });
|
|
132
|
+
const button = wrapper.find("button");
|
|
133
|
+
expect(button.attributes("data-pending")).toBe("");
|
|
134
|
+
expect(button.attributes("data-pending-effect")).toBe("");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not use collision-prone bare state classes", async () => {
|
|
138
|
+
wrapper = await createWrapper(
|
|
139
|
+
{ isPill: true, isPending: true, hasPendingEffect: true, variant: "secondary" },
|
|
140
|
+
{ iconOnly: "<span>⚡</span>" }
|
|
141
|
+
);
|
|
132
142
|
const classes = wrapper.find("button").classes();
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
for (const name of ["secondary", "pill", "is-pending", "pending-effect", "icon-only"]) {
|
|
144
|
+
expect(classes).not.toContain(name);
|
|
145
|
+
}
|
|
146
|
+
expect(wrapper.find(".btn-icon").exists()).toBe(false);
|
|
147
|
+
expect(wrapper.find(".button-text").exists()).toBe(false);
|
|
135
148
|
});
|
|
136
149
|
|
|
137
150
|
it("applies correct type attribute", async () => {
|
|
@@ -168,9 +181,9 @@ describe("InputButton", () => {
|
|
|
168
181
|
// Accessibility
|
|
169
182
|
// -------------------------
|
|
170
183
|
describe("Accessibility", () => {
|
|
171
|
-
it("
|
|
184
|
+
it("omits aria-disabled when not readonly", async () => {
|
|
172
185
|
wrapper = await createWrapper();
|
|
173
|
-
expect(wrapper.find("button").attributes("aria-disabled")).
|
|
186
|
+
expect(wrapper.find("button").attributes("aria-disabled")).toBeUndefined();
|
|
174
187
|
});
|
|
175
188
|
|
|
176
189
|
it("sets aria-disabled and readonly attr when readonly", async () => {
|
|
@@ -182,12 +195,12 @@ describe("InputButton", () => {
|
|
|
182
195
|
|
|
183
196
|
it("keeps button text visible (not sr-only) by default", async () => {
|
|
184
197
|
wrapper = await createWrapper();
|
|
185
|
-
expect(wrapper.find(".button-text").classes()).not.toContain("sr-only");
|
|
198
|
+
expect(wrapper.find(".input-button-text").classes()).not.toContain("sr-only");
|
|
186
199
|
});
|
|
187
200
|
|
|
188
201
|
it("makes button text sr-only with iconOnly slot, but keeps it in DOM for screen readers", async () => {
|
|
189
202
|
wrapper = await createWrapper({ buttonText: "Save document" }, { iconOnly: "<span>💾</span>" });
|
|
190
|
-
const buttonText = wrapper.find(".button-text");
|
|
203
|
+
const buttonText = wrapper.find(".input-button-text");
|
|
191
204
|
expect(buttonText.text()).toBe("Save document");
|
|
192
205
|
expect(buttonText.classes()).toContain("sr-only");
|
|
193
206
|
});
|
|
@@ -199,20 +212,20 @@ describe("InputButton", () => {
|
|
|
199
212
|
describe("Slots", () => {
|
|
200
213
|
it("renders left slot", async () => {
|
|
201
214
|
wrapper = await createWrapper({}, { left: '<span data-testid="left-icon">←</span>' });
|
|
202
|
-
expect(wrapper.find(
|
|
215
|
+
expect(wrapper.find('.input-button-icon[data-slot="left"]').exists()).toBe(true);
|
|
203
216
|
expect(wrapper.find('[data-testid="left-icon"]').exists()).toBe(true);
|
|
204
217
|
});
|
|
205
218
|
|
|
206
219
|
it("renders right slot", async () => {
|
|
207
220
|
wrapper = await createWrapper({}, { right: '<span data-testid="right-icon">→</span>' });
|
|
208
|
-
expect(wrapper.find(
|
|
221
|
+
expect(wrapper.find('.input-button-icon[data-slot="right"]').exists()).toBe(true);
|
|
209
222
|
expect(wrapper.find('[data-testid="right-icon"]').exists()).toBe(true);
|
|
210
223
|
});
|
|
211
224
|
|
|
212
|
-
it("renders iconOnly slot and
|
|
225
|
+
it("renders iconOnly slot and sets data-icon-only on the button", async () => {
|
|
213
226
|
wrapper = await createWrapper({}, { iconOnly: '<span data-testid="icon-only">⚡</span>' });
|
|
214
|
-
expect(wrapper.find(
|
|
215
|
-
expect(wrapper.find("button").
|
|
227
|
+
expect(wrapper.find('.input-button-icon[data-slot="icon-only"]').exists()).toBe(true);
|
|
228
|
+
expect(wrapper.find("button").attributes("data-icon-only")).toBe("");
|
|
216
229
|
});
|
|
217
230
|
|
|
218
231
|
it("iconOnly suppresses left and right slots", async () => {
|
|
@@ -224,9 +237,9 @@ describe("InputButton", () => {
|
|
|
224
237
|
iconOnly: '<span data-testid="icon">⚡</span>',
|
|
225
238
|
}
|
|
226
239
|
);
|
|
227
|
-
expect(wrapper.find(
|
|
228
|
-
expect(wrapper.find(
|
|
229
|
-
expect(wrapper.find(
|
|
240
|
+
expect(wrapper.find('.input-button-icon[data-slot="left"]').exists()).toBe(false);
|
|
241
|
+
expect(wrapper.find('.input-button-icon[data-slot="right"]').exists()).toBe(false);
|
|
242
|
+
expect(wrapper.find('.input-button-icon[data-slot="icon-only"]').exists()).toBe(true);
|
|
230
243
|
});
|
|
231
244
|
|
|
232
245
|
it("renders left and right slots together when no iconOnly", async () => {
|
|
@@ -237,9 +250,9 @@ describe("InputButton", () => {
|
|
|
237
250
|
right: '<span data-testid="right">→</span>',
|
|
238
251
|
}
|
|
239
252
|
);
|
|
240
|
-
expect(wrapper.find(
|
|
241
|
-
expect(wrapper.find(
|
|
242
|
-
expect(wrapper.find(
|
|
253
|
+
expect(wrapper.find('.input-button-icon[data-slot="left"]').exists()).toBe(true);
|
|
254
|
+
expect(wrapper.find('.input-button-icon[data-slot="right"]').exists()).toBe(true);
|
|
255
|
+
expect(wrapper.find('.input-button-icon[data-slot="icon-only"]').exists()).toBe(false);
|
|
243
256
|
});
|
|
244
257
|
});
|
|
245
258
|
|
|
@@ -265,9 +278,9 @@ describe("InputButton", () => {
|
|
|
265
278
|
expect(link.attributes("href")).toBe("/api/auth/github");
|
|
266
279
|
});
|
|
267
280
|
|
|
268
|
-
it("
|
|
281
|
+
it("sets data-link for a link", async () => {
|
|
269
282
|
wrapper = await createWrapper({ href: "/api/auth/github", external: true });
|
|
270
|
-
expect(wrapper.find("a").
|
|
283
|
+
expect(wrapper.find("a").attributes("data-link")).toBe("");
|
|
271
284
|
});
|
|
272
285
|
});
|
|
273
286
|
|
|
@@ -297,8 +310,19 @@ describe("InputButton", () => {
|
|
|
297
310
|
});
|
|
298
311
|
});
|
|
299
312
|
|
|
313
|
+
const looseMounts: { unmount: () => void }[] = [];
|
|
314
|
+
const track = <T extends { unmount: () => void }>(mounted: T) => {
|
|
315
|
+
looseMounts.push(mounted);
|
|
316
|
+
return mounted;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
afterEach(() => {
|
|
320
|
+
looseMounts.splice(0).forEach((mounted) => mounted.unmount());
|
|
321
|
+
vi.restoreAllMocks();
|
|
322
|
+
});
|
|
323
|
+
|
|
300
324
|
describe("InputButton slot toggling", () => {
|
|
301
|
-
it("adds and removes
|
|
325
|
+
it("adds and removes data-icon-only when the iconOnly slot changes after mount", async () => {
|
|
302
326
|
const Host = defineComponent({
|
|
303
327
|
components: { InputButton },
|
|
304
328
|
setup() {
|
|
@@ -310,23 +334,25 @@ describe("InputButton slot toggling", () => {
|
|
|
310
334
|
</InputButton>
|
|
311
335
|
`,
|
|
312
336
|
});
|
|
313
|
-
const wrapper = await mountSuspended(Host);
|
|
337
|
+
const wrapper = track(await mountSuspended(Host));
|
|
314
338
|
const button = () => wrapper.find(".input-button");
|
|
315
|
-
expect(button().
|
|
339
|
+
expect(button().attributes("data-icon-only")).toBeUndefined();
|
|
316
340
|
(wrapper.vm as unknown as { iconOnly: boolean }).iconOnly = true;
|
|
317
341
|
await nextTick();
|
|
318
|
-
expect(button().
|
|
319
|
-
expect(wrapper.find(".button-text").classes()).toContain("sr-only");
|
|
342
|
+
expect(button().attributes("data-icon-only")).toBe("");
|
|
343
|
+
expect(wrapper.find(".input-button-text").classes()).toContain("sr-only");
|
|
320
344
|
});
|
|
321
345
|
});
|
|
322
346
|
|
|
323
347
|
describe("InputButton readonly and reactivity", () => {
|
|
324
348
|
it("does not fire a consumer click handler while readonly (keyboard activation included)", async () => {
|
|
325
349
|
const onClick = vi.fn();
|
|
326
|
-
const wrapper =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
350
|
+
const wrapper = track(
|
|
351
|
+
await mountSuspended(InputButton, {
|
|
352
|
+
props: { buttonText: "Save", readonly: true },
|
|
353
|
+
attrs: { onClick },
|
|
354
|
+
})
|
|
355
|
+
);
|
|
330
356
|
await wrapper.trigger("click");
|
|
331
357
|
expect(onClick).not.toHaveBeenCalled();
|
|
332
358
|
expect(wrapper.attributes("aria-disabled")).toBe("true");
|
|
@@ -334,18 +360,22 @@ describe("InputButton readonly and reactivity", () => {
|
|
|
334
360
|
|
|
335
361
|
it("fires the click handler when not readonly", async () => {
|
|
336
362
|
const onClick = vi.fn();
|
|
337
|
-
const wrapper =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
363
|
+
const wrapper = track(
|
|
364
|
+
await mountSuspended(InputButton, {
|
|
365
|
+
props: { buttonText: "Save" },
|
|
366
|
+
attrs: { onClick },
|
|
367
|
+
})
|
|
368
|
+
);
|
|
341
369
|
await wrapper.trigger("click");
|
|
342
370
|
expect(onClick).toHaveBeenCalledTimes(1);
|
|
343
371
|
});
|
|
344
372
|
|
|
345
373
|
it("updates classes when styleClassPassthrough changes after mount", async () => {
|
|
346
|
-
const wrapper =
|
|
347
|
-
|
|
348
|
-
|
|
374
|
+
const wrapper = track(
|
|
375
|
+
await mountSuspended(InputButton, {
|
|
376
|
+
props: { buttonText: "Save", styleClassPassthrough: ["original"] },
|
|
377
|
+
})
|
|
378
|
+
);
|
|
349
379
|
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
350
380
|
expect(wrapper.classes()).not.toContain("original");
|
|
351
381
|
expect(wrapper.classes()).toContain("updated");
|
|
@@ -1,72 +1,72 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
3
|
exports[`InputButton > Snapshots > icon-only slot 1`] = `
|
|
4
|
-
"<button type="button"
|
|
4
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" data-icon-only="" class="input-button">
|
|
5
5
|
<!--v-if-->
|
|
6
|
-
<!--v-if--><span class="button-text sr-only">Click me</span>
|
|
7
|
-
<!--v-if--><span class="
|
|
6
|
+
<!--v-if--><span class="input-button-text sr-only">Click me</span>
|
|
7
|
+
<!--v-if--><span class="input-button-icon" data-slot="icon-only"><span data-testid="icon-only">⚡</span></span>
|
|
8
8
|
</button>"
|
|
9
9
|
`;
|
|
10
10
|
|
|
11
11
|
exports[`InputButton > Snapshots > pending state 1`] = `
|
|
12
|
-
"<button type="button"
|
|
12
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" data-pending="" data-pending-effect="" class="input-button"><svg class="pending-effect-container" data-theme="default" aria-hidden="true" focusable="false">
|
|
13
13
|
<rect pathLength="100" stroke-linecap="round" class="pending-effect-blur"></rect>
|
|
14
14
|
<rect pathLength="100" stroke-linecap="round" class="pending-effect-line"></rect>
|
|
15
15
|
</svg>
|
|
16
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
16
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
17
17
|
<!--v-if-->
|
|
18
18
|
<!--v-if-->
|
|
19
19
|
</button>"
|
|
20
20
|
`;
|
|
21
21
|
|
|
22
22
|
exports[`InputButton > Snapshots > pill 1`] = `
|
|
23
|
-
"<button type="button"
|
|
23
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" data-pill="" class="input-button">
|
|
24
24
|
<!--v-if-->
|
|
25
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
25
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
26
26
|
<!--v-if-->
|
|
27
27
|
<!--v-if-->
|
|
28
28
|
</button>"
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
31
|
exports[`InputButton > Snapshots > primary (default) 1`] = `
|
|
32
|
-
"<button type="button"
|
|
32
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" class="input-button">
|
|
33
33
|
<!--v-if-->
|
|
34
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
34
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
35
35
|
<!--v-if-->
|
|
36
36
|
<!--v-if-->
|
|
37
37
|
</button>"
|
|
38
38
|
`;
|
|
39
39
|
|
|
40
40
|
exports[`InputButton > Snapshots > readonly 1`] = `
|
|
41
|
-
"<button type="button" readonly="" aria-disabled="true" data-testid="input-button" data-theme="default" class="input-button
|
|
41
|
+
"<button type="button" readonly="" aria-disabled="true" data-testid="input-button" data-theme="default" data-variant="primary" class="input-button">
|
|
42
42
|
<!--v-if-->
|
|
43
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
43
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
44
44
|
<!--v-if-->
|
|
45
45
|
<!--v-if-->
|
|
46
46
|
</button>"
|
|
47
47
|
`;
|
|
48
48
|
|
|
49
49
|
exports[`InputButton > Snapshots > secondary variant 1`] = `
|
|
50
|
-
"<button type="button"
|
|
50
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="secondary" class="input-button">
|
|
51
51
|
<!--v-if-->
|
|
52
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
52
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
53
53
|
<!--v-if-->
|
|
54
54
|
<!--v-if-->
|
|
55
55
|
</button>"
|
|
56
56
|
`;
|
|
57
57
|
|
|
58
58
|
exports[`InputButton > Snapshots > tertiary variant 1`] = `
|
|
59
|
-
"<button type="button"
|
|
59
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="tertiary" class="input-button">
|
|
60
60
|
<!--v-if-->
|
|
61
|
-
<!--v-if--><span class="button-text">Click me</span>
|
|
61
|
+
<!--v-if--><span class="input-button-text">Click me</span>
|
|
62
62
|
<!--v-if-->
|
|
63
63
|
<!--v-if-->
|
|
64
64
|
</button>"
|
|
65
65
|
`;
|
|
66
66
|
|
|
67
67
|
exports[`InputButton > Snapshots > with left and right slots 1`] = `
|
|
68
|
-
"<button type="button"
|
|
69
|
-
<!--v-if--><span class="
|
|
68
|
+
"<button type="button" data-testid="input-button" data-theme="default" data-variant="primary" class="input-button">
|
|
69
|
+
<!--v-if--><span class="input-button-icon" data-slot="left"><span data-testid="left-icon">←</span></span><span class="input-button-text">Click me</span><span class="input-button-icon" data-slot="right"><span data-testid="right-icon">→</span></span>
|
|
70
70
|
<!--v-if-->
|
|
71
71
|
</button>"
|
|
72
72
|
`;
|
|
@@ -25,6 +25,8 @@ declared in `04.elements/forms/03.generic-input-geometry.css`; see `theming-form
|
|
|
25
25
|
|---|---|---|
|
|
26
26
|
| `--input-checkbox-surface` | `var(--theme-checkbox-symbol-surface)` | Background |
|
|
27
27
|
| `--input-checkbox-border` | `var(--theme-border)` | Border colour |
|
|
28
|
+
| `--input-checkbox-border-hover` | the resolved border | 1px outline colour on hover |
|
|
29
|
+
| `--input-checkbox-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border (not inside the button style). Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
28
30
|
| `--input-checkbox-border-focus` | `var(--theme-border-focus)` | Outline colour on `:focus-visible` (not in button mode, the button draws its own) |
|
|
29
31
|
| `--input-checkbox-icon-color` | `var(--theme-text)` | Checked icon colour |
|
|
30
32
|
| `--input-checkbox-transition-duration` | `var(--theme-form-transition-duration)` (`0.2s`) | Border/background and icon fade transitions, all three components |
|
|
@@ -50,7 +52,8 @@ Also reads `--input-checked-icon-gap` (control to label gap) and `--input-min-he
|
|
|
50
52
|
| `--input-checkbox-button-surface` | `var(--theme-input-surface)` | Background (resting) |
|
|
51
53
|
| `--input-checkbox-button-border` | `var(--theme-border)` | Border colour |
|
|
52
54
|
| `--input-checkbox-button-surface-hover` | `var(--theme-input-surface-hover)` | Background on hover |
|
|
53
|
-
| `--input-checkbox-button-ring-hover` | `var(--
|
|
55
|
+
| `--input-checkbox-button-ring-hover` | `var(--input-checkbox-button-border)` | 1px outline colour on hover, no offset |
|
|
56
|
+
| `--input-checkbox-button-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Resting shadow ring outside the border. Width is the global `--form-element-shadow-width` (`0.2rem`; `0` removes it) |
|
|
54
57
|
| `--input-checkbox-button-surface-focus` | `var(--theme-surface-subtle)` | Background when the inner control has `:focus-visible` |
|
|
55
58
|
| `--input-checkbox-button-ring-focus` | `var(--theme-border-focus)` | Outline colour when the inner control has `:focus-visible` |
|
|
56
59
|
| `--input-checkbox-button-label-color` | `var(--theme-text)` | Option label colour |
|
|
@@ -90,6 +93,11 @@ transitions) and `--_icon-size` (the button shrinks the inner control's checked
|
|
|
90
93
|
|
|
91
94
|
---
|
|
92
95
|
|
|
96
|
+
> **Changed 2026-10-17**: bordered controls carry a light shadow ring (`box-shadow`, theme step 2,
|
|
97
|
+
> so red when invalid), and hover draws a 1px outline in the border colour with no offset, over the
|
|
98
|
+
> ring's inner edge. Hover used to draw a 2px outline at a 2px offset in the focus colour. Focus is
|
|
99
|
+
> unchanged and sits just outside the ring.
|
|
100
|
+
|
|
93
101
|
## State hooks
|
|
94
102
|
|
|
95
103
|
| Attribute | On | When |
|
|
@@ -112,6 +112,14 @@ watch(
|
|
|
112
112
|
border-radius: 100vw;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
&:not([data-button]) {
|
|
116
|
+
box-shadow: 0 0 0 var(--form-element-shadow-width) var(--input-checkbox-shadow, var(--form-element-shadow-colour, var(--colour-theme-2)));
|
|
117
|
+
|
|
118
|
+
&:has(input:hover) {
|
|
119
|
+
outline-color: var(--input-checkbox-border-hover, var(--input-checkbox-border, var(--theme-border)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
&:not([data-button]):has(input:focus-visible) {
|
|
116
124
|
outline: var(--form-element-outline-width-focus) solid var(--input-checkbox-border-focus, var(--theme-border-focus));
|
|
117
125
|
outline-offset: var(--form-element-outline-offset-focus);
|