@c15t/react 3.0.0-alpha.5 → 3.0.0-alpha.6

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 (67) hide show
  1. package/AGENTS.md +6 -4
  2. package/dist/component-hooks/use-headless-consent-ui.js +1 -1
  3. package/dist/components/iab-panel/atoms/vendor-list.js +1 -1
  4. package/dist/components/iab-panel/hooks/use-gvl-data.js +1 -1
  5. package/dist/components/iab-panel/index.js +1 -1
  6. package/dist/components/panel/index.js +1 -1
  7. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  8. package/dist/components/panel-trigger/atoms/trigger-icons.js +1 -1
  9. package/dist/components/panel-trigger/panel-trigger.js +1 -1
  10. package/dist/components/preferences/index.js +1 -1
  11. package/dist/components/shared/ui/accordion/accordion.js +1 -1
  12. package/dist/components/shared/ui/collapsible/collapsible.js +1 -1
  13. package/dist/components/shared/ui/preference-item/preference-item.js +1 -1
  14. package/dist/components/shared/ui/switch/switch.js +1 -1
  15. package/dist/components/shared/ui/tabs/tabs.js +1 -1
  16. package/dist/devtools-launcher.js +2 -0
  17. package/dist/devtools.js +1 -1
  18. package/dist/hooks/use-headless-iab-consent-ui.js +1 -1
  19. package/dist/index-parts/core.js +1 -1
  20. package/dist/provider.js +1 -1
  21. package/dist/styles.tw3.css +27 -3
  22. package/dist/version.js +1 -1
  23. package/dist-types/component-hooks/use-headless-consent-ui.d.ts +4 -4
  24. package/dist-types/components/iab-panel/index.d.ts +0 -1
  25. package/dist-types/components/panel/index.d.ts +0 -1
  26. package/dist-types/components/panel-trigger/atoms/trigger-icons.d.ts +5 -0
  27. package/dist-types/components/panel-trigger/panel-trigger.d.ts +0 -8
  28. package/dist-types/components/preferences/index.d.ts +0 -1
  29. package/dist-types/components/shared/ui/accordion/accordion.d.ts +0 -1
  30. package/dist-types/components/shared/ui/collapsible/collapsible.d.ts +0 -1
  31. package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +0 -1
  32. package/dist-types/components/shared/ui/switch/switch.d.ts +0 -1
  33. package/dist-types/components/shared/ui/tabs/tabs.d.ts +0 -1
  34. package/dist-types/devtools-launcher.d.ts +29 -0
  35. package/dist-types/hooks/use-iab-manager.d.ts +4 -4
  36. package/dist-types/hooks.d.ts +27 -5
  37. package/dist-types/index-parts/core.d.ts +1 -1
  38. package/dist-types/index.d.ts +1 -1
  39. package/dist-types/provider.d.ts +12 -1
  40. package/dist-types/version.d.ts +1 -1
  41. package/docs/README.md +6 -4
  42. package/docs/concepts/choose-your-setup.md +22 -5
  43. package/docs/concepts/consent-state.md +41 -0
  44. package/docs/concepts/data-fetching.md +34 -11
  45. package/docs/customization/recipes.md +4 -5
  46. package/docs/customization/slots.md +3 -2
  47. package/docs/customization/stylesheets.md +29 -39
  48. package/docs/customization/tailwind.md +5 -5
  49. package/docs/customization/tokens.md +1 -1
  50. package/docs/customization/translations.md +32 -5
  51. package/docs/frameworks/react/components/consent-dialog-trigger.md +5 -0
  52. package/docs/frameworks/react/components/consent-dialog.md +2 -1
  53. package/docs/frameworks/react/components/dev-tools.md +23 -0
  54. package/docs/frameworks/react/headless.md +4 -4
  55. package/docs/frameworks/react/hooks.md +10 -5
  56. package/docs/frameworks/react/iab.md +24 -18
  57. package/docs/frameworks/react/quickstart.md +77 -16
  58. package/docs/frameworks/react/rendering.md +82 -11
  59. package/docs/frameworks/react/scripts.md +8 -3
  60. package/docs/frameworks/react/upgrade-v3.md +596 -0
  61. package/docs/frameworks/react/vendor-consent.md +6 -0
  62. package/docs/guides/banner-experiments.md +26 -5
  63. package/docs/guides/troubleshooting.md +3 -2
  64. package/docs/integrations/overview.md +28 -27
  65. package/docs/integrations/sentry.md +416 -0
  66. package/docs/upgrade-v3.md +201 -314
  67. package/package.json +8 -8
@@ -27,6 +27,42 @@ itself stays refused: its `effectivePermissions` entry is `false`, and targets
27
27
  that name only the category, or that also declare a consent purpose or special
28
28
  feature, stay blocked.
29
29
 
30
+ ## Categories under an IAB policy
31
+
32
+ Under an IAB policy the visitor chooses TCF purposes, and c15t derives each
33
+ category from them when the choice is saved. A category is granted when the
34
+ visitor consented to every purpose in it that a listed vendor processes on
35
+ consent:
36
+
37
+ | Category | TCF purposes |
38
+ | --------------- | ------------ |
39
+ | `marketing` | 2, 3, 4 |
40
+ | `measurement` | 7, 8, 9 |
41
+ | `experience` | 5, 6 |
42
+ | `functionality` | 10, 11 |
43
+
44
+ `necessary` is always granted. Purpose 1, device storage, decides no category.
45
+
46
+ Accept All consents only to the purposes and special features that the vendors
47
+ in your list declare, because the TCF Policies allow no consent signal for a
48
+ purpose the visitor was not shown. A category whose purposes no listed vendor
49
+ processes on consent therefore stays denied after Accept All, and so does every
50
+ script, iframe or network rule gated on it. With a vendor list whose vendors
51
+ declare only purposes 1 to 4 and 7, for example, `experience` and
52
+ `functionality` stay denied.
53
+
54
+ If your own scripts depend on such a category, do one of these:
55
+
56
+ * Declare the processing. Add a custom vendor for it, with the purposes it
57
+ uses, through the `customVendors` option of your IAB setup. The preference
58
+ centre then lists those purposes, and Accept All and the purpose switches
59
+ can grant the category.
60
+ * Gate the script on a category that your listed vendors' purposes cover, or on
61
+ `necessary` if it needs no consent.
62
+
63
+ Check `effectivePermissions` after Accept All on a fresh visit to see which
64
+ categories your vendor list leaves denied.
65
+
30
66
  ## When a choice is saved
31
67
 
32
68
  A save records the choice in the browser first and sends it to the backend
@@ -76,6 +112,11 @@ unchanged selection, closes the surface when it resolves successfully. Opening
76
112
  or closing a surface, or starting another save, before then leaves the surface
77
113
  as the visitor set it.
78
114
 
115
+ Closing the dialog without saving, with Escape or the adapter's close call,
116
+ leaves the same surface a save would. A visitor who opens preferences from the
117
+ banner and backs out still owes a choice, so the banner comes back. Once a
118
+ choice is recorded, closing the dialog leaves nothing open.
119
+
79
120
  ## Preserve records during hydration
80
121
 
81
122
  Server helpers return records with policy information and evaluation time.
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  title: Data fetching
3
- description: How c15t gets policy data through a cached manifest, backend /init,
4
- the browser or offline rules, and where consent choices are saved.
3
+ description: Choose the recommended build-time policy snapshot or runtime
4
+ manifest fetching, backend /init, browser resolution or offline rules, and
5
+ understand where consent choices are saved.
5
6
  group: concepts
6
7
  ---
7
8
 
@@ -13,13 +14,14 @@ the same protocol, so every path below works with either. Only `offline()`
13
14
  removes backend requests. For a recommendation by framework, start with
14
15
  [choose your setup](./choose-your-setup.md).
15
16
 
16
- | Fetching path | Where policy resolves | Where choices are submitted | Choose it when |
17
- | ------------------------------ | ------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ |
18
- | Cached manifest on your server | Your application resolves public policy data with each request's location and signals | Inth or your c15t backend | Your host has a runtime server and you want to reuse policy data across requests |
19
- | Regular backend `/init` | The consent backend | The same backend | You want the fewest moving parts, need backend-owned request resolution, or have no application server |
20
- | Manifest in the browser | The browser, using supplied or unknown location | Inth or your c15t backend | You deliberately want client resolution and have planned geography, bundle size and policy refresh |
21
- | Offline | The browser or local runtime, using bundled rules | No backend submission | Local development and tests. Not recommended for production environments. |
22
- | 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 | Your server or browser resolves a bundled policy snapshot with each visitor's inputs | Inth or your c15t backend | Your framework 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 want the fewest moving parts, need backend-owned request resolution, or have no application server |
22
+ | Manifest in the browser | The browser, using supplied or unknown location | Inth or your c15t backend | You deliberately want client resolution and have planned geography, bundle size and policy refresh |
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 |
23
25
 
24
26
  ## What is a consent manifest?
25
27
 
@@ -38,6 +40,24 @@ Do not put secrets, visitor identifiers or consent records into a manifest.
38
40
  Keep personalized init responses out of shared caches. Changing a policy also
39
41
  requires a refresh strategy for cached or build-time manifests.
40
42
 
43
+ ## Use a build-time manifest by default
44
+
45
+ The build fetches your public policy once and bundles it, so the server never
46
+ fetches it at runtime.
47
+
48
+ The framework quickstarts include the build integration for supported
49
+ deployments. Server resolution uses the snapshot without an upstream manifest
50
+ request. Browser resolution includes the policy and resolver in the client
51
+ bundle, and may still request location for regional policies. Static Nuxt and
52
+ Astro sites and script-tag sites keep the runtime setup from their guides.
53
+
54
+ The snapshot is fixed at build time:
55
+
56
+ * Rebuild after changing policies, translations or vendors. If your CI caches
57
+ build output, force a fresh build.
58
+ * The build must reach the backend. A failed fetch fails the build.
59
+ * Consent choices still go to the backend.
60
+
41
61
  ## How manifest mode counts visitors
42
62
 
43
63
  The backend counts visitors from `/init` requests, and manifest resolution
@@ -156,10 +176,13 @@ the kernel contract. Prefer a built-in transport when your backend supports it.
156
176
  ## Verify the selected path
157
177
 
158
178
  Inspect browser and server requests separately. A server manifest fetch will
159
- not appear in the browser's Network panel. On a warm manifest path, check that
179
+ not appear in the browser's Network panel. With a build-time snapshot, start a
180
+ fresh production server and confirm it makes no upstream `/manifest` request.
181
+ After a policy edit, rebuild and confirm the new policy appears. On a runtime
182
+ manifest path, check that
160
183
  page requests do not call the backend `/init`, a visitor's choice still reaches
161
184
  the backend's `/subjects`, and policy changes become visible after the configured
162
- refresh. In the recommended Next.js setup, the browser should call only
185
+ refresh. With a same-origin consent proxy, the browser should call only
163
186
  `/api/c15t` paths for consent HTTP traffic; inspect server logs to verify their
164
187
  upstream destinations.
165
188
 
@@ -517,8 +517,7 @@ from your Inth project, and put the block before `</body>`.
517
517
 
518
518
  ```html title="index.html"
519
519
  <script
520
- src="https://cdn.jsdelivr.net/npm/@c15t/browser@alpha/dist/c15t.headless.js"
521
- data-backend-url="https://your-project.inth.app"
520
+ src="https://your-project.inth.app/c15t.headless.js"
522
521
  defer
523
522
  ></script>
524
523
 
@@ -622,8 +621,8 @@ from your Inth project, and put the block before `</body>`.
622
621
  hidden
623
622
  >
624
623
  <p class="cookie-bar__text">
625
- We use cookies to measure traffic and improve this site. Choose which ones
626
- can run.
624
+ We use cookies to measure traffic and improve this site. Choose which
625
+ ones can run.
627
626
  </p>
628
627
  <fieldset
629
628
  class="cookie-bar__prefs"
@@ -867,5 +866,5 @@ The source repository renders each design in its Storybook under
867
866
  `Docs / Banner designs`: `apps/storybook-react`, `apps/storybook-vue`,
868
867
  `apps/storybook-svelte` and `apps/storybook-astro`. Each story uses an
869
868
  in-memory policy and loads no analytics. The HTML headless bar runs at
870
- `/consent-example/headless` in `examples/script-tag`, which the example
869
+ `/consent-example/headless` in `internals/fixtures/script-tag`, which the example
871
870
  acceptance tests cover.
@@ -129,8 +129,9 @@ The widget is the list of categories inside the dialog. React and Vue call it
129
129
  | Label | `trigger.text` | None |
130
130
  | Toolbar, its items and icons | `trigger.toolbar`, `trigger.toolbarItem`, `trigger.toolbarIcon` | `consentDialogTriggerToolbar`, `consentDialogTriggerToolbarItem`, `consentDialogTriggerToolbarIcon` |
131
131
 
132
- Only React renders the toolbar. Vue has no toolbar, and the toolbar slots only
133
- reach Astro's React dialog.
132
+ React renders the toolbar from `ConsentDialogTriggerToolbar`, and while
133
+ `ConsentDevTools` is mounted. Vue renders it only while `ConsentDevTools` is
134
+ mounted. In Astro, the toolbar slots reach the React dialog.
134
135
 
135
136
  ## Legal links and IAB TCF parts
136
137
 
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  title: Stylesheets and CSS layers
3
- description: Load the right c15t stylesheet for your framework, see when the
4
- dialog's CSS loads, order c15t's cascade layer against your own, and run c15t
5
- without its styles.
3
+ description: Load the right c15t stylesheet for your framework, see which file
4
+ holds the dialog's rules, order c15t's cascade layer against your own, and run
5
+ c15t without its styles.
6
6
  group: customization
7
7
  ---
8
8
 
@@ -26,32 +26,25 @@ when the integration sets `iab`. Tailwind 3 uses the same files and adds a
26
26
  PostCSS plugin; [Tailwind CSS](./tailwind.md#set-up-tailwind-css-3)
27
27
  shows the setup.
28
28
 
29
- ## What loads with the first paint
30
-
31
- A stylesheet that blocks rendering should hold only what the first paint can
32
- show. c15t's main stylesheet holds the default tokens, every c15t CSS variable,
33
- and the rules for the banner, the floating trigger and the `ConsentGate`
34
- placeholder. The preference dialog's rules load separately in most frameworks:
35
-
36
- | Framework | The preference dialog's rules |
37
- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
38
- | Next.js, TanStack Start, React | The dialog's code imports `@c15t/ui/styles/dialog.css`. Your bundler emits it with the dialog's lazy chunk and loads it before that chunk runs, so the dialog never renders unstyled |
39
- | Nuxt, Vue | The dialog component imports its stylesheet, so the rules load with the dialog's code, not the banner's |
40
- | Astro | c15t links the dialog's stylesheets when a visitor first points at, focuses or opens a control that opens the dialog, and keeps them across `ClientRouter` navigation |
41
- | Svelte, SvelteKit | Part of `@c15t/svelte/styles.css`, so they load up front |
42
- | HTML, JavaScript | Part of the stylesheet in the shadow root, which holds only the rules the stock surfaces use |
43
-
44
- In React, Next.js and TanStack Start, do not import `dialog.css` yourself. The
45
- `@c15t/react/components/consent-dialog`, `/components/consent-widget`,
46
- `/primitives`, `/primitives/*` and `/iab` entry points import it as soon as
47
- you import them, because they render dialog parts outside the lazy chunk.
48
-
49
- These modules import the stylesheet through `@c15t/ui/styles/dialog`. Under
50
- the `node` export condition that module imports nothing, so server code that
51
- loads `@c15t/react` with plain Node, such as the Pages Router or an SSR build
52
- that keeps dependencies external, does not fail on the `.css` file. In your own
53
- components, import `@c15t/ui/styles/dialog` rather than the `.css` file if the
54
- module can run on the server.
29
+ ## Where the dialog's rules come from
30
+
31
+ `styles.css` holds the default tokens, every c15t CSS variable and the rules
32
+ for every stock surface: the banner, the floating trigger, the `ConsentGate`
33
+ placeholder, the preference dialog and the preference widget. Only the IAB TCF
34
+ surfaces need a second file, `iab/styles.css`.
35
+
36
+ | Framework | The preference dialog's rules |
37
+ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | Next.js, TanStack Start, React | 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` |
39
+ | Nuxt, Vue | The dialog component imports its stylesheet, so the rules load with the dialog's code, not the banner's |
40
+ | Astro | In `c15t/astro/styles.css`. The Svelte dialog, the default `ui`, also needs the primitives stylesheet, which c15t links when a visitor first points at, focuses or opens a control that opens the dialog, and keeps across `ClientRouter` navigation |
41
+ | Svelte, SvelteKit | In `@c15t/svelte/styles.css` |
42
+ | HTML, JavaScript | Part of the stylesheet in the shadow root, which holds only the rules the stock surfaces use |
43
+
44
+ Earlier 3.0 alphas shipped the dialog's rules in `@c15t/ui/styles/dialog.css`,
45
+ which the dialog's code imported. That file, the `@c15t/ui/styles/dialog`
46
+ module and `c15t/astro/dialog.css` are now empty and stay only so existing
47
+ imports keep resolving. Remove those imports.
55
48
 
56
49
  ## Order c15t's layer against your CSS
57
50
 
@@ -78,11 +71,9 @@ What this means for your overrides:
78
71
  [theme tokens](./tokens.md).
79
72
 
80
73
  If your CSS declares its own layer order, load that statement before c15t's
81
- stylesheet. If you import c15t's stylesheet into a named layer, such as
82
- `@import 'c15t/react/styles.css' layer(c15t)`, the dialog's lazy rules still
83
- join the top-level `components` layer. List `components` in your statement,
84
- for example `@layer c15t, components, app;`, so it does not land after your
85
- own layers.
74
+ stylesheet. You can import c15t's stylesheet into a named layer, such as
75
+ `@import 'c15t/react/styles.css' layer(c15t)`. Its component rules then sit
76
+ in `c15t.components`, so order `c15t` against your own layers.
86
77
 
87
78
  ## Load Astro's stylesheets yourself
88
79
 
@@ -90,12 +81,11 @@ The Astro integration adds `c15t/astro/styles.css` to every page ahead of your
90
81
  own CSS. To control the order yourself, for example from a global stylesheet
91
82
  that names its own layers, set `styles: false` in the `c15t()` options and
92
83
  import the files after your layer order statement. With `styles: false`, c15t
93
- links no dialog stylesheet either, so import the dialog's rules too. The
94
- Svelte dialog, the default `ui`, also needs the primitives stylesheet:
84
+ links no stylesheet when the dialog opens either. The Svelte dialog, the
85
+ default `ui`, needs the primitives stylesheet next to `styles.css`:
95
86
 
96
87
  ```css title="src/styles/global.css"
97
88
  @import 'c15t/astro/styles.css';
98
- @import 'c15t/astro/dialog.css';
99
89
  /* Only with ui: 'svelte'. */
100
90
  @import 'c15t/astro/primitives.css';
101
91
  /* Only when the integration sets iab. */
@@ -141,7 +131,7 @@ selector before you reach for `noStyle`.
141
131
 
142
132
  1. Open DevTools Network, clear site data and reload. Your framework's c15t
143
133
  stylesheet loads once, and no second copy appears.
144
- 2. Open the preference dialog. It is styled on its first frame, with no flash
145
- of unstyled content.
134
+ 2. Open the preference dialog. It is styled on its first frame, and in React,
135
+ Next.js and TanStack Start no new stylesheet request appears.
146
136
  3. Select a banner part and read the Styles panel. c15t's rules appear under
147
137
  `@layer components`, and your unlayered rules for the same property win.
@@ -282,8 +282,8 @@ Make three changes:
282
282
  3. Set `darkMode: 'class'` so Tailwind's `dark:` variant follows the same
283
283
  `dark` class c15t reads.
284
284
 
285
- The plugin also processes the stylesheets c15t loads for you, such as the
286
- dialog's lazy stylesheet and Astro's injected one. Without it, Tailwind 3 stops
285
+ The plugin also processes the stylesheets c15t loads for you, such as Astro's
286
+ injected one. Without it, Tailwind 3 stops
287
287
  the build with "`@layer components` is used but no matching
288
288
  `@tailwind components` directive is present", or purges c15t's rules, because
289
289
  c15t's class names never appear in your source.
@@ -300,7 +300,7 @@ cannot edit your PostCSS config, it prints the step to do by hand. See
300
300
  [setup](https://c15t.com/docs/cli/commands/setup#set-up-styles-and-tailwind-css).
301
301
 
302
302
  Create React App ignores PostCSS config files, so Tailwind 3 cannot run the
303
- plugin and the build fails on c15t's dialog stylesheet. `c15t setup` warns
303
+ plugin and the build fails on c15t's stylesheet. `c15t setup` warns
304
304
  about this. Add the plugin before `tailwindcss` through CRACO, eject, or move
305
305
  the app to Vite.
306
306
 
@@ -771,7 +771,7 @@ stylesheet, and the layer order keeps it above preflight:
771
771
 
772
772
  ```html title="light-dom.html"
773
773
  <script
774
- src="/c15t.js"
774
+ src="/c15t.offline.js"
775
775
  data-mode="offline"
776
776
  data-country="DE"
777
777
  data-shadow="false"
@@ -793,7 +793,7 @@ instead:
793
793
  window.c15t = [['config', { ui: { shadow: false, styles: false } }]];
794
794
  </script>
795
795
  <script
796
- src="/c15t.js"
796
+ src="/c15t.offline.js"
797
797
  data-mode="offline"
798
798
  data-country="DE"
799
799
  defer
@@ -101,7 +101,7 @@ easing tokens.
101
101
  | `--c15t-text-on-primary` | `colors.textOnPrimary` | auto-derived from `colors.primary` when omitted |
102
102
  | `--c15t-overlay` | `colors.overlay` | `hsla(0, 0%, 0%, 0.5)` |
103
103
  | `--c15t-switch-track` | `colors.switchTrack` | `hsl(0, 0%, 85%)` |
104
- | `--c15t-switch-track-active` | `colors.switchTrackActive` | `hsl(228, 100%, 60%)` |
104
+ | `--c15t-switch-track-active` | `colors.switchTrackActive` | `colors.primary` |
105
105
  | `--c15t-switch-thumb` | `colors.switchThumb` | `hsl(0, 0%, 100%)` |
106
106
  | `--c15t-font-family` | `typography.fontFamily` | `system-ui, -apple-system, sans-serif` |
107
107
  | `--c15t-font-size-sm` | `typography.fontSize.sm` | `0.875rem` |
@@ -31,6 +31,33 @@ site-wide change. Astro's serializable integration options have their own
31
31
  types, so verify that shape before copying a React object. The Vue plugin and
32
32
  Nuxt module have no `i18n` option; their copy comes from the backend.
33
33
 
34
+ ## Load only the languages you use
35
+
36
+ c15t bundles English. Every other language it ships is a separate entry point
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,
39
+ and your messages that repeat its wording don't hide backend edits. Import
40
+ them in the file that configures the provider, so they load in the browser:
41
+
42
+ ```ts
43
+ import '@c15t/translations/de';
44
+ import '@c15t/translations/fr';
45
+ ```
46
+
47
+ Each entry also exports that copy as `translations`, if you want to read a
48
+ string or build your messages from it:
49
+
50
+ ```ts
51
+ import { translations as de } from '@c15t/translations/de';
52
+ ```
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`.
57
+ `@c15t/translations/all` still loads every language at once. The `c15t`
58
+ package doesn't re-export these entries, so add `@c15t/translations` to your
59
+ dependencies.
60
+
34
61
  ## Combine messages with backend copy
35
62
 
36
63
  With a backend or a manifest, the copy it sends for the visitor's language is
@@ -41,11 +68,11 @@ its primary language, so `de-AT` uses your `de` messages when there is no
41
68
  `de-AT` entry. Messages for other languages are not applied.
42
69
 
43
70
  A key only overrides the backend when your text differs from c15t's built-in
44
- wording for that language. So passing the stock bundles from
45
- `@c15t/translations/all` to enable languages keeps backend edits visible,
46
- while a key you actually reworded stays pinned in code. Core bundles only
47
- English; for other languages, import `@c15t/translations/all` so c15t can
48
- recognize its stock wording.
71
+ wording for that language. So passing a stock bundle to enable a language
72
+ keeps backend edits visible, while a key you actually reworded stays pinned
73
+ in code. c15t only recognizes the built-in wording of English and of the
74
+ languages you [import](#load-only-the-languages-you-use), such as
75
+ `@c15t/translations/de` or `@c15t/translations/all`.
49
76
 
50
77
  ## Write labels that describe the action
51
78
 
@@ -112,6 +112,11 @@ nothing only when it has no visible item.
112
112
  | `ariaLabel` | `string` | `'Privacy controls'` | Accessible name for the toolbar group. |
113
113
  | `noStyle` | `boolean` | `false` | Remove the default styling. |
114
114
 
115
+ While `<ConsentDevTools>` is mounted, the toolbar adds a DevTools button at
116
+ the end farthest from its corner and opens the DevTools panel beside itself.
117
+ The ready-made `ConsentDialogTrigger` switches to this toolbar layout for as
118
+ long as DevTools is mounted.
119
+
115
120
  ### Toolbar styling
116
121
 
117
122
  The toolbar follows the standard precedence: bundled styles, then provider slots, then direct `className` and `style` on the toolbar or on one action. Use `noStyle` for a fully custom implementation.
@@ -132,7 +132,8 @@ cached: the next hover, focus or open tries the download again.
132
132
  The dialog's CSS travels in the same chunk and is applied before the dialog
133
133
  renders, so your app's `styles.css` import stays small.
134
134
 
135
- Without an `open` prop, Escape closes the dialog. Save Settings, Accept All and
135
+ Without an `open` prop, Escape closes the dialog without saving. If the
136
+ policy still owes a choice, the banner comes back. Save Settings, Accept All and
136
137
  Reject All close it as soon as the choice is recorded in the browser, in
137
138
  the same task as the click; the exit animation still plays. The request to
138
139
  the backend runs afterwards and never reopens the dialog: a failed request
@@ -76,6 +76,29 @@ panel renders inside a shadow root by default, so page CSS does not reach
76
76
  it. `shadow={false}` mounts it in the light DOM with its stylesheet in
77
77
  `<head>`.
78
78
 
79
+ ## Open DevTools from the consent trigger
80
+
81
+ While `ConsentDialogTriggerToolbar` or the ready-made `ConsentDialogTrigger`
82
+ is visible, the trigger shows a DevTools button and DevTools hides its own
83
+ floating launcher, so one control occupies the corner. The button sits at the
84
+ end of the toolbar farthest from its corner. The panel opens just past the
85
+ toolbar, aligned with its outer edge, and follows it when a visitor drags the
86
+ toolbar to another corner. `position` applies again once no trigger is
87
+ visible.
88
+
89
+ `ConsentDialogTrigger` becomes a two-button toolbar while DevTools is
90
+ mounted. A trigger composed from `ConsentDialogTrigger.Root` keeps its own
91
+ markup, and DevTools keeps its floating launcher beside it.
92
+
93
+ DevTools brings its launcher back whenever no trigger is visible, for example
94
+ while `showWhen="after-prompt"` waits for a choice. A production build that
95
+ loads DevTools only in development shows no DevTools button in the trigger.
96
+
97
+ With the imperative engine, call `devTools.dock({ position, inline, block })`
98
+ to hide the launcher and anchor the panel `inline` and `block` pixels from the
99
+ viewport edges of `position`'s corner. Call `devTools.dock(null)` to restore
100
+ the launcher.
101
+
79
102
  ## Panels
80
103
 
81
104
  Accept and reject apply only to categories displayed by the provider, leaving
@@ -34,13 +34,13 @@ import { useHeadlessConsentUI, useTranslations } from 'c15t/react/headless';
34
34
 
35
35
  const ACTION_LABELS = {
36
36
  accept: 'acceptAll',
37
- reject: 'rejectAll',
38
37
  customize: 'customize',
39
38
  dismiss: 'acknowledge',
39
+ reject: 'rejectAll',
40
40
  save: 'save',
41
41
  } as const;
42
42
 
43
- export function Banner() {
43
+ export const Banner = () => {
44
44
  const { banner, performAction, openDialog } = useHeadlessConsentUI();
45
45
  const { common, rights } = useTranslations();
46
46
 
@@ -49,7 +49,7 @@ export function Banner() {
49
49
  }
50
50
 
51
51
  return (
52
- <section role="region" aria-label="Privacy">
52
+ <section aria-label="Privacy">
53
53
  {banner.preferenceControls.map((right) => (
54
54
  <button key={right} type="button" onClick={openDialog}>
55
55
  {right === 'opt-out' ? rights?.optOut : rights?.preferences}
@@ -71,7 +71,7 @@ export function Banner() {
71
71
  ))}
72
72
  </section>
73
73
  );
74
- }
74
+ };
75
75
  ```
76
76
 
77
77
  `banner.actionGroups` is the resolved layout: reject and accept share a group
@@ -14,10 +14,10 @@ from components rendered inside that provider and import them from
14
14
  ```tsx title="src/optional-feature.tsx"
15
15
  import { useConsent } from 'c15t/react';
16
16
 
17
- export function OptionalFeature() {
17
+ export const OptionalFeature = () => {
18
18
  const allowed = useConsent('marketing');
19
19
  return allowed ? <div>Optional marketing content</div> : null;
20
- }
20
+ };
21
21
  ```
22
22
 
23
23
  Call hooks inside the consent provider. `useConsent` reads effective permission,
@@ -42,14 +42,19 @@ Render this component inside your existing `ConsentProvider`.
42
42
  ```tsx title="src/reject-button.tsx"
43
43
  import { useSaveConsents } from 'c15t/react';
44
44
 
45
- export function RejectButton() {
45
+ export const RejectButton = () => {
46
46
  const save = useSaveConsents();
47
47
  return (
48
- <button type="button" onClick={() => void save('none')}>
48
+ <button
49
+ type="button"
50
+ onClick={() => {
51
+ void save('none');
52
+ }}
53
+ >
49
54
  Reject optional
50
55
  </button>
51
56
  );
52
- }
57
+ };
53
58
  ```
54
59
 
55
60
  This is an isolated action example, not a complete policy-aware banner. Use the
@@ -33,14 +33,12 @@ import {
33
33
  IABConsentDialog,
34
34
  } from 'c15t/react/iab';
35
35
 
36
- export function IABConsent({ cmpId }: { cmpId: number }) {
37
- return (
38
- <IABProvider cmpId={cmpId}>
39
- <IABConsentBanner />
40
- <IABConsentDialog />
41
- </IABProvider>
42
- );
43
- }
36
+ export const IABConsent = ({ cmpId }: { cmpId: number }) => (
37
+ <IABProvider cmpId={cmpId}>
38
+ <IABConsentBanner />
39
+ <IABConsentDialog />
40
+ </IABProvider>
41
+ );
44
42
  ```
45
43
 
46
44
  Supply the CMP ID from your deployment's IAB configuration. Keep a persistent
@@ -85,14 +83,12 @@ const publisherRestrictions = [
85
83
  { purposeId: 7, restrictionType: 2 as const, vendorIds: [755] },
86
84
  ];
87
85
 
88
- export function IABConsent({ cmpId }: { cmpId: number }) {
89
- return (
90
- <IABProvider cmpId={cmpId} publisherRestrictions={publisherRestrictions}>
91
- <IABConsentBanner />
92
- <IABConsentDialog />
93
- </IABProvider>
94
- );
95
- }
86
+ export const IABConsent = ({ cmpId }: { cmpId: number }) => (
87
+ <IABProvider cmpId={cmpId} publisherRestrictions={publisherRestrictions}>
88
+ <IABConsentBanner />
89
+ <IABConsentDialog />
90
+ </IABProvider>
91
+ );
96
92
  ```
97
93
 
98
94
  Each restriction type has a requirement that depends on the vendor list:
@@ -154,8 +150,10 @@ because turning one off would change nothing the vendors rely on; its
154
150
  objection control is the opt-out. Such a purpose does not decide its c15t
155
151
  category: the category follows the purposes in it that the visitor can
156
152
  consent to. Legitimate interest never grants a category on its own. If none of
157
- a category's purposes has a consent basis, the category needs all of them
158
- consented, which only Accept All does. Stack switches
153
+ a category's purposes has a consent basis, nothing can consent to them, Accept
154
+ All included, so the category stays denied. See
155
+ [categories under an IAB policy](../../concepts/consent-state.md#categories-under-an-iab-policy).
156
+ Stack switches
159
157
  cover only the purposes some vendor processes on consent. Display-model rows
160
158
  expose this as `hasConsentBasis`. Custom preference UIs can call
161
159
  `applyPublisherRestrictionsToGVL` from `@c15t/iab/headless`, or pass
@@ -209,6 +207,14 @@ the special purposes only. `featureRows` holds the features, with
209
207
  `featuresStandardText` is the standard text, or `null` when you should use the
210
208
  translation.
211
209
 
210
+ Each vendor's privacy policy and legitimate interest links come from its
211
+ `urls[]` entry in the Global Vendor List. The stock preference centres pick
212
+ the entry for the language the banner shows, then English. A custom UI gets
213
+ the same links from `resolveIABVendorUrls(vendor, language)` in
214
+ `@c15t/iab/headless`, or from `policyUrl` and `legitimateInterestUrl` on the
215
+ vendors `processGVLForDialog` returns when you pass it `language`. Vendor lists
216
+ older than GVL v3 have a single `policyUrl` field, which c15t still reads.
217
+
212
218
  ## Consent scope
213
219
 
214
220
  c15t stores TCF choices per site and browser, which the TCF calls