srcdev-nuxt-components 9.6.2 → 9.6.4
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/consumer-overrides.md +17 -0
- package/.claude/rules/consumer/css-comment-slash.md +13 -0
- package/.claude/rules/consumer/css-native-nesting.md +13 -0
- package/.claude/rules/consumer/css-no-deep.md +13 -0
- package/.claude/rules/consumer/css-rem-base.md +11 -0
- package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
- package/.claude/rules/consumer/css-webkit-support.md +15 -0
- package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
- package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
- package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
- package/.claude/rules/consumer/story-args.md +12 -0
- package/.claude/rules/consumer/story-register-components.md +12 -0
- package/.claude/rules/consumer/vue-auto-imports.md +14 -0
- package/.claude/rules/consumer/vue-no-self-closing.md +12 -0
- package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
- package/.claude/rules/consumer/vue-props-pattern.md +13 -0
- package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
- package/.claude/rules/consumer/vue-video-source.md +12 -0
- package/.claude/skills/component-compliance-checklist.md +31 -3
- package/.claude/skills/component-inline-action-button.md +33 -2
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/cookie-consent-banner.md +9 -6
- package/.claude/skills/components/input-copy.md +47 -100
- package/.claude/skills/components/pricing-card.md +68 -84
- package/.claude/skills/composable-cookie-consent.md +1 -1
- package/.claude/skills/index.md +11 -5
- package/.claude/skills/new-app-scaffold.md +4 -2
- package/.claude/skills/setup-postinstall.md +5 -4
- package/.claude/skills/storybook-add-story.md +44 -0
- package/.claude/skills/testing-add-unit-test.md +19 -0
- package/.claude/skills/theming-component-token-pattern.md +47 -1
- package/.claude/skills/theming-partial-override.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-pricing-card.code-snippets +46 -84
- package/README.md +5 -5
- package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
- package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
- package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
- package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
- 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 +108 -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 -48
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
- package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
- package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
- package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
- package/app/composables/useAriaDescribedById.ts +1 -1
- package/app/types/components/cookie-consent-banner.d.ts +2 -0
- package/package.json +2 -1
|
@@ -4,97 +4,65 @@
|
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
6
|
"<PricingCard",
|
|
7
|
-
"
|
|
7
|
+
" plan-name=\"$1Single Site\"",
|
|
8
8
|
" :price=\"$2199\"",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
" '$7Feature 3',",
|
|
15
|
-
" ]\"",
|
|
16
|
-
" @select=\"$8handleSelect\"",
|
|
9
|
+
" currency-symbol=\"$3£\"",
|
|
10
|
+
" billing-period=\"$4one-time\"",
|
|
11
|
+
" description=\"$5Perfect for freelancers and solo practitioners.\"",
|
|
12
|
+
" :features=\"['$6Feature 1', '$7Feature 2', '$8Feature 3']\"",
|
|
13
|
+
" @select=\"$9handleSelect\"",
|
|
17
14
|
"/>"
|
|
18
15
|
]
|
|
19
16
|
},
|
|
20
17
|
"SRCDEV PricingCard Highlighted": {
|
|
21
|
-
"description": "PricingCard with 'Most Popular' highlight
|
|
18
|
+
"description": "PricingCard with 'Most Popular' highlight",
|
|
22
19
|
"scope": "vue,html",
|
|
23
20
|
"body": [
|
|
24
21
|
"<PricingCard",
|
|
25
|
-
"
|
|
22
|
+
" plan-name=\"$1Agency\"",
|
|
26
23
|
" :price=\"$2499\"",
|
|
27
|
-
"
|
|
24
|
+
" billing-period=\"$3one-time\"",
|
|
28
25
|
" description=\"$4Best for growing teams.\"",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
" ]\"",
|
|
35
|
-
" @select=\"$8handleSelect\"",
|
|
36
|
-
">",
|
|
37
|
-
" <template #cta=\"{ ctaText, isDisabled, onSelect }\">",
|
|
38
|
-
" <InputButton",
|
|
39
|
-
" :button-text=\"ctaText\"",
|
|
40
|
-
" variant=\"$9primary\"",
|
|
41
|
-
" :readonly=\"isDisabled\"",
|
|
42
|
-
" @click=\"onSelect\"",
|
|
43
|
-
" />",
|
|
44
|
-
" </template>",
|
|
45
|
-
"</PricingCard>"
|
|
26
|
+
" is-highlighted",
|
|
27
|
+
" badge-text=\"$5Most Popular\"",
|
|
28
|
+
" :features=\"['$6Feature 1', '$7Feature 2', '$8Feature 3']\"",
|
|
29
|
+
" @select=\"$9handleSelect\"",
|
|
30
|
+
"/>"
|
|
46
31
|
]
|
|
47
32
|
},
|
|
48
33
|
"SRCDEV PricingCard Grid": {
|
|
49
|
-
"description": "
|
|
34
|
+
"description": "Pricing comparison grid from a plans array",
|
|
50
35
|
"scope": "vue,html",
|
|
51
36
|
"body": [
|
|
52
|
-
"<div class=\"
|
|
53
|
-
" <PricingCard",
|
|
54
|
-
" planName=\"Single Site\"",
|
|
55
|
-
" :price=\"$1199\"",
|
|
56
|
-
" billingPeriod=\"one-time\"",
|
|
57
|
-
" description=\"$2For freelancers.\"",
|
|
58
|
-
" :features=\"['$3Feature 1', '$4Feature 2']\"",
|
|
59
|
-
" @select=\"handleSelect\"",
|
|
60
|
-
" />",
|
|
61
|
-
" <PricingCard",
|
|
62
|
-
" planName=\"Agency\"",
|
|
63
|
-
" :price=\"$5499\"",
|
|
64
|
-
" billingPeriod=\"one-time\"",
|
|
65
|
-
" description=\"$6For growing teams.\"",
|
|
66
|
-
" :is-highlighted=\"true\"",
|
|
67
|
-
" :features=\"['$7Feature 1', '$8Feature 2', '$9Feature 3']\"",
|
|
68
|
-
" @select=\"handleSelect\"",
|
|
69
|
-
" />",
|
|
37
|
+
"<div class=\"$1pricing-grid\">",
|
|
70
38
|
" <PricingCard",
|
|
71
|
-
"
|
|
72
|
-
" :
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
" :
|
|
76
|
-
"
|
|
39
|
+
" v-for=\"plan in $2plans\"",
|
|
40
|
+
" :key=\"plan.id\"",
|
|
41
|
+
" :plan-name=\"plan.name\"",
|
|
42
|
+
" :price=\"plan.price\"",
|
|
43
|
+
" :description=\"plan.description\"",
|
|
44
|
+
" :features=\"plan.features\"",
|
|
45
|
+
" :is-highlighted=\"plan.id === '$3pro'\"",
|
|
46
|
+
" heading-tag=\"$4h2\"",
|
|
47
|
+
" @select=\"$5handleSelect\"",
|
|
77
48
|
" />",
|
|
78
49
|
"</div>"
|
|
79
50
|
]
|
|
80
51
|
},
|
|
81
52
|
"SRCDEV PricingCard Custom Button": {
|
|
82
|
-
"description": "PricingCard with custom button
|
|
53
|
+
"description": "PricingCard with custom button via #cta slot",
|
|
83
54
|
"scope": "vue,html",
|
|
84
55
|
"body": [
|
|
85
56
|
"<PricingCard",
|
|
86
|
-
"
|
|
57
|
+
" plan-name=\"$1Premium\"",
|
|
87
58
|
" :price=\"$2799\"",
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
" :features=\"['$5Feature 1', '$6Feature 2']\"",
|
|
91
|
-
" @select=\"$7handleSelect\"",
|
|
59
|
+
" :features=\"['$3Feature 1', '$4Feature 2']\"",
|
|
60
|
+
" @select=\"$5handleSelect\"",
|
|
92
61
|
">",
|
|
93
62
|
" <template #cta=\"{ ctaText, isDisabled, onSelect }\">",
|
|
94
63
|
" <InputButton",
|
|
95
64
|
" :button-text=\"ctaText\"",
|
|
96
|
-
" variant=\"$
|
|
97
|
-
" theme=\"$9default\"",
|
|
65
|
+
" variant=\"$6secondary\"",
|
|
98
66
|
" :readonly=\"isDisabled\"",
|
|
99
67
|
" @click=\"onSelect\"",
|
|
100
68
|
" />",
|
|
@@ -107,17 +75,12 @@
|
|
|
107
75
|
"scope": "vue,html",
|
|
108
76
|
"body": [
|
|
109
77
|
"<PricingCard",
|
|
110
|
-
"
|
|
78
|
+
" plan-name=\"$1Annual\"",
|
|
111
79
|
" :price=\"$249\"",
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
" '$6Feature 1',",
|
|
117
|
-
" '$7Feature 2',",
|
|
118
|
-
" '$8Feature 3',",
|
|
119
|
-
" ]\"",
|
|
120
|
-
" @select=\"$9handleSelect\"",
|
|
80
|
+
" billing-period=\"$3per year\"",
|
|
81
|
+
" ribbon-text=\"$4EARLYBIRD50 · 50% off\"",
|
|
82
|
+
" :features=\"['$5Feature 1', '$6Feature 2']\"",
|
|
83
|
+
" @select=\"$7handleSelect\"",
|
|
121
84
|
"/>"
|
|
122
85
|
]
|
|
123
86
|
},
|
|
@@ -127,21 +90,20 @@
|
|
|
127
90
|
"body": [
|
|
128
91
|
".$1pricing-page {",
|
|
129
92
|
" .pricing-card {",
|
|
130
|
-
" --pricing-card-
|
|
131
|
-
" --pricing-card-
|
|
132
|
-
" --pricing-card-
|
|
133
|
-
" --pricing-card-
|
|
93
|
+
" --pricing-card-background: $2;",
|
|
94
|
+
" --pricing-card-border-radius: $3;",
|
|
95
|
+
" --pricing-card-padding: $4;",
|
|
96
|
+
" --pricing-card-gap: $5;",
|
|
134
97
|
" --pricing-card-name-color: $6;",
|
|
135
|
-
" --pricing-card-
|
|
136
|
-
" --pricing-card-
|
|
137
|
-
" --pricing-card-
|
|
138
|
-
" --
|
|
139
|
-
" --pricing-card-ribbon-text: $11;",
|
|
98
|
+
" --pricing-card-description-line-clamp: $7;",
|
|
99
|
+
" --pricing-card-feature-icon-color: $8;",
|
|
100
|
+
" --pricing-card-ribbon-bg: $9;",
|
|
101
|
+
" --input-button-primary-surface: $10;",
|
|
140
102
|
"",
|
|
141
103
|
" &.is-highlighted {",
|
|
142
|
-
" --pricing-card-highlight-border: $
|
|
143
|
-
" --pricing-card-highlight-
|
|
144
|
-
" --pricing-card-
|
|
104
|
+
" --pricing-card-highlight-border: $11;",
|
|
105
|
+
" --pricing-card-highlight-scale: $12;",
|
|
106
|
+
" --pricing-card-badge-bg: $13;",
|
|
145
107
|
" }",
|
|
146
108
|
" }",
|
|
147
109
|
"}"
|
package/README.md
CHANGED
|
@@ -25,12 +25,12 @@ defineNuxtConfig({
|
|
|
25
25
|
|
|
26
26
|
## Claude Code Skills
|
|
27
27
|
|
|
28
|
-
This package ships Claude Code skills
|
|
28
|
+
This package ships Claude Code skills (reference docs for components and development tasks) and a curated set of Claude Code rules (short constraints for working with the layer: CSS nesting, overrides, props, NuxtImg, Storybook args) in the `.claude/` directory.
|
|
29
29
|
|
|
30
30
|
To make them available in your project, add this script to your `package.json`:
|
|
31
31
|
|
|
32
32
|
```json
|
|
33
|
-
"setup:claude": "cp -r node_modules/srcdev-nuxt-components/.claude/skills .claude/skills/srcdev-nuxt-components"
|
|
33
|
+
"setup:claude": "rm -rf .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Then run it after install:
|
|
@@ -39,7 +39,7 @@ Then run it after install:
|
|
|
39
39
|
npm run setup:claude
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Skills are copied into `.claude/skills/srcdev-nuxt-components/` so they never conflict with or overwrite
|
|
42
|
+
Skills are copied into `.claude/skills/srcdev-nuxt-components/` and rules into `.claude/rules/srcdev-nuxt-components/`, so they never conflict with or overwrite your own project's. Re-running the script after a package update is safe: it replaces only those two `srcdev-nuxt-components` folders, so skills and rules dropped or renamed upstream are removed too, and your own skills and rules are never touched.
|
|
43
43
|
|
|
44
44
|
### Automate with postinstall (recommended)
|
|
45
45
|
|
|
@@ -47,7 +47,7 @@ To ensure skills are always up to date, VSCode snippets are installed, and `nuxt
|
|
|
47
47
|
|
|
48
48
|
```json
|
|
49
49
|
"scripts": {
|
|
50
|
-
"setup:claude": "mkdir -p .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components",
|
|
50
|
+
"setup:claude": "rm -rf .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components",
|
|
51
51
|
"postinstall": "node node_modules/srcdev-nuxt-components/scripts/copy-snippets.mjs && nuxt prepare && npm run setup:claude"
|
|
52
52
|
}
|
|
53
53
|
```
|
|
@@ -56,7 +56,7 @@ To ensure skills are always up to date, VSCode snippets are installed, and `nuxt
|
|
|
56
56
|
|
|
57
57
|
1. `copy-snippets.mjs` — copies VSCode snippets (`.code-snippets` files) from the layer to your `.vscode/` folder
|
|
58
58
|
2. `nuxt prepare` — generates Nuxt type declarations
|
|
59
|
-
3. `setup:claude` — copies skills into `.claude/skills/srcdev-nuxt-components/`
|
|
59
|
+
3. `setup:claude` — copies skills into `.claude/skills/srcdev-nuxt-components/` and rules into `.claude/rules/srcdev-nuxt-components/`
|
|
60
60
|
|
|
61
61
|
> **Note**: The snippet copy must run from your consumer app's postinstall, not the layer's. npm doesn't invoke postinstall hooks for dependencies, so each consumer app is responsible for copying snippets into its own `.vscode/` folder. Don't set `SRCDEV_STANDALONE` in a consumer app: that flag is only for running this layer on its own (it enables dev-only modules and the ramp watcher).
|
|
62
62
|
|
package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts
CHANGED
|
@@ -25,7 +25,9 @@ const meta: Meta<typeof ScrollRevealFrame> = {
|
|
|
25
25
|
},
|
|
26
26
|
},
|
|
27
27
|
parameters: {
|
|
28
|
+
// Own iframe on the Docs page: the effect depends on page scroll.
|
|
28
29
|
docs: {
|
|
30
|
+
story: { inline: false, height: "600px" },
|
|
29
31
|
description: {
|
|
30
32
|
component:
|
|
31
33
|
"Generic clipping frame that pans any slot content vertically as it scrolls through the viewport — driven entirely by CSS Scroll-driven Animations. No scroll listeners, no JS. Use this when you need the parallax reveal with arbitrary content (grids, video, etc.). For a single NuxtImg with optimisation and focal-point control, use ScrollRevealImage instead.",
|
package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts
CHANGED
|
@@ -50,7 +50,9 @@ const meta: Meta<typeof ScrollRevealImage> = {
|
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
52
|
parameters: {
|
|
53
|
+
// Own iframe on the Docs page: the effect depends on page scroll.
|
|
53
54
|
docs: {
|
|
55
|
+
story: { inline: false, height: "600px" },
|
|
54
56
|
description: {
|
|
55
57
|
component:
|
|
56
58
|
"Single-image parallax reveal built on ScrollRevealFrame. Adds NuxtImg optimisation (src, alt, intrinsic dimensions) and a horizontal focal-point prop (focalX). For arbitrary slot content — grids, video, etc. — use ScrollRevealFrame directly.",
|
package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts
CHANGED
|
@@ -45,6 +45,10 @@ export default {
|
|
|
45
45
|
showOverlay: true,
|
|
46
46
|
overlayText: "A quiet moment.",
|
|
47
47
|
},
|
|
48
|
+
parameters: {
|
|
49
|
+
// Own iframe on the Docs page: the effect depends on page scroll and vh heights.
|
|
50
|
+
docs: { story: { inline: false, height: "600px" } },
|
|
51
|
+
},
|
|
48
52
|
} as Meta<StoryArgs>;
|
|
49
53
|
|
|
50
54
|
const Template: StoryFn<StoryArgs> = (args) => ({
|
package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts
CHANGED
|
@@ -20,7 +20,9 @@ const meta: Meta<typeof WipeAwayVertical> = {
|
|
|
20
20
|
styleClassPassthrough: { table: { disable: true } },
|
|
21
21
|
},
|
|
22
22
|
parameters: {
|
|
23
|
+
// Own iframe on the Docs page: the effect depends on page scroll and vh heights.
|
|
23
24
|
docs: {
|
|
25
|
+
story: { inline: false, height: "600px" },
|
|
24
26
|
description: {
|
|
25
27
|
component:
|
|
26
28
|
"Scroll-driven vertical wipe effect: each sticky panel wipes away to reveal the next as its paired scrolling section crosses the viewport. Pure CSS — a single-column grid overlays `.sticky-items-container` across all `.scrolling-section` rows via `animation-timeline: view()`, so there's no scroll listener or JS measurement involved. Consumers supply `stickyItem-{n}` slots for each of `itemCount` panels, and `scrollingItem-{n}` slots for only the first `itemCount - 1` — the last panel has nothing left to reveal, so it has no wipe animation or scrolling section of its own, just a small built-in trailing buffer to release cleanly. The sticky panel's own visible height is `--wipe-away-vertical-height` (default `100vh`) — leave the root's own `height` unset (`auto`) so it sizes itself from its children. Browsers without `animation-timeline: view()` support (via `@supports not (...)`) just get the sticky items stacked in normal document flow instead.",
|
|
@@ -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
|
|
package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import StorybookComponent from "../CookieConsentBanner.vue";
|
|
3
4
|
|
|
4
5
|
export default {
|
|
@@ -21,6 +22,16 @@ export default {
|
|
|
21
22
|
description: "Render the icon column",
|
|
22
23
|
table: { category: "Appearance" },
|
|
23
24
|
},
|
|
25
|
+
acceptLabel: {
|
|
26
|
+
control: "text",
|
|
27
|
+
description: "Accept button text",
|
|
28
|
+
table: { category: "Content" },
|
|
29
|
+
},
|
|
30
|
+
rejectLabel: {
|
|
31
|
+
control: "text",
|
|
32
|
+
description: "Reject button text",
|
|
33
|
+
table: { category: "Content" },
|
|
34
|
+
},
|
|
24
35
|
ariaLabel: {
|
|
25
36
|
control: "text",
|
|
26
37
|
description: "Accessible name for the banner region",
|
|
@@ -35,19 +46,26 @@ export default {
|
|
|
35
46
|
args: {
|
|
36
47
|
theme: "info",
|
|
37
48
|
ariaLabel: "Cookie consent",
|
|
49
|
+
acceptLabel: "Accept",
|
|
50
|
+
rejectLabel: "Reject",
|
|
38
51
|
icon: "material-symbols:cookie-outline",
|
|
39
52
|
showIcon: true,
|
|
40
53
|
styleClassPassthrough: [],
|
|
41
54
|
},
|
|
42
55
|
parameters: {
|
|
56
|
+
// Own iframe on the Docs page: the banner is fixed to the viewport.
|
|
43
57
|
docs: {
|
|
58
|
+
story: { inline: false, height: "400px" },
|
|
44
59
|
description: {
|
|
45
60
|
// Drives itself from the real useCookieConsent()/useCookie() state, so
|
|
46
61
|
// once accepted/rejected in this browser it stays hidden across story
|
|
47
62
|
// reloads. Each story below has "Show banner again" (resets state
|
|
48
63
|
// in-place) and "Delete cookie" (clears document.cookie directly, for
|
|
49
64
|
// verifying the cookie itself is actually gone, e.g. in devtools).
|
|
50
|
-
component:
|
|
65
|
+
component:
|
|
66
|
+
"Fixed, non-modal cookie consent banner built on AlertContent, with Accept/Reject InputButtons wired to useCookieConsent(). " +
|
|
67
|
+
"Copy comes from the title and message slots and the acceptLabel, rejectLabel and ariaLabel props; theme, icon and showIcon set the look. " +
|
|
68
|
+
"It reads/writes a real 'privacy-notice-consent' cookie, so use the story's reset controls to bring the banner back.",
|
|
51
69
|
},
|
|
52
70
|
},
|
|
53
71
|
},
|
|
@@ -82,7 +100,7 @@ const Template: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
82
100
|
<div style="padding: 2rem; min-height: 240px; position: relative;">
|
|
83
101
|
<button type="button" @click="showAgain">Show banner again</button>
|
|
84
102
|
<button type="button" @click="deleteCookie" style="margin-inline-start: 0.8rem;">Delete cookie</button>
|
|
85
|
-
<StorybookComponent :theme="args.theme" :icon="args.icon" :show-icon="args.showIcon" :aria-label="args.ariaLabel" :style-class-passthrough="args.styleClassPassthrough">
|
|
103
|
+
<StorybookComponent :theme="args.theme" :icon="args.icon" :show-icon="args.showIcon" :aria-label="args.ariaLabel" :accept-label="args.acceptLabel" :reject-label="args.rejectLabel" :style-class-passthrough="args.styleClassPassthrough">
|
|
86
104
|
<template #message>This site uses cookies for analytics. You can accept or reject them.</template>
|
|
87
105
|
</StorybookComponent>
|
|
88
106
|
</div>
|
|
@@ -101,12 +119,97 @@ export const CustomCopy: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
101
119
|
<div style="padding: 2rem; min-height: 240px; position: relative;">
|
|
102
120
|
<button type="button" @click="showAgain">Show banner again</button>
|
|
103
121
|
<button type="button" @click="deleteCookie" style="margin-inline-start: 0.8rem;">Delete cookie</button>
|
|
104
|
-
<StorybookComponent :theme="args.theme" :icon="args.icon" :show-icon="args.showIcon" :aria-label="args.ariaLabel">
|
|
122
|
+
<StorybookComponent :theme="args.theme" :icon="args.icon" :show-icon="args.showIcon" :aria-label="args.ariaLabel" :accept-label="args.acceptLabel" :reject-label="args.rejectLabel">
|
|
105
123
|
<template #title>Your privacy</template>
|
|
106
124
|
<template #message>We use cookies to understand traffic to this site. No personal data is sold.</template>
|
|
107
|
-
<template #acceptLabel>Allow cookies</template>
|
|
108
|
-
<template #rejectLabel>No thanks</template>
|
|
109
125
|
</StorybookComponent>
|
|
110
126
|
</div>
|
|
111
127
|
`,
|
|
112
128
|
});
|
|
129
|
+
CustomCopy.args = { acceptLabel: "Allow cookies", rejectLabel: "No thanks" };
|
|
130
|
+
|
|
131
|
+
const stressCases = {
|
|
132
|
+
german: {
|
|
133
|
+
title: "Datenschutzeinstellungen",
|
|
134
|
+
message:
|
|
135
|
+
"Diese Website verwendet Cookies, um zu verstehen, wie sie genutzt wird, und um Ihnen ein besseres Erlebnis zu bieten. Weitere Informationen finden Sie in unserer Datenschutzerklärung.",
|
|
136
|
+
acceptLabel: "Alle Cookies akzeptieren und fortfahren",
|
|
137
|
+
rejectLabel: "Alle nicht notwendigen Cookies ablehnen",
|
|
138
|
+
dir: "ltr",
|
|
139
|
+
},
|
|
140
|
+
unbroken: {
|
|
141
|
+
title: "Datenschutzgrundverordnungskonformitätsbestätigungseinstellungen",
|
|
142
|
+
message: "https://example.com/a-very-long-path-segment-without-any-spaces-at-all-in-it/privacy-and-cookie-policy",
|
|
143
|
+
acceptLabel: "Cookieeinwilligungsbestätigung",
|
|
144
|
+
rejectLabel: "Cookieeinwilligungsablehnung",
|
|
145
|
+
dir: "ltr",
|
|
146
|
+
},
|
|
147
|
+
emojiHtml: {
|
|
148
|
+
title: "🍪 <strong>Cookies</strong>",
|
|
149
|
+
message: "<script>alert('xss')</script> We use cookies 🍪🍪🍪 & <em>similar</em> technologies.",
|
|
150
|
+
acceptLabel: "✅ <b>Yes</b>",
|
|
151
|
+
rejectLabel: "❌ <i>No</i>",
|
|
152
|
+
dir: "ltr",
|
|
153
|
+
},
|
|
154
|
+
rtl: {
|
|
155
|
+
title: "ملفات تعريف الارتباط",
|
|
156
|
+
message: "يستخدم هذا الموقع ملفات تعريف الارتباط لفهم كيفية استخدامه. يمكنك قبولها أو رفضها.",
|
|
157
|
+
acceptLabel: "قبول",
|
|
158
|
+
rejectLabel: "رفض",
|
|
159
|
+
dir: "rtl",
|
|
160
|
+
},
|
|
161
|
+
minimal: {
|
|
162
|
+
title: "",
|
|
163
|
+
message: "C",
|
|
164
|
+
acceptLabel: "✓",
|
|
165
|
+
rejectLabel: "✗",
|
|
166
|
+
dir: "ltr",
|
|
167
|
+
},
|
|
168
|
+
} as const;
|
|
169
|
+
|
|
170
|
+
type StressCase = keyof typeof stressCases;
|
|
171
|
+
|
|
172
|
+
export const StressTest: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
173
|
+
components: { StorybookComponent },
|
|
174
|
+
setup() {
|
|
175
|
+
const { showAgain, deleteCookie } = useResetConsent();
|
|
176
|
+
const copy = computed(() => stressCases[(args as { stressCase?: StressCase }).stressCase ?? "german"]);
|
|
177
|
+
return { args, copy, showAgain, deleteCookie };
|
|
178
|
+
},
|
|
179
|
+
template: `
|
|
180
|
+
<div style="padding: 2rem; min-height: 320px; position: relative;">
|
|
181
|
+
<button type="button" @click="showAgain">Show banner again</button>
|
|
182
|
+
<button type="button" @click="deleteCookie" style="margin-inline-start: 0.8rem;">Delete cookie</button>
|
|
183
|
+
<StorybookComponent :theme="args.theme" :icon="args.icon" :show-icon="args.showIcon" :aria-label="args.ariaLabel" :accept-label="copy.acceptLabel" :reject-label="copy.rejectLabel" :style-class-passthrough="args.styleClassPassthrough">
|
|
184
|
+
<template #title><span :dir="copy.dir">{{ copy.title }}</span></template>
|
|
185
|
+
<template #message><span :dir="copy.dir">{{ copy.message }}</span></template>
|
|
186
|
+
</StorybookComponent>
|
|
187
|
+
</div>
|
|
188
|
+
`,
|
|
189
|
+
});
|
|
190
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
191
|
+
StressTest.argTypes = {
|
|
192
|
+
stressCase: {
|
|
193
|
+
name: "Stress case",
|
|
194
|
+
control: { type: "select" },
|
|
195
|
+
options: Object.keys(stressCases),
|
|
196
|
+
description:
|
|
197
|
+
"**Story control, not a prop.** Picks which worst-case copy fills the title and message slots and the acceptLabel/rejectLabel props (overriding their controls).",
|
|
198
|
+
table: { category: "Story only" },
|
|
199
|
+
},
|
|
200
|
+
} as Meta<typeof StorybookComponent>["argTypes"];
|
|
201
|
+
StressTest.args = { stressCase: "german" } as typeof StressTest.args;
|
|
202
|
+
StressTest.parameters = {
|
|
203
|
+
docs: {
|
|
204
|
+
story: { inline: false, height: "480px" },
|
|
205
|
+
description: {
|
|
206
|
+
story:
|
|
207
|
+
"Hostile slot content, switched with the Stress case control: long German copy and button labels, an unbroken compound title " +
|
|
208
|
+
"with a long URL as the message and unbroken button labels, emoji and HTML-like text (must render as text, never as markup), " +
|
|
209
|
+
"right-to-left Arabic, and a minimal case with an empty title and one-character copy. The banner is fixed to the viewport, so " +
|
|
210
|
+
"use Storybook's viewport toolbar (not a canvas width) to check narrow screens. Check that the banner stays inside the gutter " +
|
|
211
|
+
"and caps at 64rem, unbroken text wraps instead of being clipped (including inside the buttons), and the buttons wrap onto " +
|
|
212
|
+
"their own lines rather than overflowing.",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
};
|