@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,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Inlines the server-resolved consent config for the browser.
|
|
4
|
+
*
|
|
5
|
+
* Put this in your layout's `<head>`. It hands the client boot script the
|
|
6
|
+
* decision the server already made — consents, policy, translations — so
|
|
7
|
+
* the page boots with no `/init` roundtrip and no banner flicker, and it
|
|
8
|
+
* writes the colour-scheme class before the stylesheet paints, so a
|
|
9
|
+
* system-dark visitor does not get a light banner for a frame.
|
|
10
|
+
*
|
|
11
|
+
* `<ConsentBanner />` renders both itself if they have not been emitted
|
|
12
|
+
* yet, so a site with a banner in every layout does not need this
|
|
13
|
+
* explicitly — but `<head>` is where the colour-scheme script belongs.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
buildColorSchemeScript,
|
|
17
|
+
buildConfigScript,
|
|
18
|
+
markConfigEmitted,
|
|
19
|
+
} from '@c15t/astro/server';
|
|
20
|
+
|
|
21
|
+
const { c15t } = Astro.locals;
|
|
22
|
+
const shouldEmit = c15t !== undefined && markConfigEmitted(Astro.locals);
|
|
23
|
+
const colorSchemeScript = shouldEmit
|
|
24
|
+
? buildColorSchemeScript(c15t.options.colorScheme)
|
|
25
|
+
: '';
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
{colorSchemeScript && <script is:inline set:html={colorSchemeScript} />}
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
shouldEmit && (
|
|
32
|
+
<script is:inline set:html={buildConfigScript(c15t.config)} />
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The IAB TCF preference centre.
|
|
4
|
+
*
|
|
5
|
+
* Like `<ConsentDialog />`, this renders only a host element; the TCF
|
|
6
|
+
* surface and `@c15t/iab` are both loaded on first open, so a site with IAB
|
|
7
|
+
* configured still serves a banner-only page with no TCF bytes.
|
|
8
|
+
*
|
|
9
|
+
* ```astro
|
|
10
|
+
* ---
|
|
11
|
+
* import IABConsentDialog from '@c15t/astro/components/iab-consent-dialog.astro';
|
|
12
|
+
* ---
|
|
13
|
+
* <IABConsentDialog />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
interface Props {
|
|
17
|
+
/** Warm the island's chunk once the browser is idle. */
|
|
18
|
+
preload?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { preload = false } = Astro.props;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<div data-c15t-dialog-host="iab" data-preload={preload ? 'true' : undefined}></div>
|
|
25
|
+
|
|
26
|
+
<script>
|
|
27
|
+
import { preloadDialog } from '@c15t/astro/client';
|
|
28
|
+
|
|
29
|
+
// The integration registers the island for the configured `ui`; see
|
|
30
|
+
// `<ConsentDialog />` for why the specifier does not live here.
|
|
31
|
+
const host = document.querySelector('[data-c15t-dialog-host="iab"]');
|
|
32
|
+
|
|
33
|
+
if (host?.getAttribute('data-preload') === 'true') {
|
|
34
|
+
const schedule = () => {
|
|
35
|
+
void preloadDialog();
|
|
36
|
+
};
|
|
37
|
+
if ('requestIdleCallback' in window) {
|
|
38
|
+
window.requestIdleCallback(schedule);
|
|
39
|
+
} else {
|
|
40
|
+
window.setTimeout(schedule, 2000);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Server-rendered IAB TCF banner.
|
|
4
|
+
*
|
|
5
|
+
* The same deal as `<ConsentBanner />`: markup, copy and the purposes
|
|
6
|
+
* summary are produced on the server with no framework JavaScript, and the
|
|
7
|
+
* only client code is the delegated `data-c15t-action` handler every banner
|
|
8
|
+
* on the site already shares. `customize` opens the TCF preference centre
|
|
9
|
+
* through `<IABConsentDialog />`'s island, so the TCF surface downloads
|
|
10
|
+
* only when someone asks for it.
|
|
11
|
+
*
|
|
12
|
+
* The summary — which purposes, stacks and special features the banner
|
|
13
|
+
* names, how many it leaves out, how many vendors there are — comes from
|
|
14
|
+
* the shared model in `@c15t/iab/headless`, the same one the React, Svelte
|
|
15
|
+
* and Vue banners read. Same DOM shape and the same `data-testid`s, so the
|
|
16
|
+
* cross-framework parity gate compares like with like.
|
|
17
|
+
*
|
|
18
|
+
* It renders only when the server already has a vendor list. Hosted and
|
|
19
|
+
* manifest mode get one from `/init`; offline mode needs `iab.gvl` or
|
|
20
|
+
* `iab.gvlURL` on the integration options.
|
|
21
|
+
*
|
|
22
|
+
* ```astro
|
|
23
|
+
* ---
|
|
24
|
+
* import IABConsentBanner from '@c15t/astro/components/iab-consent-banner.astro';
|
|
25
|
+
* import IABConsentDialog from '@c15t/astro/components/iab-consent-dialog.astro';
|
|
26
|
+
* ---
|
|
27
|
+
* <IABConsentBanner />
|
|
28
|
+
* <IABConsentDialog />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
import { defaultTranslationConfig, resolveConsentPresentation } from '@c15t/core';
|
|
32
|
+
import type { TranslationsResponse } from '@c15t/core';
|
|
33
|
+
import { resolveIABBannerSummary } from '@c15t/iab/headless';
|
|
34
|
+
import {
|
|
35
|
+
buildColorSchemeScript,
|
|
36
|
+
buildConfigScript,
|
|
37
|
+
markConfigEmitted,
|
|
38
|
+
} from '@c15t/astro/server';
|
|
39
|
+
import Branding from './branding.astro';
|
|
40
|
+
import actionStyles from '@c15t/ui/styles/components/consent-actions';
|
|
41
|
+
import styles from '@c15t/ui/styles/components/iab-consent-banner';
|
|
42
|
+
import buttonStyles from '@c15t/ui/styles/components/button';
|
|
43
|
+
import { getTextDirection } from '@c15t/ui/utils';
|
|
44
|
+
|
|
45
|
+
type IABAction = 'reject' | 'accept' | 'customize';
|
|
46
|
+
|
|
47
|
+
interface Props {
|
|
48
|
+
/** Which action gets the filled treatment. */
|
|
49
|
+
primaryButton?: IABAction;
|
|
50
|
+
/**
|
|
51
|
+
* Which consent models this banner responds to. Matches the `models`
|
|
52
|
+
* prop on the React, Svelte and Vue IAB banners.
|
|
53
|
+
*/
|
|
54
|
+
models?: string[];
|
|
55
|
+
/** Ship the DOM without the bundled stylesheet's class names. */
|
|
56
|
+
noStyle?: boolean;
|
|
57
|
+
/** Extra class on the banner root. */
|
|
58
|
+
class?: string;
|
|
59
|
+
/** Drop the "Secured by c15t" tag. */
|
|
60
|
+
hideBranding?: boolean;
|
|
61
|
+
/** Paint the backdrop that goes with the banner's `aria-modal`. */
|
|
62
|
+
scrollLock?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Render even when the server decided the banner should stay hidden.
|
|
65
|
+
* Useful for visual testing.
|
|
66
|
+
*/
|
|
67
|
+
force?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
primaryButton = 'customize',
|
|
72
|
+
models = ['iab'],
|
|
73
|
+
noStyle = false,
|
|
74
|
+
class: className,
|
|
75
|
+
hideBranding = false,
|
|
76
|
+
scrollLock,
|
|
77
|
+
force = false,
|
|
78
|
+
} = Astro.props;
|
|
79
|
+
|
|
80
|
+
const { c15t } = Astro.locals;
|
|
81
|
+
|
|
82
|
+
if (!c15t) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'@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`.'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { snapshot } = c15t;
|
|
89
|
+
const { blocking } = resolveConsentPresentation({
|
|
90
|
+
override: { scrollLock },
|
|
91
|
+
policy: snapshot.policyRule,
|
|
92
|
+
presentation: c15t.options.presentation,
|
|
93
|
+
surface: 'prompt',
|
|
94
|
+
});
|
|
95
|
+
const emitConfig = markConfigEmitted(Astro.locals);
|
|
96
|
+
const colorSchemeScript = emitConfig
|
|
97
|
+
? buildColorSchemeScript(c15t.options.colorScheme)
|
|
98
|
+
: '';
|
|
99
|
+
|
|
100
|
+
const summary = resolveIABBannerSummary(
|
|
101
|
+
snapshot.iab?.enabled === false ? null : (snapshot.iab ?? null)
|
|
102
|
+
);
|
|
103
|
+
// No vendor list, no banner: every line of copy below interpolates a count
|
|
104
|
+
// the summary derives from it. The browser boots, fetches one and mounts
|
|
105
|
+
// the client banner instead.
|
|
106
|
+
const shouldRender =
|
|
107
|
+
c15t.hasConsentUi &&
|
|
108
|
+
(force || c15t.shouldShowBanner) &&
|
|
109
|
+
summary.isReady &&
|
|
110
|
+
models.includes(snapshot.policyRule.model);
|
|
111
|
+
|
|
112
|
+
const fallback = defaultTranslationConfig.translations.en as TranslationsResponse;
|
|
113
|
+
const bundle = (snapshot.translations?.translations ?? fallback) as TranslationsResponse;
|
|
114
|
+
const iabFallback = fallback.iab as NonNullable<TranslationsResponse['iab']>;
|
|
115
|
+
const iab = (bundle.iab ?? iabFallback) as NonNullable<TranslationsResponse['iab']>;
|
|
116
|
+
const copy = {
|
|
117
|
+
accept: iab.common?.acceptAll ?? iabFallback.common.acceptAll,
|
|
118
|
+
andMore: iab.banner?.andMore ?? iabFallback.banner.andMore,
|
|
119
|
+
customize: iab.common?.customize ?? iabFallback.common.customize,
|
|
120
|
+
description: iab.banner?.description ?? iabFallback.banner.description,
|
|
121
|
+
legitimateInterestNotice:
|
|
122
|
+
iab.banner?.legitimateInterestNotice ??
|
|
123
|
+
iabFallback.banner.legitimateInterestNotice,
|
|
124
|
+
partnersLink: iab.banner?.partnersLink ?? iabFallback.banner.partnersLink,
|
|
125
|
+
reject: iab.common?.rejectAll ?? iabFallback.common.rejectAll,
|
|
126
|
+
scopeServiceSpecific:
|
|
127
|
+
iab.banner?.scopeServiceSpecific ?? iabFallback.banner.scopeServiceSpecific,
|
|
128
|
+
securedBy: bundle.common?.securedBy ?? fallback.common.securedBy,
|
|
129
|
+
title: iab.banner?.title ?? iabFallback.banner.title,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const textDirection = getTextDirection(snapshot.translations?.language);
|
|
133
|
+
|
|
134
|
+
const descriptionText = copy.description.replace(
|
|
135
|
+
'{partnerCount}',
|
|
136
|
+
String(summary.vendorCount)
|
|
137
|
+
);
|
|
138
|
+
const partnersLinkText = copy.partnersLink.replace(
|
|
139
|
+
'{count}',
|
|
140
|
+
String(summary.vendorCount)
|
|
141
|
+
);
|
|
142
|
+
// The link sits inside the sentence, so the copy is split around it rather
|
|
143
|
+
// than concatenated — a translation is free to put it anywhere.
|
|
144
|
+
const [descriptionBefore = descriptionText, descriptionAfter = ''] =
|
|
145
|
+
descriptionText.split(partnersLinkText);
|
|
146
|
+
|
|
147
|
+
const labels: Record<IABAction, string> = {
|
|
148
|
+
accept: copy.accept,
|
|
149
|
+
customize: copy.customize,
|
|
150
|
+
reject: copy.reject,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The shared button stylesheet keys its variants off `data-*`. `reject` is
|
|
155
|
+
* never filled even when it is the primary action, which is what the React
|
|
156
|
+
* and Svelte banners do too.
|
|
157
|
+
*/
|
|
158
|
+
const buttonAttrs = function buttonAttrs(action: IABAction) {
|
|
159
|
+
const primary = action === primaryButton;
|
|
160
|
+
return {
|
|
161
|
+
mode: primary && action !== 'reject' ? 'filled' : 'stroke',
|
|
162
|
+
variant: primary ? 'primary' : 'neutral',
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const rootClass = [noStyle ? '' : styles.root, noStyle ? '' : styles.bannerVisible, className]
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(' ');
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
{
|
|
172
|
+
emitConfig && colorSchemeScript && (
|
|
173
|
+
<script is:inline set:html={colorSchemeScript} />
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
{emitConfig && <script is:inline set:html={buildConfigScript(c15t.config)} />}
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
shouldRender && blocking && (
|
|
181
|
+
<div
|
|
182
|
+
aria-hidden="true"
|
|
183
|
+
class={noStyle ? '' : `${styles.overlay} ${styles.overlayVisible}`}
|
|
184
|
+
data-testid="iab-consent-banner-overlay"
|
|
185
|
+
/>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
shouldRender && (
|
|
191
|
+
<div
|
|
192
|
+
class={rootClass}
|
|
193
|
+
data-c15t-visible="true"
|
|
194
|
+
data-blocking={blocking ? 'true' : undefined}
|
|
195
|
+
data-position={textDirection === 'ltr' ? 'bottom-left' : 'bottom-right'}
|
|
196
|
+
data-testid="iab-consent-banner-root"
|
|
197
|
+
dir={textDirection}
|
|
198
|
+
lang={snapshot.translations?.language}
|
|
199
|
+
tabindex="-1"
|
|
200
|
+
>
|
|
201
|
+
<div class={noStyle ? '' : styles.cardShell}>
|
|
202
|
+
<Branding
|
|
203
|
+
branding={snapshot.branding}
|
|
204
|
+
data-testid="iab-consent-banner-branding"
|
|
205
|
+
hide={hideBranding}
|
|
206
|
+
hostname={Astro.url.hostname}
|
|
207
|
+
noStyle={noStyle}
|
|
208
|
+
securedBy={copy.securedBy}
|
|
209
|
+
variant="banner-tag"
|
|
210
|
+
/>
|
|
211
|
+
<div
|
|
212
|
+
aria-label={copy.title}
|
|
213
|
+
aria-modal={blocking ? 'true' : undefined}
|
|
214
|
+
class={noStyle ? '' : styles.card}
|
|
215
|
+
data-testid="iab-consent-banner-card"
|
|
216
|
+
role={blocking ? 'dialog' : 'region'}
|
|
217
|
+
>
|
|
218
|
+
<div
|
|
219
|
+
class={noStyle ? '' : styles.header}
|
|
220
|
+
data-testid="iab-consent-banner-header"
|
|
221
|
+
>
|
|
222
|
+
<h2 class={noStyle ? '' : styles.title}>{copy.title}</h2>
|
|
223
|
+
<p class={noStyle ? '' : styles.description}>
|
|
224
|
+
{descriptionBefore}
|
|
225
|
+
<button
|
|
226
|
+
class={noStyle ? '' : styles.partnersLink}
|
|
227
|
+
data-c15t-action="customize"
|
|
228
|
+
data-c15t-dialog="iab"
|
|
229
|
+
data-c15t-tab="vendors"
|
|
230
|
+
data-testid="iab-consent-banner-partners-link"
|
|
231
|
+
type="button"
|
|
232
|
+
>
|
|
233
|
+
{partnersLinkText}
|
|
234
|
+
</button>
|
|
235
|
+
{descriptionAfter}
|
|
236
|
+
</p>
|
|
237
|
+
<ul class={noStyle ? '' : styles.purposeList}>
|
|
238
|
+
{summary.displayItems.map((name) => (
|
|
239
|
+
<li>{name}</li>
|
|
240
|
+
))}
|
|
241
|
+
{summary.remainingCount > 0 && (
|
|
242
|
+
<li class={noStyle ? '' : styles.purposeMore}>
|
|
243
|
+
{copy.andMore.replace(
|
|
244
|
+
'{count}',
|
|
245
|
+
String(summary.remainingCount)
|
|
246
|
+
)}
|
|
247
|
+
</li>
|
|
248
|
+
)}
|
|
249
|
+
</ul>
|
|
250
|
+
<p class={noStyle ? '' : styles.legitimateInterestNotice}>
|
|
251
|
+
{`${copy.legitimateInterestNotice} ${copy.scopeServiceSpecific}`}
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
<div
|
|
255
|
+
class={[noStyle ? '' : actionStyles.actionRoot, noStyle ? '' : styles.footer]
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.join(' ')}
|
|
258
|
+
data-direction="row"
|
|
259
|
+
data-split="true"
|
|
260
|
+
data-testid="iab-consent-banner-footer"
|
|
261
|
+
>
|
|
262
|
+
<div
|
|
263
|
+
class={noStyle ? '' : actionStyles.actionGroup}
|
|
264
|
+
data-direction="row"
|
|
265
|
+
>
|
|
266
|
+
{(['reject', 'accept'] as const).map((action) => (
|
|
267
|
+
<button
|
|
268
|
+
class={noStyle ? '' : buttonStyles.button}
|
|
269
|
+
data-action={action}
|
|
270
|
+
data-c15t-action={action}
|
|
271
|
+
data-mode={noStyle ? undefined : buttonAttrs(action).mode}
|
|
272
|
+
data-size={noStyle ? undefined : 'small'}
|
|
273
|
+
data-testid={`iab-consent-banner-${action}-button`}
|
|
274
|
+
data-variant={
|
|
275
|
+
noStyle ? undefined : buttonAttrs(action).variant
|
|
276
|
+
}
|
|
277
|
+
type="button"
|
|
278
|
+
>
|
|
279
|
+
{labels[action]}
|
|
280
|
+
</button>
|
|
281
|
+
))}
|
|
282
|
+
</div>
|
|
283
|
+
<div
|
|
284
|
+
class={noStyle ? '' : actionStyles.actionGroup}
|
|
285
|
+
data-direction="row"
|
|
286
|
+
>
|
|
287
|
+
<button
|
|
288
|
+
class={noStyle ? '' : buttonStyles.button}
|
|
289
|
+
data-action="customize"
|
|
290
|
+
data-c15t-action="customize"
|
|
291
|
+
data-c15t-dialog="iab"
|
|
292
|
+
data-mode={noStyle ? undefined : buttonAttrs('customize').mode}
|
|
293
|
+
data-size={noStyle ? undefined : 'small'}
|
|
294
|
+
data-testid="iab-consent-banner-customize-button"
|
|
295
|
+
data-variant={
|
|
296
|
+
noStyle ? undefined : buttonAttrs('customize').variant
|
|
297
|
+
}
|
|
298
|
+
type="button"
|
|
299
|
+
>
|
|
300
|
+
{labels.customize}
|
|
301
|
+
</button>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
<script>
|
|
311
|
+
// One delegated listener, shared with `<ConsentBanner />` and every
|
|
312
|
+
// dialog trigger on the site. Astro bundles and dedupes it across pages.
|
|
313
|
+
import { attachBannerActions } from '@c15t/astro/client';
|
|
314
|
+
|
|
315
|
+
attachBannerActions();
|
|
316
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The IAB TCF preference centre, on its own chunk.
|
|
3
|
+
|
|
4
|
+
The TCF surface is the biggest thing either dialog can render — vendor
|
|
5
|
+
lists, purposes, stacks — and only a site that configured IAB ever shows
|
|
6
|
+
it. Keeping it in its own island means the preference-centre chunk that
|
|
7
|
+
every other site downloads does not carry it.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { IABConsentDialog } from '@c15t/svelte';
|
|
11
|
+
|
|
12
|
+
let { tab }: { tab?: 'purposes' | 'vendors' } = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<IABConsentDialog initialTab={tab} />
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IAB TCF preference centre, on its own chunk.
|
|
3
|
+
*
|
|
4
|
+
* The TCF surface is the biggest thing either dialog can render — vendor
|
|
5
|
+
* lists, purposes, stacks — and only a site that configured IAB ever shows
|
|
6
|
+
* it. Keeping it in its own island means the preference-centre chunk that
|
|
7
|
+
* every other site downloads does not carry it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ConsentDraftProvider } from '@c15t/react';
|
|
11
|
+
import { IABConsentDialog } from '@c15t/react/iab';
|
|
12
|
+
|
|
13
|
+
/** Props the dialog island passes down. */
|
|
14
|
+
export interface IABDialogSurfaceProps {
|
|
15
|
+
/** Which preference-centre tab to open on. */
|
|
16
|
+
tab?: 'purposes' | 'vendors';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const IABDialogSurface = ({ tab }: IABDialogSurfaceProps) => (
|
|
20
|
+
<ConsentDraftProvider>
|
|
21
|
+
<IABConsentDialog initialTab={tab} />
|
|
22
|
+
</ConsentDraftProvider>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export default IABDialogSurface;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The IAB TCF preference centre, on its own chunk.
|
|
3
|
+
|
|
4
|
+
The TCF surface is the biggest thing either dialog can render — vendor
|
|
5
|
+
lists, purposes, stacks — and only a site that configured IAB ever shows
|
|
6
|
+
it. Keeping it in its own island means the preference-centre chunk that
|
|
7
|
+
every other site downloads does not carry it.
|
|
8
|
+
-->
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import IABConsentDialog from '@c15t/vue/runtime/components/iab-consent-dialog.vue';
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
/** Which preference-centre tab to open on. */
|
|
14
|
+
tab?: 'purposes' | 'vendors';
|
|
15
|
+
}>();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<IABConsentDialog :initial-tab="props.tab" />
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The on-demand dialog island.
|
|
3
|
+
|
|
4
|
+
Mounted with Svelte 5's `mount()` the first time something opens a
|
|
5
|
+
dialog — never with `client:load` — so a visitor who never opens the
|
|
6
|
+
preference centre downloads no framework code at all.
|
|
7
|
+
|
|
8
|
+
The provider renders against the page's runtime rather than building one
|
|
9
|
+
of its own: Astro islands cannot see each other's context, so the kernel
|
|
10
|
+
has to be owned outside the component tree. The provider borrows it and
|
|
11
|
+
leaves `start()`/`dispose()` to the owner.
|
|
12
|
+
-->
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import type { ConsentRuntime } from '@c15t/core/runtime';
|
|
15
|
+
import { ConsentDialog, ConsentManagerProvider } from '@c15t/svelte';
|
|
16
|
+
import type { Component } from 'svelte';
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
options,
|
|
20
|
+
runtime,
|
|
21
|
+
kind = 'preferences',
|
|
22
|
+
tab,
|
|
23
|
+
}: {
|
|
24
|
+
options: Record<string, unknown>;
|
|
25
|
+
runtime: ConsentRuntime;
|
|
26
|
+
kind?: 'preferences' | 'iab';
|
|
27
|
+
/** Which IAB preference-centre tab to open on. */
|
|
28
|
+
tab?: 'purposes' | 'vendors';
|
|
29
|
+
} = $props();
|
|
30
|
+
|
|
31
|
+
// The TCF surface is the larger half of this island and only an IAB site
|
|
32
|
+
// ever opens it, so it arrives on its own chunk.
|
|
33
|
+
let IABDialog = $state<Component | null>(null);
|
|
34
|
+
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (kind !== 'iab' || IABDialog) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
void (async () => {
|
|
40
|
+
const module = await import('./iab-dialog-surface.svelte');
|
|
41
|
+
IABDialog = module.default as Component;
|
|
42
|
+
})();
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<ConsentManagerProvider
|
|
47
|
+
{runtime}
|
|
48
|
+
options={options as never}
|
|
49
|
+
>
|
|
50
|
+
{#if kind === 'iab'}
|
|
51
|
+
{#if IABDialog}
|
|
52
|
+
<IABDialog {tab} />
|
|
53
|
+
{/if}
|
|
54
|
+
{:else}
|
|
55
|
+
<ConsentDialog />
|
|
56
|
+
{/if}
|
|
57
|
+
</ConsentManagerProvider>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The on-demand React dialog island.
|
|
3
|
+
*
|
|
4
|
+
* Mounted with `createRoot()` the first time something opens a dialog —
|
|
5
|
+
* never with `client:load` — so a visitor who never opens the preference
|
|
6
|
+
* centre downloads no React from us at all.
|
|
7
|
+
*
|
|
8
|
+
* The provider renders against the page's runtime rather than building one
|
|
9
|
+
* of its own: Astro islands cannot see each other's context, so the kernel
|
|
10
|
+
* has to be owned outside the component tree. `<ConsentProvider runtime>`
|
|
11
|
+
* borrows it and leaves `start()`/`dispose()` to the owner.
|
|
12
|
+
*
|
|
13
|
+
* `<ConsentDraftProvider>` is what makes Save work. `useConsentDraft()`
|
|
14
|
+
* falls back to a draft per hook call when no provider is in scope, so the
|
|
15
|
+
* category toggles and the Save button would each stage into their own
|
|
16
|
+
* copy: the switch would flip, and the save would commit nothing.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ConsentRuntime } from '@c15t/core/runtime';
|
|
20
|
+
import {
|
|
21
|
+
ConsentDialog,
|
|
22
|
+
ConsentDraftProvider,
|
|
23
|
+
ConsentProvider,
|
|
24
|
+
} from '@c15t/react';
|
|
25
|
+
import { lazy, Suspense } from 'react';
|
|
26
|
+
|
|
27
|
+
import type { DialogPresentationOptions } from '../../ui/provider-props';
|
|
28
|
+
|
|
29
|
+
// The TCF surface is the larger half of this island and only an IAB site
|
|
30
|
+
// ever opens it, so it arrives on its own chunk.
|
|
31
|
+
const IABDialogSurface = lazy(() => import('./iab-dialog-surface'));
|
|
32
|
+
|
|
33
|
+
/** Props the React dialog adapter passes in. */
|
|
34
|
+
export interface ConsentDialogSurfaceProps {
|
|
35
|
+
/** The page-level runtime. The provider borrows it, it does not own it. */
|
|
36
|
+
runtime: ConsentRuntime;
|
|
37
|
+
/** Presentation options forwarded to the provider. */
|
|
38
|
+
options: DialogPresentationOptions;
|
|
39
|
+
/** Which dialog to render. */
|
|
40
|
+
kind?: 'preferences' | 'iab';
|
|
41
|
+
/** Which IAB preference-centre tab to open on. */
|
|
42
|
+
tab?: 'purposes' | 'vendors';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ConsentDialogSurface = ({
|
|
46
|
+
runtime,
|
|
47
|
+
options,
|
|
48
|
+
kind = 'preferences',
|
|
49
|
+
tab,
|
|
50
|
+
}: ConsentDialogSurfaceProps) => (
|
|
51
|
+
<ConsentProvider
|
|
52
|
+
runtime={runtime}
|
|
53
|
+
options={options}
|
|
54
|
+
>
|
|
55
|
+
{kind === 'iab' ? (
|
|
56
|
+
<Suspense fallback={null}>
|
|
57
|
+
<IABDialogSurface tab={tab} />
|
|
58
|
+
</Suspense>
|
|
59
|
+
) : (
|
|
60
|
+
<ConsentDraftProvider>
|
|
61
|
+
<ConsentDialog />
|
|
62
|
+
</ConsentDraftProvider>
|
|
63
|
+
)}
|
|
64
|
+
</ConsentProvider>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
export default ConsentDialogSurface;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The on-demand Vue dialog island.
|
|
3
|
+
|
|
4
|
+
Mounted as its own Vue app the first time something opens a dialog —
|
|
5
|
+
never with `client:load` — so a visitor who never opens the preference
|
|
6
|
+
centre downloads no consent UI at all.
|
|
7
|
+
|
|
8
|
+
The adapter installs `c15tVue` with the page's runtime before mounting
|
|
9
|
+
this component: Astro islands cannot see each other's provides, so the
|
|
10
|
+
kernel has to be owned outside the app. The plugin borrows it and leaves
|
|
11
|
+
`start()`/`dispose()` to the owner.
|
|
12
|
+
-->
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import ConsentManager from '@c15t/vue/runtime/components/consent-manager.vue';
|
|
15
|
+
import { defineAsyncComponent } from 'vue';
|
|
16
|
+
|
|
17
|
+
withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
kind?: 'preferences' | 'iab';
|
|
20
|
+
/** Which IAB preference-centre tab to open on. */
|
|
21
|
+
tab?: 'purposes' | 'vendors';
|
|
22
|
+
}>(),
|
|
23
|
+
{ kind: 'preferences', tab: undefined }
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// The TCF surface is the larger half of this island and only an IAB site
|
|
27
|
+
// ever opens it, so it arrives on its own chunk.
|
|
28
|
+
const IABDialogSurface = defineAsyncComponent(
|
|
29
|
+
() => import('./iab-dialog-surface.vue')
|
|
30
|
+
);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<IABDialogSurface
|
|
35
|
+
v-if="kind === 'iab'"
|
|
36
|
+
:tab="tab"
|
|
37
|
+
/>
|
|
38
|
+
<ConsentManager v-else />
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A button that opens a consent dialog.
|
|
4
|
+
*
|
|
5
|
+
* Ships no JavaScript of its own — it carries `data-c15t-action` and the
|
|
6
|
+
* one delegated listener the banner already installed picks it up. Use it
|
|
7
|
+
* for the "Cookie preferences" link every footer needs.
|
|
8
|
+
*
|
|
9
|
+
* ```astro
|
|
10
|
+
* <ConsentDialogTrigger>Cookie preferences</ConsentDialogTrigger>
|
|
11
|
+
* <ConsentDialogTrigger kind="iab">Ad partners</ConsentDialogTrigger>
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Which dialog to open. */
|
|
16
|
+
kind?: 'preferences' | 'iab';
|
|
17
|
+
/** Extra class names. */
|
|
18
|
+
class?: string;
|
|
19
|
+
/** Render an `<a href="#">` instead of a `<button>`. */
|
|
20
|
+
as?: 'button' | 'a';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { kind = 'preferences', class: className, as = 'button' } = Astro.props;
|
|
24
|
+
const Tag = as;
|
|
25
|
+
// Hidden until a resolved rule owes consent UI: nothing to manage before a
|
|
26
|
+
// rule resolves, and nothing owed under a `none` rule with no rights. The
|
|
27
|
+
// browser client toggles `hidden` as the snapshot changes.
|
|
28
|
+
const hasConsentUi = Astro.locals.c15t?.hasConsentUi ?? true;
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<Tag
|
|
32
|
+
class={className}
|
|
33
|
+
data-c15t-action="customize"
|
|
34
|
+
data-c15t-dialog={kind}
|
|
35
|
+
data-c15t-surface="trigger"
|
|
36
|
+
data-testid="consent-dialog-trigger"
|
|
37
|
+
hidden={hasConsentUi ? undefined : true}
|
|
38
|
+
href={as === 'a' ? '#c15t-preferences' : undefined}
|
|
39
|
+
type={as === 'button' ? 'button' : undefined}
|
|
40
|
+
>
|
|
41
|
+
<slot />
|
|
42
|
+
</Tag>
|
|
43
|
+
|
|
44
|
+
<script>
|
|
45
|
+
import { attachBannerActions } from '@c15t/astro/client';
|
|
46
|
+
|
|
47
|
+
attachBannerActions();
|
|
48
|
+
</script>
|