@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.
Files changed (117) hide show
  1. package/AGENTS.md +86 -0
  2. package/SKILL.md +14 -0
  3. package/dist/api/handlers.js +91 -0
  4. package/dist/api/index.js +3 -0
  5. package/dist/api/init.js +10 -0
  6. package/dist/api/manifest-init.js +103 -0
  7. package/dist/api/manifest.js +10 -0
  8. package/dist/browser/iab.js +5 -0
  9. package/dist/browser/inline-scripts.js +47 -0
  10. package/dist/client.js +276 -0
  11. package/dist/index.js +5 -0
  12. package/dist/integration.js +170 -0
  13. package/dist/libs/cookies.js +9 -0
  14. package/dist/middleware-handler.js +31 -0
  15. package/dist/middleware.js +6 -0
  16. package/dist/mode.js +50 -0
  17. package/dist/server.js +291 -0
  18. package/dist/styles.css +2 -0
  19. package/dist/types.js +0 -0
  20. package/dist/ui/adapter.js +37 -0
  21. package/dist/ui/provider-props.js +12 -0
  22. package/dist/ui/react.js +34 -0
  23. package/dist/ui/svelte.js +36 -0
  24. package/dist/ui/vue.js +38 -0
  25. package/dist-types/api/handlers.d.ts +85 -0
  26. package/dist-types/api/index.d.ts +13 -0
  27. package/dist-types/api/init.d.ts +10 -0
  28. package/dist-types/api/manifest-init.d.ts +73 -0
  29. package/dist-types/api/manifest.d.ts +9 -0
  30. package/dist-types/browser/iab.d.ts +14 -0
  31. package/dist-types/browser/inline-scripts.d.ts +38 -0
  32. package/dist-types/client.d.ts +152 -0
  33. package/dist-types/index.d.ts +33 -0
  34. package/dist-types/integration.d.ts +53 -0
  35. package/dist-types/libs/cookies.d.ts +15 -0
  36. package/dist-types/middleware-handler.d.ts +35 -0
  37. package/dist-types/middleware.d.ts +20 -0
  38. package/dist-types/mode.d.ts +75 -0
  39. package/dist-types/server.d.ts +130 -0
  40. package/dist-types/types.d.ts +305 -0
  41. package/dist-types/ui/adapter.d.ts +122 -0
  42. package/dist-types/ui/provider-props.d.ts +37 -0
  43. package/dist-types/ui/react.d.ts +19 -0
  44. package/dist-types/ui/svelte.d.ts +21 -0
  45. package/dist-types/ui/vue.d.ts +19 -0
  46. package/docs/README.md +86 -0
  47. package/docs/assets/v3/brand-bar.png +0 -0
  48. package/docs/assets/v3/brand-card.png +0 -0
  49. package/docs/assets/v3/choice-wall.png +0 -0
  50. package/docs/assets/v3/mobile-card.png +0 -0
  51. package/docs/assets/v3/preferences.png +0 -0
  52. package/docs/customization/overview.md +45 -0
  53. package/docs/customization/recipes.md +79 -0
  54. package/docs/customization/slots.md +55 -0
  55. package/docs/customization/tokens.md +76 -0
  56. package/docs/customization/translations.md +49 -0
  57. package/docs/frameworks/astro/quickstart.md +186 -0
  58. package/docs/guides/consent-state.md +60 -0
  59. package/docs/guides/data-fetching.md +163 -0
  60. package/docs/guides/deployment-modes.md +63 -0
  61. package/docs/guides/troubleshooting.md +68 -0
  62. package/docs/guides/verify-consent.md +62 -0
  63. package/docs/integrations/adobe-analytics.md +294 -0
  64. package/docs/integrations/ahrefs-analytics.md +291 -0
  65. package/docs/integrations/amplitude.md +292 -0
  66. package/docs/integrations/building-integrations.md +47 -0
  67. package/docs/integrations/clearbit.md +291 -0
  68. package/docs/integrations/cloudflare-web-analytics.md +292 -0
  69. package/docs/integrations/crisp.md +294 -0
  70. package/docs/integrations/databuddy.md +311 -0
  71. package/docs/integrations/fathom-analytics.md +293 -0
  72. package/docs/integrations/google-maps.md +384 -0
  73. package/docs/integrations/google-tag-manager.md +306 -0
  74. package/docs/integrations/google-tag.md +305 -0
  75. package/docs/integrations/heap.md +292 -0
  76. package/docs/integrations/hightouch.md +293 -0
  77. package/docs/integrations/hotjar.md +292 -0
  78. package/docs/integrations/intercom.md +285 -0
  79. package/docs/integrations/linkedin-insights.md +291 -0
  80. package/docs/integrations/logrocket.md +293 -0
  81. package/docs/integrations/matomo-analytics.md +316 -0
  82. package/docs/integrations/meta-pixel.md +285 -0
  83. package/docs/integrations/microsoft-clarity.md +292 -0
  84. package/docs/integrations/microsoft-uet.md +291 -0
  85. package/docs/integrations/mixpanel-analytics.md +295 -0
  86. package/docs/integrations/openai-pixel.md +441 -0
  87. package/docs/integrations/overview.md +111 -0
  88. package/docs/integrations/pirsch.md +295 -0
  89. package/docs/integrations/plausible-analytics.md +295 -0
  90. package/docs/integrations/posthog.md +397 -0
  91. package/docs/integrations/promptwatch.md +291 -0
  92. package/docs/integrations/reddit-pixel.md +294 -0
  93. package/docs/integrations/rudderstack.md +328 -0
  94. package/docs/integrations/rybbit-analytics.md +296 -0
  95. package/docs/integrations/segment.md +292 -0
  96. package/docs/integrations/snapchat-pixel.md +293 -0
  97. package/docs/integrations/tiktok-pixel.md +291 -0
  98. package/docs/integrations/umami-analytics.md +295 -0
  99. package/docs/integrations/vercel-analytics.md +293 -0
  100. package/docs/integrations/x-pixel.md +291 -0
  101. package/docs/integrations/youtube.md +405 -0
  102. package/docs/upgrade-v3.md +334 -0
  103. package/package.json +184 -0
  104. package/src/components/branding.astro +152 -0
  105. package/src/components/consent-script.astro +34 -0
  106. package/src/components/iab-panel.astro +43 -0
  107. package/src/components/iab-prompt.astro +316 -0
  108. package/src/components/islands/iab-dialog-surface.svelte +15 -0
  109. package/src/components/islands/iab-dialog-surface.tsx +25 -0
  110. package/src/components/islands/iab-dialog-surface.vue +20 -0
  111. package/src/components/islands/panel-surface.svelte +57 -0
  112. package/src/components/islands/panel-surface.tsx +67 -0
  113. package/src/components/islands/panel-surface.vue +39 -0
  114. package/src/components/panel-trigger.astro +48 -0
  115. package/src/components/panel.astro +65 -0
  116. package/src/components/prompt-deferred.astro +54 -0
  117. 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>