@c15t/react 2.0.0-rc.6 → 2.0.0-rc.8

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 (168) hide show
  1. package/README.md +9 -0
  2. package/dist/components/consent-banner/atoms/root.cjs +1 -1
  3. package/dist/components/consent-banner/atoms/root.js +1 -1
  4. package/dist/components/consent-banner/consent-banner.cjs +1 -1
  5. package/dist/components/consent-banner/consent-banner.js +1 -1
  6. package/dist/components/consent-banner/index.cjs +1 -1
  7. package/dist/components/consent-banner/index.js +1 -1
  8. package/dist/components/consent-banner/policy-actions.cjs +1 -0
  9. package/dist/components/consent-banner/policy-actions.js +1 -0
  10. package/dist/components/consent-dialog/atoms/card.cjs +1 -1
  11. package/dist/components/consent-dialog/atoms/card.js +1 -1
  12. package/dist/components/consent-dialog/atoms/root.cjs +1 -1
  13. package/dist/components/consent-dialog/atoms/root.js +1 -1
  14. package/dist/components/consent-dialog-trigger/atoms/icon.cjs +1 -1
  15. package/dist/components/consent-dialog-trigger/atoms/icon.js +1 -1
  16. package/dist/components/consent-widget/atoms/accordion.cjs +1 -1
  17. package/dist/components/consent-widget/atoms/accordion.js +1 -1
  18. package/dist/components/consent-widget/atoms/footer.cjs +1 -1
  19. package/dist/components/consent-widget/atoms/footer.js +1 -1
  20. package/dist/components/consent-widget/consent-widget.cjs +1 -1
  21. package/dist/components/consent-widget/consent-widget.js +1 -1
  22. package/dist/components/consent-widget/index.cjs +1 -1
  23. package/dist/components/consent-widget/index.js +1 -1
  24. package/dist/components/consent-widget/policy-actions.cjs +1 -0
  25. package/dist/components/consent-widget/policy-actions.js +1 -0
  26. package/dist/components/iab-consent-banner/atoms/card.cjs +1 -1
  27. package/dist/components/iab-consent-banner/atoms/card.js +1 -1
  28. package/dist/components/iab-consent-banner/atoms/footer.cjs +1 -1
  29. package/dist/components/iab-consent-banner/atoms/footer.js +1 -1
  30. package/dist/components/iab-consent-banner/atoms/header.cjs +1 -1
  31. package/dist/components/iab-consent-banner/atoms/header.js +1 -1
  32. package/dist/components/iab-consent-banner/atoms/root.cjs +1 -1
  33. package/dist/components/iab-consent-banner/atoms/root.js +1 -1
  34. package/dist/components/iab-consent-banner/iab-consent-banner.cjs +1 -1
  35. package/dist/components/iab-consent-banner/iab-consent-banner.js +1 -1
  36. package/dist/components/iab-consent-dialog/atoms/card.cjs +1 -1
  37. package/dist/components/iab-consent-dialog/atoms/card.js +1 -1
  38. package/dist/components/iab-consent-dialog/atoms/footer.cjs +1 -1
  39. package/dist/components/iab-consent-dialog/atoms/footer.js +1 -1
  40. package/dist/components/iab-consent-dialog/atoms/header.cjs +1 -1
  41. package/dist/components/iab-consent-dialog/atoms/header.js +1 -1
  42. package/dist/components/iab-consent-dialog/atoms/root.cjs +1 -1
  43. package/dist/components/iab-consent-dialog/atoms/root.js +1 -1
  44. package/dist/components/iab-consent-dialog/iab-consent-dialog.cjs +1 -1
  45. package/dist/components/iab-consent-dialog/iab-consent-dialog.js +1 -1
  46. package/dist/components/shared/policy-actions.cjs +1 -0
  47. package/dist/components/shared/policy-actions.js +1 -0
  48. package/dist/components/shared/primitives/box.cjs +1 -1
  49. package/dist/components/shared/primitives/box.js +1 -1
  50. package/dist/components/shared/ui/branding.cjs +1 -0
  51. package/dist/components/shared/ui/branding.js +1 -0
  52. package/dist/components/shared/ui/logo.cjs +1 -1
  53. package/dist/components/shared/ui/logo.js +1 -1
  54. package/dist/components/shared/ui/switch/index.cjs +1 -1
  55. package/dist/components/shared/ui/switch/index.js +1 -1
  56. package/dist/hooks/index.cjs +1 -1
  57. package/dist/hooks/use-consent-manager.cjs +1 -1
  58. package/dist/hooks/use-consent-manager.js +1 -1
  59. package/dist/hooks/use-headless-consent-ui.cjs +1 -1
  60. package/dist/hooks/use-headless-consent-ui.js +1 -1
  61. package/dist/iab/styles.css +11 -1
  62. package/dist/iab/styles.tw3.css +14 -0
  63. package/dist/providers/consent-manager-provider.cjs +1 -1
  64. package/dist/providers/consent-manager-provider.js +1 -1
  65. package/dist/server/fetch-ssr-data.cjs +1 -1
  66. package/dist/server/fetch-ssr-data.js +1 -1
  67. package/dist/styles.css +10 -1
  68. package/dist/styles.tw3.css +13 -0
  69. package/dist/version.cjs +1 -1
  70. package/dist/version.js +1 -1
  71. package/dist-types/components/consent-banner/atoms/root.d.ts +44 -13
  72. package/dist-types/components/consent-banner/consent-banner.d.ts +7 -2
  73. package/dist-types/components/consent-banner/index.d.ts +47 -24
  74. package/dist-types/components/consent-banner/policy-actions.d.ts +11 -0
  75. package/dist-types/components/consent-dialog/atoms/card.d.ts +7 -2
  76. package/dist-types/components/consent-dialog/atoms/root.d.ts +1 -1
  77. package/dist-types/components/consent-dialog/consent-dialog.d.ts +1 -1
  78. package/dist-types/components/consent-dialog/index.d.ts +29 -8
  79. package/dist-types/components/consent-dialog-trigger/atoms/icon.d.ts +1 -1
  80. package/dist-types/components/consent-dialog-trigger/types.d.ts +1 -1
  81. package/dist-types/components/consent-preferences-link/consent-preferences-link.d.ts +0 -1
  82. package/dist-types/components/consent-widget/atoms/accordion.d.ts +4 -3
  83. package/dist-types/components/consent-widget/index.d.ts +26 -6
  84. package/dist-types/components/consent-widget/policy-actions.d.ts +11 -0
  85. package/dist-types/components/frame/atoms.d.ts +1 -1
  86. package/dist-types/components/frame/types.d.ts +1 -1
  87. package/dist-types/components/iab-consent-banner/atoms/root.d.ts +1 -1
  88. package/dist-types/components/iab-consent-banner/iab-consent-banner.d.ts +1 -1
  89. package/dist-types/components/iab-consent-dialog/atoms/root.d.ts +1 -1
  90. package/dist-types/components/iab-consent-dialog/atoms/vendor-list.d.ts +1 -1
  91. package/dist-types/components/iab-consent-dialog/iab-consent-dialog.d.ts +1 -1
  92. package/dist-types/components/shared/policy-actions.d.ts +29 -0
  93. package/dist-types/components/shared/primitives/button.d.ts +1 -1
  94. package/dist-types/components/shared/primitives/button.types.d.ts +1 -1
  95. package/dist-types/components/shared/primitives/legal-links/index.d.ts +1 -1
  96. package/dist-types/components/shared/ui/accordion/accordion.d.ts +2 -2
  97. package/dist-types/components/shared/ui/accordion/accordion.types.d.ts +1 -1
  98. package/dist-types/components/shared/ui/branding.d.ts +23 -0
  99. package/dist-types/components/shared/ui/button/button.d.ts +1 -1
  100. package/dist-types/components/shared/ui/logo.d.ts +8 -0
  101. package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +1 -1
  102. package/dist-types/components/shared/ui/switch/index.d.ts +1 -1
  103. package/dist-types/components/shared/ui/switch/switch.d.ts +1 -1
  104. package/dist-types/components/shared/ui/switch/switch.types.d.ts +1 -1
  105. package/dist-types/components/shared/ui/tabs/tabs.d.ts +1 -1
  106. package/dist-types/context/consent-manager-context.d.ts +1 -1
  107. package/dist-types/headless.d.ts +1 -1
  108. package/dist-types/hooks/use-component-config.d.ts +1 -1
  109. package/dist-types/hooks/use-consent-manager.d.ts +1 -1
  110. package/dist-types/hooks/use-headless-consent-ui.d.ts +1 -1
  111. package/dist-types/hooks/use-ssr-status.d.ts +2 -1
  112. package/dist-types/hooks/use-translations.d.ts +1 -1
  113. package/dist-types/index.d.ts +2 -2
  114. package/dist-types/server/fetch-ssr-data.d.ts +1 -1
  115. package/dist-types/server/init-cache-key.d.ts +1 -1
  116. package/dist-types/server/types.d.ts +1 -1
  117. package/dist-types/types/consent-manager.d.ts +1 -1
  118. package/dist-types/types/theme/style-keys.d.ts +1 -1
  119. package/dist-types/types/theme/style-types.d.ts +2 -2
  120. package/dist-types/utils/cn.d.ts +1 -1
  121. package/dist-types/utils/theme-utils.d.ts +1 -1
  122. package/dist-types/version.d.ts +1 -1
  123. package/docs/building-headless-components.md +43 -22
  124. package/docs/callbacks.md +76 -9
  125. package/docs/components/consent-banner.md +148 -24
  126. package/docs/components/consent-dialog.md +42 -3
  127. package/docs/components/consent-manager-provider.md +3 -1
  128. package/docs/components/consent-widget.md +91 -9
  129. package/docs/concepts/client-modes.md +16 -4
  130. package/docs/concepts/initialization-flow.md +9 -2
  131. package/docs/concepts/policy-packs.md +2 -2
  132. package/docs/headless.md +13 -7
  133. package/docs/hooks/use-consent-manager/overview.md +17 -3
  134. package/docs/hooks/use-ssr-status.md +1 -1
  135. package/docs/hooks/use-translations.md +1 -0
  136. package/docs/iab/consent-banner.md +2 -5
  137. package/docs/iab/consent-dialog.md +3 -6
  138. package/docs/iab/overview.md +11 -5
  139. package/docs/integrations/building-integrations.md +405 -0
  140. package/docs/integrations/databuddy.md +22 -5
  141. package/docs/integrations/google-tag-manager.md +2 -2
  142. package/docs/integrations/google-tag.md +2 -29
  143. package/docs/integrations/linkedin-insights.md +1 -1
  144. package/docs/integrations/meta-pixel.md +1 -1
  145. package/docs/integrations/microsoft-uet.md +1 -1
  146. package/docs/integrations/overview.md +18 -2
  147. package/docs/integrations/posthog.md +39 -17
  148. package/docs/integrations/tiktok-pixel.md +1 -1
  149. package/docs/integrations/x-pixel.md +1 -1
  150. package/docs/optimization.md +2 -6
  151. package/docs/policy-packs.md +7 -7
  152. package/docs/quickstart.md +11 -5
  153. package/docs/script-loader.md +22 -1
  154. package/docs/styling/classnames.md +17 -9
  155. package/docs/styling/overview.md +166 -29
  156. package/docs/styling/slots.md +37 -7
  157. package/docs/styling/tailwind.md +25 -27
  158. package/iab/styles.css +1 -0
  159. package/package.json +15 -9
  160. package/readme.json +6 -0
  161. package/src/iab/styles.css +11 -0
  162. package/src/iab/styles.tw3.css +14 -0
  163. package/src/styles.css +10 -0
  164. package/src/styles.tw3.css +13 -0
  165. package/styles.css +1 -0
  166. package/dist/components/shared/libs/policy-actions.cjs +0 -1
  167. package/dist/components/shared/libs/policy-actions.js +0 -1
  168. package/dist-types/components/shared/libs/policy-actions.d.ts +0 -26
@@ -2,7 +2,7 @@
2
2
  * @packageDocumentation
3
3
  * Hook for accessing and managing consent state.
4
4
  */
5
- import type { ConsentManagerInterface, ConsentStoreState } from '../../../core/dist-types/index.d.ts';
5
+ import type { ConsentManagerInterface, ConsentStoreState } from 'c15t';
6
6
  /**
7
7
  * Hook for accessing and managing consent state.
8
8
  *
@@ -1,4 +1,4 @@
1
- import { type PolicyUiAction, type PolicyUiActionDirection, type PolicyUiActionGroup, type PolicyUiProfile } from '../components/shared/libs/policy-actions';
1
+ import { type PolicyUiAction, type PolicyUiActionDirection, type PolicyUiActionGroup, type PolicyUiProfile } from '@c15t/ui/utils';
2
2
  import { useConsentManager } from './use-consent-manager';
3
3
  export type HeadlessConsentSurface = 'banner' | 'dialog';
4
4
  export type HeadlessConsentSurfaceAction = PolicyUiAction;
@@ -20,8 +20,9 @@ export interface SSRStatus {
20
20
  *
21
21
  * - `'no_data'` — no `ssrData` prop was provided to the provider
22
22
  * - `'fetch_failed'` — `ssrData` was provided but the Promise resolved with no data
23
+ * - `'context_mismatch'` — `ssrData` did not match the current runtime request context
23
24
  */
24
- ssrSkippedReason: 'no_data' | 'fetch_failed' | null;
25
+ ssrSkippedReason: 'no_data' | 'fetch_failed' | 'context_mismatch' | null;
25
26
  }
26
27
  /**
27
28
  * Returns information about SSR data usage.
@@ -1,4 +1,4 @@
1
- import type { Translations } from '../../../core/dist-types/index.d.ts';
1
+ import type { Translations } from 'c15t';
2
2
  /**
3
3
  * Hook for accessing translations in the current language.
4
4
  *
@@ -1,5 +1,5 @@
1
- export type { AllConsentNames, ConsentManagerInterface, ConsentStoreState, ConsentType, EuropePolicyMode, Overrides, PolicyPackPresets, Translations, } from '../../core/dist-types/index.d.ts';
2
- export { configureConsentManager, defaultTranslationConfig, detectBrowserLanguage, mergeTranslationConfigs, policyPackPresets, prepareTranslationConfig, } from '../../core/dist-types/index.d.ts';
1
+ export type { AllConsentNames, ConsentManagerInterface, ConsentStoreState, ConsentType, EuropePolicyMode, Overrides, PolicyPackPresets, Translations, } from 'c15t';
2
+ export { configureConsentManager, defaultTranslationConfig, detectBrowserLanguage, mergeTranslationConfigs, policyPackPresets, prepareTranslationConfig, } from 'c15t';
3
3
  export { ConsentBanner, type ConsentBannerProps, } from './components/consent-banner';
4
4
  export * from './components/consent-banner/components';
5
5
  export { ConsentDialog, type ConsentDialogProps, } from './components/consent-dialog';
@@ -1,4 +1,4 @@
1
- import type { SSRInitialData } from '../../../core/dist-types/index.d.ts';
1
+ import type { SSRInitialData } from 'c15t';
2
2
  import type { FetchSSRDataOptions } from './types';
3
3
  /**
4
4
  * Fetches initial consent data on the server for SSR hydration.
@@ -1,4 +1,4 @@
1
- import type { Overrides } from '../../../core/dist-types/index.d.ts';
1
+ import type { Overrides } from 'c15t';
2
2
  /**
3
3
  * Creates a stable cache key for SSR init responses.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { Overrides, SSRInitialData } from '../../../core/dist-types/index.d.ts';
1
+ import type { Overrides, SSRInitialData } from 'c15t';
2
2
  /**
3
3
  * Base options for SSR data fetching, shared across framework integrations.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { BaseConsentManagerOptions, UIOptions } from '../../../ui/dist-types/theme/index.d.ts';
1
+ import type { BaseConsentManagerOptions, UIOptions } from '@c15t/ui/theme';
2
2
  import type { ReactNode } from 'react';
3
3
  /**
4
4
  * React-specific configuration options
@@ -1 +1 @@
1
- export type { AllThemeKeys } from '../../../../ui/dist-types/theme/index.d.ts';
1
+ export type { AllThemeKeys } from '@c15t/ui/theme';
@@ -1,7 +1,7 @@
1
- import type { ClassNameStyle as BaseClassNameStyle, Theme as BaseTheme } from '../../../../ui/dist-types/theme/index.d.ts';
1
+ import type { ClassNameStyle as BaseClassNameStyle, Theme as BaseTheme } from '@c15t/ui/theme';
2
2
  import type { CSSProperties } from 'react';
3
3
  import type { AllThemeKeys } from './style-keys';
4
- export type { ColorTokens, ComponentSlots, CSSVariables, MotionTokens, RadiusTokens, ShadowTokens, SlotStyle, SpacingTokens, TypographyTokens, } from '../../../../ui/dist-types/theme/index.d.ts';
4
+ export type { ColorTokens, ComponentSlots, CSSVariables, MotionTokens, RadiusTokens, ShadowTokens, SlotStyle, SpacingTokens, TypographyTokens, } from '@c15t/ui/theme';
5
5
  /**
6
6
  * Represents CSS properties with optional CSS variables.
7
7
  * @public
@@ -1,4 +1,4 @@
1
- import type { ClassValue } from '../../../ui/dist-types/utils/index.d.ts';
1
+ import type { ClassValue } from '@c15t/ui/utils';
2
2
  export type { ClassValue };
3
3
  /**
4
4
  * Utilizes framework-agnostic `cn` from @c15t/ui.
@@ -2,7 +2,7 @@
2
2
  * Utility functions for processing the v2 theme system.
3
3
  * Handles token-to-CSS variable conversion and dark mode overrides.
4
4
  */
5
- import type { ThemeCSSVariables } from '../../../ui/dist-types/theme/index.d.ts';
5
+ import type { ThemeCSSVariables } from '@c15t/ui/theme';
6
6
  import type { Theme } from '../types/theme/style-types';
7
7
  /**
8
8
  * Default design tokens for the v2 theme system.
@@ -1 +1 @@
1
- export declare const version = "2.0.0-rc.6";
1
+ export declare const version = "2.0.0-rc.8";
@@ -2,15 +2,27 @@
2
2
  title: Building Headless Components
3
3
  description: Build policy-aware custom consent components in React using the headless hooks and policy-pack tooling.
4
4
  ---
5
- Building headless components is easier now because c15t exposes policy-aware primitives instead of forcing you to reconstruct banner rules by hand.
5
+ Building custom consent UI is easier now because c15t exposes multiple layers of policy-aware primitives instead of forcing you to reconstruct banner rules by hand.
6
6
 
7
- The headless stack is:
7
+ The layering is:
8
+
9
+ * stock component props for the shortest path
10
+ * `ConsentBanner.PolicyActions` and `ConsentWidget.PolicyActions` for custom structure with policy-aware actions
11
+ * `useHeadlessConsentUI()` for fully manual action rendering and non-standard controls
12
+
13
+ > ⚠️ **Warning:**
14
+ > Headless is the last step in the customization ladder. Use this guide only when pre-built components, tokens, slots, compound components, and noStyle are no longer sufficient.
15
+
16
+ The headless stack underneath that is:
8
17
 
9
18
  * `useHeadlessConsentUI()` for policy-aware banner/dialog actions, ordering, layout, and primary actions hints
19
+ * `@c15t/ui/utils` for the pure policy-action helpers that framework packages build on
10
20
  * `useConsentManager()` for runtime state, categories, selected consent state, and policy metadata
11
21
  * `useTranslations()` for the resolved copy
12
22
  * `offlinePolicy.policyPacks` for offline previews that behave like backend policy resolution
13
23
 
24
+ The split is intentional: `@c15t/ui` owns pure policy-action resolution, while the framework hooks own visibility, consent mutations, and reactive state.
25
+
14
26
  > ℹ️ **Info:**
15
27
  > This guide is about building your own components while still respecting resolved policy-pack behavior. For the general headless overview, see Headless Mode.
16
28
 
@@ -30,6 +42,11 @@ The main win is that your custom UI can stay aligned with policy packs without d
30
42
 
31
43
  That means your component mostly focuses on markup and design-system concerns instead of re-implementing policy interpretation.
32
44
 
45
+ For most compound-component layouts, start with `ConsentBanner.PolicyActions` or `ConsentWidget.PolicyActions`. They render stock c15t buttons and translations by default, and `renderAction` is only needed when you want to override the action mapping. Reach for manual `actionGroups` mapping when you need action rendering that no longer fits the stock button compounds.
46
+
47
+ > ℹ️ **Info:**
48
+ > For custom layouts built from c15t compound components, prefer ConsentBanner.PolicyActions and ConsentWidget.PolicyActions. The examples below intentionally use manual actionGroups mapping to show the fully headless escape hatch.
49
+
33
50
  ## Provider Setup for Local Policy Testing
34
51
 
35
52
  ```tsx
@@ -165,25 +182,29 @@ export function CustomConsentDialog() {
165
182
  ))}
166
183
  </div>
167
184
 
168
- <div className="mt-4 flex gap-2">
169
- {dialog.orderedActions.map((action) => (
170
- <button
171
- key={action}
172
- type="button"
173
- onClick={() => {
174
- if (action === 'customize') {
175
- void saveCustomPreferences();
176
- return;
177
- }
178
- void performDialogAction(action);
179
- }}
180
- >
181
- {action === 'accept'
182
- ? translations.common.acceptAll
183
- : action === 'reject'
184
- ? translations.common.rejectAll
185
- : translations.common.save}
186
- </button>
185
+ <div className="mt-4 space-y-2">
186
+ {dialog.actionGroups.map((group, index) => (
187
+ <div key={`${group.join('-')}-${index}`} className="flex gap-2">
188
+ {group.map((action) => (
189
+ <button
190
+ key={action}
191
+ type="button"
192
+ onClick={() => {
193
+ if (action === 'customize') {
194
+ void saveCustomPreferences();
195
+ return;
196
+ }
197
+ void performDialogAction(action);
198
+ }}
199
+ >
200
+ {action === 'accept'
201
+ ? translations.common.acceptAll
202
+ : action === 'reject'
203
+ ? translations.common.rejectAll
204
+ : translations.common.save}
205
+ </button>
206
+ ))}
207
+ </div>
187
208
  ))}
188
209
  </div>
189
210
  </section>
@@ -196,7 +217,7 @@ export function CustomConsentDialog() {
196
217
  When you build custom banner or dialog components, make sure they use:
197
218
 
198
219
  * `activeUI` or `banner.isVisible` / `dialog.isVisible` for visibility
199
- * `allowedActions`, `orderedActions`, or `actionGroups` instead of hard-coding buttons
220
+ * `allowedActions`, `actionGroups`, and `primaryActions` instead of hard-coding buttons
200
221
  * `primaryActions` for visual emphasis
201
222
  * `consentCategories` when deciding which category toggles to render
202
223
  * `policyDecision` when you want to debug why a specific UI state was chosen
package/docs/callbacks.md CHANGED
@@ -2,7 +2,12 @@
2
2
  title: Callbacks
3
3
  description: React to consent lifecycle events - initialization, consent changes, errors, and revocation reloads.
4
4
  ---
5
- Callbacks let you run custom code at key points in the consent lifecycle. Define them in the provider's `callbacks` option, or register them dynamically via `useConsentManager()`.
5
+ Callbacks let you run custom code at key points in the consent lifecycle. Define them in the provider or runtime `callbacks` option, or register them dynamically after initialization.
6
+
7
+ For analytics SDKs and other change-only integrations, prefer `subscribeToConsentChanges()` or `onConsentChanged`. Use `onConsentSet` when you want the broader lifecycle signal, including initialization, automatic defaults, and replay-aware registration.
8
+
9
+ > ℹ️ **Info:**
10
+ > subscribeToConsentChanges() is the recommended API for analytics SDKs and consent-mode integrations. It only emits future saves that actually changed persisted preferences.
6
11
 
7
12
  ## Configuration
8
13
 
@@ -23,8 +28,10 @@ export function ConsentManager({ children }: { children: ReactNode }) {
23
28
  console.log('Language:', translations.language);
24
29
  },
25
30
  onConsentSet: ({ preferences }) => {
26
- // Fire analytics event
27
- analytics.track('consent_updated', { preferences });
31
+ console.log('Consent lifecycle event:', preferences);
32
+ },
33
+ onConsentChanged: ({ allowedCategories, deniedCategories }) => {
34
+ analytics.syncConsent({ allowedCategories, deniedCategories });
28
35
  },
29
36
  onError: ({ error }) => {
30
37
  errorReporter.captureMessage(error);
@@ -42,6 +49,18 @@ export function ConsentManager({ children }: { children: ReactNode }) {
42
49
  }
43
50
  ```
44
51
 
52
+ ## Choose the Right Surface
53
+
54
+ |Surface|Replays when registered late?|Fires on init / hydration / auto-grants?|Best for|
55
+ |--|--|--|--|
56
+ |`onBannerFetched`|Yes, via `setCallback('onBannerFetched', ...)` after init|Yes|Logging resolved policy, location, and translations|
57
+ |`onConsentSet`|Yes, via `setCallback('onConsentSet', ...)`|Yes|Broad lifecycle hooks, debugging, and integrations that want the latest full state regardless of how it was reached|
58
+ |`onConsentChanged`|No|No|Declarative change-only integrations|
59
+ |`subscribeToConsentChanges()`|No|No|Canonical change-only subscriptions after mount|
60
+
61
+ > ℹ️ **Info:**
62
+ > Script.onConsentChange is a script-scoped lifecycle hook. It is not the global consent change API for analytics SDKs or other app-wide integrations.
63
+
45
64
  ## Available Callbacks
46
65
 
47
66
  ### `onBannerFetched`
@@ -58,14 +77,34 @@ onBannerFetched: ({ jurisdiction, location, translations }) => {
58
77
 
59
78
  ### `onConsentSet`
60
79
 
61
- Called whenever consent preferences are saved - whether by user action (`saveConsents()`), automatic defaults, or on initialization when no jurisdiction is detected.
80
+ Called whenever c15t broadly settles consent state: store initialization, automatic defaults during init, explicit saves, and replay via `setCallback('onConsentSet', ...)`.
62
81
 
63
82
  ```tsx
64
83
  onConsentSet: ({ preferences }) => {
65
84
  // preferences: { necessary: true, measurement: true, marketing: false, ... }
66
- if (preferences.measurement) {
67
- loadAnalytics();
68
- }
85
+ console.log('Latest consent state:', preferences);
86
+ }
87
+ ```
88
+
89
+ ### `onConsentChanged`
90
+
91
+ Called only after an explicit `saveConsents()` or `setConsent()` that actually changes the saved consent state. It never fires on store creation, hydration, automatic grants, unchanged saves, or `setCallback('onConsentChanged', ...)`.
92
+
93
+ ```tsx
94
+ onConsentChanged: ({
95
+ preferences,
96
+ previousPreferences,
97
+ allowedCategories,
98
+ deniedCategories,
99
+ previousAllowedCategories,
100
+ previousDeniedCategories,
101
+ }) => {
102
+ analytics.syncConsent({
103
+ allowedCategories,
104
+ deniedCategories,
105
+ previousAllowedCategories,
106
+ previousDeniedCategories,
107
+ });
69
108
  }
70
109
  ```
71
110
 
@@ -91,7 +130,28 @@ onBeforeConsentRevocationReload: ({ preferences }) => {
91
130
  }
92
131
  ```
93
132
 
94
- ## Dynamic Registration
133
+ ## Change-Only Subscriptions
134
+
135
+ Use `subscribeToConsentChanges()` when you want a stable listener for real preference changes after mount:
136
+
137
+ ```tsx
138
+ import { useEffect } from 'react';
139
+ import { useConsentManager } from '@c15t/react';
140
+
141
+ function ConsentAnalytics() {
142
+ const { subscribeToConsentChanges } = useConsentManager();
143
+
144
+ useEffect(() => {
145
+ return subscribeToConsentChanges(({ allowedCategories, deniedCategories }) => {
146
+ analytics.syncConsent({ allowedCategories, deniedCategories });
147
+ });
148
+ }, [subscribeToConsentChanges]);
149
+
150
+ return null;
151
+ }
152
+ ```
153
+
154
+ ## Runtime Callback Registration
95
155
 
96
156
  Register or update callbacks at runtime using `setCallback()`:
97
157
 
@@ -103,11 +163,16 @@ function ConsentAnalytics() {
103
163
  const { setCallback } = useConsentManager();
104
164
 
105
165
  useEffect(() => {
166
+ setCallback('onBannerFetched', ({ jurisdiction, location }) => {
167
+ console.log('Resolved init data:', { jurisdiction, location });
168
+ });
169
+
106
170
  setCallback('onConsentSet', ({ preferences }) => {
107
- analytics.track('consent_changed', preferences);
171
+ console.log('Broad consent lifecycle event:', preferences);
108
172
  });
109
173
 
110
174
  return () => {
175
+ setCallback('onBannerFetched', undefined);
111
176
  setCallback('onConsentSet', undefined);
112
177
  };
113
178
  }, [setCallback]);
@@ -115,3 +180,5 @@ function ConsentAnalytics() {
115
180
  return null;
116
181
  }
117
182
  ```
183
+
184
+ `setCallback('onConsentSet', ...)` immediately replays the current consent state. For change-only logic, prefer `subscribeToConsentChanges()` or `onConsentChanged`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: ConsentBanner
3
- description: A pre-built consent banner that appears when user consent is needed. Supports custom layout, button arrangement, and full compound component composition.
3
+ description: A pre-built consent banner that appears when user consent is needed. Supports policy-aware layout, theming, and advanced composition when markup must change.
4
4
  ---
5
5
  `ConsentBanner` is a ready-to-use consent banner that appears automatically in **opt-in jurisdictions** (like GDPR) where explicit consent is required before tracking. In opt-out jurisdictions (like CCPA), the banner won't appear — users get an opt-out mechanism instead. It includes reject, accept, and customize buttons with configurable layout.
6
6
 
@@ -18,20 +18,6 @@ export function ConsentManager() {
18
18
  }
19
19
  ```
20
20
 
21
- ## Customizing Content
22
-
23
- Override the default title, description, and button labels:
24
-
25
- ```tsx
26
- <ConsentBanner
27
- title="We value your privacy"
28
- description="We use cookies to enhance your browsing experience and analyze site traffic."
29
- acceptButtonText="Accept all"
30
- rejectButtonText="Reject all"
31
- customizeButtonText="Manage preferences"
32
- />
33
- ```
34
-
35
21
  ## Button Layout
36
22
 
37
23
  The `layout` prop controls button arrangement. Each item is either a button ID or an array of button IDs (which groups them together):
@@ -86,6 +72,37 @@ Use the provider `theme` prop to control how stock consent actions look:
86
72
 
87
73
  Policy packs control grouping, ordering, and direction. The theme controls button appearance.
88
74
 
75
+ ## Styling First
76
+
77
+ > ℹ️ **Info:**
78
+ > For pure theming, stay inside the pre-built banner. Start with layout props, theme.consentActions, design tokens, and theme.slots before reaching for compound components. See Styling Overview.
79
+
80
+ The stock banner maps common visual changes to the theme system:
81
+
82
+ * Card background -> `theme.colors.surface`
83
+ * Footer background -> `theme.colors.surfaceHover`
84
+ * Card, footer, and title tweaks -> `theme.slots.consentBannerCard`, `consentBannerFooter`, and `consentBannerTitle`
85
+
86
+ ```tsx
87
+ <ConsentManagerProvider
88
+ options={{
89
+ theme: {
90
+ colors: {
91
+ surface: '#fffdf8',
92
+ surfaceHover: '#f6f3ee',
93
+ },
94
+ slots: {
95
+ consentBannerCard: 'rounded-[28px] shadow-xl',
96
+ consentBannerFooter: 'border-t border-black/10 px-6',
97
+ consentBannerTitle: 'tracking-tight',
98
+ },
99
+ },
100
+ }}
101
+ >
102
+ <ConsentBanner />
103
+ </ConsentManagerProvider>
104
+ ```
105
+
89
106
  ### Primary Button
90
107
 
91
108
  Highlight specific button(s) as the primary action:
@@ -116,9 +133,40 @@ Control which legal links appear in the banner description:
116
133
  > ℹ️ **Info:**
117
134
  > Legal link URLs are configured in the ConsentManagerProvider options via the legalLinks prop, not on the banner itself.
118
135
 
119
- ## Compound Components
136
+ ## Customizing Copy
120
137
 
121
- Build fully custom banner layouts using sub-components:
138
+ Prefer provider `i18n` when you want to rename the stock banner content:
139
+
140
+ ```tsx
141
+ <ConsentManagerProvider
142
+ options={{
143
+ i18n: {
144
+ locale: 'en',
145
+ messages: {
146
+ en: {
147
+ cookieBanner: {
148
+ title: 'We value your privacy',
149
+ description: 'We use cookies to improve the site and measure performance.',
150
+ },
151
+ common: {
152
+ acceptAll: 'Accept all',
153
+ rejectAll: 'Reject all',
154
+ customize: 'Manage preferences',
155
+ },
156
+ },
157
+ },
158
+ },
159
+ }}
160
+ >
161
+ <ConsentBanner />
162
+ </ConsentManagerProvider>
163
+ ```
164
+
165
+ Direct text props such as `title`, `description`, and `acceptButtonText` are still supported for one-off overrides, but `i18n` is the preferred path for copy changes.
166
+
167
+ ## Advanced: Compound Components
168
+
169
+ Use compound components only when the stock banner structure is no longer enough and you need to rearrange existing c15t primitives while keeping policy-driven action grouping and emphasis:
122
170
 
123
171
  ```tsx
124
172
  <ConsentBanner.Root>
@@ -128,13 +176,7 @@ Build fully custom banner layouts using sub-components:
128
176
  <ConsentBanner.Title />
129
177
  <ConsentBanner.Description />
130
178
  </ConsentBanner.Header>
131
- <ConsentBanner.Footer>
132
- <ConsentBanner.FooterSubGroup>
133
- <ConsentBanner.RejectButton />
134
- <ConsentBanner.AcceptButton />
135
- </ConsentBanner.FooterSubGroup>
136
- <ConsentBanner.CustomizeButton />
137
- </ConsentBanner.Footer>
179
+ <ConsentBanner.PolicyActions />
138
180
  </ConsentBanner.Card>
139
181
  </ConsentBanner.Root>
140
182
  ```
@@ -144,6 +186,7 @@ Build fully custom banner layouts using sub-components:
144
186
  * `ConsentBanner.Header` — Contains title and description
145
187
  * `ConsentBanner.Title` — Heading, defaults to translation `consentBanner.title`
146
188
  * `ConsentBanner.Description` — Description text, supports `legalLinks` prop
189
+ * `ConsentBanner.PolicyActions` — Renders policy-aware grouped actions inside the banner footer
147
190
  * `ConsentBanner.Footer` — Action buttons container
148
191
  * `ConsentBanner.FooterSubGroup` — Groups related buttons together
149
192
  * `ConsentBanner.RejectButton` — Rejects all consent
@@ -151,6 +194,86 @@ Build fully custom banner layouts using sub-components:
151
194
  * `ConsentBanner.AcceptButton` — Accepts all consent
152
195
  * `ConsentBanner.Overlay` — Optional backdrop overlay
153
196
 
197
+ For a fixed layout that intentionally ignores policy grouping, render the footer manually:
198
+
199
+ ```tsx
200
+ <ConsentBanner.Root>
201
+ <ConsentBanner.Card>
202
+ <ConsentBanner.Header>
203
+ <ConsentBanner.Title />
204
+ <ConsentBanner.Description />
205
+ </ConsentBanner.Header>
206
+ <ConsentBanner.Footer>
207
+ <ConsentBanner.FooterSubGroup>
208
+ <ConsentBanner.RejectButton />
209
+ <ConsentBanner.AcceptButton />
210
+ </ConsentBanner.FooterSubGroup>
211
+ <ConsentBanner.CustomizeButton />
212
+ </ConsentBanner.Footer>
213
+ </ConsentBanner.Card>
214
+ </ConsentBanner.Root>
215
+ ```
216
+
217
+ ## Using `renderAction` with c15t Defaults
218
+
219
+ `ConsentBanner.PolicyActions` renders stock c15t buttons and translations by default.
220
+
221
+ ```tsx
222
+ <ConsentBanner.PolicyActions />
223
+ ```
224
+
225
+ `renderAction` is optional. When you want custom mapping but still want the built-in c15t button behavior and copy, return the stock button compounds:
226
+
227
+ ```tsx
228
+ <ConsentBanner.PolicyActions
229
+ renderAction={(action, props) => {
230
+ const { key, ...buttonProps } = props
231
+
232
+ switch (action) {
233
+ case 'accept':
234
+ return <ConsentBanner.AcceptButton key={key} {...buttonProps} />
235
+ case 'reject':
236
+ return <ConsentBanner.RejectButton key={key} {...buttonProps} />
237
+ case 'customize':
238
+ return <ConsentBanner.CustomizeButton key={key} {...buttonProps} />
239
+ }
240
+ }}
241
+ />
242
+ ```
243
+
244
+ Use `useTranslations()` only when you are replacing the button markup entirely:
245
+
246
+ ```tsx
247
+ import { ConsentBanner, useTranslations } from '@c15t/react';
248
+
249
+ export function CustomBannerActions() {
250
+ const { common } = useTranslations();
251
+
252
+ return (
253
+ <ConsentBanner.PolicyActions
254
+ renderAction={(action, props) => (
255
+ <button
256
+ key={props.key}
257
+ type="button"
258
+ className={props.isPrimary ? 'btn-primary' : 'btn-secondary'}
259
+ style={props.style}
260
+ >
261
+ {action === 'accept'
262
+ ? common.acceptAll
263
+ : action === 'reject'
264
+ ? common.rejectAll
265
+ : common.customize}
266
+ </button>
267
+ )}
268
+ />
269
+ );
270
+ }
271
+ ```
272
+
273
+ For maximum control, use `useHeadlessConsentUI()` and render `banner.actionGroups` manually.
274
+
275
+ If you only need styling changes, stay with tokens and slots instead of rebuilding the banner layout.
276
+
154
277
  ## Props
155
278
 
156
279
  ### ConsentBannerProps
@@ -167,6 +290,7 @@ Build fully custom banner layouts using sub-components:
167
290
  |trapFocus|boolean \|undefined|When true, the consent banner will trap focus|true|Optional|
168
291
  |disableAnimation|boolean \|undefined|When true, disables the entrance/exit animations|false|Optional|
169
292
  |legalLinks|(keyof LegalLinksTranslations)\[] \|null \|undefined|Controls which legal links to display. Options: \`undefined\` (default): Shows all available legal links; \`null\`: Explicitly hides all legal links; Array of keys: Shows only the specified legal links|-|Optional|
293
+ |hideBranding|boolean \|undefined|When true, hides the branding tag on the banner.|false|Optional|
170
294
  |layout|ConsentBannerLayout \|undefined|Defines the layout of buttons in the footer. Allows reordering and grouping of buttons.|-|Optional|
171
295
  |direction|PolicyUiActionDirection \|undefined|Defines how footer button groups flow.|-|Optional|
172
296
  |primaryButton|ConsentBannerButton \|undefined|Specifies which button(s) should be highlighted as the primary action.|-|Optional|
@@ -81,9 +81,36 @@ Hide the c15t branding in the dialog footer:
81
81
  <ConsentDialog hideBranding />
82
82
  ```
83
83
 
84
- ## Compound Components
84
+ ## Styling First
85
85
 
86
- Build fully custom dialog layouts using sub-components:
86
+ > ℹ️ **Info:**
87
+ > If you are only changing visuals, stay with the stock dialog and use the theme system first. Start with tokens and slots such as consentDialogCard, consentDialogHeader, and consentDialogFooter. See Styling Overview.
88
+
89
+ ```tsx
90
+ <ConsentManagerProvider
91
+ options={{
92
+ theme: {
93
+ colors: {
94
+ surface: '#fffdf8',
95
+ surfaceHover: '#f6f3ee',
96
+ },
97
+ slots: {
98
+ consentDialogCard: 'rounded-[32px] shadow-xl',
99
+ consentDialogHeader: 'gap-3',
100
+ consentDialogFooter: 'border-t border-black/10 px-6',
101
+ },
102
+ },
103
+ }}
104
+ >
105
+ <ConsentDialog />
106
+ </ConsentManagerProvider>
107
+ ```
108
+
109
+ Dialog copy should be changed through `ConsentManagerProvider.options.i18n`, not by rebuilding the dialog structure.
110
+
111
+ ## Advanced: Compound Components
112
+
113
+ Use compound components only when you need custom dialog markup while still keeping c15t primitives and policy-aware footer actions:
87
114
 
88
115
  ```tsx
89
116
  <ConsentDialog.Root>
@@ -94,7 +121,12 @@ Build fully custom dialog layouts using sub-components:
94
121
  <ConsentDialog.HeaderDescription />
95
122
  </ConsentDialog.Header>
96
123
  <ConsentDialog.Content>
97
- <ConsentWidget />
124
+ <ConsentWidget.Root>
125
+ <ConsentWidget.Accordion type="single">
126
+ <ConsentWidget.AccordionItems />
127
+ </ConsentWidget.Accordion>
128
+ <ConsentWidget.PolicyActions />
129
+ </ConsentWidget.Root>
98
130
  </ConsentDialog.Content>
99
131
  <ConsentDialog.Footer />
100
132
  </ConsentDialog.Card>
@@ -109,6 +141,7 @@ Build fully custom dialog layouts using sub-components:
109
141
  * `ConsentDialog.Content` — Main content area (typically contains `ConsentWidget`)
110
142
  * `ConsentDialog.Footer` — Footer with optional branding (`hideBranding` prop)
111
143
  * `ConsentDialog.Overlay` — Backdrop overlay
144
+ * `ConsentWidget.PolicyActions` — Renders policy-aware grouped dialog actions
112
145
 
113
146
  For a quick pre-composed layout, use the shorthand card:
114
147
 
@@ -118,6 +151,12 @@ For a quick pre-composed layout, use the shorthand card:
118
151
  </ConsentDialog.Root>
119
152
  ```
120
153
 
154
+ `ConsentWidget.PolicyActions` uses stock c15t widget buttons and translations by default. Pass `renderAction` only when you need to customize the action mapping, and return stock widget button compounds if you want to preserve built-in behavior and copy.
155
+
156
+ For fully manual control over dialog action rendering, use `useHeadlessConsentUI()` and map `dialog.actionGroups` yourself.
157
+
158
+ If the stock dialog structure still works, prefer tokens, slots, and provider configuration instead.
159
+
121
160
  ## Props
122
161
 
123
162
  ### ConsentDialogProps