@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,334 @@
1
+ ---
2
+ title: Upgrade to v3 policies
3
+ description: Migrate policy configuration, consent records, callbacks, and
4
+ custom transports to the v3 policy system.
5
+ group: reference
6
+ ---
7
+
8
+ ## Start with a policy rule
9
+
10
+ ```ts
11
+ import { policyRulePresets } from '@c15t/schema';
12
+ import { offline } from '@c15t/react';
13
+
14
+ const mode = offline({
15
+ policyRules: [policyRulePresets.europeOptIn()],
16
+ });
17
+ ```
18
+
19
+ Pass `mode` to `ConsentProvider`. For a backend integration, configure
20
+ `policyRules` on the backend and use `hosted({ url })` in the provider. Follow the
21
+ [React quickstart](https://c15t.com/docs/frameworks/react/quickstart),
22
+ [Next.js quickstart](https://c15t.com/docs/frameworks/next/quickstart), or
23
+ [JavaScript quickstart](https://c15t.com/docs/frameworks/javascript/quickstart) for a complete
24
+ integration.
25
+
26
+ The CLI no longer offers `offline-add-policy-packs`, which generated v2
27
+ configuration. For offline integrations, configure `offline({ policyRules })`
28
+ as shown above.
29
+
30
+ Replace legacy `policyPacks` and nested `consent` configuration with rules:
31
+
32
+ ```ts
33
+ import type { PolicyRule } from '@c15t/schema';
34
+
35
+ const policyRules = [
36
+ {
37
+ id: 'default-opt-in',
38
+ match: { fallback: true },
39
+ model: 'opt-in',
40
+ prompt: 'choice',
41
+ categories: ['measurement', 'marketing'],
42
+ scopeMode: 'strict',
43
+ },
44
+ ] satisfies PolicyRule[];
45
+ ```
46
+
47
+ `model` controls permission defaults. `prompt` controls whether the visitor must
48
+ make a choice, dismiss a notice, or see no prompt. Opt-in and IAB models require
49
+ `prompt: 'choice'`. Opt-out supports `choice`, `notice`, and `none`. The `none`
50
+ model permits optional categories in scope, allows only `prompt: 'none'`, owes
51
+ no rights, and renders no consent UI. v2's `none` model maps to it directly.
52
+
53
+ When `categories` selects only some optional categories, you must set
54
+ `scopeMode` explicitly. A strict scope blocks categories outside the rule. A permissive scope allows
55
+ those categories unless another restriction applies. An omitted scope, `['*']`,
56
+ or a list containing only `necessary` expands to the default optional categories.
57
+ `necessary` is always permitted.
58
+
59
+ `offline()` without `policyRules` resolves `recommendedPolicyRules()`, whose
60
+ Europe rule also matches a visitor with no country. A known US country with a
61
+ missing state uses US opt-out with GPC and persistent preferences; a missing
62
+ Canadian province stays strict opt-in. The last rule is `none` for other known
63
+ unmatched locations. Resolution exposes `matched`, `no-match`,
64
+ `unconfigured`, or `failed`; while nothing has matched, optional categories stay
65
+ denied and no consent surface renders. A missing or malformed policy never grants
66
+ optional permissions by itself.
67
+
68
+ ## Read the state you need
69
+
70
+ | Purpose | Snapshot field | React hook |
71
+ | ----------------------------------------------- | ---------------------- | --------------------------------------- |
72
+ | Gate scripts and optional features | `effectivePermissions` | `useConsent(category)`, `useConsents()` |
73
+ | Inspect recorded choices and confirmation times | `explicitChoice` | `useExplicitChoice()` |
74
+ | Decide whether to prompt | `promptRequirement` | `usePromptRequirement()` |
75
+ | Inspect the resolved rule | `policyRule` | `usePolicyRule()` |
76
+ | Inspect matching or failure | `resolution` | `usePolicyResolution()` |
77
+
78
+ An effective permission is not evidence of a grant. Under an opt-out rule it can
79
+ be true before a visitor acts. A notice dismissal updates `noticeDismissal` and
80
+ does not record consent. Global Privacy Control updates privacy signals and
81
+ configured opt-out directives without turning a browser signal into a choice.
82
+
83
+ ## Replace choice callbacks
84
+
85
+ ```tsx
86
+ <ConsentProvider
87
+ options={{
88
+ mode,
89
+ callbacks: {
90
+ onChoiceRecorded(event) {
91
+ console.log('Visitor recorded a choice', event);
92
+ },
93
+ onPermissionsChanged(event) {
94
+ console.log('Effective permissions changed', event);
95
+ },
96
+ },
97
+ }}
98
+ >
99
+ {children}
100
+ </ConsentProvider>
101
+ ```
102
+
103
+ Replace `onConsentSet` with `onChoiceRecorded` for explicit accept, reject, or
104
+ save actions. Use `onPermissionsChanged` for permission changes caused by a
105
+ choice, expiry, policy update, or privacy signal. Provider callbacks no longer
106
+ include `onBannerFetched`; use the resolved policy and pending state to render
107
+ loading UI.
108
+
109
+ Only `kernel.commands.save()` creates an explicit choice. `save('all')` accepts,
110
+ `save('none')` rejects, and `save({ marketing: false })` confirms only marketing.
111
+ A partial save keeps the other categories' confirmation times. Use
112
+ `commands.dismissNotice()` for the notice action and `kernel.hydrate()` to apply
113
+ validated records without recording an action.
114
+
115
+ ## Keep existing storage
116
+
117
+ The reader accepts valid v2 storage and translates it into per-category receipts.
118
+ It does not rewrite storage on startup. Existing denials continue to restrict
119
+ permissions; expired or incompatible positive receipts cannot restore grants.
120
+ The next explicit action writes the v3 record. Notice dismissal and privacy
121
+ opt-out directives have separate records.
122
+
123
+ Pass server-prepared `initialRecords`, `initialPolicyResolution`, and `now`
124
+ through the adapter's prefetch configuration. Do not rebuild consent from a
125
+ boolean `hasConsented` or copy effective permissions into explicit choice records.
126
+ Pending prefetch results cannot restore records after a clear or overwrite a
127
+ newer choice.
128
+
129
+ ## Update custom transports and backend clients
130
+
131
+ Use `KernelTransport` from `@c15t/core/transports`. Init returns a versioned
132
+ `policyResolution` with a matched rule and fingerprints, or an explicit
133
+ non-matched outcome. Use `mapInitOutputToInitResponse` for an HTTP init payload.
134
+ Do not return legacy `policy` or `policyDecision` fields.
135
+
136
+ Save requests contain the explicit choice and the categories confirmed by this
137
+ action. Return a `SaveResult` with `ok`; optional identity and record methods
138
+ must preserve the same receipt format. Forward the policy contract headers when
139
+ implementing an HTTP proxy. Keep client, schema, backend, and framework adapters
140
+ on compatible v3 versions.
141
+
142
+ Use the supplied hosted or manifest transports to capture policy evidence for
143
+ saves and retries. They preserve the action's policy, location, language, and
144
+ privacy signal inputs. A stale policy assertion is an error; it must not silently
145
+ save under a different rule. See the
146
+ [backend endpoint reference](https://c15t.com/docs/self-host/api/endpoints) for wire fields.
147
+
148
+ ## Shared runtime ownership
149
+
150
+ Astro and SvelteKit can create a runtime outside a component tree with
151
+ `createConsentRuntime` from `@c15t/core/runtime`. Pass that runtime to a
152
+ framework provider with its `runtime` prop. The owner calls `start()` after
153
+ mount and `dispose()` when the page no longer needs it. Borrowing providers
154
+ subscribe to the kernel and render UI without initializing or disposing it.
155
+
156
+ Runtime construction preserves the prepared server snapshot. Storage reads,
157
+ privacy-signal activation and script loading begin on `start()`. Pass server
158
+ records through `prefetch.initialRecords` with their evaluation time to keep
159
+ the first browser render consistent with the server. `runtime.clearRecords()`
160
+ clears both persisted and in-memory records.
161
+
162
+ Runtime callbacks use `onChoiceRecorded`, `onPermissionsChanged` and
163
+ `onError`. Hydrating records does not report a new visitor choice.
164
+
165
+ Astro's serializable offline descriptor accepts `policyRules`, just like the
166
+ other adapters' offline factories. Configure a preset or explicit rules;
167
+ omitting them keeps the conservative fallback.
168
+
169
+ ## Presentation corrections
170
+
171
+ Use `blocking` for backdrop, scroll locking and focus trapping together.
172
+ Choice banners default to non-blocking, choice walls always block, and notices
173
+ never block. A notice configured as a wall falls back to a floating card.
174
+ Preferences remain centered; `variant` and `position` apply only to prompts.
175
+
176
+ Replace reads of `uncoveredRights` with `preferenceControls`. The latter is a
177
+ list of additional preferences buttons recommended for the stock UI. It does
178
+ not verify disclosure or persistent access to policy rights.
179
+
180
+ Notice acknowledgement uses `common.acknowledge`, falling back to
181
+ `common.dismiss` for older translation bundles. It does not record a choice.
182
+
183
+ ## Astro notice acknowledgement
184
+
185
+ ```astro
186
+ ---
187
+ import ConsentBanner from '@c15t/astro/components/consent-banner.astro';
188
+ ---
189
+
190
+ <ConsentBanner dismissButtonText="Got it" />
191
+ ```
192
+
193
+ Astro's `ConsentBanner` accepts `dismissButtonText` as an optional string. It
194
+ labels the notice acknowledgement button, which defaults to
195
+ `common.acknowledge` and falls back to `common.dismiss` in older translation
196
+ bundles. Acknowledging a notice preserves the visitor's consent choices.
197
+
198
+ ## IAB blocking behavior
199
+
200
+ IAB banners and dialogs use the same `presentation.prompt.blocking` and
201
+ `presentation.preferences.blocking` settings as the other consent components.
202
+ A blocking surface shows a backdrop, traps focus and locks page scrolling.
203
+ Explicit `blocking` values take precedence over deprecated `scrollLock` and
204
+ `trapFocus` options. IAB components stay hidden without a matched policy,
205
+ including when a dialog receives `open={true}`.
206
+
207
+ The React consent dialog still blocks pointer interaction with the page when
208
+ `blocking` is true and you hide its backdrop with `overlay={false}`.
209
+
210
+ Legacy prompt options now control the whole blocking behavior. For example,
211
+ `scrollLock: true` alone enables a backdrop and focus trapping as well as
212
+ scroll locking. Set `blocking: false` explicitly for a non-modal banner.
213
+
214
+ Custom dialog backdrops follow the same rule. A dialog resolved as non-blocking
215
+ omits both the default backdrop and a supplied `overlay`. To retain a custom
216
+ backdrop when migrating from `trapFocus: false`, set
217
+ `presentation.preferences.blocking: true` and keep your `overlay` prop.
218
+
219
+ ## Next.js and TanStack Start renames
220
+
221
+ These names changed without deprecated aliases. `defineConsentConfig`,
222
+ `ConsentConfig` (the URL config) and `ConsentProvider` from `@c15t/react` are
223
+ unchanged.
224
+
225
+ Next.js (`c15t/next`, `c15t/next/server`, `c15t/next/pages`):
226
+
227
+ | v2 | v3 |
228
+ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
229
+ | `ConsentBoundary` | `ConsentRoot` |
230
+ | `ConsentBoundaryProps` | `ConsentRootProps` |
231
+ | `config={initialConsent}` (the visitor's resolved state) | `state={...}` |
232
+ | `consent={consentConfig}` (the `defineConsentConfig` result) | `config={consentConfig}` |
233
+ | `prefetchInitialConsent(options)` | `resolveConsent(options)` with `config` or `backendURL` |
234
+ | `readInitialConsentConfig(options)` | `resolveConsent(options)` without `config` or `backendURL` |
235
+ | `InitialConsentConfig`, and `KernelConfig` where it named the returned value | `ConsentState` |
236
+ | `PrefetchInitialConsentOptions` | `ResolveConsentOptions` |
237
+ | `ReadInitialConsentConfigOptions` | `ConsentRequestOptions` |
238
+ | Pages Router `readInitialConsentConfig(req, opts)` and `prefetchInitialConsent({ req, ... })` | `resolveConsent({ req, ... })` |
239
+
240
+ TanStack Start (`c15t/tanstack-start`, `c15t/tanstack-start/server`):
241
+
242
+ | v2 | v3 |
243
+ | ------------------------------------- | ---------------------------------------------- |
244
+ | `ConsentBoundary` | `ConsentRoot` |
245
+ | `config={config}` | `state={state}` |
246
+ | `ConsentConfig` (the returned state) | `ConsentState` |
247
+ | `prefetchInitialConsent` | `resolveConsent(options)` with `backendURL` |
248
+ | `readInitialConsentConfig` | `resolveConsent(options)` without `backendURL` |
249
+ | `createConsentConfigHandler(options)` | `createConsentStateHandler(options)` |
250
+ | `mergeInitIntoConsentConfig` | `mergeInitIntoConsentState` |
251
+
252
+ `consentLoaderOptions`, `backendURL`, `initRoute` and `DEFAULT_INIT_ROUTE` are
253
+ unchanged.
254
+
255
+ ## Next.js RSC banner removal
256
+
257
+ The `@c15t/nextjs/rsc` entry (`c15t/next/rsc`) and its `RscConsentBanner`,
258
+ `RscBannerGate` and `RscBannerActions` exports are gone. Imports of that
259
+ entry now fail to resolve. Use the regular banner for server rendering.
260
+ The retained benchmark results showed slightly faster banner visibility and
261
+ interaction for the experimental shell, so this removal should not be read
262
+ as a performance improvement.
263
+
264
+ Replace `<RscConsentBanner config={config} />` with `<ConsentBanner />` inside
265
+ the same `ConsentRoot`, importing it from `c15t/next` when you use the
266
+ umbrella package or from `@c15t/nextjs` when you depend on the scoped package
267
+ directly. With an awaited `resolveConsent` result, the server renders
268
+ the banner into the response, which is what the RSC variant was for. Move
269
+ `presentation` to `options.presentation` on `ConsentRoot`.
270
+
271
+ Two defaults differ from the removed shell. It rendered no branding link, so
272
+ pass `hideBranding` to keep that. It also styled only its root and action row
273
+ (through the stock `ConsentBanner.Root` and `PolicyActions`) and left the
274
+ card, title, description, footer and buttons without base classes, whereas
275
+ `ConsentBanner` merges the stock styles into every part. If that changes a
276
+ custom layout, do not reach for `noStyle` on the whole banner, which also
277
+ drops the root positioning and action-row layout the old shell kept; compose
278
+ the compound parts instead and pass `noStyle` only to the parts you styled
279
+ yourself.
280
+
281
+ `ConsentBanner` has no `classNames` or `children` props. Move each legacy
282
+ `classNames` key to the `ConsentRoot` `options.components` slots, which take
283
+ `{ className }`:
284
+
285
+ | `classNames` key | Replacement |
286
+ | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
287
+ | `root`, `card`, `title`, `footer` | `components.banner.root`, `.card`, `.title`, `.footer` |
288
+ | `description` | `components.description.banner` |
289
+ | `rights`, `rightLink` | `components.banner.rights`, `components.banner.rightLink` |
290
+ | `acceptButton`, `rejectButton`, `customizeButton`, `dismissButton` | No per-button slot. Use `components.button.primary` / `.secondary` for shared styles, or target `[data-action="accept"]`, `[data-action="reject"]`, `[data-action="customize"]` and `[data-action="dismiss"]` in CSS. |
291
+
292
+ Custom `children`, and any direct use of `RscBannerGate` or
293
+ `RscBannerActions`, map to the compound parts of `ConsentBanner` in a Client
294
+ Component. `RscBannerGate` becomes `ConsentBanner.Root`, which mounts only
295
+ while the policy owes a prompt and reopens on expiry. Remove the gate
296
+ `prompt` and `model` props; the root derives `data-prompt` and `data-model`
297
+ from the `ConsentRoot` policy. Rename `title` to `aria-label` to preserve its
298
+ accessible label. Keep `children`, `className`, `variant`, `position` and
299
+ `blocking` on the root. Add `disableAnimation` and `trapFocus={false}` to
300
+ retain the old gate defaults.
301
+
302
+ `RscBannerActions`
303
+ becomes `ConsentBanner.PolicyActions`, which renders the rights links and the
304
+ action row the policy requires; `ConsentBanner.Rights`, `RightLink`, `Footer`,
305
+ `FooterSubGroup` and the four buttons are available for finer control.
306
+ `PolicyActions` takes none of the old `acceptLabel`, `rejectLabel`,
307
+ `customizeLabel`, `dismissLabel`, `rightLabels` or `classNames` props: set
308
+ labels through the provider `i18n` translation overrides (`common.acceptAll`,
309
+ `common.rejectAll`, `common.customize`, `common.acknowledge`, `rights.*`), or
310
+ render `ConsentBanner.AcceptButton` and the other buttons with your own
311
+ children; `renderAction` on `PolicyActions` replaces one button; classes move
312
+ to the `components` slots listed in the table. Place the former `children`
313
+ inside `ConsentBanner.Card`:
314
+
315
+ ```tsx title="components/banner.tsx"
316
+ 'use client';
317
+
318
+ import { ConsentBanner } from 'c15t/next';
319
+
320
+ export function Banner() {
321
+ return (
322
+ <ConsentBanner.Root>
323
+ <ConsentBanner.Card>
324
+ <ConsentBanner.Header>
325
+ <ConsentBanner.Title />
326
+ <ConsentBanner.Description />
327
+ </ConsentBanner.Header>
328
+ <a href="/privacy">Privacy policy</a>
329
+ <ConsentBanner.PolicyActions />
330
+ </ConsentBanner.Card>
331
+ </ConsentBanner.Root>
332
+ );
333
+ }
334
+ ```
package/package.json ADDED
@@ -0,0 +1,184 @@
1
+ {
2
+ "name": "@c15t/astro",
3
+ "version": "3.0.0-alpha.0",
4
+ "description": "Astro consent management: server-rendered cookie banner, on-demand Svelte, React or Vue preference-centre islands, geo-aware middleware, and consent-gated script loading.",
5
+ "keywords": [
6
+ "astro",
7
+ "astro-integration",
8
+ "ccpa",
9
+ "cmp",
10
+ "consent",
11
+ "consent-banner",
12
+ "consent-management-platform",
13
+ "cookie-banner",
14
+ "gdpr",
15
+ "iab",
16
+ "islands",
17
+ "lgpd",
18
+ "privacy",
19
+ "react",
20
+ "ssr",
21
+ "svelte",
22
+ "tcf",
23
+ "typescript",
24
+ "vue"
25
+ ],
26
+ "homepage": "https://c15t.com/docs/frameworks/astro/quickstart",
27
+ "bugs": {
28
+ "url": "https://github.com/c15t/c15t/issues"
29
+ },
30
+ "license": "Apache-2.0",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/c15t/c15t.git",
34
+ "directory": "packages/astro"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "dist-types",
39
+ "src/components",
40
+ "docs",
41
+ "AGENTS.md",
42
+ "SKILL.md"
43
+ ],
44
+ "type": "module",
45
+ "sideEffects": [
46
+ "**/*.css"
47
+ ],
48
+ "main": "./dist/index.js",
49
+ "types": "./dist-types/index.d.ts",
50
+ "exports": {
51
+ ".": {
52
+ "types": "./dist-types/index.d.ts",
53
+ "import": "./dist/index.js",
54
+ "default": "./dist/index.js"
55
+ },
56
+ "./client": {
57
+ "types": "./dist-types/client.d.ts",
58
+ "import": "./dist/client.js",
59
+ "default": "./dist/client.js"
60
+ },
61
+ "./middleware": {
62
+ "types": "./dist-types/middleware.d.ts",
63
+ "import": "./dist/middleware.js",
64
+ "default": "./dist/middleware.js"
65
+ },
66
+ "./server": {
67
+ "types": "./dist-types/server.d.ts",
68
+ "import": "./dist/server.js",
69
+ "default": "./dist/server.js"
70
+ },
71
+ "./api": {
72
+ "types": "./dist-types/api/index.d.ts",
73
+ "import": "./dist/api/index.js",
74
+ "default": "./dist/api/index.js"
75
+ },
76
+ "./ui/svelte": {
77
+ "types": "./dist-types/ui/svelte.d.ts",
78
+ "import": "./dist/ui/svelte.js",
79
+ "default": "./dist/ui/svelte.js"
80
+ },
81
+ "./ui/react": {
82
+ "types": "./dist-types/ui/react.d.ts",
83
+ "import": "./dist/ui/react.js",
84
+ "default": "./dist/ui/react.js"
85
+ },
86
+ "./ui/vue": {
87
+ "types": "./dist-types/ui/vue.d.ts",
88
+ "import": "./dist/ui/vue.js",
89
+ "default": "./dist/ui/vue.js"
90
+ },
91
+ "./components/*": "./src/components/*",
92
+ "./islands/*": {
93
+ "svelte": "./src/components/islands/*",
94
+ "default": "./src/components/islands/*"
95
+ },
96
+ "./api/init": {
97
+ "types": "./dist-types/api/init.d.ts",
98
+ "import": "./dist/api/init.js",
99
+ "default": "./dist/api/init.js"
100
+ },
101
+ "./api/manifest": {
102
+ "types": "./dist-types/api/manifest.d.ts",
103
+ "import": "./dist/api/manifest.js",
104
+ "default": "./dist/api/manifest.js"
105
+ },
106
+ "./styles.css": "./dist/styles.css",
107
+ "./package.json": "./package.json",
108
+ "./components/consent-banner-deferred.astro": "./src/components/prompt-deferred.astro",
109
+ "./components/consent-banner.astro": "./src/components/prompt.astro",
110
+ "./components/consent-dialog-trigger.astro": "./src/components/panel-trigger.astro",
111
+ "./components/consent-dialog.astro": "./src/components/panel.astro",
112
+ "./components/iab-consent-banner.astro": "./src/components/iab-prompt.astro",
113
+ "./components/iab-consent-dialog.astro": "./src/components/iab-panel.astro",
114
+ "./components/islands/consent-dialog-surface.svelte": "./src/components/islands/panel-surface.svelte",
115
+ "./islands/consent-dialog-surface.svelte": "./src/components/islands/panel-surface.svelte",
116
+ "./components/islands/consent-dialog-surface.tsx": "./src/components/islands/panel-surface.tsx",
117
+ "./islands/consent-dialog-surface.tsx": "./src/components/islands/panel-surface.tsx",
118
+ "./components/islands/consent-dialog-surface.vue": "./src/components/islands/panel-surface.vue",
119
+ "./islands/consent-dialog-surface.vue": "./src/components/islands/panel-surface.vue"
120
+ },
121
+ "publishConfig": {
122
+ "access": "public"
123
+ },
124
+ "scripts": {
125
+ "build": "rslib build && bun ../../scripts/normalize-dist-types.mjs",
126
+ "build:docs": "bun ../../scripts/generate-package-docs.ts @c15t/astro",
127
+ "check-types": "tsc --noEmit",
128
+ "dev": "sh -c 'rslib build --no-dts --no-clean && rslib build --watch --no-dts --no-clean'",
129
+ "fmt": "bun oxfmt .",
130
+ "lint": "bun oxlint ./src",
131
+ "prepack": "bun run build:docs && bun ../../scripts/verify-package-artifacts.ts",
132
+ "test": "vitest run",
133
+ "test:watch": "vitest"
134
+ },
135
+ "dependencies": {
136
+ "@c15t/core": "3.0.0-alpha.0",
137
+ "@c15t/iab": "3.0.0-alpha.0",
138
+ "@c15t/schema": "3.0.0-alpha.0",
139
+ "@c15t/svelte": "3.0.0-alpha.0",
140
+ "@c15t/translations": "3.0.0-alpha.0",
141
+ "@c15t/ui": "3.0.0-alpha.0"
142
+ },
143
+ "devDependencies": {
144
+ "@c15t/conformance": "0.0.1",
145
+ "@c15t/typescript-config": "0.0.1",
146
+ "@c15t/vitest-config": "1.0.0",
147
+ "astro": "^5.16.0",
148
+ "jsdom": "^25.0.0",
149
+ "react": "^19.0.0",
150
+ "react-dom": "^19.0.0",
151
+ "svelte": "^5.0.0",
152
+ "typescript": "6.0.3",
153
+ "vue": "^3.5.0"
154
+ },
155
+ "peerDependencies": {
156
+ "@c15t/react": "^3.0.0-alpha.0",
157
+ "@c15t/vue": "^3.0.0-alpha.0",
158
+ "astro": "^5.0.0",
159
+ "react": "^18.0.0 || ^19.0.0",
160
+ "react-dom": "^18.0.0 || ^19.0.0",
161
+ "svelte": "^5.0.0",
162
+ "vue": "^3.5.0"
163
+ },
164
+ "peerDependenciesMeta": {
165
+ "@c15t/react": {
166
+ "optional": true
167
+ },
168
+ "@c15t/vue": {
169
+ "optional": true
170
+ },
171
+ "react": {
172
+ "optional": true
173
+ },
174
+ "react-dom": {
175
+ "optional": true
176
+ },
177
+ "svelte": {
178
+ "optional": true
179
+ },
180
+ "vue": {
181
+ "optional": true
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,152 @@
1
+ ---
2
+ /**
3
+ * The "Secured by c15t" tag.
4
+ *
5
+ * Server-rendered with the same markup, classes and `data-testid` as the
6
+ * Svelte and React banners, so the shared stylesheet positions it
7
+ * identically and cross-framework tests compare like with like.
8
+ *
9
+ * Rendered by `<ConsentBanner />`; a site turns it off with
10
+ * `<ConsentBanner hideBranding />`. Which brand it shows comes from the
11
+ * consent snapshot, not from a prop.
12
+ */
13
+ import type { ConsentSnapshot } from '@c15t/core';
14
+ import styles from '@c15t/ui/styles/components/branding';
15
+
16
+ type BrandingVariant = 'footer' | 'dialog-tag' | 'banner-tag';
17
+
18
+ interface Props {
19
+ /** Render nothing. */
20
+ hide?: boolean;
21
+ /** Ship the DOM without the bundled stylesheet's class names. */
22
+ noStyle?: boolean;
23
+ /** Which surface the tag belongs to. */
24
+ variant?: BrandingVariant;
25
+ /** The brand the server resolved. */
26
+ branding: ConsentSnapshot['branding'];
27
+ /** "Secured by", already resolved from the translation bundle. */
28
+ securedBy: string;
29
+ /** The host to attribute the referral to. */
30
+ hostname?: string;
31
+ /** Test id, so each surface keeps its own. */
32
+ 'data-testid'?: string;
33
+ }
34
+
35
+ const {
36
+ hide = false,
37
+ noStyle = false,
38
+ variant = 'banner-tag',
39
+ branding,
40
+ securedBy,
41
+ hostname,
42
+ 'data-testid': testId,
43
+ } = Astro.props;
44
+
45
+ const resolveBranding = function resolveBranding(
46
+ value: ConsentSnapshot['branding']
47
+ ): 'c15t' | 'inth' | 'none' {
48
+ if (value === 'none') {
49
+ return 'none';
50
+ }
51
+ if (value === 'inth' || value === 'consent') {
52
+ return 'inth';
53
+ }
54
+ return 'c15t';
55
+ };
56
+
57
+ const resolved = resolveBranding(branding);
58
+ const show = !hide && resolved !== 'none';
59
+ const refParam = hostname ? `?ref=${hostname}` : '';
60
+ const href =
61
+ resolved === 'inth'
62
+ ? `https://inth.com${refParam}`
63
+ : `https://c15t.com${refParam}`;
64
+
65
+ const className = noStyle
66
+ ? ''
67
+ : [
68
+ styles.branding,
69
+ variant === 'footer' ? '' : styles.brandingTag,
70
+ variant === 'dialog-tag' ? styles.brandingTagDialog : '',
71
+ variant === 'banner-tag' ? styles.brandingTagBanner : '',
72
+ ]
73
+ .filter(Boolean)
74
+ .join(' ');
75
+ ---
76
+
77
+ {
78
+ show && (
79
+ <a
80
+ class={className}
81
+ data-branding={resolved}
82
+ data-testid={testId}
83
+ data-variant={variant}
84
+ href={href}
85
+ >
86
+ <span class={noStyle ? '' : styles.brandingContent} data-slot="tag-content">
87
+ <span class={noStyle ? '' : styles.brandingCopy}>
88
+ <span class={noStyle ? '' : styles.brandingText}>{securedBy}</span>
89
+ </span>
90
+ {resolved === 'inth' ? (
91
+ <span
92
+ class={
93
+ noStyle
94
+ ? ''
95
+ : `${styles.brandingWordmark} ${styles.brandingInth}`
96
+ }
97
+ dir="ltr"
98
+ >
99
+ <svg
100
+ aria-hidden="true"
101
+ aria-labelledby="inth-logo"
102
+ fill="none"
103
+ viewBox="0 0 88 90"
104
+ xmlns="http://www.w3.org/2000/svg"
105
+ >
106
+ <title id="inth-logo">INTH</title>
107
+ <path
108
+ d="M40.9164 0V8.26444H27.6933V26.7966H40.9164V35.0608H6.15594V26.7966H19.3788V8.26444H6.15594V0H40.9164Z"
109
+ fill="currentColor"
110
+ />
111
+ <path
112
+ d="M72.1149 20.1264V0H80.0343V35.0608H74.2747L54.9798 14.8193V35.0608H47.0604V0H52.964L72.1149 20.1264Z"
113
+ fill="currentColor"
114
+ />
115
+ <path
116
+ clip-rule="evenodd"
117
+ d="M71.36 41.6H88V89.6H0V41.6H61.12V31.04L71.36 41.6ZM6.15594 48.0891V56.4034H19.1784V83.2H27.4428V56.4034H40.5656V48.0891H6.15594ZM47.0603 48.1391V83.2H55.3247V70.2441H71.7531V83.2H80.0675V48.1391H71.7531V61.9797H55.3247V48.1391H47.0603Z"
118
+ fill="currentColor"
119
+ fill-rule="evenodd"
120
+ />
121
+ </svg>
122
+ </span>
123
+ ) : (
124
+ <span
125
+ class={
126
+ noStyle
127
+ ? ''
128
+ : `${styles.brandingWordmark} ${styles.brandingC15T}`
129
+ }
130
+ dir="ltr"
131
+ >
132
+ <span class={noStyle ? '' : styles.brandingC15TMark}>
133
+ <svg
134
+ aria-hidden="true"
135
+ aria-labelledby="c15t-icon"
136
+ viewBox="0 0 446 445"
137
+ xmlns="http://www.w3.org/2000/svg"
138
+ >
139
+ <title id="c15t-icon">c15t</title>
140
+ <path
141
+ d="M223.178.313c39.064 0 70.732 31.668 70.732 70.732-.001 39.064-31.668 70.731-70.732 70.731-12.181 0-23.642-3.079-33.649-8.502l-55.689 55.689a70.267 70.267 0 0 1 5.574 13.441h167.531c8.695-29.217 35.762-50.523 67.804-50.523 39.064 0 70.731 31.668 70.731 70.732s-31.668 70.732-70.731 70.732c-32.042 0-59.108-21.306-67.803-50.523H139.413a70.417 70.417 0 0 1-7.888 17.396l54.046 54.046c10.893-6.851 23.786-10.815 37.605-10.815 39.064 0 70.732 31.669 70.732 70.733 0 39.064-31.668 70.731-70.732 70.731s-70.732-31.667-70.732-70.731c0-10.518 2.296-20.499 6.414-29.471l-57.78-57.78c-8.972 4.117-18.952 6.414-29.47 6.414-39.063 0-70.731-31.668-70.732-70.732 0-39.064 31.669-70.732 70.733-70.732 12.18 0 23.642 3.079 33.649 8.502l55.688-55.688c-5.423-10.007-8.502-21.469-8.502-33.65 0-39.064 31.668-70.733 70.732-70.733Zm0 343.555c-16.742 0-30.314 13.572-30.314 30.314 0 16.741 13.572 30.313 30.314 30.313s30.314-13.572 30.314-30.313c0-16.742-13.572-30.314-30.314-30.314ZM71.611 192.299c-16.742 0-30.315 13.572-30.315 30.314s13.573 30.314 30.315 30.314c16.741 0 30.313-13.572 30.313-30.314 0-16.741-13.572-30.314-30.313-30.314Zm303.138 0c-16.729 0-30.294 13.551-30.315 30.275l.001.039-.001.038c.021 16.725 13.586 30.276 30.315 30.276 16.741 0 30.313-13.572 30.313-30.314 0-16.741-13.572-30.314-30.313-30.314ZM223.178 40.73c-16.742 0-30.314 13.573-30.314 30.315s13.573 30.313 30.314 30.313c16.742 0 30.313-13.572 30.314-30.313 0-16.742-13.572-30.314-30.314-30.315Z"
142
+ fill="currentColor"
143
+ />
144
+ </svg>
145
+ </span>
146
+ <span class={noStyle ? '' : styles.brandingWordmarkLabel}>c15t</span>
147
+ </span>
148
+ )}
149
+ </span>
150
+ </a>
151
+ )
152
+ }