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.
- package/.claude/rules/consumer/vue-no-self-closing.md +3 -2
- package/.claude/skills/component-compliance-checklist.md +17 -3
- package/.claude/skills/component-inline-action-button.md +33 -2
- package/.claude/skills/components/cookie-consent-banner.md +9 -6
- package/.claude/skills/components/input-copy.md +47 -100
- package/.claude/skills/components/progress-tracker.md +140 -0
- package/.claude/skills/components/stepper-list.md +10 -3
- package/.claude/skills/components/tabbed-content.md +4 -0
- package/.claude/skills/composable-cookie-consent.md +1 -1
- package/.claude/skills/index.md +4 -3
- package/.claude/skills/storybook-add-story.md +14 -0
- package/.claude/skills/testing-add-unit-test.md +19 -0
- package/.claude/skills/theming-component-token-pattern.md +1 -1
- package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
- package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
- package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
- package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
- package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +5 -1
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +2 -2
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +26 -0
- package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap +28 -28
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
- package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +106 -5
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
- package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -47
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
- package/app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md +116 -0
- package/app/components/02.molecules/progress-tracker/ProgressTracker.vue +275 -0
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +424 -0
- package/app/components/02.molecules/progress-tracker/tests/ProgressTracker.spec.ts +253 -0
- package/app/components/02.molecules/progress-tracker/tests/__snapshots__/ProgressTracker.spec.ts.snap +46 -0
- package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +42 -7
- package/app/components/02.molecules/stepper-list/StepperList.vue +42 -19
- package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +288 -2
- package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +161 -34
- package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +40 -40
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
- package/app/types/components/cookie-consent-banner.d.ts +2 -0
- package/app/types/components/index.ts +1 -0
- package/app/types/components/progress-tracker.d.ts +6 -0
- 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
|
-
|
|
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
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
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": "
|
|
3
|
+
"description": "InputCopy: readonly input with a copy-to-clipboard button",
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
6
|
"<InputCopy",
|
|
7
|
-
" value=\"$
|
|
8
|
-
" label=\"$
|
|
9
|
-
" description=\"$
|
|
10
|
-
" @
|
|
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
|
|
15
|
-
"description": "InputCopy
|
|
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=\"
|
|
20
|
-
" label=\"
|
|
21
|
-
" description=\"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
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=\"$
|
|
35
|
-
" label=\"$
|
|
36
|
-
" :is-disabled=\"true\"",
|
|
37
|
-
"
|
|
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
|
-
".$
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
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-
|
|
35
|
-
" --stepper-list-counter-disc-
|
|
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 = [
|
package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap
CHANGED
|
@@ -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`
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
|