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
|
@@ -7,14 +7,21 @@ Every colour token below is `var(--input-button-{variant}-{property}, {default})
|
|
|
7
7
|
the shared `--theme-*` slot every other themed component (inputs, alerts, ...) also falls back to.
|
|
8
8
|
See `theming-component-token-pattern.md` for why this two-tier shape exists.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`--button-focus-ring-width`/`-offset`,
|
|
10
|
+
The variants follow the usual hierarchy: **primary** is filled, **secondary** is outlined
|
|
11
|
+
(transparent background, border and text in the theme colour), **tertiary** is text-only
|
|
12
|
+
(no background or border until hover), and **inline** has no colours at all, for wrapper
|
|
13
|
+
components that style the button themselves. Primary and secondary also carry a light **shadow ring** (`box-shadow`, 2px of theme step 2)
|
|
14
|
+
outside the border, and on **hover** their 1px outline (no offset) takes the hover border colour,
|
|
15
|
+
so it draws over the inner edge of that ring. Tertiary has neither. The `outline` is transparent
|
|
16
|
+
at rest. **`:focus-visible` is a separate token set**
|
|
17
|
+
(its `outline-width`/`-offset` come from `--button-focus-ring-width`/`-offset`, see
|
|
18
|
+
`theming-form-geometry-tokens.md`).
|
|
19
|
+
|
|
20
|
+
> **Changed 2026-10-17**: secondary was filled with `--theme-surface-inverted`, which read as a
|
|
21
|
+
> darker primary; tertiary was a grey `--slate-01` fill with an underlined label. To bring back
|
|
22
|
+
> the old look for one site, set the secondary or tertiary tokens below (e.g.
|
|
23
|
+
> `--input-button-secondary-surface: var(--theme-surface-inverted)` and
|
|
24
|
+
> `--input-button-secondary-text: var(--theme-text-inverted)`).
|
|
18
25
|
|
|
19
26
|
Geometry and typography (`--button-*`) are separate, already-global tokens with real defaults —
|
|
20
27
|
see `theming-form-geometry-tokens.md`. Not duplicated here. One geometry token worth calling out:
|
|
@@ -32,60 +39,78 @@ the `InlineWithTextInput` story). `box-sizing: content-box` means it's a floor,
|
|
|
32
39
|
| `--input-button-primary-ring` | `transparent` | Outline colour (resting) |
|
|
33
40
|
| `--input-button-primary-surface-hover` | `color-mix(in oklab, surface 85%, black)` | Background on hover |
|
|
34
41
|
| `--input-button-primary-border-hover` | the resolved hover surface above | Border colour on hover |
|
|
35
|
-
| `--input-button-primary-ring-hover` |
|
|
42
|
+
| `--input-button-primary-ring-hover` | the resolved hover border above | Outline colour on hover (1px, no offset) |
|
|
43
|
+
| `--input-button-primary-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Shadow ring colour (all states) |
|
|
36
44
|
| `--input-button-primary-border-focus` | `var(--theme-border-focus)` | Border colour on `:focus-visible` |
|
|
37
45
|
| `--input-button-primary-ring-focus` | `var(--theme-ring)` | Outline colour on `:focus-visible` |
|
|
38
46
|
|
|
39
|
-
### Secondary
|
|
47
|
+
### Secondary (outlined)
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
| Token | Falls back to | Controls |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `--input-button-secondary-surface` | `transparent` | Background (resting) |
|
|
52
|
+
| `--input-button-secondary-text` | `var(--theme-surface)` | Text colour (all states) |
|
|
53
|
+
| `--input-button-secondary-border` | `var(--theme-surface)` | Border colour (resting) |
|
|
54
|
+
| `--input-button-secondary-ring` | `transparent` | Outline colour (resting) |
|
|
55
|
+
| `--input-button-secondary-ring-hover` | the resolved hover border | Outline colour on hover (1px, no offset) |
|
|
56
|
+
| `--input-button-secondary-shadow` | `var(--form-element-shadow-colour, var(--colour-theme-2))` | Shadow ring colour (all states) |
|
|
57
|
+
| `--input-button-secondary-surface-hover` | `var(--theme-surface-subtle)` | Background on hover |
|
|
58
|
+
| `--input-button-secondary-border-hover` | the resolved border above | Border colour on hover |
|
|
59
|
+
| `--input-button-secondary-border-focus` | `var(--theme-border-focus)` | Border colour on `:focus-visible` |
|
|
60
|
+
| `--input-button-secondary-ring-focus` | `var(--theme-ring)` | Outline colour on `:focus-visible` |
|
|
61
|
+
|
|
62
|
+
### Tertiary (text-only)
|
|
42
63
|
|
|
43
|
-
| Token | Falls back to |
|
|
44
|
-
|
|
45
|
-
| `--input-button-
|
|
46
|
-
| `--input-button-
|
|
47
|
-
| `--input-button-
|
|
48
|
-
| `--input-button-
|
|
49
|
-
| `--input-button-
|
|
50
|
-
| `--input-button-
|
|
51
|
-
| `--input-button-
|
|
52
|
-
| `--input-button-
|
|
64
|
+
| Token | Falls back to | Controls |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `--input-button-tertiary-surface` | `transparent` | Background (resting) |
|
|
67
|
+
| `--input-button-tertiary-text` | `var(--theme-surface)` | Text colour (all states) |
|
|
68
|
+
| `--input-button-tertiary-border` | `transparent` | Border colour (resting) |
|
|
69
|
+
| `--input-button-tertiary-ring` / `-ring-hover` | `transparent` | Outline colour |
|
|
70
|
+
| `--input-button-tertiary-surface-hover` | `var(--theme-surface-subtle)` | Background on hover |
|
|
71
|
+
| `--input-button-tertiary-border-hover` | `transparent` | Border colour on hover |
|
|
72
|
+
| `--input-button-tertiary-border-focus` | `var(--theme-border-focus)` | Border colour on `:focus-visible` |
|
|
73
|
+
| `--input-button-tertiary-ring-focus` | `var(--theme-ring)` | Outline colour on `:focus-visible` |
|
|
74
|
+
|
|
75
|
+
| Token | Default | Controls |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `--input-button-shadow-width` | `var(--form-element-shadow-width)` (`0.2rem`) | Width of the primary and secondary shadow ring. Set `0` to remove it from buttons only; set the global token to `0` to remove it from every form control |
|
|
53
78
|
|
|
54
|
-
|
|
79
|
+
The focus ring's offset (`--button-focus-ring-offset`, `0.2rem`) puts it just outside the shadow
|
|
80
|
+
ring; if you widen the ring, raise the offset to match or the focus ring will draw over it. An
|
|
81
|
+
ancestor with `overflow: hidden` and no spare room clips the ring.
|
|
55
82
|
|
|
56
|
-
|
|
83
|
+
Secondary and tertiary text sits on the page background, so on a dark section set their `-text`
|
|
84
|
+
and `-border` tokens to a light colour.
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
| `--input-button-tertiary-text` | `var(--theme-text)` |
|
|
62
|
-
| `--input-button-tertiary-border` | the resolved surface above (flat) |
|
|
63
|
-
| `--input-button-tertiary-ring` / `-ring-hover` | `transparent` |
|
|
64
|
-
| `--input-button-tertiary-surface-hover` | `color-mix(in oklab, surface 85%, black)` |
|
|
65
|
-
| `--input-button-tertiary-border-hover` | the resolved hover surface above |
|
|
66
|
-
| `--input-button-tertiary-border-focus` | `var(--theme-border-focus)` |
|
|
67
|
-
| `--input-button-tertiary-ring-focus` | `var(--theme-ring)` |
|
|
68
|
-
|
|
69
|
-
`.is-pending` always dims the *primary* surface regardless of variant — a pre-existing quirk, not
|
|
70
|
-
something this token set changes. The animated border added by `has-pending-effect` has its own
|
|
71
|
-
`--pending-effect-*` tokens, see `../pending-effect/CONSUMER-STYLING.md`.
|
|
86
|
+
`[data-pending]` dims a primary button's surface to 50%; secondary and tertiary keep their own
|
|
87
|
+
background. The animated border added by `has-pending-effect` has its own `--pending-effect-*`
|
|
88
|
+
tokens, see `../pending-effect/CONSUMER-STYLING.md`.
|
|
72
89
|
|
|
73
90
|
### Text wrapping
|
|
74
91
|
|
|
75
92
|
| Token | Default | Controls |
|
|
76
93
|
|---|---|---|
|
|
77
|
-
| `--input-button-text-white-space` | `
|
|
94
|
+
| `--input-button-text-white-space` | `normal` | Long labels wrap onto several lines inside the button, and unbroken strings break anywhere. Set `nowrap` to keep a label on one line; the button then grows to fit it and can overflow a narrow container |
|
|
78
95
|
|
|
79
|
-
> Added 2026-10-06.
|
|
96
|
+
> Added 2026-10-06. **Changed 2026-10-17**: the default was `nowrap`, which pushed a long label
|
|
97
|
+
> past a narrow column. If you relied on one-line labels in a tight row, set
|
|
98
|
+
> `--input-button-text-white-space: nowrap` on that row.
|
|
99
|
+
|
|
100
|
+
Button text has no line-clamp token on purpose: a cut-off action label can't be trusted.
|
|
101
|
+
|
|
102
|
+
Private tokens (not public API): `--_surface` and `--_surface-hover` (primary's resolved
|
|
103
|
+
surfaces, which its border, hover and contrast colours derive from) and `--_primary-border-hover`, `--_secondary-border` and `--_secondary-border-hover`.
|
|
80
104
|
|
|
81
105
|
`InputCopy` and `PricingCard` fall back through `--input-button-primary-*` for their own copy/CTA
|
|
82
106
|
buttons (see their own `CONSUMER-STYLING.md`) rather than duplicating this chain.
|
|
83
107
|
|
|
84
108
|
> **Trial, 2026-09-27: automatic text contrast.** In browsers that support CSS `contrast-color()`
|
|
85
|
-
> (Safari/iOS 26, Firefox 146, Chrome/Edge 147), an unset `--input-button-
|
|
109
|
+
> (Safari/iOS 26, Firefox 146, Chrome/Edge 147), an unset `--input-button-primary-text` resolves
|
|
86
110
|
> to `contrast-color()` of that state's own surface: pure black or pure white, whichever has more
|
|
87
111
|
> contrast, re-evaluated on hover. Older browsers keep the defaults in the tables above. Setting
|
|
88
|
-
> the `-text` token explicitly always wins, so you can opt
|
|
112
|
+
> the `-text` token explicitly always wins, so you can opt out. Secondary and tertiary aren't part of the trial since
|
|
113
|
+
> 2026-10-17: their surface is transparent. The whole behaviour is
|
|
89
114
|
> one `@supports` block in `InputButton.vue` and may be removed if the trial doesn't hold up.
|
|
90
115
|
|
|
91
116
|
---
|
|
@@ -101,15 +126,23 @@ buttons (see their own `CONSUMER-STYLING.md`) rather than duplicating this chain
|
|
|
101
126
|
|
|
102
127
|
| Hook | When |
|
|
103
128
|
|---|---|
|
|
104
|
-
|
|
|
129
|
+
| `[data-variant="primary" \| "secondary" \| "tertiary" \| "inline"]` | The `variant` prop |
|
|
105
130
|
| `[data-theme]` | The `theme` prop (re-declares the `--theme-*` palette on the button) |
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
131
|
+
| `[data-icon-only]` | The `#iconOnly` slot is used (square, text becomes `sr-only`) |
|
|
132
|
+
| `[data-pill]` | `is-pill` |
|
|
133
|
+
| `[data-pending]`, `[data-pending-effect]` | `is-pending` / `has-pending-effect` |
|
|
134
|
+
| `[data-link]` | `href` is set (renders `NuxtLink` or `<a>`) |
|
|
110
135
|
| `[readonly]` + `aria-disabled="true"` | `readonly` (dimmed, not operable by mouse or keyboard) |
|
|
111
136
|
|
|
112
|
-
Inner
|
|
137
|
+
Inner elements: `.input-button-text`, and `.input-button-icon` with `data-slot="left"`,
|
|
138
|
+
`"right"` or `"icon-only"`.
|
|
139
|
+
|
|
140
|
+
> **Changed 2026-10-17**: these were bare classes, which collided with consumer CSS. Update
|
|
141
|
+
> selectors: `.input-button.primary` → `.input-button[data-variant="primary"]`, `.pill` →
|
|
142
|
+
> `[data-pill]`, `.is-pending` → `[data-pending]`, `.pending-effect` → `[data-pending-effect]`,
|
|
143
|
+
> `.is-link` → `[data-link]`, `.icon-only` → `[data-icon-only]`, `.button-text` →
|
|
144
|
+
> `.input-button-text`, `.btn-icon.left` → `.input-button-icon[data-slot="left"]`. Same
|
|
145
|
+
> specificity, so overrides keep winning where they did.
|
|
113
146
|
|
|
114
147
|
|
|
115
148
|
## Global theming
|
|
@@ -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 ---
|