@c15t/astro 3.0.0-alpha.0
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/AGENTS.md +86 -0
- package/SKILL.md +14 -0
- package/dist/api/handlers.js +91 -0
- package/dist/api/index.js +3 -0
- package/dist/api/init.js +10 -0
- package/dist/api/manifest-init.js +103 -0
- package/dist/api/manifest.js +10 -0
- package/dist/browser/iab.js +5 -0
- package/dist/browser/inline-scripts.js +47 -0
- package/dist/client.js +276 -0
- package/dist/index.js +5 -0
- package/dist/integration.js +170 -0
- package/dist/libs/cookies.js +9 -0
- package/dist/middleware-handler.js +31 -0
- package/dist/middleware.js +6 -0
- package/dist/mode.js +50 -0
- package/dist/server.js +291 -0
- package/dist/styles.css +2 -0
- package/dist/types.js +0 -0
- package/dist/ui/adapter.js +37 -0
- package/dist/ui/provider-props.js +12 -0
- package/dist/ui/react.js +34 -0
- package/dist/ui/svelte.js +36 -0
- package/dist/ui/vue.js +38 -0
- package/dist-types/api/handlers.d.ts +85 -0
- package/dist-types/api/index.d.ts +13 -0
- package/dist-types/api/init.d.ts +10 -0
- package/dist-types/api/manifest-init.d.ts +73 -0
- package/dist-types/api/manifest.d.ts +9 -0
- package/dist-types/browser/iab.d.ts +14 -0
- package/dist-types/browser/inline-scripts.d.ts +38 -0
- package/dist-types/client.d.ts +152 -0
- package/dist-types/index.d.ts +33 -0
- package/dist-types/integration.d.ts +53 -0
- package/dist-types/libs/cookies.d.ts +15 -0
- package/dist-types/middleware-handler.d.ts +35 -0
- package/dist-types/middleware.d.ts +20 -0
- package/dist-types/mode.d.ts +75 -0
- package/dist-types/server.d.ts +130 -0
- package/dist-types/types.d.ts +305 -0
- package/dist-types/ui/adapter.d.ts +122 -0
- package/dist-types/ui/provider-props.d.ts +37 -0
- package/dist-types/ui/react.d.ts +19 -0
- package/dist-types/ui/svelte.d.ts +21 -0
- package/dist-types/ui/vue.d.ts +19 -0
- package/docs/README.md +86 -0
- package/docs/assets/v3/brand-bar.png +0 -0
- package/docs/assets/v3/brand-card.png +0 -0
- package/docs/assets/v3/choice-wall.png +0 -0
- package/docs/assets/v3/mobile-card.png +0 -0
- package/docs/assets/v3/preferences.png +0 -0
- package/docs/customization/overview.md +45 -0
- package/docs/customization/recipes.md +79 -0
- package/docs/customization/slots.md +55 -0
- package/docs/customization/tokens.md +76 -0
- package/docs/customization/translations.md +49 -0
- package/docs/frameworks/astro/quickstart.md +186 -0
- package/docs/guides/consent-state.md +60 -0
- package/docs/guides/data-fetching.md +163 -0
- package/docs/guides/deployment-modes.md +63 -0
- package/docs/guides/troubleshooting.md +68 -0
- package/docs/guides/verify-consent.md +62 -0
- package/docs/integrations/adobe-analytics.md +294 -0
- package/docs/integrations/ahrefs-analytics.md +291 -0
- package/docs/integrations/amplitude.md +292 -0
- package/docs/integrations/building-integrations.md +47 -0
- package/docs/integrations/clearbit.md +291 -0
- package/docs/integrations/cloudflare-web-analytics.md +292 -0
- package/docs/integrations/crisp.md +294 -0
- package/docs/integrations/databuddy.md +311 -0
- package/docs/integrations/fathom-analytics.md +293 -0
- package/docs/integrations/google-maps.md +384 -0
- package/docs/integrations/google-tag-manager.md +306 -0
- package/docs/integrations/google-tag.md +305 -0
- package/docs/integrations/heap.md +292 -0
- package/docs/integrations/hightouch.md +293 -0
- package/docs/integrations/hotjar.md +292 -0
- package/docs/integrations/intercom.md +285 -0
- package/docs/integrations/linkedin-insights.md +291 -0
- package/docs/integrations/logrocket.md +293 -0
- package/docs/integrations/matomo-analytics.md +316 -0
- package/docs/integrations/meta-pixel.md +285 -0
- package/docs/integrations/microsoft-clarity.md +292 -0
- package/docs/integrations/microsoft-uet.md +291 -0
- package/docs/integrations/mixpanel-analytics.md +295 -0
- package/docs/integrations/openai-pixel.md +441 -0
- package/docs/integrations/overview.md +111 -0
- package/docs/integrations/pirsch.md +295 -0
- package/docs/integrations/plausible-analytics.md +295 -0
- package/docs/integrations/posthog.md +397 -0
- package/docs/integrations/promptwatch.md +291 -0
- package/docs/integrations/reddit-pixel.md +294 -0
- package/docs/integrations/rudderstack.md +328 -0
- package/docs/integrations/rybbit-analytics.md +296 -0
- package/docs/integrations/segment.md +292 -0
- package/docs/integrations/snapchat-pixel.md +293 -0
- package/docs/integrations/tiktok-pixel.md +291 -0
- package/docs/integrations/umami-analytics.md +295 -0
- package/docs/integrations/vercel-analytics.md +293 -0
- package/docs/integrations/x-pixel.md +291 -0
- package/docs/integrations/youtube.md +405 -0
- package/docs/upgrade-v3.md +334 -0
- package/package.json +184 -0
- package/src/components/branding.astro +152 -0
- package/src/components/consent-script.astro +34 -0
- package/src/components/iab-panel.astro +43 -0
- package/src/components/iab-prompt.astro +316 -0
- package/src/components/islands/iab-dialog-surface.svelte +15 -0
- package/src/components/islands/iab-dialog-surface.tsx +25 -0
- package/src/components/islands/iab-dialog-surface.vue +20 -0
- package/src/components/islands/panel-surface.svelte +57 -0
- package/src/components/islands/panel-surface.tsx +67 -0
- package/src/components/islands/panel-surface.vue +39 -0
- package/src/components/panel-trigger.astro +48 -0
- package/src/components/panel.astro +65 -0
- package/src/components/prompt-deferred.astro +54 -0
- package/src/components/prompt.astro +325 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The preference-centre dialog.
|
|
4
|
+
*
|
|
5
|
+
* Renders nothing but an empty host element. The surface itself is an
|
|
6
|
+
* island in whichever framework the `ui` option names, mounted the first
|
|
7
|
+
* time something opens it — not `client:load` — so a visitor who accepts
|
|
8
|
+
* or rejects from the banner never downloads a framework at all.
|
|
9
|
+
*
|
|
10
|
+
* ```astro
|
|
11
|
+
* ---
|
|
12
|
+
* import ConsentDialog from '@c15t/astro/components/consent-dialog.astro';
|
|
13
|
+
* ---
|
|
14
|
+
* <ConsentDialog />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
interface Props {
|
|
18
|
+
/**
|
|
19
|
+
* Warm the island's chunk once the browser is idle, so the first open
|
|
20
|
+
* is instant. Costs a background download on every page.
|
|
21
|
+
*
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
preload?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { preload = false } = Astro.props;
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
data-c15t-dialog-host="preferences"
|
|
32
|
+
data-preload={preload ? 'true' : undefined}
|
|
33
|
+
>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
import {
|
|
38
|
+
getConsentClient,
|
|
39
|
+
openDialog,
|
|
40
|
+
preloadDialog,
|
|
41
|
+
} from '@c15t/astro/client';
|
|
42
|
+
|
|
43
|
+
// The island's specifier is not written here: only the integration knows
|
|
44
|
+
// which framework `ui` selected, and naming all of them would make every
|
|
45
|
+
// build resolve every framework. It registers the one surface from the
|
|
46
|
+
// page script it injects.
|
|
47
|
+
const host = document.querySelector('[data-c15t-dialog-host="preferences"]');
|
|
48
|
+
|
|
49
|
+
if (host?.getAttribute('data-preload') === 'true') {
|
|
50
|
+
const schedule = () => {
|
|
51
|
+
void preloadDialog();
|
|
52
|
+
};
|
|
53
|
+
if ('requestIdleCallback' in window) {
|
|
54
|
+
window.requestIdleCallback(schedule);
|
|
55
|
+
} else {
|
|
56
|
+
window.setTimeout(schedule, 2000);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A page can ask for the dialog before the runtime has booted (a link
|
|
61
|
+
// with `#c15t-preferences`, say); replaying it here keeps that working.
|
|
62
|
+
if (window.location.hash === '#c15t-preferences' && getConsentClient()) {
|
|
63
|
+
void openDialog('preferences');
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The banner as a server island.
|
|
4
|
+
*
|
|
5
|
+
* A statically cached page cannot carry a per-visitor consent decision: the
|
|
6
|
+
* first visitor's country would be baked into the HTML every later visitor
|
|
7
|
+
* receives. This variant renders the page from cache and defers only the
|
|
8
|
+
* banner to a per-request island, so geo, GPC and the stored consent cookie
|
|
9
|
+
* are all still honoured.
|
|
10
|
+
*
|
|
11
|
+
* ```astro
|
|
12
|
+
* ---
|
|
13
|
+
* import ConsentBannerDeferred from '@c15t/astro/components/consent-banner-deferred.astro';
|
|
14
|
+
* ---
|
|
15
|
+
* <ConsentBannerDeferred />
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Needs an adapter with on-demand rendering available (`output: 'server'`,
|
|
19
|
+
* or `output: 'static'` with the page's own `prerender` left on and the
|
|
20
|
+
* adapter configured). Without one, use `<ConsentBanner />` on an
|
|
21
|
+
* on-demand-rendered page instead.
|
|
22
|
+
*/
|
|
23
|
+
import type { LegalLinks } from '@c15t/core';
|
|
24
|
+
import ConsentBanner from './prompt.astro';
|
|
25
|
+
|
|
26
|
+
/** Every `<ConsentBanner />` prop, forwarded to the deferred island. */
|
|
27
|
+
interface Props {
|
|
28
|
+
/** Override the banner heading. */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Override the banner body copy. */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Override the reject button label. */
|
|
33
|
+
rejectButtonText?: string;
|
|
34
|
+
/** Override the accept button label. */
|
|
35
|
+
acceptButtonText?: string;
|
|
36
|
+
/** Override the customize button label. */
|
|
37
|
+
customizeButtonText?: string;
|
|
38
|
+
/** Ship the DOM without the bundled stylesheet's class names. */
|
|
39
|
+
noStyle?: boolean;
|
|
40
|
+
/** Extra class on the banner root. */
|
|
41
|
+
class?: string;
|
|
42
|
+
/** Which legal links to render inline. `null` renders none. */
|
|
43
|
+
legalLinks?: (keyof LegalLinks)[] | null;
|
|
44
|
+
/**
|
|
45
|
+
* Render even when the server decided the banner should stay hidden.
|
|
46
|
+
* Useful for visual testing.
|
|
47
|
+
*/
|
|
48
|
+
force?: boolean;
|
|
49
|
+
}
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
<ConsentBanner server:defer {...Astro.props}>
|
|
53
|
+
<slot name="fallback" slot="fallback" />
|
|
54
|
+
</ConsentBanner>
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Server-rendered consent banner.
|
|
4
|
+
*
|
|
5
|
+
* The shell — markup, copy, layout, the policy-driven button order — is
|
|
6
|
+
* produced on the server with zero framework JavaScript. Only one small
|
|
7
|
+
* bundled script ships, and it is shared by every banner on the site: a
|
|
8
|
+
* delegated click handler that turns `data-c15t-action` into runtime calls.
|
|
9
|
+
*
|
|
10
|
+
* The server renders the banner only when the decision it already resolved
|
|
11
|
+
* says to, so a returning visitor gets no banner markup at all — no client
|
|
12
|
+
* bytes and no flash. The same DOM shape and the same `data-testid`s as the
|
|
13
|
+
* Svelte and React banners, so cross-framework tests and CSS both hold.
|
|
14
|
+
*
|
|
15
|
+
* ```astro
|
|
16
|
+
* ---
|
|
17
|
+
* import ConsentBanner from '@c15t/astro/components/consent-banner.astro';
|
|
18
|
+
* ---
|
|
19
|
+
* <ConsentBanner />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import {defaultTranslationConfig, resolveConsentPresentation} from '@c15t/core';
|
|
23
|
+
import type { LegalLinks, TranslationsResponse } from '@c15t/core';
|
|
24
|
+
import {
|
|
25
|
+
buildColorSchemeScript,
|
|
26
|
+
buildConfigScript,
|
|
27
|
+
markConfigEmitted,
|
|
28
|
+
} from '@c15t/astro/server';
|
|
29
|
+
import Branding from './branding.astro';
|
|
30
|
+
import actionStyles from '@c15t/ui/styles/components/consent-actions';
|
|
31
|
+
import styles from '@c15t/ui/styles/components/consent-banner';
|
|
32
|
+
import buttonStyles from '@c15t/ui/styles/components/button';
|
|
33
|
+
import { getTextDirection } from '@c15t/ui/utils';
|
|
34
|
+
|
|
35
|
+
interface Props {
|
|
36
|
+
/** Override the banner heading. */
|
|
37
|
+
title?: string;
|
|
38
|
+
/** Override the banner body copy. */
|
|
39
|
+
description?: string;
|
|
40
|
+
/** Override the reject button label. */
|
|
41
|
+
rejectButtonText?: string;
|
|
42
|
+
/** Override the accept button label. */
|
|
43
|
+
acceptButtonText?: string;
|
|
44
|
+
/** Override the notice acknowledgement label. */
|
|
45
|
+
dismissButtonText?: string;
|
|
46
|
+
/** Override the customize button label. */
|
|
47
|
+
customizeButtonText?: string;
|
|
48
|
+
/** Ship the DOM without the bundled stylesheet's class names. */
|
|
49
|
+
noStyle?: boolean;
|
|
50
|
+
/** Extra class on the banner root. */
|
|
51
|
+
class?: string;
|
|
52
|
+
/** Drop the "Secured by c15t" tag. */
|
|
53
|
+
hideBranding?: boolean;
|
|
54
|
+
/** Which legal links to render inline. `null` renders none. */
|
|
55
|
+
legalLinks?: (keyof LegalLinks)[] | null;
|
|
56
|
+
/**
|
|
57
|
+
* Render even when the server decided the banner should stay hidden.
|
|
58
|
+
* Useful for visual testing.
|
|
59
|
+
*/
|
|
60
|
+
force?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
title,
|
|
65
|
+
description,
|
|
66
|
+
rejectButtonText,
|
|
67
|
+
acceptButtonText,
|
|
68
|
+
dismissButtonText,
|
|
69
|
+
customizeButtonText,
|
|
70
|
+
noStyle = false,
|
|
71
|
+
class: className,
|
|
72
|
+
hideBranding = false,
|
|
73
|
+
legalLinks = null,
|
|
74
|
+
force = false,
|
|
75
|
+
} = Astro.props;
|
|
76
|
+
|
|
77
|
+
const { c15t } = Astro.locals;
|
|
78
|
+
|
|
79
|
+
if (!c15t) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'@c15t/astro: `Astro.locals.c15t` is missing. The c15t() integration registers the middleware that populates it — check it is listed in astro.config, and that you have not set `middleware: false`.'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { snapshot } = c15t;
|
|
86
|
+
// No resolved policy means no consent surface, even when forced.
|
|
87
|
+
const shouldRender = c15t.hasConsentUi && (force || c15t.shouldShowBanner);
|
|
88
|
+
const emitConfig = markConfigEmitted(Astro.locals);
|
|
89
|
+
// A fallback for layouts with no <ConsentScript /> in <head>. It still runs
|
|
90
|
+
// before the banner markup it precedes, so the first paint is right.
|
|
91
|
+
const colorSchemeScript = emitConfig
|
|
92
|
+
? buildColorSchemeScript(c15t.options.colorScheme)
|
|
93
|
+
: '';
|
|
94
|
+
|
|
95
|
+
const fallback = defaultTranslationConfig.translations.en as TranslationsResponse;
|
|
96
|
+
const bundle = (snapshot.translations?.translations ?? fallback) as TranslationsResponse;
|
|
97
|
+
// A notice prompt explains and points at the opt-out; it never asks.
|
|
98
|
+
const notice = snapshot.policyRule.prompt === 'notice';
|
|
99
|
+
const copy = {
|
|
100
|
+
accept: acceptButtonText ?? bundle.common?.acceptAll ?? fallback.common.acceptAll,
|
|
101
|
+
customize:
|
|
102
|
+
customizeButtonText ?? bundle.common?.customize ?? fallback.common.customize,
|
|
103
|
+
description:
|
|
104
|
+
description ??
|
|
105
|
+
(notice
|
|
106
|
+
? (bundle.cookieBanner?.noticeDescription ?? fallback.cookieBanner.noticeDescription)
|
|
107
|
+
: (bundle.cookieBanner?.description ?? fallback.cookieBanner.description)),
|
|
108
|
+
// Acknowledgement leaves category choices and permissions unchanged.
|
|
109
|
+
dismiss: dismissButtonText ?? bundle.common?.acknowledge ?? bundle.common?.dismiss ?? fallback.common.acknowledge,
|
|
110
|
+
reject: rejectButtonText ?? bundle.common?.rejectAll ?? fallback.common.rejectAll,
|
|
111
|
+
securedBy: bundle.common?.securedBy ?? fallback.common.securedBy,
|
|
112
|
+
title:
|
|
113
|
+
title ??
|
|
114
|
+
(notice
|
|
115
|
+
? (bundle.cookieBanner?.noticeTitle ?? fallback.cookieBanner.noticeTitle)
|
|
116
|
+
: (bundle.cookieBanner?.title ?? fallback.cookieBanner.title)),
|
|
117
|
+
};
|
|
118
|
+
const rightLabels: Record<string, string> = {
|
|
119
|
+
'opt-out': bundle.rights?.optOut ?? fallback.rights.optOut,
|
|
120
|
+
preferences: bundle.rights?.preferences ?? fallback.rights.preferences,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const textDirection = getTextDirection(snapshot.translations?.language);
|
|
124
|
+
|
|
125
|
+
// Same policy-driven action resolution the Svelte and React banners use, so
|
|
126
|
+
// an opt-out or IAB policy produces the same buttons in the same order.
|
|
127
|
+
const presentation = resolveConsentPresentation({policy: snapshot.policyRule, presentation: c15t.options.presentation, surface: 'prompt'});
|
|
128
|
+
const {
|
|
129
|
+
actionGroups,
|
|
130
|
+
blocking,
|
|
131
|
+
direction,
|
|
132
|
+
primaryActions,
|
|
133
|
+
shouldFillActions: shouldFill,
|
|
134
|
+
preferenceControls,
|
|
135
|
+
variant,
|
|
136
|
+
} = presentation;
|
|
137
|
+
|
|
138
|
+
// A corner the host did not choose follows the text direction, the same
|
|
139
|
+
// mirroring the React and Svelte roots apply; a host corner is kept as is.
|
|
140
|
+
const mirrorCorner = function mirrorCorner(position: string): string {
|
|
141
|
+
if (position.endsWith('-left')) {
|
|
142
|
+
return position.replace(/-left$/u, '-right');
|
|
143
|
+
}
|
|
144
|
+
if (position.endsWith('-right')) {
|
|
145
|
+
return position.replace(/-right$/u, '-left');
|
|
146
|
+
}
|
|
147
|
+
return position;
|
|
148
|
+
};
|
|
149
|
+
const position =
|
|
150
|
+
textDirection === 'rtl' &&
|
|
151
|
+
presentation.positionSource === 'default' &&
|
|
152
|
+
(variant === 'floating' || variant === 'widget')
|
|
153
|
+
? mirrorCorner(presentation.position)
|
|
154
|
+
: presentation.position;
|
|
155
|
+
|
|
156
|
+
const labels: Record<string, string> = {
|
|
157
|
+
accept: copy.accept,
|
|
158
|
+
customize: copy.customize,
|
|
159
|
+
dismiss: copy.dismiss,
|
|
160
|
+
reject: copy.reject,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const rootClass = [
|
|
164
|
+
noStyle ? '' : styles.root,
|
|
165
|
+
noStyle ? '' : styles.bannerVisible,
|
|
166
|
+
className,
|
|
167
|
+
]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join(' ');
|
|
170
|
+
|
|
171
|
+
const inlineLegalLinks = (legalLinks ?? []).filter(
|
|
172
|
+
(key) => c15t.options.legalLinks?.[key]
|
|
173
|
+
);
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
{
|
|
177
|
+
emitConfig && colorSchemeScript && (
|
|
178
|
+
<script is:inline set:html={colorSchemeScript} />
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
{emitConfig && <script is:inline set:html={buildConfigScript(c15t.config)} />}
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
shouldRender && blocking && (
|
|
186
|
+
<div
|
|
187
|
+
aria-hidden="true"
|
|
188
|
+
class={noStyle ? '' : `${styles.overlay} ${styles.overlayVisible}`}
|
|
189
|
+
data-testid="consent-banner-overlay"
|
|
190
|
+
/>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
{
|
|
195
|
+
shouldRender && (
|
|
196
|
+
<div
|
|
197
|
+
class={rootClass}
|
|
198
|
+
data-blocking={blocking ? 'true' : undefined}
|
|
199
|
+
data-c15t-visible="true"
|
|
200
|
+
data-model={snapshot.policyRule.model}
|
|
201
|
+
data-position={position}
|
|
202
|
+
data-prompt={snapshot.policyRule.prompt === 'none' ? undefined : snapshot.policyRule.prompt}
|
|
203
|
+
data-testid="consent-banner-root"
|
|
204
|
+
data-variant={variant}
|
|
205
|
+
dir={textDirection}
|
|
206
|
+
lang={snapshot.translations?.language}
|
|
207
|
+
>
|
|
208
|
+
<div class={noStyle ? '' : styles.cardShell}>
|
|
209
|
+
<Branding
|
|
210
|
+
branding={snapshot.branding}
|
|
211
|
+
data-testid="consent-banner-branding"
|
|
212
|
+
hide={hideBranding}
|
|
213
|
+
hostname={Astro.url.hostname}
|
|
214
|
+
noStyle={noStyle}
|
|
215
|
+
securedBy={copy.securedBy}
|
|
216
|
+
variant="banner-tag"
|
|
217
|
+
/>
|
|
218
|
+
<div
|
|
219
|
+
aria-label={copy.title}
|
|
220
|
+
aria-modal={blocking ? "true" : undefined}
|
|
221
|
+
class={noStyle ? '' : styles.card}
|
|
222
|
+
data-testid="consent-banner-card"
|
|
223
|
+
role={blocking ? "dialog" : "region"}
|
|
224
|
+
tabindex="-1"
|
|
225
|
+
>
|
|
226
|
+
<div
|
|
227
|
+
class={noStyle ? '' : styles.header}
|
|
228
|
+
data-testid="consent-banner-header"
|
|
229
|
+
>
|
|
230
|
+
<h2
|
|
231
|
+
class={noStyle ? '' : styles.title}
|
|
232
|
+
data-testid="consent-banner-title"
|
|
233
|
+
>
|
|
234
|
+
{copy.title}
|
|
235
|
+
</h2>
|
|
236
|
+
<div
|
|
237
|
+
class={noStyle ? '' : styles.description}
|
|
238
|
+
data-context="banner"
|
|
239
|
+
data-testid="consent-banner-description"
|
|
240
|
+
>
|
|
241
|
+
{copy.description}
|
|
242
|
+
{inlineLegalLinks.map((key) => (
|
|
243
|
+
<a
|
|
244
|
+
data-testid={`consent-banner-legal-link-${key}`}
|
|
245
|
+
href={c15t.options.legalLinks?.[key]?.href}
|
|
246
|
+
rel="noreferrer"
|
|
247
|
+
target="_blank"
|
|
248
|
+
>
|
|
249
|
+
{c15t.options.legalLinks?.[key]?.label ?? key}
|
|
250
|
+
</a>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
<div
|
|
255
|
+
class={[noStyle ? '' : actionStyles.actionRoot, noStyle ? '' : styles.footer]
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.join(' ')}
|
|
258
|
+
data-direction={direction}
|
|
259
|
+
data-fill={shouldFill ? 'true' : undefined}
|
|
260
|
+
data-split={actionGroups.length > 1 && !shouldFill ? 'true' : undefined}
|
|
261
|
+
data-testid="consent-banner-footer"
|
|
262
|
+
>
|
|
263
|
+
{preferenceControls.length > 0 && (
|
|
264
|
+
<div
|
|
265
|
+
class={noStyle ? '' : styles.rights}
|
|
266
|
+
data-testid="consent-banner-rights"
|
|
267
|
+
>
|
|
268
|
+
{preferenceControls.map((right) => (
|
|
269
|
+
<button
|
|
270
|
+
class={noStyle ? '' : styles.rightLink}
|
|
271
|
+
data-action="right"
|
|
272
|
+
data-c15t-action="customize"
|
|
273
|
+
data-right={right}
|
|
274
|
+
data-testid={`consent-banner-right-link-${right}`}
|
|
275
|
+
type="button"
|
|
276
|
+
>
|
|
277
|
+
{rightLabels[right] ?? right}
|
|
278
|
+
</button>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
{actionGroups.map((group) => (
|
|
283
|
+
<div
|
|
284
|
+
class={noStyle ? '' : actionStyles.actionGroup}
|
|
285
|
+
data-direction={direction}
|
|
286
|
+
data-fill={shouldFill ? 'true' : undefined}
|
|
287
|
+
data-testid="consent-banner-footer-sub-group"
|
|
288
|
+
>
|
|
289
|
+
{group.map((action) => (
|
|
290
|
+
<button
|
|
291
|
+
class={noStyle ? '' : buttonStyles.button}
|
|
292
|
+
data-action={action}
|
|
293
|
+
data-c15t-action={action}
|
|
294
|
+
data-mode={noStyle ? undefined : 'stroke'}
|
|
295
|
+
data-size={noStyle ? undefined : 'small'}
|
|
296
|
+
data-testid={`consent-banner-${action}-button`}
|
|
297
|
+
data-variant={
|
|
298
|
+
noStyle
|
|
299
|
+
? undefined
|
|
300
|
+
: primaryActions.includes(action)
|
|
301
|
+
? 'primary'
|
|
302
|
+
: 'neutral'
|
|
303
|
+
}
|
|
304
|
+
type="button"
|
|
305
|
+
>
|
|
306
|
+
{labels[action] ?? action}
|
|
307
|
+
</button>
|
|
308
|
+
))}
|
|
309
|
+
</div>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
<script>
|
|
319
|
+
// The banner's only client-side code: one delegated listener, shared by
|
|
320
|
+
// every banner and dialog trigger on the site. Astro bundles and dedupes
|
|
321
|
+
// this module across pages.
|
|
322
|
+
import { attachBannerActions } from '@c15t/astro/client';
|
|
323
|
+
|
|
324
|
+
attachBannerActions();
|
|
325
|
+
</script>
|