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

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 (219) hide show
  1. package/AGENTS.md +92 -72
  2. package/README.md +21 -17
  3. package/SKILL.md +34 -0
  4. package/dist/component-hooks/use-headless-consent-ui.js +1 -1
  5. package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
  6. package/dist/components/consent-gate/atoms.js +1 -1
  7. package/dist/components/consent-gate/consent-gate.js +1 -1
  8. package/dist/components/iab-panel/atoms/card.js +1 -1
  9. package/dist/components/iab-panel/atoms/feature-item.js +2 -0
  10. package/dist/components/iab-panel/atoms/header.js +1 -1
  11. package/dist/components/iab-panel/iab-panel.js +1 -1
  12. package/dist/components/iab-panel/index.js +1 -1
  13. package/dist/components/iab-prompt/atoms/overlay.js +1 -1
  14. package/dist/components/iab-prompt/atoms/root.js +1 -1
  15. package/dist/components/panel/atoms/card.js +1 -1
  16. package/dist/components/panel/atoms/root.js +1 -1
  17. package/dist/components/panel-trigger/atoms/button.js +1 -1
  18. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  19. package/dist/components/preferences/atoms/accordion.js +1 -1
  20. package/dist/components/prompt/atoms/overlay.js +1 -1
  21. package/dist/components/prompt/atoms/root.js +1 -1
  22. package/dist/components/prompt/components.js +1 -1
  23. package/dist/components/shared/libs/compose-refs.js +1 -0
  24. package/dist/components/shared/libs/slot.js +1 -1
  25. package/dist/components/shared/primitives/button.js +1 -1
  26. package/dist/components/shared/ui/dialog/dialog.js +1 -1
  27. package/dist/components/shared/ui/icon.js +1 -1
  28. package/dist/context/consent-tracking-context.js +1 -1
  29. package/dist/devtools.js +1 -1
  30. package/dist/draft-context.js +2 -0
  31. package/dist/draft.js +1 -1
  32. package/dist/external-iab-context.js +1 -1
  33. package/dist/gpp.js +2 -0
  34. package/dist/hooks/use-color-scheme.js +1 -1
  35. package/dist/hooks/use-focus-trap.js +1 -1
  36. package/dist/hooks.js +1 -1
  37. package/dist/iab/styles.tw3.css +4 -4
  38. package/dist/index-parts/consent-banner.js +1 -0
  39. package/dist/index-parts/consent-dialog-link.js +1 -0
  40. package/dist/index-parts/consent-dialog-trigger.js +1 -0
  41. package/dist/index-parts/consent-dialog.js +1 -0
  42. package/dist/index-parts/consent-gate.js +1 -0
  43. package/dist/index-parts/core.js +1 -0
  44. package/dist/index-parts/draft.js +1 -0
  45. package/dist/index-parts/hooks.js +1 -0
  46. package/dist/index-parts/module-hooks.js +1 -0
  47. package/dist/index-parts/provider.js +1 -0
  48. package/dist/index-parts/theme.js +1 -0
  49. package/dist/index-parts/translations.js +1 -0
  50. package/dist/index.js +1 -1
  51. package/dist/kernel-selector.js +1 -1
  52. package/dist/module-hooks/iframe-blocker.js +1 -1
  53. package/dist/postcss-tailwind3.js +1 -0
  54. package/dist/provider.js +1 -1
  55. package/dist/server/fetch-ssr-data.js +1 -1
  56. package/dist/server/headers.js +1 -1
  57. package/dist/server/normalize-url.js +1 -1
  58. package/dist/styles.tw3.css +16 -16
  59. package/dist/transports/offline.js +1 -1
  60. package/dist/utils/merge-slot-props.js +1 -1
  61. package/dist/utils/theme-utils.js +1 -1
  62. package/dist/version.js +1 -1
  63. package/dist-types/component-hooks/use-headless-consent-ui.d.ts +1 -1
  64. package/dist-types/components/iab-panel/atoms/feature-item.d.ts +28 -0
  65. package/dist-types/components/iab-panel/iab-panel.d.ts +2 -2
  66. package/dist-types/components/iab-panel/index.d.ts +5 -2
  67. package/dist-types/components/iab-panel/use-iab-translations.d.ts +4 -0
  68. package/dist-types/components/iab-prompt/iab-prompt.d.ts +2 -2
  69. package/dist-types/components/iab-prompt/index.d.ts +2 -2
  70. package/dist-types/components/panel/atoms/card.d.ts +1 -0
  71. package/dist-types/components/prompt/components.d.ts +6 -0
  72. package/dist-types/components/shared/libs/compose-refs.d.ts +20 -0
  73. package/dist-types/components/shared/ui/icon.d.ts +2 -1
  74. package/dist-types/context/consent-tracking-context.d.ts +10 -0
  75. package/dist-types/context/theme-context.d.ts +2 -2
  76. package/dist-types/context.d.ts +6 -0
  77. package/dist-types/devtools.d.ts +13 -2
  78. package/dist-types/draft-context.d.ts +31 -0
  79. package/dist-types/draft.d.ts +13 -30
  80. package/dist-types/gpp.d.ts +27 -0
  81. package/dist-types/hooks/use-focus-trap.d.ts +3 -1
  82. package/dist-types/hooks.d.ts +57 -13
  83. package/dist-types/iab.d.ts +1 -1
  84. package/dist-types/index-parts/consent-banner.d.ts +7 -0
  85. package/dist-types/index-parts/consent-dialog-link.d.ts +7 -0
  86. package/dist-types/index-parts/consent-dialog-trigger.d.ts +7 -0
  87. package/dist-types/index-parts/consent-dialog.d.ts +7 -0
  88. package/dist-types/index-parts/consent-gate.d.ts +7 -0
  89. package/dist-types/index-parts/core.d.ts +8 -0
  90. package/dist-types/index-parts/draft.d.ts +7 -0
  91. package/dist-types/index-parts/hooks.d.ts +7 -0
  92. package/dist-types/index-parts/module-hooks.d.ts +7 -0
  93. package/dist-types/index-parts/provider.d.ts +7 -0
  94. package/dist-types/index-parts/theme.d.ts +8 -0
  95. package/dist-types/index-parts/translations.d.ts +7 -0
  96. package/dist-types/index.d.ts +14 -15
  97. package/dist-types/kernel-selector.d.ts +0 -8
  98. package/dist-types/module-hooks/iframe-blocker.d.ts +12 -7
  99. package/dist-types/postcss-tailwind3.d.ts +21 -0
  100. package/dist-types/provider.d.ts +57 -17
  101. package/dist-types/server/headers.d.ts +14 -4
  102. package/dist-types/server/normalize-url.d.ts +14 -7
  103. package/dist-types/server/types.d.ts +12 -1
  104. package/dist-types/transports/offline.d.ts +7 -16
  105. package/dist-types/ui-config-context.d.ts +7 -1
  106. package/dist-types/utils/merge-slot-props.d.ts +15 -1
  107. package/dist-types/utils/theme-utils.d.ts +17 -2
  108. package/dist-types/version.d.ts +1 -1
  109. package/docs/README.md +92 -72
  110. package/docs/assets/v3/bottom-bar.png +0 -0
  111. package/docs/assets/v3/brand-card.png +0 -0
  112. package/docs/assets/v3/brand-preferences.png +0 -0
  113. package/docs/assets/v3/choice-wall.png +0 -0
  114. package/docs/assets/v3/headless-bar-html.png +0 -0
  115. package/docs/assets/v3/headless-bar-mobile.png +0 -0
  116. package/docs/assets/v3/headless-bar.png +0 -0
  117. package/docs/assets/v3/slim-bar.png +0 -0
  118. package/docs/concepts/choose-your-setup.md +87 -0
  119. package/docs/{frameworks/react/concepts → concepts}/consent-categories.md +22 -7
  120. package/docs/{guides → concepts}/consent-state.md +89 -105
  121. package/docs/{guides → concepts}/data-fetching.md +31 -27
  122. package/docs/concepts/how-consent-works.md +123 -0
  123. package/docs/{frameworks/react/concepts/policy-presets.md → concepts/policies.md} +7 -59
  124. package/docs/customization/class-names.md +202 -0
  125. package/docs/customization/dark-mode.md +157 -0
  126. package/docs/customization/motion.md +119 -0
  127. package/docs/customization/overview.md +67 -34
  128. package/docs/customization/recipes.md +839 -49
  129. package/docs/customization/slots.md +216 -35
  130. package/docs/customization/stylesheets.md +147 -0
  131. package/docs/customization/tailwind.md +842 -0
  132. package/docs/customization/tokens.md +163 -96
  133. package/docs/customization/translations.md +60 -3
  134. package/docs/frameworks/react/callbacks.md +118 -0
  135. package/docs/frameworks/react/clear-on-revocation.md +106 -0
  136. package/docs/frameworks/react/components/consent-banner.md +42 -28
  137. package/docs/frameworks/react/components/consent-dialog-link.md +13 -11
  138. package/docs/frameworks/react/components/consent-dialog-trigger.md +81 -76
  139. package/docs/frameworks/react/components/consent-dialog.md +56 -50
  140. package/docs/frameworks/react/components/consent-gate.md +37 -20
  141. package/docs/frameworks/react/components/consent-provider.md +130 -0
  142. package/docs/frameworks/react/components/consent-widget.md +23 -20
  143. package/docs/frameworks/react/components/dev-tools.md +53 -85
  144. package/docs/frameworks/react/components.md +105 -0
  145. package/docs/frameworks/react/compose.md +354 -0
  146. package/docs/frameworks/react/content-security-policy.md +126 -0
  147. package/docs/frameworks/react/customize.md +250 -0
  148. package/docs/frameworks/react/embeds.md +89 -0
  149. package/docs/frameworks/react/gpp.md +163 -0
  150. package/docs/frameworks/react/{hooks/overview.md → hooks.md} +21 -16
  151. package/docs/frameworks/react/{iab/overview.md → iab.md} +80 -31
  152. package/docs/frameworks/react/network-blocker.md +35 -41
  153. package/docs/frameworks/react/quickstart.md +139 -101
  154. package/docs/frameworks/react/rendering.md +128 -0
  155. package/docs/frameworks/react/scripts.md +115 -0
  156. package/docs/frameworks/react/translations.md +155 -0
  157. package/docs/frameworks/react/troubleshooting.md +92 -47
  158. package/docs/frameworks/react/vendor-consent.md +170 -0
  159. package/docs/guides/banner-experiments.md +654 -0
  160. package/docs/guides/troubleshooting.md +120 -47
  161. package/docs/guides/verify-consent.md +81 -49
  162. package/docs/integrations/adobe-analytics.md +167 -159
  163. package/docs/integrations/ahrefs-analytics.md +152 -154
  164. package/docs/integrations/amplitude.md +162 -156
  165. package/docs/integrations/building-integrations.md +136 -37
  166. package/docs/integrations/clearbit.md +154 -154
  167. package/docs/integrations/cloudflare-web-analytics.md +156 -156
  168. package/docs/integrations/cloudflare-zaraz.md +209 -261
  169. package/docs/integrations/crisp.md +164 -158
  170. package/docs/integrations/databuddy.md +157 -173
  171. package/docs/integrations/fathom-analytics.md +158 -156
  172. package/docs/integrations/front-chat.md +167 -167
  173. package/docs/integrations/google-maps.md +118 -83
  174. package/docs/integrations/google-tag-manager.md +178 -163
  175. package/docs/integrations/google-tag.md +163 -160
  176. package/docs/integrations/heap.md +163 -155
  177. package/docs/integrations/hightouch.md +161 -157
  178. package/docs/integrations/hotjar.md +159 -155
  179. package/docs/integrations/intercom.md +183 -153
  180. package/docs/integrations/klaviyo.md +486 -0
  181. package/docs/integrations/linkedin-insights.md +174 -150
  182. package/docs/integrations/logrocket.md +160 -156
  183. package/docs/integrations/matomo-analytics.md +188 -178
  184. package/docs/integrations/meta-pixel.md +188 -150
  185. package/docs/integrations/microsoft-clarity.md +163 -155
  186. package/docs/integrations/microsoft-uet.md +148 -154
  187. package/docs/integrations/mixpanel-analytics.md +155 -160
  188. package/docs/integrations/one-dollar-stats.md +166 -165
  189. package/docs/integrations/openai-pixel.md +204 -301
  190. package/docs/integrations/overview.md +137 -80
  191. package/docs/integrations/pinterest-tag.md +191 -183
  192. package/docs/integrations/pirsch.md +169 -159
  193. package/docs/integrations/plausible-analytics.md +172 -158
  194. package/docs/integrations/posthog.md +313 -240
  195. package/docs/integrations/promptwatch.md +154 -154
  196. package/docs/integrations/reddit-pixel.md +185 -157
  197. package/docs/integrations/rudderstack.md +201 -186
  198. package/docs/integrations/rybbit-analytics.md +171 -160
  199. package/docs/integrations/segment.md +182 -154
  200. package/docs/integrations/snapchat-pixel.md +186 -156
  201. package/docs/integrations/tiktok-pixel.md +171 -150
  202. package/docs/integrations/umami-analytics.md +163 -158
  203. package/docs/integrations/vercel-analytics.md +166 -157
  204. package/docs/integrations/x-pixel.md +176 -150
  205. package/docs/integrations/youtube.md +121 -86
  206. package/docs/upgrade-v3.md +496 -467
  207. package/package.json +20 -9
  208. package/dist/ui-save.js +0 -1
  209. package/dist-types/ui-save.d.ts +0 -25
  210. package/docs/assets/v3/brand-bar.png +0 -0
  211. package/docs/assets/v3/mobile-card.png +0 -0
  212. package/docs/assets/v3/preferences.png +0 -0
  213. package/docs/frameworks/react/components/consent-manager-provider.md +0 -95
  214. package/docs/frameworks/react/script-loader.md +0 -69
  215. package/docs/frameworks/react/styling/overview.md +0 -266
  216. package/docs/guides/deployment-modes.md +0 -75
  217. package/docs/guides/shared-consent-controls.md +0 -158
  218. package/docs/integrations/clear-on-revocation.md +0 -167
  219. package/docs/integrations/granular-consent.md +0 -210
@@ -1,26 +1,42 @@
1
1
  ---
2
2
  title: ConsentBanner
3
- description: Pre-built consent banner shown when consent is required.
3
+ description: Render the c15t consent banner in a React app, choose its variant
4
+ and position, and compose its parts.
4
5
  group: frameworks
5
6
  ---
6
7
 
7
- ## Usage
8
-
9
- ```tsx
10
- import { ConsentBanner, ConsentDialog, ConsentProvider, offline } from 'c15t/react';
11
- import { policyRulePresets } from '@c15t/schema';
12
-
13
- const mode = offline({ policyRules: [policyRulePresets.europeOptIn()] });
14
-
15
- export function Consent({ children }: { children: React.ReactNode }) {
16
- return (
17
- <ConsentProvider options={{ mode }}>
18
- {children}
19
- <ConsentBanner />
20
- <ConsentDialog />
21
- </ConsentProvider>
22
- );
23
- }
8
+ ## Render the banner inside `ConsentProvider`
9
+
10
+ Render `ConsentBanner` once inside `ConsentProvider`, next to `ConsentDialog`.
11
+ The [quickstart](../quickstart.md) mounts both in
12
+ `src/consent.tsx`:
13
+
14
+ ```tsx title="src/consent.tsx"
15
+ import {
16
+ ConsentBanner,
17
+ ConsentDialog,
18
+ ConsentDialogLink,
19
+ ConsentProvider,
20
+ hosted,
21
+ } from 'c15t/react';
22
+ import type { ReactNode } from 'react';
23
+
24
+ import { scripts } from './scripts';
25
+
26
+ import 'c15t/react/styles.css';
27
+
28
+ const mode = hosted({ url: 'https://your-project.inth.app' });
29
+
30
+ export const Consent = ({ children }: { children: ReactNode }) => (
31
+ <ConsentProvider options={{ mode, scripts }}>
32
+ {children}
33
+ <ConsentBanner />
34
+ <ConsentDialog />
35
+ <footer>
36
+ <ConsentDialogLink>Privacy settings</ConsentDialogLink>
37
+ </footer>
38
+ </ConsentProvider>
39
+ );
24
40
  ```
25
41
 
26
42
  The banner renders whatever the active policy rule requires. It reads the rule
@@ -122,16 +138,14 @@ button through the provider's theme:
122
138
  ```ts
123
139
  const theme = {
124
140
  colors: { primary: '#2f6f4e', primaryHover: '#24563c' },
125
- consentActions: {
126
- primary: { variant: 'primary', mode: 'filled' },
127
- },
141
+ consentActions: { primary: { variant: 'primary', mode: 'filled' } },
128
142
  } as const;
129
143
  ```
130
144
 
131
145
  Pass `theme` in `ConsentProvider` options. Per-action theme overrides such
132
146
  as `consentActions.dismiss` take precedence over this primary style.
133
- See [Styling](../styling/overview.md) for tokens and slots, and
134
- [Policy presets](../concepts/policy-presets.md) for the rule IDs and coverage.
147
+ See [customize](../customize.md) for tokens and slots, and
148
+ [policies](../../../concepts/policies.md) for the rule IDs and coverage.
135
149
 
136
150
  ## Variants
137
151
 
@@ -143,14 +157,14 @@ Set the variant on the banner, or on the provider under
143
157
  `presentation.prompt` when every banner should share it. The prop wins.
144
158
 
145
159
  ```tsx
146
- <ConsentBanner variant="floating" position="bottom-center" />
160
+ <ConsentBanner variant="floating" position="bottom-center" />;
147
161
  ```
148
162
 
149
163
  A floating card in a corner or centered on an edge. This is the default for
150
164
  every prompt. A notice keeps the same card, with its right link and "OK" in the footer.
151
165
 
152
166
  ```tsx
153
- <ConsentBanner variant="bar" position="top" />
167
+ <ConsentBanner variant="bar" position="top" />;
154
168
  ```
155
169
 
156
170
  A bar across the full width of the viewport. From 1024px wide the text, the
@@ -158,14 +172,14 @@ right links, and the controls share one row. Opt in to it for regions that
158
172
  expect a classic cookie bar.
159
173
 
160
174
  ```tsx
161
- <ConsentBanner variant="widget" position="bottom-right" />
175
+ <ConsentBanner variant="widget" position="bottom-right" />;
162
176
  ```
163
177
 
164
178
  A compact card with smaller type. The full description and its legal links
165
179
  remain visible. Pair it with a short notice.
166
180
 
167
181
  ```tsx
168
- <ConsentBanner variant="wall" />
182
+ <ConsentBanner variant="wall" />;
169
183
  ```
170
184
 
171
185
  A centered card over a backdrop that blocks the page until the visitor
@@ -248,7 +262,7 @@ Use the individual parts when you need a different order or your own markup:
248
262
  <ConsentBanner.RightLink right="opt-out" asChild>
249
263
  <a href="/privacy/do-not-sell">Do not sell or share my data</a>
250
264
  </ConsentBanner.RightLink>
251
- </ConsentBanner.Rights>
265
+ </ConsentBanner.Rights>;
252
266
  ```
253
267
 
254
268
  `useBannerCopy()` returns the title, description, and prompt kind the banner
@@ -16,15 +16,17 @@ same `ConsentProvider`; the [quickstart](../quickstart.md) mounts both.
16
16
  import { ConsentDialogLink } from 'c15t/react';
17
17
 
18
18
  export function SiteFooter() {
19
- return (
20
- <footer>
21
- <nav aria-label="Legal">
22
- <a href="/privacy">Privacy policy</a>
23
- <a href="/terms">Terms</a>
24
- <ConsentDialogLink className="footer-link">Privacy settings</ConsentDialogLink>
25
- </nav>
26
- </footer>
27
- );
19
+ return (
20
+ <footer>
21
+ <nav aria-label="Legal">
22
+ <a href="/privacy">Privacy policy</a>
23
+ <a href="/terms">Terms</a>
24
+ <ConsentDialogLink className="footer-link">
25
+ Privacy settings
26
+ </ConsentDialogLink>
27
+ </nav>
28
+ </footer>
29
+ );
28
30
  }
29
31
  ```
30
32
 
@@ -37,8 +39,8 @@ To reuse an existing link component, pass `asChild`:
37
39
 
38
40
  ```tsx
39
41
  <ConsentDialogLink asChild>
40
- <a href="#cookie-preferences">Manage cookies</a>
41
- </ConsentDialogLink>
42
+ <a href="#cookie-preferences">Manage cookies</a>
43
+ </ConsentDialogLink>;
42
44
  ```
43
45
 
44
46
  ## Props
@@ -1,35 +1,20 @@
1
1
  ---
2
2
  title: ConsentDialogTrigger
3
- description: Floating button and toolbar that reopen the preference center after
4
- the first prompt.
3
+ description: Add the c15t ConsentDialogTrigger floating button or toolbar inside
4
+ a React ConsentProvider so visitors can reopen the preference center after the
5
+ first prompt.
5
6
  group: frameworks
6
7
  ---
7
8
 
8
- ## Usage
9
+ ## Add the trigger inside ConsentProvider
9
10
 
10
- `ConsentDialogTrigger` is a draggable floating button that opens the preference center. Place it once, next to the banner and dialog, so visitors can revisit their choices without a footer link.
11
+ `ConsentDialogTrigger` is a draggable floating button that opens the preference center. Place it once inside `ConsentProvider`, next to the banner and dialog from the [quickstart](../quickstart.md), so visitors can revisit their choices without a footer link.
11
12
 
12
- ```tsx
13
- import {
14
- ConsentBanner,
15
- ConsentDialog,
16
- ConsentDialogTrigger,
17
- ConsentProvider,
18
- offline,
19
- } from 'c15t/react';
20
- import { policyRulePresets } from '@c15t/schema';
21
-
22
- const mode = offline({ policyRules: [policyRulePresets.europeOptIn()] });
23
-
24
- export function App() {
25
- return (
26
- <ConsentProvider options={{ mode }}>
27
- <ConsentBanner />
28
- <ConsentDialog />
29
- <ConsentDialogTrigger showWhen="after-prompt" />
30
- </ConsentProvider>
31
- );
32
- }
13
+ ```tsx title="src/consent.tsx"
14
+ import { ConsentDialogTrigger } from 'c15t/react';
15
+
16
+ // Inside the ConsentProvider from your quickstart, next to the dialog:
17
+ <ConsentDialogTrigger showWhen="after-prompt" />;
33
18
  ```
34
19
 
35
20
  `showWhen="after-prompt"` keeps the button out of the way while a choice or notice is still owed and shows it once the visitor has answered, so the banner and the trigger never compete for the same corner. The default, `always`, shows it whenever the preference center is closed.
@@ -56,43 +41,63 @@ Use `ConsentDialogTriggerToolbar` when you want app-owned controls beside the pr
56
41
  import { ConsentDialogTriggerToolbar } from 'c15t/react';
57
42
 
58
43
  <ConsentDialogTriggerToolbar
59
- ariaLabel="Site controls"
60
- actions={[
61
- {
62
- id: 'theme',
63
- label: isDark ? 'Switch to light theme' : 'Switch to dark theme',
64
- icon: isDark ? <SunIcon /> : <MoonIcon />,
65
- pressed: isDark,
66
- onSelect: toggleColorScheme,
67
- },
68
- {
69
- id: 'support',
70
- label: 'Open support chat',
71
- icon: <ChatIcon />,
72
- onSelect: openSupportChat,
73
- },
74
- ]}
75
- preferences={{
76
- icon: 'fingerprint',
77
- label: 'Manage privacy settings',
78
- }}
79
- />
44
+ ariaLabel="Site controls"
45
+ actions={[
46
+ {
47
+ id: 'theme',
48
+ label: isDark ? 'Switch to light theme' : 'Switch to dark theme',
49
+ icon: isDark ? <SunIcon /> : <MoonIcon />,
50
+ pressed: isDark,
51
+ onSelect: toggleColorScheme,
52
+ },
53
+ {
54
+ id: 'support',
55
+ label: 'Open support chat',
56
+ icon: <ChatIcon />,
57
+ onSelect: openSupportChat,
58
+ },
59
+ ]}
60
+ preferences={{ icon: 'fingerprint', label: 'Manage privacy settings' }}
61
+ />;
80
62
  ```
81
63
 
82
64
  Each custom action needs a stable `id`, an accessible `label`, an `icon`, and an `onSelect` callback. Use `pressed` for toggle actions and `disabled` for unavailable ones. Your app owns the state behind each action. The preferences action moves to the edge nearest the toolbar's snapped corner while custom actions keep their configured order.
83
65
 
84
- The toolbar does not depend on the consent policy. Your actions always render, so a theme toggle stays available while a banner is up. On the toolbar, `showWhen` applies only to the built-in preferences action: `after-prompt` hides that one item while a choice or notice is owed and shows it once the visitor has answered, and `never` leaves it out. The toolbar renders nothing only when it has no visible item. On the single `ConsentDialogTrigger`, `showWhen` applies to the whole control.
85
-
86
- No policy, no UI. When no rule has resolved, because resolution failed, no rule matched and you set no default, or init is still withheld, the built-in preferences action and the single `ConsentDialogTrigger` render nothing; app-owned toolbar actions still do. They appear as soon as a rule resolves, without a remount. A rule with `model: 'none'` owes no rights, so the built-in action and the single trigger hide under it too, unless the rule adds `rights: ['preferences']`. The policy chooses what the control says, you choose where and when it sits, and it disappears only when there is nothing to manage.
87
-
88
- The built-in action names the strongest right the active rule guarantees. Under a rule that carries the `opt-out` right, such as a US opt-out notice, its accessible name is the translated "Do not sell or share my data" and the button carries `data-right="opt-out"`. Under any other rule it reads "Manage preferences" with `data-right="preferences"`. Either way it opens the preference center, and a `preferences.label` you pass replaces the default. The button also carries `data-c15t-rights` with every right on the rule.
89
-
90
66
  Toolbars are horizontal by default. Set `orientation="vertical"` to stack the actions and switch arrow-key navigation to up and down:
91
67
 
92
68
  ```tsx
93
- <ConsentDialogTriggerToolbar orientation="vertical" actions={toolbarActions} />
69
+ <ConsentDialogTriggerToolbar orientation="vertical" actions={toolbarActions} />;
94
70
  ```
95
71
 
72
+ ### When the controls show
73
+
74
+ Your toolbar actions do not depend on the consent policy, so a theme toggle
75
+ stays available while a banner is up. The built-in preferences action, and the
76
+ single `ConsentDialogTrigger`, follow the policy:
77
+
78
+ | Situation | Built-in action and `ConsentDialogTrigger` |
79
+ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
80
+ | A choice or notice is still owed, with `showWhen="after-prompt"` | Hidden until the visitor answers |
81
+ | `showWhen="never"` on the toolbar | Left out of the toolbar |
82
+ | No policy rule has resolved: resolution failed, no rule matched and you set no default, or init is still withheld | Hidden; appears without a remount once a rule resolves |
83
+ | The rule has `model: 'none'` | Hidden, unless the rule adds `rights: ['preferences']` |
84
+ | Any other case | Shown |
85
+
86
+ On the toolbar, `showWhen` applies only to the built-in action. On the single
87
+ `ConsentDialogTrigger`, it applies to the whole control. The toolbar renders
88
+ nothing only when it has no visible item.
89
+
90
+ ### What the built-in action says
91
+
92
+ * Under a rule with the `opt-out` right, such as a US opt-out notice, its
93
+ accessible name is the translated "Do not sell or share my data" and it
94
+ carries `data-right="opt-out"`.
95
+ * Under any other rule it reads "Manage preferences" and carries
96
+ `data-right="preferences"`.
97
+ * Either way it opens the preference center. A `preferences.label` you pass
98
+ replaces the default name, and the button carries `data-c15t-rights` with
99
+ every right on the rule.
100
+
96
101
  ### Toolbar props
97
102
 
98
103
  | Prop | Type | Default | Description |
@@ -113,30 +118,30 @@ The toolbar follows the standard precedence: bundled styles, then provider slots
113
118
 
114
119
  ```tsx
115
120
  <ConsentProvider
116
- options={{
117
- mode: offline({ policyRules: [policyRulePresets.europeOptIn()] }),
118
- components: {
119
- trigger: {
120
- toolbar: { className: 'my-toolbar' },
121
- toolbarItem: { className: 'my-toolbar-item' },
122
- toolbarIcon: { className: 'my-toolbar-icon' },
123
- },
124
- },
125
- }}
121
+ options={{
122
+ mode,
123
+ components: {
124
+ trigger: {
125
+ toolbar: { className: 'my-toolbar' },
126
+ toolbarItem: { className: 'my-toolbar-item' },
127
+ toolbarIcon: { className: 'my-toolbar-icon' },
128
+ },
129
+ },
130
+ }}
126
131
  >
127
- <ConsentDialogTriggerToolbar
128
- className="fixed-toolbar"
129
- actions={[
130
- {
131
- id: 'support',
132
- label: 'Open support chat',
133
- icon: <ChatIcon />,
134
- onSelect: openSupportChat,
135
- className: 'support-action',
136
- },
137
- ]}
138
- />
139
- </ConsentProvider>
132
+ <ConsentDialogTriggerToolbar
133
+ className="fixed-toolbar"
134
+ actions={[
135
+ {
136
+ id: 'support',
137
+ label: 'Open support chat',
138
+ icon: <ChatIcon />,
139
+ onSelect: openSupportChat,
140
+ className: 'support-action',
141
+ },
142
+ ]}
143
+ />
144
+ </ConsentProvider>;
140
145
  ```
141
146
 
142
147
  The toolbar slot keys are `trigger.toolbar`, `trigger.toolbarItem`, and `trigger.toolbarIcon`. With `noStyle`, style the current position and interaction state through `data-corner`, `data-dragging`, and `data-snapping` on the toolbar element.
@@ -14,32 +14,37 @@ group: frameworks
14
14
  code.
15
15
 
16
16
  ```tsx title="src/consent.tsx"
17
- import type { ReactNode } from 'react';
18
17
  import {
19
- ConsentBanner,
20
- ConsentDialog,
21
- ConsentDialogLink,
22
- ConsentProvider,
23
- hosted,
18
+ ConsentBanner,
19
+ ConsentDialog,
20
+ ConsentDialogLink,
21
+ ConsentProvider,
22
+ hosted,
24
23
  } from 'c15t/react';
24
+ import type { ReactNode } from 'react';
25
+
26
+ import { scripts } from './scripts';
27
+
25
28
  import 'c15t/react/styles.css';
26
29
 
27
- const backendURL = import.meta.env.VITE_C15T_BACKEND_URL;
28
- if (!backendURL) throw new Error('Set VITE_C15T_BACKEND_URL');
29
- const mode = hosted({ url: backendURL });
30
-
31
- export function Consent({ children }: { children: ReactNode }) {
32
- return (
33
- <ConsentProvider options={{ mode }}>
34
- {children}
35
- <ConsentBanner />
36
- <ConsentDialog hideBranding />
37
- <footer>
38
- <ConsentDialogLink>Privacy settings</ConsentDialogLink>
39
- </footer>
40
- </ConsentProvider>
41
- );
42
- }
30
+ const mode = hosted({ url: 'https://your-project.inth.app' });
31
+
32
+ export const Consent = ({ children }: { children: ReactNode }) => (
33
+ <ConsentProvider options={{ mode, scripts }}>
34
+ {children}
35
+ <ConsentBanner />
36
+ <ConsentDialog />
37
+ <footer>
38
+ <ConsentDialogLink>Privacy settings</ConsentDialogLink>
39
+ </footer>
40
+ </ConsentProvider>
41
+ );
42
+ ```
43
+
44
+ To hide the "Secured by" tag in the dialog card, pass `hideBranding`:
45
+
46
+ ```tsx
47
+ <ConsentDialog hideBranding />;
43
48
  ```
44
49
 
45
50
  To open it from your own component, set the active surface:
@@ -48,41 +53,41 @@ To open it from your own component, set the active surface:
48
53
  import { useSetActiveUI } from 'c15t/react';
49
54
 
50
55
  export function PrivacyMenuItem() {
51
- const setActiveUI = useSetActiveUI();
52
- return (
53
- <button type="button" onClick={() => setActiveUI('dialog')}>
54
- Cookie preferences
55
- </button>
56
- );
56
+ const setActiveUI = useSetActiveUI();
57
+ return (
58
+ <button type="button" onClick={() => setActiveUI('dialog')}>
59
+ Cookie preferences
60
+ </button>
61
+ );
57
62
  }
58
63
  ```
59
64
 
60
65
  `ConsentDialogLink` does the same with the policy's rights exposed for
61
66
  styling; see [ConsentDialogLink](./consent-dialog-link.md). For a floating
62
67
  button, see [ConsentDialogTrigger](./consent-dialog-trigger.md). Under an IAB
63
- policy, mount `IABConsentDialog` from the [IAB guide](../iab/overview.md)
68
+ policy, mount `IABConsentDialog` from the [IAB guide](../iab.md)
64
69
  instead; this dialog stays closed for the `iab` model.
65
70
 
66
71
  ## Props
67
72
 
68
- | Prop | Type | Default | Description |
69
- | ------------------------- | ------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
- | `open` | `boolean` | follows the active surface | Controls the open state. When set, Escape and the dialog's own Save, Accept all and Reject all buttons no longer close it; change the prop instead. A missing policy or an unlisted model still keeps it closed. |
71
- | `showTrigger` | `boolean \|ConsentDialogTriggerProps` | `false` | Renders a floating `ConsentDialogTrigger` next to the dialog. Pass an object to configure that trigger. |
72
- | `models` | `Model[]` | `['opt-in', 'opt-out', 'none']` | Policy models the dialog responds to. Under a model that is not listed, such as `iab`, it stays closed. |
73
- | `legalLinks` | `(keyof LegalLinks)[] \|null` | none | Which of the links configured in the provider `options.legalLinks` render after the description. Omitting the prop renders none. `null` hides them. |
74
- | `hideBranding` | `boolean` | `false` | Hides the "Secured by" tag in the card. |
75
- | `uiSource` | `string` | `'dialog'` | Source identifier recorded with saves made from this dialog. |
76
- | `scrollLock`, `trapFocus` | `boolean` | from `blocking` | Deprecated. Either one set to `false` makes the dialog non-blocking, unless the provider sets `presentation.preferences.blocking` explicitly, which wins. Prefer the provider option. |
77
- | `disableAnimation` | `boolean` | `false` | Skips the enter and exit animation of the backdrop and the card. |
78
- | `noStyle` | `boolean` | `false` | Removes the built-in styling from every part. |
73
+ | Prop | Type | Default | Description |
74
+ | ------------------------- | ------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | `open` | `boolean` | follows the active surface | Controls the open state. When set, Escape and the dialog's own Save Settings, Accept All and Reject All buttons no longer close it; change the prop instead. A missing policy or an unlisted model still keeps it closed. |
76
+ | `showTrigger` | `boolean \|ConsentDialogTriggerProps` | `false` | Renders a floating `ConsentDialogTrigger` next to the dialog. Pass an object to configure that trigger. |
77
+ | `models` | `Model[]` | `['opt-in', 'opt-out', 'none']` | Policy models the dialog responds to. Under a model that is not listed, such as `iab`, it stays closed. |
78
+ | `legalLinks` | `(keyof LegalLinks)[] \|null` | none | Which of the links configured in the provider `options.legalLinks` render after the description. Omitting the prop renders none. `null` hides them. |
79
+ | `hideBranding` | `boolean` | `false` | Hides the "Secured by" tag in the card. |
80
+ | `uiSource` | `string` | `'dialog'` | Source identifier recorded with saves made from this dialog. |
81
+ | `scrollLock`, `trapFocus` | `boolean` | from `blocking` | Deprecated. Either one set to `false` makes the dialog non-blocking, unless the provider sets `presentation.preferences.blocking` explicitly, which wins. Prefer the provider option. |
82
+ | `disableAnimation` | `boolean` | `false` | Skips the enter and exit animation of the backdrop and the card. |
83
+ | `noStyle` | `boolean` | `false` | Removes the built-in styling from every part. |
79
84
 
80
85
  ## Behavior
81
86
 
82
87
  The dialog is a modal wrapper around the preference center. Its card has a
83
88
  header with the title `consentManagerDialog.title` and the description
84
89
  `consentManagerDialog.description` followed by the legal links, then the
85
- same category accordion and Reject all, Accept all and Save actions that
90
+ same category accordion and Reject All, Accept All and Save Settings actions that
86
91
  `ConsentWidget` renders, then the branding tag. It mounts through a portal
87
92
  into `document.body` after hydration, so it is never part of the server
88
93
  HTML.
@@ -127,12 +132,12 @@ cached: the next hover, focus or open tries the download again.
127
132
  The dialog's CSS travels in the same chunk and is applied before the dialog
128
133
  renders, so your app's `styles.css` import stays small.
129
134
 
130
- Without an `open` prop, Escape closes the dialog. Save, Accept all and
131
- Reject all close it as soon as the choice is recorded in the browser, in
135
+ Without an `open` prop, Escape closes the dialog. Save Settings, Accept All and
136
+ Reject All close it as soon as the choice is recorded in the browser, in
132
137
  the same task as the click; the exit animation still plays. The request to
133
138
  the backend runs afterwards and never reopens the dialog: a failed request
134
139
  keeps the choice, reports through `onError` and is retried later. See
135
- [when a choice is saved](../../../guides/consent-state.md#when-a-choice-is-saved)
140
+ [when a choice is saved](../../../concepts/consent-state.md#when-a-choice-is-saved)
136
141
  for the order of events. A draft that went stale because the policy changed keeps the dialog open
137
142
  for review. With `open={true}`, these actions leave the dialog visible; the
138
143
  parent must set `open={false}` to close it. Clicking the backdrop does not
@@ -168,11 +173,12 @@ The panel carries `role="dialog"`, `aria-labelledby="consent-dialog-title"`
168
173
  and `aria-describedby="consent-dialog-description"`, plus `aria-modal="true"`
169
174
  while it is blocking. Its `dir` attribute follows the active language.
170
175
 
171
- While blocking, focus moves on open to the panel itself rather than to a
172
- button, so a screen reader announces the title and description first; Tab
173
- then enters the legal links and category switches, Tab and Shift+Tab wrap
174
- inside the panel, and on close focus returns to the element that opened the
175
- dialog. A non-blocking dialog manages no focus: nothing moves focus into the
176
+ While blocking, focus moves on open to the first tabbable control in the
177
+ panel, so a keyboard user sees the focus ring on a control rather than
178
+ around the whole panel; the `aria-labelledby` and `aria-describedby`
179
+ wiring means a screen reader still announces the title and description as
180
+ focus enters. Tab and Shift+Tab wrap inside the panel, and on close focus
181
+ returns to the element that opened the dialog. A non-blocking dialog manages no focus: nothing moves focus into the
176
182
  panel or back to the opener. The backdrop is `aria-hidden` and is not
177
183
  focusable.
178
184
 
@@ -227,7 +233,7 @@ download but every visitor downloads the dialog.
227
233
 
228
234
  The compound parts import from `c15t/react` as properties of `ConsentDialog`,
229
235
  and `ConsentWidget` from the same module. See
230
- [Styling](../styling/overview.md) for slots and tokens.
236
+ [customize](../customize.md) for slots and tokens.
231
237
 
232
238
  ## Verify
233
239
 
@@ -19,17 +19,17 @@ placeholder button opens.
19
19
  import { ConsentGate } from 'c15t/react';
20
20
 
21
21
  export function ProductVideo() {
22
- return (
23
- <ConsentGate category="marketing" className="video-frame">
24
- <iframe
25
- src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
26
- title="Product tour"
27
- loading="lazy"
28
- allowFullScreen
29
- style={{ width: '100%', aspectRatio: '16 / 9', border: 0 }}
30
- />
31
- </ConsentGate>
32
- );
22
+ return (
23
+ <ConsentGate category="marketing" className="video-frame">
24
+ <iframe
25
+ src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
26
+ title="Product tour"
27
+ loading="lazy"
28
+ allowFullScreen
29
+ style={{ width: '100%', aspectRatio: '16 / 9', border: 0 }}
30
+ />
31
+ </ConsentGate>
32
+ );
33
33
  }
34
34
  ```
35
35
 
@@ -53,8 +53,8 @@ placeholder is replaced.
53
53
  Other `div` attributes such as `className`, `style` and `ref` apply to the
54
54
  wrapper element that stays in the page in both states. `ConsentGateProps` also
55
55
  declares `noStyle` and `theme`, but the component does not apply them yet;
56
- style the wrapper with `className` and theme the placeholder through the
57
- provider `theme` option.
56
+ style the wrapper with `className`, and style the built-in placeholder
57
+ through its parts in the provider options.
58
58
 
59
59
  ## Behavior
60
60
 
@@ -70,16 +70,17 @@ children unmount and the embed disappears. `ConsentGate` does not depend on the
70
70
  network blocker or the iframe blocker modules; use those for markup you
71
71
  cannot wrap in a component.
72
72
 
73
- The built-in placeholder shows the title `frame.title`, "Accept {category}
74
- consent to view this content.", with `{category}` replaced by
75
- `consentTypes.<category>.title`, and a button labelled `frame.actionButton`,
76
- "Enable {category} consent". The button opens the preference center; it
73
+ The built-in placeholder shows the title `consentGate.title`,
74
+ `Accept {category} consent to view this content.`, with `{category}`
75
+ replaced by `consentTypes.<category>.title`, and a button labelled
76
+ `consentGate.actionButton`, `Enable {category} consent`. The button opens the
77
+ preference center; it
77
78
  does not grant the category by itself, because the visitor still has to
78
79
  save. Mount a `ConsentDialog` in the same provider so the button has
79
80
  something to open.
80
81
 
81
82
  Under a policy with `scopeMode: 'strict'`, a category outside the rule's
82
- scope cannot be granted. The built-in placeholder then shows `frame.policyBlocked`,
83
+ scope cannot be granted. The built-in placeholder then shows `consentGate.policyBlocked`,
83
84
  "This content is unavailable under your region's consent policy.", without
84
85
  the button, and a stored grant for that category stays blocked.
85
86
 
@@ -126,8 +127,24 @@ translated text; pass children to either to replace it.
126
127
  </ConsentGate>
127
128
  ```
128
129
 
129
- The built-in placeholder carries `data-testid="frame-placeholder"` and its
130
- button `data-testid="frame-open-dialog"`.
130
+ To restyle the built-in placeholder without replacing it, set its parts
131
+ under `components['consent-gate']` in the provider options. `root` is the
132
+ card, and `title` and `button` are its text and button. The `consentGate`, `consentGateTitle` and
133
+ `consentGateButton` theme slots reach the same parts. The button part
134
+ applies on top of `button.primary`.
135
+
136
+ ```tsx
137
+ components: {
138
+ 'consent-gate': {
139
+ root: { className: 'rounded-none' },
140
+ button: { className: 'font-semibold' },
141
+ },
142
+ },
143
+ ```
144
+
145
+ The built-in placeholder carries `data-testid="consent-gate-placeholder"`,
146
+ its title `data-testid="consent-gate-title"` and its button
147
+ `data-testid="consent-gate-button"`.
131
148
 
132
149
  ## Accessibility
133
150