@c15t/react 3.1.0 → 3.2.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 (146) hide show
  1. package/AGENTS.md +35 -5
  2. package/README.md +2 -2
  3. package/dist/component-hooks/use-consent-gate.js +2 -0
  4. package/dist/component-hooks/use-consent-labels.js +2 -0
  5. package/dist/component-hooks/use-consent-text-direction.js +2 -0
  6. package/dist/component-hooks/use-vendors-listed-under.js +2 -0
  7. package/dist/components/consent-gate/atoms.js +1 -1
  8. package/dist/components/consent-gate/consent-gate.js +1 -1
  9. package/dist/components/iab-prompt/atoms/root.js +1 -1
  10. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  11. package/dist/components/preferences/atoms/button.js +1 -1
  12. package/dist/components/preferences/atoms/vendor-list.js +1 -1
  13. package/dist/components/prompt/atoms/root.js +1 -1
  14. package/dist/components/prompt/components.js +1 -1
  15. package/dist/components/prompt/policy-actions.js +1 -1
  16. package/dist/components/prompt/prompt.js +1 -1
  17. package/dist/components/prompt/surface-context.js +1 -1
  18. package/dist/components/prompt/use-banner-copy.js +1 -1
  19. package/dist/components/shared/streamed-surface.js +1 -1
  20. package/dist/headless.js +1 -1
  21. package/dist/index-parts/offline.js +1 -1
  22. package/dist/modes.js +1 -1
  23. package/dist/styles.tw3.css +2 -2
  24. package/dist/transports/offline.js +1 -1
  25. package/dist/version.js +1 -1
  26. package/dist-types/component-hooks/use-consent-gate.d.ts +102 -0
  27. package/dist-types/component-hooks/use-consent-labels.d.ts +32 -0
  28. package/dist-types/component-hooks/use-consent-text-direction.d.ts +22 -0
  29. package/dist-types/component-hooks/use-vendors-listed-under.d.ts +30 -0
  30. package/dist-types/components/prompt/atoms/root.d.ts +3 -3
  31. package/dist-types/components/prompt/prompt.d.ts +4 -3
  32. package/dist-types/components/prompt/surface-context.d.ts +1 -14
  33. package/dist-types/components/prompt/use-banner-copy.d.ts +8 -3
  34. package/dist-types/components/shared/streamed-surface.d.ts +47 -10
  35. package/dist-types/headless.d.ts +10 -1
  36. package/dist-types/hooks/use-iab-policy-guard.d.ts +17 -1
  37. package/dist-types/hooks/use-scroll-lock.d.ts +16 -2
  38. package/dist-types/index-parts/offline.d.ts +1 -1
  39. package/dist-types/modes.d.ts +1 -1
  40. package/dist-types/module-hooks/script-loader.d.ts +9 -0
  41. package/dist-types/provider.d.ts +3 -3
  42. package/dist-types/transports/offline.d.ts +2 -2
  43. package/dist-types/version.d.ts +1 -1
  44. package/docs/README.md +35 -5
  45. package/docs/concepts/choose-your-setup.md +9 -9
  46. package/docs/concepts/consent-state.md +81 -9
  47. package/docs/concepts/data-fetching.md +30 -23
  48. package/docs/concepts/modes.md +87 -35
  49. package/docs/customization/recipes.md +4 -4
  50. package/docs/customization/stylesheets.md +42 -3
  51. package/docs/customization/tailwind.md +17 -34
  52. package/docs/customization/translations.md +18 -12
  53. package/docs/frameworks/react/clear-on-revocation.md +3 -2
  54. package/docs/frameworks/react/components/consent-banner.md +3 -3
  55. package/docs/frameworks/react/components/consent-provider.md +29 -29
  56. package/docs/frameworks/react/gpp.md +1 -1
  57. package/docs/frameworks/react/headless.md +146 -19
  58. package/docs/frameworks/react/hooks.md +2 -1
  59. package/docs/frameworks/react/iab.md +8 -1
  60. package/docs/frameworks/react/quickstart.md +2 -2
  61. package/docs/frameworks/react/rendering.md +1 -1
  62. package/docs/frameworks/react/translations.md +9 -9
  63. package/docs/frameworks/react/troubleshooting.md +1 -1
  64. package/docs/frameworks/react/upgrade-v3.md +39 -32
  65. package/docs/frameworks/react/vendor-consent.md +5 -0
  66. package/docs/guides/banner-experiments.md +3 -3
  67. package/docs/guides/troubleshooting.md +3 -2
  68. package/docs/integrations/activecampaign.md +297 -0
  69. package/docs/integrations/ada.md +396 -0
  70. package/docs/integrations/adobe-analytics.md +2 -2
  71. package/docs/integrations/adobe-target.md +460 -0
  72. package/docs/integrations/adroll.md +336 -0
  73. package/docs/integrations/ahrefs-analytics.md +2 -2
  74. package/docs/integrations/amazon-ads.md +424 -0
  75. package/docs/integrations/amplitude.md +2 -2
  76. package/docs/integrations/appcues.md +368 -0
  77. package/docs/integrations/attentive.md +313 -0
  78. package/docs/integrations/building-integrations.md +29 -16
  79. package/docs/integrations/clay.md +305 -0
  80. package/docs/integrations/clearbit.md +3 -3
  81. package/docs/integrations/cloudflare-web-analytics.md +2 -2
  82. package/docs/integrations/cloudflare-zaraz.md +2 -2
  83. package/docs/integrations/crisp.md +2 -2
  84. package/docs/integrations/customer-io.md +369 -0
  85. package/docs/integrations/databuddy.md +127 -52
  86. package/docs/integrations/default-com.md +497 -0
  87. package/docs/integrations/dynamic-yield.md +373 -0
  88. package/docs/integrations/fathom-analytics.md +2 -2
  89. package/docs/integrations/front-chat.md +2 -2
  90. package/docs/integrations/fullstory.md +298 -0
  91. package/docs/integrations/gainsight-px.md +418 -0
  92. package/docs/integrations/google-maps.md +2 -2
  93. package/docs/integrations/google-tag-manager.md +59 -2
  94. package/docs/integrations/google-tag.md +2 -2
  95. package/docs/integrations/gorgias.md +365 -0
  96. package/docs/integrations/heap.md +2 -2
  97. package/docs/integrations/hightouch.md +2 -2
  98. package/docs/integrations/hotjar.md +40 -26
  99. package/docs/integrations/hubspot.md +529 -0
  100. package/docs/integrations/insider.md +550 -0
  101. package/docs/integrations/intercom.md +2 -2
  102. package/docs/integrations/klaviyo.md +2 -2
  103. package/docs/integrations/linkedin-insights.md +2 -2
  104. package/docs/integrations/liveintent.md +327 -0
  105. package/docs/integrations/logrocket.md +2 -2
  106. package/docs/integrations/lucky-orange.md +336 -0
  107. package/docs/integrations/marketo.md +309 -0
  108. package/docs/integrations/matomo-analytics.md +2 -2
  109. package/docs/integrations/meta-pixel.md +2 -2
  110. package/docs/integrations/microsoft-clarity.md +2 -2
  111. package/docs/integrations/microsoft-uet.md +2 -2
  112. package/docs/integrations/mixpanel-analytics.md +2 -2
  113. package/docs/integrations/one-dollar-stats.md +2 -2
  114. package/docs/integrations/openai-pixel.md +2 -2
  115. package/docs/integrations/optimizely.md +411 -0
  116. package/docs/integrations/oracle-eloqua.md +301 -0
  117. package/docs/integrations/overview.md +128 -81
  118. package/docs/integrations/pendo.md +339 -0
  119. package/docs/integrations/pinterest-tag.md +2 -2
  120. package/docs/integrations/pirsch.md +2 -2
  121. package/docs/integrations/plausible-analytics.md +2 -2
  122. package/docs/integrations/posthog.md +2 -2
  123. package/docs/integrations/promptwatch.md +2 -2
  124. package/docs/integrations/rb2b.md +363 -0
  125. package/docs/integrations/reddit-pixel.md +2 -2
  126. package/docs/integrations/rudderstack.md +2 -2
  127. package/docs/integrations/rybbit-analytics.md +2 -2
  128. package/docs/integrations/salesforce-pardot.md +342 -0
  129. package/docs/integrations/segment.md +2 -2
  130. package/docs/integrations/sentry.md +2 -2
  131. package/docs/integrations/shopify.md +323 -0
  132. package/docs/integrations/snapchat-pixel.md +2 -2
  133. package/docs/integrations/statable-analytics.md +345 -0
  134. package/docs/integrations/the-trade-desk.md +328 -0
  135. package/docs/integrations/tiktok-pixel.md +2 -2
  136. package/docs/integrations/umami-analytics.md +2 -2
  137. package/docs/integrations/vector.md +333 -0
  138. package/docs/integrations/vercel-analytics.md +2 -2
  139. package/docs/integrations/vercel-speed-insights.md +7 -2
  140. package/docs/integrations/wisepops.md +416 -0
  141. package/docs/integrations/x-pixel.md +2 -2
  142. package/docs/integrations/youtube.md +2 -2
  143. package/docs/integrations/zendesk.md +359 -0
  144. package/package.json +10 -32
  145. package/dist/components/prompt/resolve-banner-primary-actions.js +0 -1
  146. package/dist-types/components/prompt/resolve-banner-primary-actions.d.ts +0 -14
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Consent modes
3
- description: Reference for manifest(), hosted() and offline() in every c15t
3
+ description: Reference for manifest(), hosted() and local() in every c15t
4
4
  framework package, where each one resolves the visitor's policy, what it adds
5
5
  to first-load JavaScript, the build-time manifest fetch and its failure
6
6
  policy, and the backend URL variable each framework reads.
@@ -11,7 +11,8 @@ group: concepts
11
11
 
12
12
  A mode decides how c15t finds the policy that applies to a visitor. Every
13
13
  framework package exports the same three factories, `manifest()`, `hosted()`
14
- and `offline()`. Consent choices go to the backend in `manifest()` and
14
+ and `local()`. `local()` was called `offline()` before c15t 3.2, and the old
15
+ name still works until c15t v4. Consent choices go to the backend in `manifest()` and
15
16
  `hosted()`, whether that backend is [Inth](https://inth.com) or a
16
17
  [self-hosted c15t backend](https://c15t.com/docs/self-host/quickstart).
17
18
 
@@ -19,7 +20,7 @@ and `offline()`. Consent choices go to the backend in `manifest()` and
19
20
  | ------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
21
  | `manifest()`, the default | Your server, from the policy the build downloaded. With `resolve: 'browser'`, or in a single-page app, the browser. | None to resolve the policy, unless the browser needs a location it doesn't have. Choices post to `/subjects`. | Server-rendered apps. Rebuild to ship policy changes, or set `source: 'runtime'`. |
21
22
  | `hosted()` | The backend's `GET /init` | One `/init` per page that resolves consent. | Single-page apps and static hosting without a server, where the backend reads each visitor's location from the request. Policy edits apply without a rebuild. |
22
- | `offline()` | The browser, from rules in your bundle | None. Choices stay in the browser. | Local development, tests and demos. Not recommended for production environments. |
23
+ | `local()` | The browser, from rules in your bundle | None. Choices stay in the browser, and c15t keeps no server-side record of consent. | Local development, tests and demos. Not recommended for production environments. |
23
24
 
24
25
  `manifest()` is the default in Next.js, TanStack Start, Nuxt, Astro and
25
26
  SvelteKit. React, Vue, Svelte and `@c15t/browser` have no default: pass a
@@ -38,18 +39,18 @@ custom transport and same-origin proxies.
38
39
  There are two kinds of factory, and each framework package exports the kind it
39
40
  needs under the same three names.
40
41
 
41
- | Framework | Import from | Pass the mode to | Kind |
42
- | -------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------- |
43
- | Next.js | `c15t/next` | `mode` in `c15t.config.ts` | Data |
44
- | TanStack Start | `c15t/tanstack-start` | `createConsentStateHandler({ mode })` | Data |
45
- | Nuxt | `c15t/vue` | `c15t.mode` in `nuxt.config.ts` | Data |
46
- | Astro | `c15t/astro` | `c15t({ mode })` in `astro.config.mjs` | Data |
47
- | SvelteKit | `@c15t/svelte/kit` | `c15tHandle({ mode })` in `src/hooks.server.ts` | Data |
48
- | React | `c15t/react` | `ConsentProvider`'s `options.mode` | Transport |
49
- | Vue | `c15t/vue/vue-plugin` | `app.use(c15tVue, { mode })` | Transport |
50
- | Svelte | `@c15t/svelte` | `<ConsentProvider mode={...}>` | Transport |
51
- | JavaScript | `@c15t/browser` | `init({ mode })` | Transport |
52
- | Script tag | none | The bundle file: `c15t.js` is hosted, `c15t.offline.js` offline. The headless and IAB scripts also take `data-mode="manifest"`. | Name |
42
+ | Framework | Import from | Pass the mode to | Kind |
43
+ | -------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------- |
44
+ | Next.js | `c15t/next` | `mode` in `c15t.config.ts` | Data |
45
+ | TanStack Start | `c15t/tanstack-start` | `createConsentStateHandler({ mode })` | Data |
46
+ | Nuxt | `c15t/vue` | `c15t.mode` in `nuxt.config.ts` | Data |
47
+ | Astro | `c15t/astro` | `c15t({ mode })` in `astro.config.mjs` | Data |
48
+ | SvelteKit | `c15t/svelte/kit` | `c15tHandle({ mode })` in `src/hooks.server.ts` | Data |
49
+ | React | `c15t/react` | `ConsentProvider`'s `options.mode` | Transport |
50
+ | Vue | `c15t/vue/vue-plugin` | `app.use(c15tVue, { mode })` | Transport |
51
+ | Svelte | `c15t/svelte` | `<ConsentProvider mode={...}>` | Transport |
52
+ | JavaScript | `@c15t/browser` | `init({ mode })` | Transport |
53
+ | Script tag | none | The bundle file: `c15t.js` is hosted, `c15t.local.js` local. The headless and IAB scripts also take `data-mode="manifest"`. | Name |
53
54
 
54
55
  **Data factories** return a plain object such as `{ type: 'manifest' }`.
55
56
  Server-rendered frameworks read their config on the server and in the
@@ -86,7 +87,7 @@ holds your policy rules and copy, and no visitor data.
86
87
  | `inputs` | none | `{ country, region }` when the page already knows the visitor's location, for example from an edge worker. Browser resolution only. |
87
88
 
88
89
  The transport `manifest()` from `c15t/react`, `c15t/vue/vue-plugin`,
89
- `@c15t/svelte` and `@c15t/browser` always resolves in the browser, so it has
90
+ `c15t/svelte` and `@c15t/browser` always resolve in the browser, so they have
90
91
  no `resolve` option. It also takes `backendURL`, `headers`, `credentials`,
91
92
  `domain`, `fetch` and `initFallback`. With no options it reads the snapshot
92
93
  and the backend URL your build integration downloaded. With `manifestURL`,
@@ -98,7 +99,7 @@ know the country or region, browser resolution asks `geoURL`, then the
98
99
  backend's `/init`. Set `initFallback: false` to resolve for an unknown location
99
100
  instead. A policy with location rules therefore saves no request in a
100
101
  single-page app unless you pass `inputs` or `geoURL`. The `consentManifest()`
101
- plugins from `c15t/build`, `c15t/vue/vite` and `@c15t/svelte/vite` warn
102
+ plugins from `c15t/build`, `c15t/vue/vite` and `c15t/svelte/vite` warn
102
103
  about this when a single-page app build bundles such a policy.
103
104
 
104
105
  ## hosted()
@@ -122,36 +123,87 @@ against and the backend can reject a save made against a stale policy.
122
123
  `backendURL`. The framework exports fill it in from your config or your
123
124
  build.
124
125
 
125
- ## offline()
126
+ ## local()
126
127
 
127
128
  Not recommended for production environments.
128
129
 
129
- `offline()` resolves policy rules in the browser and stores choices in browser
130
- storage. Nothing reaches a backend, so there is no consent record, no
131
- cross-device history and no IP geolocation.
130
+ `local()` resolves policy rules in the browser and stores choices in browser
131
+ storage. Nothing reaches a backend, so c15t keeps no server-side record of
132
+ consent. There is also no cross-device history, and with no IP geolocation
133
+ every visitor gets the policy for an unknown location unless you pass
134
+ `overrides`.
132
135
 
133
- | Option | Default | Behavior |
134
- | ------------- | ------------------------- | ----------------------------------------------------------- |
135
- | `policyRules` | the recommended rule pack | Rules to resolve. Passing rules replaces the pack entirely. |
136
+ | Option | Default | Behavior |
137
+ | ---------------------- | ------------------------- | ------------------------------------------------------------------------------ |
138
+ | `policyRules` | the recommended rule pack | Rules to resolve. Passing rules replaces the pack entirely. |
139
+ | `acknowledgeNoRecords` | `false` | `true` hides the development warning described below. It changes nothing else. |
136
140
 
137
141
  Unknown country and region are real inputs: test the missing-location case
138
142
  against your rules. See [policy rules](./policies.md).
139
143
 
144
+ ### The development warning
145
+
146
+ In a development build, local mode logs one `console.warn` per page load:
147
+
148
+ ```txt
149
+ [c15t] Local mode keeps consent only in this browser. c15t keeps no
150
+ server-side record of consent. If you rely on consent under GDPR, Article
151
+ 7(1) requires you to demonstrate it. Keep your own consent records or use
152
+ a c15t backend. Local mode also has no geolocation, so every visitor gets
153
+ the same policy. Use it for development and tests. For production, see
154
+ https://c15t.com/docs/concepts/modes
155
+ Set acknowledgeNoRecords: true to hide this message.
156
+ ```
157
+
158
+ Production builds never log it, and it never blocks the banner or a save.
159
+ The script-tag files are production builds, so `c15t.local.js`, and
160
+ `c15t.headless.js` or `c15t.iab.js` without a backend URL, log it on
161
+ development hosts instead:
162
+ `localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0` and names ending in
163
+ `.localhost`, `.local` or `.test`. A script tag hides it with
164
+ `data-acknowledge-no-records`.
165
+
166
+ Set `acknowledgeNoRecords: true` once you have decided a browser-only record is
167
+ enough, such as in tests, or on a site that keeps its own record through
168
+ [callbacks](https://c15t.com/docs/frameworks/javascript/callbacks) or a [`custom()` transport](./data-fetching.md#can-i-provide-my-own-transport):
169
+
170
+ ```ts
171
+ import { local } from 'c15t/react';
172
+
173
+ const mode = local({ acknowledgeNoRecords: true });
174
+ ```
175
+
176
+ c15t v4 removes the `offline()` name and makes `acknowledgeNoRecords: true`
177
+ required in `local()`'s type.
178
+
179
+ ### What a consent record proves
180
+
181
+ A consent record is the backend's copy of a choice: which categories the
182
+ visitor granted or denied, when, and against which policy and copy. Under the
183
+ GDPR, the business that relies on consent must be able to show that the
184
+ visitor gave it (Article 7(1)). Browser storage alone cannot do that: the
185
+ visitor can clear it, and it never leaves their device.
186
+
187
+ `manifest()` and `hosted()` post each choice to the backend, which keeps the
188
+ record. To keep records without Inth, run a
189
+ [self-hosted backend](https://c15t.com/docs/self-host/overview), or send each choice to your
190
+ own API with `custom()`.
191
+
140
192
  ## What each mode adds to first-load JavaScript
141
193
 
142
194
  In a server-rendered framework, the browser receives the resolved state with
143
195
  the page. It only needs the code to save a choice, and loads the rest when it
144
196
  runs:
145
197
 
146
- | Mode | First-load JavaScript | Loaded with `import()` when it runs |
147
- | ---------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
148
- | `manifest()` | The code that saves choices and builds an init request | The hosted init path, if the browser has to resolve consent again |
149
- | `manifest({ resolve: 'browser' })` | The code that saves choices | The browser resolver, English copy and the fetch of `${routePrefix}/manifest` or `${backendURL}/manifest`. Other languages load per language. |
150
- | `hosted()` | The code that saves choices | The hosted init path |
151
- | `offline()` | Nothing mode-specific | The offline transport, the rule pack and its copy |
198
+ | Mode | First-load JavaScript | Loaded with `import()` when it runs |
199
+ | ---------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
200
+ | `manifest()` | The code that saves choices and builds an init request | The hosted init path, if the browser has to resolve consent again |
201
+ | `manifest({ resolve: 'browser' })` | The code that saves choices | The browser resolver, English copy and the fetch of `${routePrefix}/manifest` or `${backendURL}/manifest`, unless the server state already holds the policy. Other languages load per language. |
202
+ | `hosted()` | The code that saves choices | The hosted init path |
203
+ | `local()` | Nothing mode-specific | The local transport, the rule pack and its copy |
152
204
 
153
205
  With `manifest()` resolved on the server, the manifest snapshot, the resolver,
154
- the offline rule pack and other languages stay out of the browser bundle.
206
+ the local rule pack and other languages stay out of the browser bundle.
155
207
 
156
208
  In a single-page app the bundle contains the mode you imported:
157
209
 
@@ -159,7 +211,7 @@ In a single-page app the bundle contains the mode you imported:
159
211
  | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
160
212
  | `manifest()` | The snapshot, when the build downloaded one, and the code that saves choices. The resolver starts loading when `manifest()` runs, unless the policy needs a location the page does not know. English copy is bundled; other languages load the first time a visitor needs one. |
161
213
  | `hosted()` | The hosted transport |
162
- | `offline()` | The offline transport, the recommended rule pack and its copy |
214
+ | `local()` | The local transport, the recommended rule pack and its copy |
163
215
 
164
216
  ## Set the backend URL
165
217
 
@@ -204,7 +256,7 @@ build integration downloads from `${backendURL}/manifest`:
204
256
  | TanStack Start | `consentManifest()` from `c15t/tanstack-start/build` | `@c15t/tanstack-start/build` |
205
257
  | React and JavaScript | `consentManifest()` from `c15t/build` | `@c15t/core/build` |
206
258
  | Vue | `consentManifest()` from `c15t/vue/vite` | `@c15t/vue/vite` |
207
- | Svelte and SvelteKit | `consentManifest()` from `@c15t/svelte/vite` | `@c15t/svelte/vite` |
259
+ | Svelte and SvelteKit | `consentManifest()` from `c15t/svelte/vite` | `@c15t/svelte/vite` |
208
260
  | Nuxt | the `c15t/vue` module | `@c15t/vue` |
209
261
  | Astro | the `c15t()` integration | `@c15t/astro` |
210
262
 
@@ -266,13 +318,13 @@ snapshot:
266
318
  * The backend URL is relative, such as `/api/c15t`. With an explicit
267
319
  `onBuildError: 'fail'`, a relative URL stops the build instead.
268
320
  * Next.js builds with `output: 'export'`, which has no server.
269
- * The mode is `hosted()`, `offline()`, `manifest({ snapshot })` or
321
+ * The mode is `hosted()`, `local()`, `manifest({ snapshot })` or
270
322
  `manifest({ source: 'runtime' })` in Next.js, Nuxt and Astro. Next.js reads
271
323
  the mode from `c15t.config.ts`.
272
324
  * A single-page app built with Vite (React, Vue, Svelte, JavaScript) does
273
325
  not use `manifest()`. The mode is set in app code, so the plugin downloads
274
326
  the manifest only once the bundle turns out to read it, after unused code
275
- is dropped. A `hosted()` or `offline()` build never contacts the backend.
327
+ is dropped. A `hosted()` or `local()` build never contacts the backend.
276
328
  SvelteKit and TanStack Start choose the mode on the server at runtime, so
277
329
  their builds always download it.
278
330
 
@@ -86,7 +86,7 @@ Render `CookieBanner` in place of `ConsentBanner`, inside
86
86
 
87
87
  ```svelte title="src/lib/cookie-banner.svelte"
88
88
  <script lang="ts">
89
- import { ConsentBanner } from '@c15t/svelte';
89
+ import { ConsentBanner } from 'c15t/svelte';
90
90
  </script>
91
91
 
92
92
  <!-- The stock banner, drawn as a full-width bar along the bottom edge. -->
@@ -250,7 +250,7 @@ Svelte scopes component styles, so the classes live in a global
250
250
  stylesheet.
251
251
 
252
252
  ```ts title="src/lib/consent-options.ts"
253
- import type { ConsentManagerOptions } from '@c15t/svelte';
253
+ import type { ConsentManagerOptions } from 'c15t/svelte';
254
254
 
255
255
  import './slim-bar.css';
256
256
 
@@ -456,8 +456,8 @@ opens.
456
456
 
457
457
  ```svelte title="src/lib/cookie-bar.svelte"
458
458
  <script lang="ts">
459
- import { getHeadlessConsent } from '@c15t/svelte';
460
- import type { PresentationAction } from '@c15t/svelte';
459
+ import { getHeadlessConsent } from 'c15t/svelte';
460
+ import type { PresentationAction } from 'c15t/svelte';
461
461
 
462
462
  import './cookie-bar.css';
463
463
 
@@ -69,7 +69,7 @@ render. The elements stay after the surface unmounts. The Svelte dialog's
69
69
  sheets, its rules and the primitives, load with the dialog's code.
70
70
 
71
71
  On a server-rendered SvelteKit page, the banner leaves a
72
- `<meta name="c15t-styles">` marker, and `c15tHandle` from `@c15t/svelte/kit`
72
+ `<meta name="c15t-styles">` marker, and `c15tHandle` from `c15t/svelte/kit`
73
73
  writes the sheets into the HTML `<head>` as `<style>` elements. The banner is
74
74
  styled before hydration with no stylesheet request. Without `c15tHandle` in
75
75
  `src/hooks.server.ts`, a server-rendered banner shows unstyled until
@@ -109,7 +109,7 @@ need control over the file:
109
109
  | Next.js | `options.styles` in `c15t.config.ts` or on `ConsentRoot` | `c15t/next/styles.css`, and `c15t/next/iab/styles.css` for IAB TCF |
110
110
  | TanStack Start | `options.styles` on `ConsentRoot` | `c15t/tanstack-start/styles.css`, and `c15t/tanstack-start/iab/styles.css` for IAB TCF |
111
111
  | React | `options.styles` on `ConsentProvider` | `c15t/react/styles.css`, and `c15t/react/iab/styles.css` for IAB TCF |
112
- | Svelte, SvelteKit | `options.styles` on `ConsentProvider` or `ConsentRoot` | `@c15t/svelte/styles.css`, and `@c15t/svelte/iab/styles.css` for IAB TCF |
112
+ | Svelte, SvelteKit | `options.styles` on `ConsentProvider` or `ConsentRoot` | `c15t/svelte/styles.css`, and `c15t/svelte/iab/styles.css` for IAB TCF |
113
113
  | Astro | `styles` in the `c15t()` integration options | See [load Astro's stylesheets yourself](#load-astros-stylesheets-yourself) |
114
114
 
115
115
  ```ts title="c15t.config.ts (Next.js)"
@@ -132,7 +132,7 @@ the first paint. Remove the import, or add `styles: false` if you need it.
132
132
  | Next.js, TanStack Start, React | In the dialog sheet that `ConsentDialog` and `ConsentWidget` render. It ships with the dialog's lazily loaded code. With `styles: false`, in `styles.css`. No `@c15t/react` or `@c15t/ui` module imports CSS, so a Next.js app that uses only the Pages Router builds without `transpilePackages` |
133
133
  | Nuxt, Vue | The dialog component imports its stylesheet, so the rules load with the dialog's code, not the banner's |
134
134
  | Astro | In `@c15t/ui/styles/sheets/dialog.css`. The Svelte dialog, the default `ui`, also needs the primitives stylesheet. c15t links both when a visitor first points at, focuses or opens a control that opens the dialog, waits for them before it shows the dialog, and keeps them across `ClientRouter` navigation. On a Tailwind CSS 3 site, the dialog's rules are in `c15t/astro/styles.css`, and c15t links only the primitives stylesheet |
135
- | Svelte, SvelteKit | In the dialog's sheets, which load with the dialog's code. With `styles: false`, in `@c15t/svelte/styles.css` |
135
+ | Svelte, SvelteKit | In the dialog's sheets, which load with the dialog's code. With `styles: false`, in `c15t/svelte/styles.css` |
136
136
  | HTML, JavaScript | Part of the stylesheet in the shadow root, which holds only the rules the stock surfaces use |
137
137
 
138
138
  `styles.css` still holds every stock surface's rules for apps that set
@@ -230,6 +230,41 @@ lists where each framework takes it.
230
230
  behavior. If a token seems to do nothing, check the layer order and your
231
231
  selector before you reach for `noStyle`.
232
232
 
233
+ ## Keep your fixed elements still during a scroll lock
234
+
235
+ A blocking banner or preference dialog locks page scrolling in every
236
+ framework and in the script tag. The lock hides the page's overflow and keeps
237
+ the scrollbar's space with `scrollbar-gutter: stable`, so nothing on the page
238
+ moves. Where that gutter does not hold, the lock pads `<body>` by the
239
+ scrollbar's width instead. This happens in browsers without
240
+ `scrollbar-gutter`, and in Chromium on macOS when the page styles its
241
+ scrollbar with `::-webkit-scrollbar`. The padding keeps your content in place,
242
+ but `position: fixed` elements, such as a fixed header or a chat button, move
243
+ right by the scrollbar's width until the lock ends.
244
+
245
+ While it pads `<body>`, the lock sets `--c15t-scroll-lock-gap` on `<html>` to
246
+ the same width. Add it to the right offset of your fixed elements:
247
+
248
+ ```css
249
+ .site-header {
250
+ position: fixed;
251
+ inset-inline: 0;
252
+ top: 0;
253
+ padding-right: var(--c15t-scroll-lock-gap, 0px);
254
+ }
255
+
256
+ .chat-button {
257
+ position: fixed;
258
+ right: calc(1rem + var(--c15t-scroll-lock-gap, 0px));
259
+ bottom: 1rem;
260
+ }
261
+ ```
262
+
263
+ The lock sets the variable only while it pads `<body>`. When the last lock
264
+ releases, `<html>` gets back any `--c15t-scroll-lock-gap` value it had before,
265
+ so the `0px` fallback applies only on pages that don't set the variable
266
+ themselves.
267
+
233
268
  ## Check the result
234
269
 
235
270
  1. Open DevTools Network, clear site data and reload. No c15t stylesheet
@@ -246,3 +281,7 @@ selector before you reach for `noStyle`.
246
281
  `<style>` element appears.
247
282
  4. Select a banner part and read the Styles panel. c15t's rules appear under
248
283
  `@layer components`, and your unlayered rules for the same property win.
284
+ 5. On a page that styles its scrollbar with `::-webkit-scrollbar`, open the
285
+ preference dialog in Chrome on macOS. `<html>` gets
286
+ `--c15t-scroll-lock-gap` in its `style` attribute, and fixed elements that
287
+ use it stay where they were.
@@ -31,7 +31,7 @@ fixture. The Next.js 15 and webpack fixtures use the same files. The check
31
31
  confirms that c15t's banner and dialog keep their styles under Tailwind's
32
32
  preflight, that a padding utility on the banner root
33
33
  beats c15t's own padding, and that the `dark:` variant applies. The fixtures run
34
- without a backend, so they use `offline()` mode or a local `backendURL`. Keep the
34
+ without a backend, so they use `local()` mode or a local `backendURL`. Keep the
35
35
  `mode` or `backendURL` from your quickstart.
36
36
 
37
37
  ## Set up Tailwind CSS 4
@@ -165,13 +165,13 @@ when it opens, so the CSS entry holds only Tailwind. Add
165
165
  import svelte from '@astrojs/svelte';
166
166
  import tailwindcss from '@tailwindcss/vite';
167
167
  import { defineConfig } from 'astro/config';
168
- import c15t, { offline } from 'c15t/astro';
168
+ import c15t, { local } from 'c15t/astro';
169
169
 
170
170
  export default defineConfig({
171
171
  integrations: [
172
172
  svelte(),
173
173
  c15t({
174
- mode: offline({
174
+ mode: local({
175
175
  policyRules: [
176
176
  {
177
177
  id: 'tailwind-matrix',
@@ -264,11 +264,10 @@ detects Tailwind 3 and links `c15t/astro/styles.css` itself. Every c15t
264
264
  package that ships a stylesheet exports the plugin as
265
265
  `<package>/postcss-tailwind3`, so use the one from the package you installed:
266
266
 
267
- | You installed | Plugin name |
268
- | --------------------------------------------------------- | --------------------------------- |
269
- | `c15t` (React, Next.js, TanStack Start, Vue, Nuxt, Astro) | `c15t/postcss-tailwind3` |
270
- | `@c15t/svelte` (Svelte, SvelteKit) | `@c15t/svelte/postcss-tailwind3` |
271
- | `@c15t/browser` (script tag in the light DOM) | `@c15t/browser/postcss-tailwind3` |
267
+ | You installed | Plugin name |
268
+ | ---------------------------------------------------------------------------- | --------------------------------- |
269
+ | `c15t` (React, Next.js, TanStack Start, Vue, Nuxt, Astro, Svelte, SvelteKit) | `c15t/postcss-tailwind3` |
270
+ | `@c15t/browser` (script tag in the light DOM) | `@c15t/browser/postcss-tailwind3` |
272
271
 
273
272
  They are the same plugin. A config that already lists
274
273
  `@c15t/ui/postcss-tailwind3` keeps working.
@@ -303,17 +302,9 @@ class. Add the important modifier to utilities you pass to c15t parts:
303
302
  `!bg-red-500`, `hover:!bg-red-500`. A bare `bg-red-500` has no effect. Theme
304
303
  tokens work the same on Tailwind 3 and 4.
305
304
 
306
- In React and Next.js apps, `c15t setup` adds the plugin and the stylesheet
307
- import when it finds Tailwind 3, but does not set `styles: false`. Add that
308
- yourself. It replaces an existing
309
- `styles.tw3.css` import from an earlier setup with `styles.css`. When it
310
- cannot edit your PostCSS config, it prints the step to do by hand. See
311
- [setup](https://c15t.com/docs/cli/commands/setup#set-up-styles-and-tailwind-css).
312
-
313
305
  Create React App ignores PostCSS config files, so Tailwind 3 cannot run the
314
- plugin and the build fails on c15t's stylesheet. `c15t setup` warns
315
- about this. Add the plugin before `tailwindcss` through CRACO, eject, or move
316
- the app to Vite.
306
+ plugin and the build fails on c15t's stylesheet. Add the plugin before
307
+ `tailwindcss` through CRACO, eject, or move the app to Vite.
317
308
 
318
309
  **Next.js**
319
310
 
@@ -521,11 +512,7 @@ stylesheet:
521
512
 
522
513
  ```js title="postcss.config.mjs"
523
514
  export default {
524
- plugins: {
525
- '@c15t/svelte/postcss-tailwind3': {},
526
- tailwindcss: {},
527
- autoprefixer: {},
528
- },
515
+ plugins: { 'c15t/postcss-tailwind3': {}, tailwindcss: {}, autoprefixer: {} },
529
516
  };
530
517
  ```
531
518
 
@@ -540,13 +527,13 @@ export default {
540
527
  } satisfies Config;
541
528
  ```
542
529
 
543
- Import `@c15t/svelte/styles.css` above the directives, import this
530
+ Import `c15t/svelte/styles.css` above the directives, import this
544
531
  stylesheet from `src/main.ts`, and pass `styles={false}` to
545
532
  `ConsentProvider`:
546
533
 
547
534
  ```css title="src/app.css"
548
535
  /* Vite ignores an @import below other rules, so it goes first. */
549
- @import '@c15t/svelte/styles.css';
536
+ @import 'c15t/svelte/styles.css';
550
537
 
551
538
  @tailwind base;
552
539
  @tailwind components;
@@ -557,11 +544,7 @@ stylesheet from `src/main.ts`, and pass `styles={false}` to
557
544
 
558
545
  ```js title="postcss.config.mjs"
559
546
  export default {
560
- plugins: {
561
- '@c15t/svelte/postcss-tailwind3': {},
562
- tailwindcss: {},
563
- autoprefixer: {},
564
- },
547
+ plugins: { 'c15t/postcss-tailwind3': {}, tailwindcss: {}, autoprefixer: {} },
565
548
  };
566
549
  ```
567
550
 
@@ -576,13 +559,13 @@ export default {
576
559
  } satisfies Config;
577
560
  ```
578
561
 
579
- Import `@c15t/svelte/styles.css` above the directives, import this
562
+ Import `c15t/svelte/styles.css` above the directives, import this
580
563
  stylesheet from your root `+layout.svelte`, and pass `styles={false}` to
581
564
  `ConsentRoot`:
582
565
 
583
566
  ```css title="src/app.css"
584
567
  /* Vite ignores an @import below other rules, so it goes first. */
585
- @import '@c15t/svelte/styles.css';
568
+ @import 'c15t/svelte/styles.css';
586
569
 
587
570
  @tailwind base;
588
571
  @tailwind components;
@@ -792,7 +775,7 @@ stylesheet, and the layer order keeps it above preflight:
792
775
 
793
776
  ```html title="light-dom.html"
794
777
  <script
795
- src="/c15t.offline.js"
778
+ src="/c15t.local.js"
796
779
  data-mode="offline"
797
780
  data-country="DE"
798
781
  data-shadow="false"
@@ -814,7 +797,7 @@ instead:
814
797
  window.c15t = [['config', { ui: { shadow: false, styles: false } }]];
815
798
  </script>
816
799
  <script
817
- src="/c15t.offline.js"
800
+ src="/c15t.local.js"
818
801
  data-mode="offline"
819
802
  data-country="DE"
820
803
  defer
@@ -35,7 +35,7 @@ Nuxt module have no `i18n` option; their copy comes from the backend.
35
35
 
36
36
  c15t bundles English. Every other language it ships is a separate entry point
37
37
  in `@c15t/translations`, named by its language code. Importing one registers
38
- c15t's built-in copy for that language. Offline mode can then switch to it,
38
+ c15t's built-in copy for that language. Local mode can then switch to it,
39
39
  and your messages that repeat its wording don't hide backend edits. Import
40
40
  them in the file that configures the provider, so they load in the browser:
41
41
 
@@ -51,9 +51,11 @@ string or build your messages from it:
51
51
  import { translations as de } from '@c15t/translations/de';
52
52
  ```
53
53
 
54
- The entries are `bg`, `cs`, `cy`, `da`, `de`, `el`, `es`, `et`, `fi`, `fr`,
55
- `ga`, `gu`, `he`, `hi`, `hr`, `hu`, `id`, `is`, `it`, `lb`, `lt`, `lv`, `mt`,
56
- `nb`, `nl`, `nn`, `pl`, `pt`, `rm`, `ro`, `sk`, `sl`, `sv` and `zh`.
54
+ The entries are `ar`, `bg`, `bn`, `cs`, `cy`, `da`, `de`, `el`, `es`, `et`,
55
+ `fi`, `fr`, `ga`, `gu`, `he`, `hi`, `hr`, `ht`, `hu`, `id`, `is`, `it`, `ja`,
56
+ `kn`, `ko`, `lb`, `lt`, `lv`, `ml`, `mr`, `ms`, `mt`, `nb`, `nl`, `nn`, `pa`,
57
+ `pl`, `pt`, `rm`, `ro`, `ru`, `sk`, `sl`, `sv`, `ta`, `te`, `th`, `tr`, `ur`,
58
+ `vi`, `yi` and `zh`.
57
59
  `@c15t/translations/all` still loads every language at once. The `c15t`
58
60
  package doesn't re-export these entries, so add `@c15t/translations` to your
59
61
  dependencies.
@@ -102,26 +104,30 @@ production. Rename the section to `consentGate` to remove the warning.
102
104
  ## Show right-to-left languages
103
105
 
104
106
  c15t sets `dir="rtl"` on its surfaces when the resolved language is Arabic,
105
- Hebrew, Persian, Urdu, Pashto, Sindhi, Kurdish or Dhivehi. It matches the
106
- primary language, so `ar-EG` counts as Arabic. Hebrew is the only
107
- right-to-left translation c15t ships. For the others, supply your own
108
- messages or the copy from your Inth project.
107
+ Hebrew, Yiddish, Persian, Urdu, Pashto, Sindhi, Central Kurdish (`ckb`),
108
+ Uyghur, Dhivehi, Syriac or N'Ko. It matches the primary language, so `ar-EG`
109
+ counts as Arabic, and accepts the older codes `iw` and `ji`. A script subtag
110
+ decides over the language: `ku-Arab` and `pa-Arab` are right to left, and
111
+ `sd-Deva` is left to right. Plain `ku` is left to right because it usually
112
+ means Kurmanji, which uses Latin script. c15t ships right-to-left
113
+ translations for Arabic, Hebrew, Urdu and Yiddish. For the others, supply
114
+ your own messages or the copy from your Inth project.
109
115
 
110
116
  What follows the direction:
111
117
 
112
118
  * Text, headings and the button row in the banner, the preference dialog and
113
119
  the preference widget flow right to left.
114
- * A floating or widget banner in its default corner moves to the mirrored
115
- corner, so `bottom-left` becomes `bottom-right`. A `position` you set
116
- yourself stays where you put it.
117
120
  * The HTML script tag, `@c15t/browser` and Astro's banner also set `lang` on
118
121
  their surfaces. React, Vue and Svelte set `dir` only, so set `lang` on
119
122
  `<html>` yourself.
120
123
 
124
+ The banner and the floating `ConsentDialogTrigger` stay in their corners. A
125
+ `bottom-left` banner is still at the bottom left in Hebrew, so it doesn't
126
+ jump across the page when the visitor's language changes.
127
+
121
128
  What does not follow yet:
122
129
 
123
130
  * The IAB TCF dialog aligns several labels, indents and borders to the left.
124
- * The floating `ConsentDialogTrigger` keeps the corner you give it.
125
131
 
126
132
  Test right-to-left pages with a real translation before you ship them.
127
133
 
@@ -62,8 +62,9 @@ replaces the automatic attempts for that field. Partitioned cookies need
62
62
  `partitioned: true`. A prefix only finds cookies the current page can read,
63
63
  so use an exact name for a cookie on another path.
64
64
 
65
- c15t never deletes its own consent records, even when a pattern matches
66
- them.
65
+ c15t never deletes its own consent records or the `c15t-sid` subject ID
66
+ cookie, even when a pattern matches them. The subject ID stays so an
67
+ opt-out can still reach what was kept under it.
67
68
 
68
69
  ## When cleanup runs
69
70
 
@@ -63,7 +63,7 @@ rule with `model: 'none'` owes no rights, so nothing renders unless you add
63
63
  failed, no rule matched and you set no default, or init is still withheld, no
64
64
  consent surface renders: not the banner, the dialog, the widget, the
65
65
  preferences link, or the trigger. They appear as soon as a rule resolves,
66
- without a remount. `offline()` without `policyRules` resolves the recommended
66
+ without a remount. `local()` without `policyRules` resolves the recommended
67
67
  pack, so it shows the strict opt-in banner until you pass a country.
68
68
 
69
69
  Acknowledging a notice records its dismissal. It does not record consent or
@@ -201,8 +201,8 @@ Each variant accepts its own positions:
201
201
  | `widget` | `bottom-left`, `bottom-right`, `top-left`, `top-right` | `bottom-right` |
202
202
  | `wall` | `center` | `center` |
203
203
 
204
- A default corner mirrors left and right for right-to-left languages. A
205
- position you set is never mirrored. A position that is not valid for the
204
+ Right-to-left languages keep the same position; only the text direction
205
+ changes. A position that is not valid for the
206
206
  variant falls back to the default and logs an `invalid-position` diagnostic
207
207
  in development.
208
208