srcdev-nuxt-components 9.6.3 → 9.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.claude/rules/consumer/vue-no-self-closing.md +3 -2
  2. package/.claude/skills/component-compliance-checklist.md +17 -3
  3. package/.claude/skills/component-inline-action-button.md +33 -2
  4. package/.claude/skills/components/cookie-consent-banner.md +9 -6
  5. package/.claude/skills/components/input-copy.md +47 -100
  6. package/.claude/skills/components/progress-tracker.md +140 -0
  7. package/.claude/skills/components/stepper-list.md +10 -3
  8. package/.claude/skills/components/tabbed-content.md +4 -0
  9. package/.claude/skills/composable-cookie-consent.md +1 -1
  10. package/.claude/skills/index.md +4 -3
  11. package/.claude/skills/storybook-add-story.md +14 -0
  12. package/.claude/skills/testing-add-unit-test.md +19 -0
  13. package/.claude/skills/theming-component-token-pattern.md +1 -1
  14. package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
  15. package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
  16. package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
  17. package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
  18. package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
  19. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +5 -1
  20. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +2 -2
  21. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +26 -0
  22. package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap +28 -28
  23. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
  24. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
  25. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +106 -5
  26. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
  27. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
  28. package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
  29. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -47
  30. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
  31. package/app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md +116 -0
  32. package/app/components/02.molecules/progress-tracker/ProgressTracker.vue +275 -0
  33. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +424 -0
  34. package/app/components/02.molecules/progress-tracker/tests/ProgressTracker.spec.ts +253 -0
  35. package/app/components/02.molecules/progress-tracker/tests/__snapshots__/ProgressTracker.spec.ts.snap +46 -0
  36. package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +42 -7
  37. package/app/components/02.molecules/stepper-list/StepperList.vue +42 -19
  38. package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +288 -2
  39. package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +161 -34
  40. package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +40 -40
  41. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
  42. package/app/types/components/cookie-consent-banner.d.ts +2 -0
  43. package/app/types/components/index.ts +1 -0
  44. package/app/types/components/progress-tracker.d.ts +6 -0
  45. package/package.json +1 -1
@@ -242,6 +242,12 @@ export default meta;
242
242
  type Story = StoryObj<typeof ComponentName>; // ← still strict for individual stories
243
243
  ```
244
244
 
245
+ When the story assigns a model arg itself (a button doing `args.modelValue = index + 1`, a wizard's
246
+ Back/Continue), a type built from `InstanceType<typeof C>["$props"]` fails `vue-tsc` with "Cannot
247
+ assign to 'modelValue' because it is a read-only property". Re-declare it writable:
248
+ `Omit<InstanceType<typeof C>["$props"], "modelValue"> & { modelValue: number; ... }`
249
+ (`ProgressTracker.stories.ts`).
250
+
245
251
  Strip extra args before `v-bind` using a `useStorySetup` helper in `setup()`:
246
252
 
247
253
  ```ts
@@ -307,6 +313,14 @@ the earlier nightly `@nuxtjs/storybook` didn't render Docs pages at all, so a lo
307
313
  },
308
314
  ```
309
315
 
316
+ - A fixed or teleported overlay (banner, toast, dialog) can't be narrowed by `CanvasSwitcher`: it
317
+ positions against the viewport and may not even live inside the decorator. Its `StressTest`
318
+ (compliance checklist item 6b) renders **one** instance, switches between hostile data sets with
319
+ a story-only select (`stressCase`, category `"Story only"`, description starting
320
+ `**Story control, not a prop.**`), tells the reader to use Storybook's viewport toolbar for narrow
321
+ screens, and sets `docs.story: { inline: false }`. `CookieConsentBanner` and `DisplayToast`
322
+ follow this. A teleported component doesn't inherit a wrapper's `dir`, so wrap RTL slot copy
323
+ in `<span dir="rtl">`.
310
324
  - Opt a file out with `tags: ["!autodocs"]` only when it isn't a component reference (tooling, or
311
325
  a file with its own attached `.mdx` page, like `ColourRamps.stories.ts` with `SetupGuide.mdx`).
312
326
 
@@ -201,6 +201,25 @@ await nextTick();
201
201
  vi.runAllTimers();
202
202
  ```
203
203
 
204
+ ### Native clicks are silently dropped under frozen time
205
+
206
+ Vue's event invoker ignores an event whose timestamp is at or before the moment its listener was
207
+ attached (a guard against an event that triggered the patch also firing the new listener). With
208
+ the clock frozen, a native `button.click()` or `dispatchEvent()` straight after mount carries the
209
+ same timestamp, so the handler never runs and the test fails with "called 0 times". Whether it
210
+ bites depends on render timing: `CookieConsentBanner`'s click tests passed with raw `<button>`s
211
+ and failed once they became `InputButton`s (2026-10-15). Either use test-utils'
212
+ `wrapper.trigger("click")` (which works, including on teleported content via `findComponent`), or
213
+ advance the clock first:
214
+
215
+ ```ts
216
+ // Vue ignores events stamped at or before its listener's attach time; fake timers freeze both.
217
+ function click(button: HTMLElement) {
218
+ vi.advanceTimersByTime(1);
219
+ button.click();
220
+ }
221
+ ```
222
+
204
223
  ## `mountSuspended` flushes component async setup — initial state may be past the first `await`
205
224
 
206
225
  `mountSuspended` calls `flushPromises()` internally, draining all pending microtasks including `await nextTick()` calls inside the component's async functions. If a component's async lifecycle starts with `await nextTick()` (e.g. to let a CSS `v-bind` update before beginning a transition), **the component will have already executed past it by the time `mountSuspended` returns**.
@@ -184,7 +184,7 @@ raw value) chains through it:
184
184
 
185
185
  ```css
186
186
  /* InputCopy's copy button defaults to InputButton's primary look */
187
- --_button-bg: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
187
+ background-color: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
188
188
  ```
189
189
 
190
190
  Three tiers deep here: instance/global override → "look like the primary button" → the theme
@@ -10,11 +10,13 @@
10
10
  "description": "CookieConsentBanner with translated copy and accessible name",
11
11
  "scope": "vue,html",
12
12
  "body": [
13
- "<CookieConsentBanner :aria-label=\"${1:t('global.cookieConsent.ariaLabel')}\">",
14
- " <template #title>{{ ${2:t('global.cookieConsent.title')} }}</template>",
15
- " <template #message>{{ ${3:t('global.cookieConsent.message')} }}</template>",
16
- " <template #acceptLabel>{{ ${4:t('global.cookieConsent.accept')} }}</template>",
17
- " <template #rejectLabel>{{ ${5:t('global.cookieConsent.reject')} }}</template>",
13
+ "<CookieConsentBanner",
14
+ " :aria-label=\"${1:t('global.cookieConsent.ariaLabel')}\"",
15
+ " :accept-label=\"${2:t('global.cookieConsent.accept')}\"",
16
+ " :reject-label=\"${3:t('global.cookieConsent.reject')}\"",
17
+ ">",
18
+ " <template #title>{{ ${4:t('global.cookieConsent.title')} }}</template>",
19
+ " <template #message>{{ ${5:t('global.cookieConsent.message')} }}</template>",
18
20
  "</CookieConsentBanner>"
19
21
  ]
20
22
  },
@@ -1,28 +1,27 @@
1
1
  {
2
2
  "SRCDEV InputCopy Basic": {
3
- "description": "Basic InputCopy component with value and label",
3
+ "description": "InputCopy: readonly input with a copy-to-clipboard button",
4
4
  "scope": "vue,html",
5
5
  "body": [
6
6
  "<InputCopy",
7
- " value=\"$1sk_live_abc123xyz\"",
8
- " label=\"$2API key\"",
9
- " description=\"$3Copy this key to authenticate with our API.\"",
10
- " @copy=\"$4handleCopy\"",
7
+ " :value=\"${1:licenseKey}\"",
8
+ " label=\"${2:License key}\"",
9
+ " description=\"${3:Copy this key to activate the component in your application.}\"",
10
+ " @copied=\"${4:handleCopied}\"",
11
11
  "/>"
12
12
  ]
13
13
  },
14
- "SRCDEV InputCopy License Key": {
15
- "description": "InputCopy for displaying a license key on success page",
14
+ "SRCDEV InputCopy Translated": {
15
+ "description": "InputCopy with translated copy and accessible names",
16
16
  "scope": "vue,html",
17
17
  "body": [
18
18
  "<InputCopy",
19
- " :value=\"licenseKey\"",
20
- " label=\"License key\"",
21
- " description=\"Your license key is ready to use. Copy it to embed in your application.\"",
22
- " buttonText=\"Copy\"",
23
- " copiedText=\"Copied to clipboard!\"",
24
- " @copy=\"$1handleCopy\"",
25
- " @copied=\"$2showCopiedFeedback\"",
19
+ " :value=\"${1:value}\"",
20
+ " :label=\"${2:t('account.key.label')}\"",
21
+ " :description=\"${3:t('account.key.description')}\"",
22
+ " :button-text=\"${4:t('common.copy')}\"",
23
+ " :copied-text=\"${5:t('common.copied')}\"",
24
+ " :button-aria-label=\"${6:t('account.key.copyAriaLabel')}\"",
26
25
  "/>"
27
26
  ]
28
27
  },
@@ -31,25 +30,10 @@
31
30
  "scope": "vue,html",
32
31
  "body": [
33
32
  "<InputCopy",
34
- " :value=\"$1value\"",
35
- " label=\"$2Label\"",
36
- " :is-disabled=\"true\"",
37
- " :button-text=\"$3Unavailable\"",
38
- "/>"
39
- ]
40
- },
41
- "SRCDEV InputCopy in Page": {
42
- "description": "InputCopy with custom aria labels",
43
- "scope": "vue,html",
44
- "body": [
45
- "<InputCopy",
46
- " :value=\"$1value\"",
47
- " label=\"$2License key\"",
48
- " aria-label=\"$3Your unique GuideMyHair license identifier\"",
49
- " description=\"$4Keep this key safe. You'll need it to embed the widget.\"",
50
- " button-text=\"$5Copy key\"",
51
- " copied-text=\"$6Key copied!\"",
52
- " @copy=\"$7logCopy\"",
33
+ " :value=\"${1:value}\"",
34
+ " label=\"${2:Label}\"",
35
+ " :is-disabled=\"${3:true}\"",
36
+ " button-text=\"${4:Unavailable}\"",
53
37
  "/>"
54
38
  ]
55
39
  },
@@ -57,15 +41,13 @@
57
41
  "description": "CSS override scaffold for InputCopy tokens",
58
42
  "scope": "css",
59
43
  "body": [
60
- ".$1success-page {",
61
- " .input-copy {",
62
- " --input-copy-input-bg: $2;",
63
- " --input-copy-input-border: $3;",
64
- " --input-copy-input-border-radius: $4;",
65
- " --input-copy-button-bg: $5;",
66
- " --input-copy-button-bg-hover: $6;",
67
- " --input-copy-button-text-color: $7;",
68
- " }",
44
+ ".${1:account-page} .input-copy {",
45
+ " --input-copy-input-bg: ${2:var(--theme-input-surface)};",
46
+ " --input-copy-input-border: ${3:0.1rem solid var(--theme-border)};",
47
+ " --input-copy-button-bg: ${4:var(--theme-surface)};",
48
+ " --input-copy-button-bg-hover: ${5:var(--theme-surface-hover)};",
49
+ " --input-copy-button-text-color: ${6:var(--theme-on-surface)};",
50
+ " --input-copy-description-line-clamp: ${7:none};",
69
51
  "}"
70
52
  ]
71
53
  }
@@ -0,0 +1,45 @@
1
+ {
2
+ "SRCDEV ProgressTracker": {
3
+ "description": "ProgressTracker showing progress through steps (v-model is the 0-based current step)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ProgressTracker v-model=\"${1:currentStep}\" :steps=\"${2:steps}\" />"
7
+ ]
8
+ },
9
+ "SRCDEV ProgressTracker Wizard": {
10
+ "description": "ProgressTracker as a wizard: each step's form lives in its #step-{n} slot and drives the model",
11
+ "scope": "vue,html",
12
+ "body": [
13
+ "<ProgressTracker v-model=\"${1:currentStep}\" :steps=\"${2:steps}\">",
14
+ " <template #step-0=\"{ headingId }\">",
15
+ " <form :aria-labelledby=\"headingId\" @submit.prevent=\"$1++\">",
16
+ " $3",
17
+ " <InputButton type=\"submit\" button-text=\"Continue\" />",
18
+ " </form>",
19
+ " </template>",
20
+ " <template #step-1=\"{ headingId }\">",
21
+ " <form :aria-labelledby=\"headingId\" @submit.prevent=\"$1 = $2.length\">",
22
+ " $4",
23
+ " <InputButton button-text=\"Back\" variant=\"secondary\" @click=\"$1--\" />",
24
+ " <InputButton type=\"submit\" button-text=\"Submit\" />",
25
+ " </form>",
26
+ " </template>",
27
+ "</ProgressTracker>"
28
+ ]
29
+ },
30
+ "SRCDEV ProgressTracker CSS Override": {
31
+ "description": "CSS override scaffold for ProgressTracker tokens",
32
+ "scope": "css",
33
+ "body": [
34
+ ".$1my-page {",
35
+ " --progress-tracker-accent: $2;",
36
+ " --progress-tracker-track: $3;",
37
+ " --progress-tracker-card-surface: $4;",
38
+ " --progress-tracker-card-shadow: $5;",
39
+ " --progress-tracker-label-font-family: $6;",
40
+ " --progress-tracker-title-line-clamp: $7;",
41
+ " --progress-tracker-body-line-clamp: $8;",
42
+ "}"
43
+ ]
44
+ }
45
+ }
@@ -31,8 +31,9 @@
31
31
  ".$1my-page {",
32
32
  " --stepper-list-gap: $2;",
33
33
  " --stepper-list-connector-color: $3;",
34
- " --stepper-list-counter-disc-background: $4;",
35
- " --stepper-list-counter-disc-text: $5;",
34
+ " --stepper-list-connector-gap: $4;",
35
+ " --stepper-list-counter-disc-background: $5;",
36
+ " --stepper-list-counter-disc-text: $6;",
36
37
  "}"
37
38
  ]
38
39
  }
@@ -58,6 +58,7 @@
58
58
  " --tabs-underline-indicator-height: ;",
59
59
  " --tabs-hover-underline-indicator-colour: ;",
60
60
  " --tabs-hover-underline-indicator-height: ;",
61
+ " --tabs-hover-indicator-stretch: ;",
61
62
  " --tabs-list-item-opacity: ;",
62
63
  " --tabs-list-item-colour: ;",
63
64
  " --tabs-list-item-colour-selected: ;",
@@ -58,6 +58,7 @@ box.
58
58
  | `--tabs-underline-indicator-height` | `0.4rem` | Thickness of the underline (`axis="x"`), also its width on `axis="y"` |
59
59
  | `--tabs-hover-underline-indicator-colour` | `--tabs-underline-indicator-colour` | Colour of the thin underline/sideline that follows the hovered tab (`trackHover` and `trackIndicator`) |
60
60
  | `--tabs-hover-underline-indicator-height` | `0.1rem` | Its thickness; `0` hides it. Sits under the active underline where they overlap |
61
+ | `--tabs-hover-indicator-stretch` | `1` | Stretch of the hover highlight and hover underline as they move: `1` stretches (leading edge first, trailing edge catches up), `0` slides both edges together. Values in between shorten the stretch |
61
62
 
62
63
  ### More menu (`overflowMode="menu"`, `axis="x"`)
63
64
 
@@ -98,7 +99,10 @@ measured layout, per indicator (`active` / `hovered`): `--_{kind}-start` / `--_{
98
99
  insets from the bar's start and end along the axis), `--_{kind}-duration`,
99
100
  `--_{kind}-start-delay` / `--_{kind}-end-delay`; each indicator reads them through its own
100
101
  `--_start` / `--_end` / `--_duration` / `--_start-delay` / `--_end-delay`. Not public API; use the
101
- `transitionDuration` prop for indicator speed (one edge moves for that long, then the other).
102
+ `transitionDuration` prop for indicator speed (one edge moves for that long, then the other), and
103
+ `--tabs-hover-indicator-stretch` to turn the hover stretch off. The More menu also uses
104
+ `--_anchor-name` (a per-instance anchor name) and the `--_anchor-*` position values
105
+ `useAnchoredPopover` writes for browsers without CSS anchor positioning.
102
106
 
103
107
  > **Changed 2026-10-08**: indicators were a bar-wide box moved with `translate` and sized with
104
108
  > `scale` (`--_x-*`, `--_width-*`, `--_y-*`, a shared `--_transition-duration`), with a timer
@@ -354,8 +354,8 @@ watch(
354
354
  --_start: var(--_hovered-start, 0px);
355
355
  --_end: var(--_hovered-end, 100%);
356
356
  --_duration: var(--_hovered-duration, 0ms);
357
- --_start-delay: var(--_hovered-start-delay, 0ms);
358
- --_end-delay: var(--_hovered-end-delay, 0ms);
357
+ --_start-delay: calc(var(--_hovered-start-delay, 0ms) * var(--tabs-hover-indicator-stretch, 1));
358
+ --_end-delay: calc(var(--_hovered-end-delay, 0ms) * var(--tabs-hover-indicator-stretch, 1));
359
359
  }
360
360
 
361
361
  .tabbed-content-indicator-active,
@@ -219,6 +219,32 @@ export const IndicatorColours: Story = {
219
219
  }),
220
220
  };
221
221
 
222
+ export const HoverWithoutStretch: Story = {
223
+ name: "Hover Without Stretch",
224
+ args: { itemCount: 5 },
225
+ parameters: {
226
+ docs: {
227
+ description: {
228
+ story:
229
+ "--tabs-hover-indicator-stretch: 0 makes the hover highlight slide between tabs instead of stretching across and catching up. Clicking a tab still stretches the active indicator.",
230
+ },
231
+ },
232
+ },
233
+ render: (args) => ({
234
+ components: { TabbedContent },
235
+ setup() {
236
+ return { args };
237
+ },
238
+ template: `
239
+ <div style="--tabs-hover-indicator-stretch: 0">
240
+ <TabbedContent v-bind="args">
241
+ ${tabSlots}
242
+ </TabbedContent>
243
+ </div>
244
+ `,
245
+ }),
246
+ };
247
+
222
248
  // ─── Stress test ──────────────────────────────────────────────────────────────
223
249
 
224
250
  const stressTabs = [
@@ -4,22 +4,22 @@ exports[`ContactSection > renders correct HTML structure with default props 1`]
4
4
  "<div class="contact-section">
5
5
  <div class="contact-section-inner">
6
6
  <div class="contact-section-info">
7
- <ul class="stepper-list">
8
- <li class="has-indicator indicator-top indicator-disc">
7
+ <ul role="list" class="stepper-list">
8
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
9
9
  <div class="stepper-list__indicator-custom"></div>
10
- <div>
10
+ <div class="stepper-list-content">
11
11
  <p class="page-body-normal">Default slot content for item 1</p>
12
12
  </div>
13
13
  </li>
14
- <li class="has-indicator indicator-top indicator-disc">
14
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
15
15
  <div class="stepper-list__indicator-custom"></div>
16
- <div>
16
+ <div class="stepper-list-content">
17
17
  <p class="page-body-normal">Default slot content for item 2</p>
18
18
  </div>
19
19
  </li>
20
- <li class="has-indicator indicator-top indicator-disc">
20
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
21
21
  <div class="stepper-list__indicator-custom"></div>
22
- <div>
22
+ <div class="stepper-list-content">
23
23
  <p class="page-body-normal">Default slot content for item 3</p>
24
24
  </div>
25
25
  </li>
@@ -34,22 +34,22 @@ exports[`ContactSection > renders correct HTML structure with form slot 1`] = `
34
34
  "<div class="contact-section">
35
35
  <div class="contact-section-inner">
36
36
  <div class="contact-section-info">
37
- <ul class="stepper-list">
38
- <li class="has-indicator indicator-top indicator-disc">
37
+ <ul role="list" class="stepper-list">
38
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
39
39
  <div class="stepper-list__indicator-custom"></div>
40
- <div>
40
+ <div class="stepper-list-content">
41
41
  <p class="page-body-normal">Default slot content for item 1</p>
42
42
  </div>
43
43
  </li>
44
- <li class="has-indicator indicator-top indicator-disc">
44
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
45
45
  <div class="stepper-list__indicator-custom"></div>
46
- <div>
46
+ <div class="stepper-list-content">
47
47
  <p class="page-body-normal">Default slot content for item 2</p>
48
48
  </div>
49
49
  </li>
50
- <li class="has-indicator indicator-top indicator-disc">
50
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
51
51
  <div class="stepper-list__indicator-custom"></div>
52
- <div>
52
+ <div class="stepper-list-content">
53
53
  <p class="page-body-normal">Default slot content for item 3</p>
54
54
  </div>
55
55
  </li>
@@ -66,22 +66,22 @@ exports[`ContactSection > renders correct HTML structure with styleClassPassthro
66
66
  "<div class="contact-section custom-class another-class">
67
67
  <div class="contact-section-inner">
68
68
  <div class="contact-section-info">
69
- <ul class="stepper-list">
70
- <li class="has-indicator indicator-top indicator-disc">
69
+ <ul role="list" class="stepper-list">
70
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
71
71
  <div class="stepper-list__indicator-custom"></div>
72
- <div>
72
+ <div class="stepper-list-content">
73
73
  <p class="page-body-normal">Default slot content for item 1</p>
74
74
  </div>
75
75
  </li>
76
- <li class="has-indicator indicator-top indicator-disc">
76
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
77
77
  <div class="stepper-list__indicator-custom"></div>
78
- <div>
78
+ <div class="stepper-list-content">
79
79
  <p class="page-body-normal">Default slot content for item 2</p>
80
80
  </div>
81
81
  </li>
82
- <li class="has-indicator indicator-top indicator-disc">
82
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
83
83
  <div class="stepper-list__indicator-custom"></div>
84
- <div>
84
+ <div class="stepper-list-content">
85
85
  <p class="page-body-normal">Default slot content for item 3</p>
86
86
  </div>
87
87
  </li>
@@ -96,18 +96,18 @@ exports[`ContactSection > renders correctly with all props and slots combined 1`
96
96
  "<section class="contact-section combined-class">
97
97
  <div class="contact-section-inner">
98
98
  <div class="contact-section-info">
99
- <ul class="stepper-list">
100
- <li class="has-indicator indicator-top indicator-disc">
99
+ <ul role="list" class="stepper-list">
100
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
101
101
  <div class="stepper-list__indicator-custom"><span class="icon">✓</span></div>
102
- <div><span>Step one</span></div>
102
+ <div class="stepper-list-content"><span>Step one</span></div>
103
103
  </li>
104
- <li class="has-indicator indicator-top indicator-disc">
104
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
105
105
  <div class="stepper-list__indicator-custom"></div>
106
- <div><span>Step two</span></div>
106
+ <div class="stepper-list-content"><span>Step two</span></div>
107
107
  </li>
108
- <li class="has-indicator indicator-top indicator-disc">
108
+ <li class="stepper-list-item has-indicator indicator-top indicator-disc">
109
109
  <div class="stepper-list__indicator-custom"></div>
110
- <div><span>Step three</span></div>
110
+ <div class="stepper-list-content"><span>Step three</span></div>
111
111
  </li>
112
112
  </ul>
113
113
  </div>
@@ -6,6 +6,11 @@ colour, border, radius, padding, accent stripe, icon, title and message sizes, a
6
6
  `app/components/02.molecules/alert-content/CONSUMER-STYLING.md`. This guide covers what the banner
7
7
  adds: position, motion and the two buttons.
8
8
 
9
+ > **Changed 2026-10-15**: the closed state class `.closed` is now `.cookie-consent-banner-closed`,
10
+ > and the buttons are `InputButton`s (`variant="inline"`, so the banner's tokens still style them):
11
+ > each button is `.cookie-consent-banner-accept.input-button` / `.cookie-consent-banner-reject.input-button`
12
+ > with the label in an inner `.button-text`. The button tokens below are unchanged.
13
+ >
9
14
  > **Changed 2026-09-27**: rebuilt on `AlertContent`. The banner's own panel tokens
10
15
  > (`--cookie-consent-banner-padding`, `-gap`, `-border-radius`, `-border`, `-accent-border-width`,
11
16
  > `-background`, `-text-colour`, `-actions-gap`) are gone; use the matching `--alert-content-*`
@@ -41,7 +46,10 @@ adds: position, motion and the two buttons.
41
46
  | `--cookie-consent-banner-accept-text-colour` | `var(--theme-on-surface)` | Accept button text |
42
47
 
43
48
  The gap between the buttons and their alignment (right by default) are `AlertContent`'s
44
- `--alert-content-actions-gap` and `--alert-content-actions-justify`.
49
+ `--alert-content-actions-gap` and `--alert-content-actions-justify`. Long labels wrap inside the
50
+ buttons (`InputButton`'s `.button-text`), and the buttons wrap onto their own lines when the row is full.
51
+ The label keeps the banner's font size and case: `InputButton`'s `--button-font-size` and
52
+ `--button-text-transform` don't apply here.
45
53
 
46
54
  Private tokens (not public API): `--_gutter`, `--_transition-duration`, `--_accent` on the root,
47
55
  each resolving a public token once for reuse.
@@ -50,11 +58,12 @@ each resolving a public token once for reuse.
50
58
 
51
59
  - `data-theme` on the root and on the inner `.alert-content`, from the `theme` prop or `app.config`
52
60
  (default `info`). It drives every `--theme-*` default, for the panel and the buttons.
53
- - `.closed` on the root once consent is granted or denied. The banner fades out and becomes
61
+ - `.cookie-consent-banner-closed` on the root once consent is granted or denied. The banner fades out and becomes
54
62
  `visibility: hidden`, so its buttons leave the tab order and the accessibility tree.
55
63
  - Classes: `.cookie-consent-banner` > `.cookie-consent-banner-inner` (the `role="region"`) >
56
64
  `.alert-content` (see its guide for the inner structure); the buttons are
57
- `.cookie-consent-banner-reject` and `.cookie-consent-banner-accept` inside `.alert-content-actions`.
65
+ `InputButton`s with `.cookie-consent-banner-reject` and `.cookie-consent-banner-accept` (plus
66
+ `.input-button.inline`) inside `.alert-content-actions`.
58
67
  - `data-test-id`: `cookie-consent-banner`, `cookie-consent-banner-reject`, `cookie-consent-banner-accept`,
59
68
  plus `AlertContent`'s `alert-icon`, `alert-title`, `alert-content`, `alert-actions`.
60
69
 
@@ -91,5 +100,5 @@ The pre-2026-09-27 look (white panel, dark text, no tint), scoped to the banner:
91
100
  ## Class passthrough
92
101
 
93
102
  `style-class-passthrough` adds classes to the teleported root `.cookie-consent-banner`, next to
94
- `data-theme` and `.closed`. It's reachable in normal use and is the most reliable place to set
103
+ `data-theme` and `.cookie-consent-banner-closed`. It's reachable in normal use and is the most reliable place to set
95
104
  tokens for this component, since ancestor wrappers don't contain it.
@@ -2,7 +2,7 @@
2
2
  <Teleport to="body">
3
3
  <div
4
4
  class="cookie-consent-banner"
5
- :class="[{ closed: isClosed }, elementClasses]"
5
+ :class="[{ 'cookie-consent-banner-closed': isClosed }, elementClasses]"
6
6
  :data-theme="resolved.theme"
7
7
  data-test-id="cookie-consent-banner"
8
8
  >
@@ -15,22 +15,22 @@
15
15
  <slot name="message">This site uses cookies to understand how it's used. You can accept or reject them.</slot>
16
16
  </template>
17
17
  <template #actions>
18
- <button
18
+ <InputButton
19
19
  type="button"
20
+ variant="inline"
20
21
  class="cookie-consent-banner-reject"
21
22
  data-test-id="cookie-consent-banner-reject"
23
+ :button-text="rejectLabel"
22
24
  @click="rejectAll()"
23
- >
24
- <slot name="rejectLabel">Reject</slot>
25
- </button>
26
- <button
25
+ />
26
+ <InputButton
27
27
  type="button"
28
+ variant="inline"
28
29
  class="cookie-consent-banner-accept"
29
30
  data-test-id="cookie-consent-banner-accept"
31
+ :button-text="acceptLabel"
30
32
  @click="acceptAll()"
31
- >
32
- <slot name="acceptLabel">Accept</slot>
33
- </button>
33
+ />
34
34
  </template>
35
35
  </AlertContent>
36
36
  </div>
@@ -44,6 +44,8 @@ import type { CookieConsentBannerProps } from "../../../types/components";
44
44
  const props = withDefaults(defineProps<CookieConsentBannerProps>(), {
45
45
  theme: undefined,
46
46
  ariaLabel: "Cookie consent",
47
+ acceptLabel: "Accept",
48
+ rejectLabel: "Reject",
47
49
  icon: "material-symbols:cookie-outline",
48
50
  showIcon: true,
49
51
  styleClassPassthrough: () => [],
@@ -91,7 +93,7 @@ const isClosed = computed(() => status.value !== "unset");
91
93
  opacity var(--_transition-duration) ease-in-out,
92
94
  visibility var(--_transition-duration);
93
95
 
94
- &.closed {
96
+ &.cookie-consent-banner-closed {
95
97
  grid-template-rows: 0fr;
96
98
  opacity: 0;
97
99
  visibility: hidden;
@@ -107,8 +109,10 @@ const isClosed = computed(() => status.value !== "unset");
107
109
  overflow: hidden;
108
110
  }
109
111
 
110
- .cookie-consent-banner-reject,
111
- .cookie-consent-banner-accept {
112
+ .cookie-consent-banner-reject.input-button,
113
+ .cookie-consent-banner-accept.input-button {
114
+ box-sizing: border-box;
115
+ min-height: auto;
112
116
  padding: var(--cookie-consent-banner-button-padding, 0.8rem 1.6rem);
113
117
  border-radius: var(--cookie-consent-banner-button-border-radius, 0.4rem);
114
118
  border: 0.1rem solid transparent;
@@ -122,9 +126,14 @@ const isClosed = computed(() => status.value !== "unset");
122
126
  outline: 0.2rem solid var(--cookie-consent-banner-focus-ring, var(--theme-border-focus));
123
127
  outline-offset: 0.2rem;
124
128
  }
129
+
130
+ .button-text {
131
+ font-size: inherit;
132
+ text-transform: none;
133
+ }
125
134
  }
126
135
 
127
- .cookie-consent-banner-reject {
136
+ .cookie-consent-banner-reject.input-button {
128
137
  background-color: transparent;
129
138
  color: inherit;
130
139
  border-color: var(--cookie-consent-banner-reject-border-colour, var(--theme-border));
@@ -135,7 +144,7 @@ const isClosed = computed(() => status.value !== "unset");
135
144
  }
136
145
  }
137
146
 
138
- .cookie-consent-banner-accept {
147
+ .cookie-consent-banner-accept.input-button {
139
148
  background-color: var(--cookie-consent-banner-accept-background, var(--_accent));
140
149
  color: var(--cookie-consent-banner-accept-text-colour, var(--theme-on-surface));
141
150