@c15t/react 3.1.0-canary.0 → 3.2.0-canary.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 (129) hide show
  1. package/AGENTS.md +11 -4
  2. package/README.md +4 -4
  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/event-dispatcher.js +2 -0
  21. package/dist/headless.js +1 -1
  22. package/dist/index-parts/event-dispatcher.js +1 -0
  23. package/dist/index-parts/offline.js +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/modes.js +1 -1
  26. package/dist/provider.js +1 -1
  27. package/dist/transports/offline.js +1 -1
  28. package/dist/version.js +1 -1
  29. package/dist-types/component-hooks/use-consent-gate.d.ts +102 -0
  30. package/dist-types/component-hooks/use-consent-labels.d.ts +32 -0
  31. package/dist-types/component-hooks/use-consent-text-direction.d.ts +22 -0
  32. package/dist-types/component-hooks/use-vendors-listed-under.d.ts +30 -0
  33. package/dist-types/components/prompt/atoms/root.d.ts +3 -3
  34. package/dist-types/components/prompt/prompt.d.ts +4 -3
  35. package/dist-types/components/prompt/surface-context.d.ts +1 -14
  36. package/dist-types/components/prompt/use-banner-copy.d.ts +8 -3
  37. package/dist-types/components/shared/streamed-surface.d.ts +47 -10
  38. package/dist-types/context.d.ts +3 -1
  39. package/dist-types/event-dispatcher.d.ts +24 -0
  40. package/dist-types/headless.d.ts +10 -1
  41. package/dist-types/hooks/use-iab-policy-guard.d.ts +17 -1
  42. package/dist-types/hooks/use-scroll-lock.d.ts +16 -2
  43. package/dist-types/index-parts/event-dispatcher.d.ts +7 -0
  44. package/dist-types/index-parts/offline.d.ts +1 -1
  45. package/dist-types/index.d.ts +2 -0
  46. package/dist-types/modes.d.ts +1 -1
  47. package/dist-types/module-hooks/script-loader.d.ts +9 -0
  48. package/dist-types/provider.d.ts +3 -3
  49. package/dist-types/transports/offline.d.ts +2 -2
  50. package/dist-types/version.d.ts +1 -1
  51. package/docs/README.md +11 -4
  52. package/docs/concepts/choose-your-setup.md +9 -9
  53. package/docs/concepts/consent-state.md +93 -9
  54. package/docs/concepts/data-fetching.md +30 -23
  55. package/docs/concepts/modes.md +85 -35
  56. package/docs/customization/recipes.md +4 -4
  57. package/docs/customization/stylesheets.md +3 -3
  58. package/docs/customization/tailwind.md +17 -34
  59. package/docs/customization/translations.md +18 -12
  60. package/docs/frameworks/react/clear-on-revocation.md +3 -2
  61. package/docs/frameworks/react/components/consent-banner.md +3 -3
  62. package/docs/frameworks/react/components/consent-provider.md +29 -29
  63. package/docs/frameworks/react/gpp.md +1 -1
  64. package/docs/frameworks/react/headless.md +146 -19
  65. package/docs/frameworks/react/iab.md +8 -1
  66. package/docs/frameworks/react/quickstart.md +1 -1
  67. package/docs/frameworks/react/rendering.md +1 -1
  68. package/docs/frameworks/react/scripts.md +44 -0
  69. package/docs/frameworks/react/translations.md +9 -9
  70. package/docs/frameworks/react/troubleshooting.md +1 -1
  71. package/docs/frameworks/react/upgrade-v3.md +39 -32
  72. package/docs/frameworks/react/vendor-consent.md +5 -0
  73. package/docs/guides/banner-experiments.md +3 -3
  74. package/docs/guides/troubleshooting.md +3 -2
  75. package/docs/integrations/adobe-analytics.md +2 -2
  76. package/docs/integrations/ahrefs-analytics.md +2 -2
  77. package/docs/integrations/amplitude.md +2 -2
  78. package/docs/integrations/clay.md +305 -0
  79. package/docs/integrations/clearbit.md +2 -2
  80. package/docs/integrations/cloudflare-web-analytics.md +2 -2
  81. package/docs/integrations/cloudflare-zaraz.md +2 -2
  82. package/docs/integrations/crisp.md +2 -2
  83. package/docs/integrations/databuddy.md +2 -2
  84. package/docs/integrations/fathom-analytics.md +2 -2
  85. package/docs/integrations/front-chat.md +2 -2
  86. package/docs/integrations/google-maps.md +2 -2
  87. package/docs/integrations/google-tag-manager.md +59 -2
  88. package/docs/integrations/google-tag.md +2 -2
  89. package/docs/integrations/heap.md +2 -2
  90. package/docs/integrations/hightouch.md +2 -2
  91. package/docs/integrations/hotjar.md +40 -26
  92. package/docs/integrations/intercom.md +2 -2
  93. package/docs/integrations/klaviyo.md +2 -2
  94. package/docs/integrations/linkedin-insights.md +2 -2
  95. package/docs/integrations/logrocket.md +2 -2
  96. package/docs/integrations/matomo-analytics.md +2 -2
  97. package/docs/integrations/meta-pixel.md +2 -2
  98. package/docs/integrations/microsoft-clarity.md +2 -2
  99. package/docs/integrations/microsoft-uet.md +2 -2
  100. package/docs/integrations/mixpanel-analytics.md +2 -2
  101. package/docs/integrations/one-dollar-stats.md +2 -2
  102. package/docs/integrations/openai-pixel.md +2 -2
  103. package/docs/integrations/other-ads-and-pixels.md +94 -0
  104. package/docs/integrations/other-analytics.md +108 -0
  105. package/docs/integrations/other-chat-and-support.md +112 -0
  106. package/docs/integrations/other-email-and-sms.md +156 -0
  107. package/docs/integrations/other-experience.md +222 -0
  108. package/docs/integrations/overview.md +102 -45
  109. package/docs/integrations/pinterest-tag.md +2 -2
  110. package/docs/integrations/pirsch.md +2 -2
  111. package/docs/integrations/plausible-analytics.md +2 -2
  112. package/docs/integrations/posthog.md +10 -2
  113. package/docs/integrations/promptwatch.md +2 -2
  114. package/docs/integrations/reddit-pixel.md +2 -2
  115. package/docs/integrations/rudderstack.md +2 -2
  116. package/docs/integrations/rybbit-analytics.md +2 -2
  117. package/docs/integrations/segment.md +2 -2
  118. package/docs/integrations/sentry.md +2 -2
  119. package/docs/integrations/shopify.md +323 -0
  120. package/docs/integrations/snapchat-pixel.md +2 -2
  121. package/docs/integrations/tiktok-pixel.md +2 -2
  122. package/docs/integrations/umami-analytics.md +2 -2
  123. package/docs/integrations/vercel-analytics.md +2 -2
  124. package/docs/integrations/vercel-speed-insights.md +7 -2
  125. package/docs/integrations/x-pixel.md +2 -2
  126. package/docs/integrations/youtube.md +2 -2
  127. package/package.json +12 -29
  128. package/dist/components/prompt/resolve-banner-primary-actions.js +0 -1
  129. package/dist-types/components/prompt/resolve-banner-primary-actions.d.ts +0 -14
@@ -63,6 +63,23 @@ If your own scripts depend on such a category, do one of these:
63
63
  Check `effectivePermissions` after Accept All on a fresh visit to see which
64
64
  categories your vendor list leaves denied.
65
65
 
66
+ ## A subject ID from the first page load
67
+
68
+ Every visitor gets a subject ID (`sub_…`) before they decide anything, so
69
+ you can link activity to the visitor from the first page load and still
70
+ find it when they opt out later. It is in `snapshot.subject.subjectId` from
71
+ the first snapshot.
72
+
73
+ * A framework server sets the ID in the `c15t-sid` cookie on the first
74
+ response, where the adapter's server hook runs. Otherwise the browser
75
+ creates it when c15t starts and stores it in `c15t-sid` at once.
76
+ * The ID stays the same across reloads. The first save records the same ID
77
+ with the choice.
78
+ * `c15t-sid` is not a choice. A visitor with only an ID still sees the
79
+ banner.
80
+ * Clearing choices and revoking consent keep the ID. Only setting the subject
81
+ to `null` starts over with a new one.
82
+
66
83
  ## When a choice is saved
67
84
 
68
85
  A save records the choice in the browser first and sends it to the backend
@@ -136,6 +153,18 @@ writes the v3 format. The upgrade guides for
136
153
  [JavaScript](https://c15t.com/docs/frameworks/javascript/upgrade-v3#keep-visitors-existing-choices)
137
154
  cover how v3 treats v2 records.
138
155
 
156
+ ## Retry interrupted saves
157
+
158
+ Before sending a recorded choice to the backend, c15t puts it in a localStorage
159
+ queue. If the page closes while the request is in flight, the next initialization
160
+ retries it with the original decision time. A successful response removes the
161
+ queued choice. A permanent rejection, such as a stale policy, removes it too.
162
+
163
+ Queueing depends on the browser allowing storage. When storage is unavailable,
164
+ c15t still attempts the request, but cannot preserve an interrupted request for
165
+ the next page. A locally remembered choice alone does not prove that the backend
166
+ received it; check the saved consent record in Inth.
167
+
139
168
  ## Keep open tabs in step
140
169
 
141
170
  Tabs and windows on the same origin (scheme, host and port) share the consent
@@ -192,10 +221,12 @@ Stored records are merged into the ones in memory:
192
221
  wins in both tabs.
193
222
  * Tabs keep one subject. A stored subject is considered only when the record
194
223
  that carries it changed, never on focus alone. It replaces a subject the tab
195
- generated on its first save or copied from storage, so a tab that opened
196
- before another tab stored a subject joins it. A subject id the server
197
- resolved (at init, from a prefetch or in a save response) is replaced only by
198
- a strictly newer stored choice, and an identity set with `identify()` is
224
+ generated at startup or on its first save, or copied from storage, so a tab
225
+ that opened before another tab stored a subject joins it. When it replaces
226
+ an ID the tab generated at startup and never used, the kernel emits
227
+ `subject:resolved` with `previousSubjectId`. A subject id the server
228
+ resolved (at init, from a prefetch or in a save response) is replaced only
229
+ by a strictly newer stored choice, and an identity set with `identify()` is
199
230
  never replaced.
200
231
  * Under an IAB policy, the TC string follows the reconciled choice. See
201
232
  [How the TC string is reconciled](#how-the-tc-string-is-reconciled).
@@ -269,11 +300,13 @@ wherever it turns up. A decision stamped in the very millisecond of the clear
269
300
  counts only if it comes from a tab that had already seen that clear, so a tab's
270
301
  own choice right after its own clear stands while another tab's decision in the
271
302
  same millisecond does not. A stored record left with no decisions after the
272
- clear is void too, subject included.
303
+ clear is void too, subject included. The subject ID itself survives the clear:
304
+ it lives in its own key, `c15t-sid`, which clearing never removes, so a
305
+ withdrawal stays linked to the same visitor.
273
306
 
274
307
  * A tab that reconciles only after another tab cleared and saved again drops
275
- its pre-clear decisions instead of merging them back. Its subject from before
276
- the clear is dropped too.
308
+ its pre-clear decisions instead of merging them back. The external identity
309
+ linked to its subject before the clear is dropped too; the subject ID stays.
277
310
  * A tab that missed the clear writes only decisions it made after it. Its queued
278
311
  write of an earlier decision is discarded, so it cannot bring back a cleared
279
312
  record.
@@ -336,13 +369,38 @@ When a server render seeded the page from the consent cookie
336
369
  localStorage is still applied on top of it when the page mounts, since it can
337
370
  only restrict, if it is newer than the seeded decision or from the same
338
371
  millisecond as a seeded grant. A stored grant is not.
372
+ `persistence: { skipHydration: false }` reads storage back over the seed
373
+ instead, so an older stored decision replaces it. A server render then starts
374
+ no scripts from the HTML, since it can't tell what that read will return.
339
375
 
340
376
  The notice dismissal and vendor denials are stored twice as well. A vendor
341
377
  list in localStorage at least as new as the cookie's adds its denials but never
342
378
  lifts one the cookie holds; a copy confirmed before the last clear is ignored, so its
343
379
  denials never come back. The newer notice dismissal applies; it still only hides a
344
- notice with the fingerprint it names. The consent record follows the rules
345
- below.
380
+ notice with the fingerprint it names.
381
+
382
+ Every notice acknowledgement also stores the visitor's subject ID with it,
383
+ including a choice that acknowledges a notice. For a visitor whose only action
384
+ was acknowledging a prompt that offers only necessary categories, this record
385
+ is what keeps the ID. The acknowledgement still grants no optional category. The `<key>-notice` cookie keeps its v1 format, so older
386
+ runtimes still read it. A separate `<key>-notice-subject` cookie binds the
387
+ subject ID to that acknowledgement: readers use it only while its timestamp and
388
+ fingerprint match the notice, so removing or replacing the notice makes a stale
389
+ identity cookie ineffective. When the subject ID changes, through a save
390
+ response or `set.subjectId()`, c15t rewrites the acknowledgement with the new
391
+ ID, so a reload does not bring the old one back. For the same acknowledgement,
392
+ the cookie identity wins unless a failed cookie write left a newer local
393
+ identity or a local identity removal. That exception lasts only while both
394
+ notice cookies match the values observed after the failed write; a later server
395
+ or sibling-subdomain cookie update takes precedence. Category-based data cleanup
396
+ protects these records and `<key>-sid` even when a configured wildcard matches
397
+ their names.
398
+
399
+ Clearing the stored records removes the acknowledgement and its
400
+ `<key>-notice-subject` cookie. The subject ID stays in `<key>-sid`, as after any
401
+ other clear. External identity fields supplied through `identify()` go with the
402
+ record that carried them. Call `set.subjectId(null)` to give the visitor a new
403
+ ID. The consent record follows the rules below.
346
404
 
347
405
  The consent record is stored twice: as a cookie, which a server render reads,
348
406
  and in localStorage. The cookie is authoritative. A well-formed cookie wins
@@ -373,6 +431,32 @@ render still reads the old consent cookie until the next successful cookie
373
431
  write. The same applies to a consent cookie set with a different `domain` than
374
432
  the current `storageConfig` uses, which the clear cannot remove.
375
433
 
434
+ The subject ID follows the same split. When its cookie writes succeed, the
435
+ server render and the browser use the same ID. When the browser refuses the
436
+ rewrite of a record cookie after the subject ID changed (the consent cookie,
437
+ `c15t-vendors` or `c15t-notice-subject`), that cookie keeps the old ID while
438
+ localStorage holds the new one. A server render then reports the old ID, and
439
+ the browser starts with the new ID from its storage. This happens even when the
440
+ request's `c15t-sid` cookie already holds the new ID. It lasts until a later
441
+ write of that record reaches the cookie, which may never happen while the
442
+ browser keeps refusing it. The server never replaces a `c15t-sid` cookie the
443
+ request already has with the old ID. When this browser's storage holds the
444
+ newer ID, the browser does not store the old one again either.
445
+
446
+ The vendor record has a separate limit. A local vendor copy at least as new as
447
+ the cookie's keeps its subject ID, and a rekey to a new subject ID keeps the
448
+ original decision time. So with `crossSubdomain`, a visitor whose only record is
449
+ a vendor decision (no consent record) can hold a local copy and a shared cookie
450
+ copy with the same time but different subject IDs, for example after another
451
+ subdomain rekeyed it at any later moment. This origin then starts with the ID
452
+ from its local copy, on server-rendered loads as well as loads that hydrate. It
453
+ does not write that ID over the shared `c15t-sid` cookie when that cookie and
454
+ the record cookies agree on another ID. A later vendor save from this origin
455
+ does store its ID in the shared vendor cookie, where the other subdomains read
456
+ it. Once that cookie carries it, this origin can also write it to the shared
457
+ `c15t-sid` cookie on a later page load. No optional category is granted and no
458
+ denial is lifted.
459
+
376
460
  ### Ordering with pending writes
377
461
 
378
462
  A tab writes its own choices in a later task, not during the click. Before it
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Data fetching
3
3
  description: Choose the recommended build-time policy snapshot or runtime
4
- manifest fetching, backend /init, browser resolution or offline rules, and
4
+ manifest fetching, backend /init, browser resolution or local rules, and
5
5
  understand where consent choices are saved.
6
6
  group: concepts
7
7
  ---
@@ -10,18 +10,18 @@ group: concepts
10
10
 
11
11
  Where policy resolves and where choices are saved are separate from who runs
12
12
  the backend. Inth and a [self-hosted backend](https://c15t.com/docs/self-host/quickstart) speak
13
- the same protocol, so every path below works with either. Only `offline()`
13
+ the same protocol, so every path below works with either. Only `local()`
14
14
  removes backend requests. For a recommendation by framework, start with
15
15
  [choose your setup](./choose-your-setup.md).
16
16
 
17
- | Fetching path | Where policy resolves | Where choices are submitted | Choose it when |
18
- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
19
- | Build-time manifest, recommended for server-rendered apps | Your server resolves a bundled policy snapshot with each visitor's inputs | Inth or your c15t backend | Your framework renders on the server, supports a build integration, and policy changes can ship with a rebuild |
20
- | Cached manifest on your server | Your application resolves public policy data with each request's location and signals | Inth or your c15t backend | Policy changes must reach the app without rebuilding, or builds cannot access the backend |
21
- | Regular backend `/init` | The consent backend | The same backend | You have no application server, as in a React, Vue or Svelte single-page app, want the fewest moving parts, or need backend-owned request resolution |
22
- | Manifest in the browser | The browser, using a supplied location, or `/init` when the policy needs one it doesn't have | Inth or your c15t backend | The policy has no location rules or the page knows the visitor's location, and you accept about 3 KB more bundle and a rebuild for policy edits |
23
- | Offline | The browser or local runtime, using bundled rules | No backend submission | Local development and tests. Not recommended for production environments. |
24
- | Custom transport | Your implementation | Your implementation | An existing service cannot use the c15t backend protocol |
17
+ | Fetching path | Where policy resolves | Where choices are submitted | Choose it when |
18
+ | --------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | Build-time manifest, recommended for server-rendered apps | Your server resolves a bundled policy snapshot with each visitor's inputs | Inth or your c15t backend | Your framework renders on the server, supports a build integration, and policy changes can ship with a rebuild |
20
+ | Cached manifest on your server | Your application resolves public policy data with each request's location and signals | Inth or your c15t backend | Policy changes must reach the app without rebuilding, or builds cannot access the backend |
21
+ | Regular backend `/init` | The consent backend | The same backend | You have no application server, as in a React, Vue or Svelte single-page app, want the fewest moving parts, or need backend-owned request resolution |
22
+ | Manifest in the browser | The browser, using a supplied location, or `/init` when the policy needs one it doesn't have | Inth or your c15t backend | The policy has no location rules or the page knows the visitor's location, and you accept about 3 KB more bundle and a rebuild for policy edits |
23
+ | Local | The browser or local runtime, using bundled rules | No backend submission. c15t keeps no server-side record of consent. | Local development and tests. Not recommended for production environments. |
24
+ | Custom transport | Your implementation | Your implementation | An existing service cannot use the c15t backend protocol |
25
25
 
26
26
  ## What is a consent manifest?
27
27
 
@@ -226,36 +226,43 @@ needing the upstream URL. A static export cannot serve a Next.js route or
226
226
  rewrite at runtime; use the absolute Inth URL or a proxy provided by the static
227
227
  host instead.
228
228
 
229
- ## When should I use offline mode?
229
+ ## When should I use local mode?
230
230
 
231
231
  Not recommended for production environments. Use Inth or a self-hosted backend
232
232
  for production policy and consent records.
233
233
 
234
- `offline()` resolves bundled policy rules without an init request and acknowledges
235
- saves locally. The runtime's persistence module stores the choice in browser
236
- storage. There is no backend audit history, cross-device record service or IP
237
- geolocation supplied by this transport.
234
+ `local()`, called `offline()` before c15t 3.2, resolves bundled policy rules
235
+ without an init request and acknowledges saves locally. The runtime's
236
+ persistence module stores the choice in browser storage. c15t keeps no
237
+ server-side record of consent, so there is no audit history, no cross-device
238
+ record and no IP geolocation. [What a consent record proves](./modes.md#what-a-consent-record-proves)
239
+ explains what that leaves out.
238
240
 
239
- Use your framework's `offline()` so its translations and provider context are
241
+ Use your framework's `local()` so its translations and provider context are
240
242
  included. In a React app it replaces the mode passed to `ConsentProvider`:
241
243
 
242
244
  ```ts title="src/consent-mode.ts"
243
- import { offline } from 'c15t/react';
245
+ import { local } from 'c15t/react';
244
246
 
245
- export const mode = offline();
247
+ export const mode = local();
246
248
  ```
247
249
 
250
+ In development builds, local mode warns once per page that c15t keeps no
251
+ server-side record of consent. Pass `local({ acknowledgeNoRecords: true })` to
252
+ hide the warning once you have decided a browser-only record is enough; see
253
+ [the development warning](./modes.md#the-development-warning).
254
+
248
255
  In Next.js, TanStack Start, Nuxt, Astro and SvelteKit, pass the data
249
- `offline()` from the framework package where the config takes `mode`; see
256
+ `local()` from the framework package where the config takes `mode`; see
250
257
  [consent modes](./modes.md#import-the-factories-from-your-framework-package).
251
258
 
252
- With no `policyRules`, the current offline transport uses the recommended rule
253
- pack. Supplying `policyRules` replaces that pack. Unknown country and region are
254
- real resolution inputs; offline mode does not discover a visitor's location.
259
+ With no `policyRules`, the local transport uses the recommended rule pack.
260
+ Supplying `policyRules` replaces that pack. Unknown country and region are
261
+ real resolution inputs; local mode does not discover a visitor's location.
255
262
  Use [policy rules](./policies.md) to understand
256
263
  matching and defaults, and test the missing-location case.
257
264
 
258
- Offline mode is an explicit architecture choice, not an automatic fallback for
265
+ Local mode is an explicit architecture choice, not an automatic fallback for
259
266
  a failed Inth request. If hosted initialization fails before a policy resolves,
260
267
  optional permissions remain denied and the stock prompt stays hidden. Observe
261
268
  initialization failures instead of silently changing policy sources.
@@ -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,85 @@ 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. Local mode also has no geolocation, so
151
+ every visitor gets the same policy. Use it for development and tests. For
152
+ production, see https://c15t.com/docs/concepts/modes
153
+ Set acknowledgeNoRecords: true to hide this message.
154
+ ```
155
+
156
+ Production builds never log it, and it never blocks the banner or a save.
157
+ The script-tag files are production builds, so `c15t.local.js`, and
158
+ `c15t.headless.js` or `c15t.iab.js` without a backend URL, log it on
159
+ development hosts instead:
160
+ `localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0` and names ending in
161
+ `.localhost`, `.local` or `.test`. A script tag hides it with
162
+ `data-acknowledge-no-records`.
163
+
164
+ Set `acknowledgeNoRecords: true` once you have decided a browser-only record is
165
+ enough, such as in tests, or on a site that keeps its own record through
166
+ [callbacks](https://c15t.com/docs/frameworks/javascript/callbacks) or a [`custom()` transport](./data-fetching.md#can-i-provide-my-own-transport):
167
+
168
+ ```ts
169
+ import { local } from 'c15t/react';
170
+
171
+ const mode = local({ acknowledgeNoRecords: true });
172
+ ```
173
+
174
+ c15t v4 removes the `offline()` name and makes `acknowledgeNoRecords: true`
175
+ required in `local()`'s type.
176
+
177
+ ### What a consent record proves
178
+
179
+ A consent record is the backend's copy of a choice: which categories the
180
+ visitor granted or denied, when, and against which policy and copy. Under the
181
+ GDPR, the business that relies on consent must be able to show that the
182
+ visitor gave it (Article 7(1)). Browser storage alone cannot do that: the
183
+ visitor can clear it, and it never leaves their device.
184
+
185
+ `manifest()` and `hosted()` post each choice to the backend, which keeps the
186
+ record. To keep records without Inth, run a
187
+ [self-hosted backend](https://c15t.com/docs/self-host/overview), or send each choice to your
188
+ own API with `custom()`.
189
+
140
190
  ## What each mode adds to first-load JavaScript
141
191
 
142
192
  In a server-rendered framework, the browser receives the resolved state with
143
193
  the page. It only needs the code to save a choice, and loads the rest when it
144
194
  runs:
145
195
 
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 |
196
+ | Mode | First-load JavaScript | Loaded with `import()` when it runs |
197
+ | ---------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
198
+ | `manifest()` | The code that saves choices and builds an init request | The hosted init path, if the browser has to resolve consent again |
199
+ | `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. |
200
+ | `hosted()` | The code that saves choices | The hosted init path |
201
+ | `local()` | Nothing mode-specific | The local transport, the rule pack and its copy |
152
202
 
153
203
  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.
204
+ the local rule pack and other languages stay out of the browser bundle.
155
205
 
156
206
  In a single-page app the bundle contains the mode you imported:
157
207
 
@@ -159,7 +209,7 @@ In a single-page app the bundle contains the mode you imported:
159
209
  | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
160
210
  | `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
211
  | `hosted()` | The hosted transport |
162
- | `offline()` | The offline transport, the recommended rule pack and its copy |
212
+ | `local()` | The local transport, the recommended rule pack and its copy |
163
213
 
164
214
  ## Set the backend URL
165
215
 
@@ -204,7 +254,7 @@ build integration downloads from `${backendURL}/manifest`:
204
254
  | TanStack Start | `consentManifest()` from `c15t/tanstack-start/build` | `@c15t/tanstack-start/build` |
205
255
  | React and JavaScript | `consentManifest()` from `c15t/build` | `@c15t/core/build` |
206
256
  | Vue | `consentManifest()` from `c15t/vue/vite` | `@c15t/vue/vite` |
207
- | Svelte and SvelteKit | `consentManifest()` from `@c15t/svelte/vite` | `@c15t/svelte/vite` |
257
+ | Svelte and SvelteKit | `consentManifest()` from `c15t/svelte/vite` | `@c15t/svelte/vite` |
208
258
  | Nuxt | the `c15t/vue` module | `@c15t/vue` |
209
259
  | Astro | the `c15t()` integration | `@c15t/astro` |
210
260
 
@@ -266,13 +316,13 @@ snapshot:
266
316
  * The backend URL is relative, such as `/api/c15t`. With an explicit
267
317
  `onBuildError: 'fail'`, a relative URL stops the build instead.
268
318
  * Next.js builds with `output: 'export'`, which has no server.
269
- * The mode is `hosted()`, `offline()`, `manifest({ snapshot })` or
319
+ * The mode is `hosted()`, `local()`, `manifest({ snapshot })` or
270
320
  `manifest({ source: 'runtime' })` in Next.js, Nuxt and Astro. Next.js reads
271
321
  the mode from `c15t.config.ts`.
272
322
  * A single-page app built with Vite (React, Vue, Svelte, JavaScript) does
273
323
  not use `manifest()`. The mode is set in app code, so the plugin downloads
274
324
  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.
325
+ is dropped. A `hosted()` or `local()` build never contacts the backend.
276
326
  SvelteKit and TanStack Start choose the mode on the server at runtime, so
277
327
  their builds always download it.
278
328
 
@@ -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