@c15t/react 3.0.0-alpha.2 → 3.0.0-alpha.4
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.
- package/AGENTS.md +91 -67
- package/README.md +21 -17
- package/SKILL.md +34 -0
- package/dist/aggregate-components.js +1 -1
- package/dist/chunk-warming.js +1 -1
- package/dist/component-hooks/use-headless-consent-ui.js +1 -1
- package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
- package/dist/component-hooks/use-panel-trigger.js +1 -1
- package/dist/components/consent-gate/atoms.js +1 -1
- package/dist/components/consent-gate/consent-gate.js +1 -1
- package/dist/components/consent-preferences-link/consent-preferences-link.js +1 -1
- package/dist/components/iab-panel/atoms/card.js +1 -1
- package/dist/components/iab-panel/atoms/feature-item.js +2 -0
- package/dist/components/iab-panel/atoms/header.js +1 -1
- package/dist/components/iab-panel/atoms/purpose-item.js +1 -1
- package/dist/components/iab-panel/atoms/stack-item.js +1 -1
- package/dist/components/iab-panel/iab-panel.js +1 -1
- package/dist/components/iab-panel/index.js +1 -1
- package/dist/components/iab-prompt/atoms/overlay.js +1 -1
- package/dist/components/iab-prompt/atoms/root.js +1 -1
- package/dist/components/panel/atoms/card.js +1 -1
- package/dist/components/panel/atoms/overlay.js +1 -1
- package/dist/components/panel/atoms/root.js +1 -1
- package/dist/components/panel/index.js +1 -1
- package/dist/components/panel/panel.js +1 -1
- package/dist/components/panel-trigger/atoms/button.js +1 -1
- package/dist/components/panel-trigger/atoms/icon.js +1 -1
- package/dist/components/panel-trigger/atoms/root.js +1 -1
- package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
- package/dist/components/panel-trigger/atoms/trigger-icons.js +1 -0
- package/dist/components/panel-trigger/panel-trigger-toolbar.js +1 -1
- package/dist/components/preferences/atoms/accordion.js +1 -1
- package/dist/components/preferences/atoms/root.js +1 -1
- package/dist/components/preferences/atoms/vendor-list.js +1 -1
- package/dist/components/preferences/index.js +1 -1
- package/dist/components/prompt/atoms/overlay.js +1 -1
- package/dist/components/prompt/atoms/root.js +1 -1
- package/dist/components/prompt/components.js +1 -1
- package/dist/components/prompt/policy-actions.js +1 -1
- package/dist/components/shared/libs/compose-refs.js +1 -0
- package/dist/components/shared/libs/slot.js +1 -1
- package/dist/components/shared/primitives/button.js +1 -1
- package/dist/components/shared/ui/accordion/accordion.js +1 -1
- package/dist/components/shared/ui/collapsible/collapsible.js +1 -1
- package/dist/components/shared/ui/dialog/dialog.js +1 -1
- package/dist/components/shared/ui/icon.js +1 -1
- package/dist/components/shared/ui/logo.js +1 -1
- package/dist/components/shared/ui/preference-item/preference-item.js +1 -1
- package/dist/components/shared/ui/switch/switch.js +1 -1
- package/dist/components/shared/ui/tabs/tabs.js +1 -1
- package/dist/consent-theme.js +1 -0
- package/dist/context/consent-tracking-context.js +1 -1
- package/dist/devtools.js +1 -1
- package/dist/draft.js +1 -1
- package/dist/external-iab-context.js +1 -1
- package/dist/headless.js +1 -1
- package/dist/hooks/use-committed-ref.js +2 -0
- package/dist/hooks/use-focus-trap.js +1 -1
- package/dist/hooks.js +1 -1
- package/dist/iab/styles.tw3.css +5 -45
- package/dist/iab-context.js +1 -1
- package/dist/index.js +1 -1
- package/dist/kernel-selector.js +1 -0
- package/dist/module-hooks/iframe-blocker.js +1 -1
- package/dist/module-hooks/network-blocker.js +1 -1
- package/dist/module-hooks/network-hold.js +2 -0
- package/dist/module-hooks/persistence.js +1 -1
- package/dist/panel.js +1 -1
- package/dist/postcss-tailwind3.js +1 -0
- package/dist/preferences.js +1 -2
- package/dist/provider.js +1 -1
- package/dist/server/fetch-ssr-data.js +1 -1
- package/dist/server/headers.js +1 -1
- package/dist/server/normalize-url.js +1 -1
- package/dist/styles.tw3.css +16 -61
- package/dist/transports/offline.js +1 -1
- package/dist/ui-save.js +1 -1
- package/dist/utils/deferred-module.js +1 -1
- package/dist/utils/merge-slot-props.js +1 -1
- package/dist/utils/theme-utils.js +1 -1
- package/dist/version.js +1 -1
- package/dist-types/chunk-warming.d.ts +41 -8
- package/dist-types/component-hooks/use-headless-consent-ui.d.ts +1 -1
- package/dist-types/component-hooks/use-headless-iab-consent-ui.d.ts +1 -1
- package/dist-types/components/iab-panel/atoms/feature-item.d.ts +28 -0
- package/dist-types/components/iab-panel/atoms/purpose-item.d.ts +4 -1
- package/dist-types/components/iab-panel/atoms/stack-item.d.ts +6 -1
- package/dist-types/components/iab-panel/iab-panel.d.ts +2 -2
- package/dist-types/components/iab-panel/index.d.ts +6 -2
- package/dist-types/components/iab-panel/use-iab-translations.d.ts +4 -0
- package/dist-types/components/iab-prompt/iab-prompt.d.ts +2 -2
- package/dist-types/components/iab-prompt/index.d.ts +2 -2
- package/dist-types/components/panel/atoms/card.d.ts +1 -0
- package/dist-types/components/panel/index.d.ts +1 -0
- package/dist-types/components/panel/panel.d.ts +1 -1
- package/dist-types/components/panel-trigger/atoms/trigger-icons.d.ts +20 -0
- package/dist-types/components/preferences/index.d.ts +1 -0
- package/dist-types/components/prompt/components.d.ts +6 -0
- package/dist-types/components/shared/libs/compose-refs.d.ts +20 -0
- package/dist-types/components/shared/ui/accordion/accordion.d.ts +1 -0
- package/dist-types/components/shared/ui/collapsible/collapsible.d.ts +1 -0
- package/dist-types/components/shared/ui/icon.d.ts +2 -1
- package/dist-types/components/shared/ui/logo.d.ts +0 -10
- package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +14 -0
- package/dist-types/components/shared/ui/switch/switch.d.ts +1 -0
- package/dist-types/components/shared/ui/tabs/tabs.d.ts +1 -0
- package/dist-types/consent-theme.d.ts +38 -0
- package/dist-types/context/consent-tracking-context.d.ts +10 -0
- package/dist-types/context/theme-context.d.ts +2 -2
- package/dist-types/context.d.ts +6 -0
- package/dist-types/devtools.d.ts +13 -2
- package/dist-types/draft.d.ts +50 -7
- package/dist-types/headless.d.ts +0 -1
- package/dist-types/hooks/use-committed-ref.d.ts +24 -0
- package/dist-types/hooks/use-focus-trap.d.ts +3 -1
- package/dist-types/hooks.d.ts +56 -13
- package/dist-types/iab.d.ts +1 -1
- package/dist-types/index.d.ts +4 -3
- package/dist-types/kernel-selector.d.ts +66 -0
- package/dist-types/module-hooks/iframe-blocker.d.ts +27 -0
- package/dist-types/module-hooks/network-blocker.d.ts +23 -0
- package/dist-types/module-hooks/network-hold.d.ts +47 -0
- package/dist-types/panel.d.ts +13 -1
- package/dist-types/postcss-tailwind3.d.ts +21 -0
- package/dist-types/preferences.d.ts +12 -1
- package/dist-types/provider.d.ts +62 -12
- package/dist-types/server/headers.d.ts +14 -4
- package/dist-types/server/normalize-url.d.ts +14 -7
- package/dist-types/server/types.d.ts +12 -1
- package/dist-types/transports/offline.d.ts +7 -16
- package/dist-types/ui-config-context.d.ts +9 -1
- package/dist-types/ui-save.d.ts +21 -1
- package/dist-types/utils/deferred-module.d.ts +3 -1
- package/dist-types/utils/merge-slot-props.d.ts +15 -1
- package/dist-types/utils/theme-utils.d.ts +17 -2
- package/dist-types/version.d.ts +1 -1
- package/docs/README.md +91 -67
- package/docs/assets/v3/bottom-bar.png +0 -0
- package/docs/assets/v3/brand-card.png +0 -0
- package/docs/assets/v3/brand-preferences.png +0 -0
- package/docs/assets/v3/choice-wall.png +0 -0
- package/docs/assets/v3/headless-bar-html.png +0 -0
- package/docs/assets/v3/headless-bar-mobile.png +0 -0
- package/docs/assets/v3/headless-bar.png +0 -0
- package/docs/assets/v3/slim-bar.png +0 -0
- package/docs/concepts/choose-your-setup.md +87 -0
- package/docs/{frameworks/react/concepts → concepts}/consent-categories.md +30 -8
- package/docs/concepts/consent-state.md +357 -0
- package/docs/{guides → concepts}/data-fetching.md +31 -27
- package/docs/concepts/how-consent-works.md +123 -0
- package/docs/{frameworks/react/concepts/policy-presets.md → concepts/policies.md} +7 -59
- package/docs/customization/class-names.md +202 -0
- package/docs/customization/dark-mode.md +157 -0
- package/docs/customization/motion.md +119 -0
- package/docs/customization/overview.md +67 -33
- package/docs/customization/recipes.md +839 -47
- package/docs/customization/slots.md +216 -35
- package/docs/customization/stylesheets.md +147 -0
- package/docs/customization/tailwind.md +842 -0
- package/docs/customization/tokens.md +166 -36
- package/docs/customization/translations.md +61 -3
- package/docs/frameworks/react/callbacks.md +118 -0
- package/docs/frameworks/react/clear-on-revocation.md +106 -0
- package/docs/frameworks/react/components/consent-banner.md +42 -28
- package/docs/frameworks/react/components/consent-dialog-link.md +13 -11
- package/docs/frameworks/react/components/consent-dialog-trigger.md +81 -76
- package/docs/frameworks/react/components/consent-dialog.md +100 -55
- package/docs/frameworks/react/components/consent-gate.md +45 -24
- package/docs/frameworks/react/components/consent-provider.md +126 -0
- package/docs/frameworks/react/components/consent-widget.md +32 -20
- package/docs/frameworks/react/components/dev-tools.md +52 -84
- package/docs/frameworks/react/components.md +105 -0
- package/docs/frameworks/react/compose.md +354 -0
- package/docs/frameworks/react/content-security-policy.md +126 -0
- package/docs/frameworks/react/customize.md +250 -0
- package/docs/frameworks/react/embeds.md +89 -0
- package/docs/frameworks/react/hooks.md +65 -0
- package/docs/frameworks/react/iab.md +222 -0
- package/docs/frameworks/react/network-blocker.md +140 -0
- package/docs/frameworks/react/quickstart.md +139 -101
- package/docs/frameworks/react/rendering.md +128 -0
- package/docs/frameworks/react/scripts.md +115 -0
- package/docs/frameworks/react/translations.md +155 -0
- package/docs/frameworks/react/troubleshooting.md +92 -47
- package/docs/frameworks/react/vendor-consent.md +170 -0
- package/docs/guides/banner-experiments.md +654 -0
- package/docs/guides/troubleshooting.md +120 -47
- package/docs/guides/verify-consent.md +81 -49
- package/docs/integrations/adobe-analytics.md +168 -160
- package/docs/integrations/ahrefs-analytics.md +153 -155
- package/docs/integrations/amplitude.md +163 -157
- package/docs/integrations/building-integrations.md +136 -37
- package/docs/integrations/clearbit.md +155 -155
- package/docs/integrations/cloudflare-web-analytics.md +157 -157
- package/docs/integrations/cloudflare-zaraz.md +210 -262
- package/docs/integrations/crisp.md +165 -159
- package/docs/integrations/databuddy.md +158 -174
- package/docs/integrations/fathom-analytics.md +159 -157
- package/docs/integrations/front-chat.md +322 -0
- package/docs/integrations/google-maps.md +119 -84
- package/docs/integrations/google-tag-manager.md +179 -164
- package/docs/integrations/google-tag.md +164 -161
- package/docs/integrations/heap.md +164 -156
- package/docs/integrations/hightouch.md +162 -158
- package/docs/integrations/hotjar.md +160 -156
- package/docs/integrations/intercom.md +184 -154
- package/docs/integrations/klaviyo.md +486 -0
- package/docs/integrations/linkedin-insights.md +175 -151
- package/docs/integrations/logrocket.md +161 -157
- package/docs/integrations/matomo-analytics.md +189 -179
- package/docs/integrations/meta-pixel.md +189 -151
- package/docs/integrations/microsoft-clarity.md +164 -156
- package/docs/integrations/microsoft-uet.md +149 -155
- package/docs/integrations/mixpanel-analytics.md +156 -161
- package/docs/integrations/one-dollar-stats.md +306 -0
- package/docs/integrations/openai-pixel.md +205 -302
- package/docs/integrations/overview.md +143 -83
- package/docs/integrations/pinterest-tag.md +329 -0
- package/docs/integrations/pirsch.md +170 -160
- package/docs/integrations/plausible-analytics.md +173 -159
- package/docs/integrations/posthog.md +227 -242
- package/docs/integrations/promptwatch.md +155 -155
- package/docs/integrations/reddit-pixel.md +186 -158
- package/docs/integrations/rudderstack.md +202 -187
- package/docs/integrations/rybbit-analytics.md +172 -161
- package/docs/integrations/segment.md +183 -155
- package/docs/integrations/snapchat-pixel.md +187 -157
- package/docs/integrations/tiktok-pixel.md +172 -151
- package/docs/integrations/umami-analytics.md +164 -159
- package/docs/integrations/vercel-analytics.md +167 -158
- package/docs/integrations/x-pixel.md +177 -151
- package/docs/integrations/youtube.md +122 -87
- package/docs/upgrade-v3.md +490 -354
- package/package.json +15 -9
- package/dist/component-hooks/use-manager.js +0 -2
- package/dist-types/component-hooks/use-manager.d.ts +0 -45
- package/docs/assets/v3/brand-bar.png +0 -0
- package/docs/assets/v3/mobile-card.png +0 -0
- package/docs/assets/v3/preferences.png +0 -0
- package/docs/frameworks/react/components/consent-manager-provider.md +0 -75
- package/docs/frameworks/react/hooks/use-consent-manager/overview.md +0 -58
- package/docs/frameworks/react/iab/overview.md +0 -53
- package/docs/frameworks/react/script-loader.md +0 -63
- package/docs/frameworks/react/styling/overview.md +0 -217
- package/docs/guides/consent-state.md +0 -60
- package/docs/guides/deployment-modes.md +0 -75
- package/docs/integrations/clear-on-revocation.md +0 -167
- package/docs/integrations/granular-consent.md +0 -208
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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:
|
|
4
|
-
the
|
|
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
|
-
##
|
|
9
|
+
## Add the trigger inside ConsentProvider
|
|
9
10
|
|
|
10
|
-
`ConsentDialogTrigger` is a draggable floating button that opens the preference center. Place it once
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
@@ -97,14 +102,44 @@ it is `dialog`. These set that surface:
|
|
|
97
102
|
* `useSetActiveUI()('dialog')` in your own component, or `openDialog()`
|
|
98
103
|
from `useHeadlessConsentUI()` on the headless subpath.
|
|
99
104
|
|
|
100
|
-
The component code is split into its own chunk
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
The component code is split into its own chunk, so it is not part of the
|
|
106
|
+
first page load. It renders nothing until that chunk is ready, and the
|
|
107
|
+
chunk downloads at the first of these:
|
|
108
|
+
|
|
109
|
+
* The browser's first idle period after the page's `load` event, while the
|
|
110
|
+
banner is shown or a `ConsentDialogLink`, `ConsentDialogTrigger`,
|
|
111
|
+
`ConsentGate` button or other button that opens the dialog is mounted.
|
|
112
|
+
This covers a visitor who taps or presses Enter with no hover or focus
|
|
113
|
+
first. Browsers without `requestIdleCallback`, such as Safari, wait
|
|
114
|
+
200 ms after `load` instead.
|
|
115
|
+
* Hover or focus on a button that opens the dialog.
|
|
116
|
+
* The dialog opening.
|
|
117
|
+
|
|
118
|
+
Passing `open={true}` or `showTrigger` loads it on mount. A visit with
|
|
119
|
+
saved consent and nothing on the page that opens the dialog never
|
|
120
|
+
downloads it.
|
|
121
|
+
|
|
122
|
+
Idle loading is skipped when the visitor has Save-Data on
|
|
123
|
+
(`navigator.connection.saveData`), on a `2g` or `slow-2g` connection, and
|
|
124
|
+
while offline; hover and focus still load it. To load the chunk only on
|
|
125
|
+
hover, focus or open, set `preloadDialog: 'intent'` in the provider
|
|
126
|
+
options. The default is `'idle'`. With Next.js, pass it through
|
|
127
|
+
`ConsentRoot`'s `options` prop.
|
|
128
|
+
|
|
129
|
+
If a preload fails, for example because the visitor is offline, nothing is
|
|
130
|
+
cached: the next hover, focus or open tries the download again.
|
|
131
|
+
|
|
132
|
+
The dialog's CSS travels in the same chunk and is applied before the dialog
|
|
133
|
+
renders, so your app's `styles.css` import stays small.
|
|
134
|
+
|
|
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
|
|
137
|
+
the same task as the click; the exit animation still plays. The request to
|
|
138
|
+
the backend runs afterwards and never reopens the dialog: a failed request
|
|
139
|
+
keeps the choice, reports through `onError` and is retried later. See
|
|
140
|
+
[when a choice is saved](../../../concepts/consent-state.md#when-a-choice-is-saved)
|
|
141
|
+
for the order of events. A draft that went stale because the policy changed keeps the dialog open
|
|
142
|
+
for review. With `open={true}`, these actions leave the dialog visible; the
|
|
108
143
|
parent must set `open={false}` to close it. Clicking the backdrop does not
|
|
109
144
|
close it. Closing discards toggles that were not saved: the next open starts
|
|
110
145
|
from the recorded choice again. After a save from an uncontrolled dialog,
|
|
@@ -138,11 +173,12 @@ The panel carries `role="dialog"`, `aria-labelledby="consent-dialog-title"`
|
|
|
138
173
|
and `aria-describedby="consent-dialog-description"`, plus `aria-modal="true"`
|
|
139
174
|
while it is blocking. Its `dir` attribute follows the active language.
|
|
140
175
|
|
|
141
|
-
While blocking, focus moves on open to the
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
182
|
panel or back to the opener. The backdrop is `aria-hidden` and is not
|
|
147
183
|
focusable.
|
|
148
184
|
|
|
@@ -186,9 +222,18 @@ for the stock structure are `dialog.root`, `dialog.container`,
|
|
|
186
222
|
`dialog.card`, `dialog.header`, `dialog.title`, `dialog.content`,
|
|
187
223
|
`dialog.overlay`, `description.dialog`, `manager.footer` and `tag.dialog`.
|
|
188
224
|
|
|
225
|
+
`ConsentDialog` from the package root and from the
|
|
226
|
+
`c15t/react/consent-dialog` subpath is the deferred component described
|
|
227
|
+
under Behavior, and each `ConsentDialog.<Part>` loads with the same chunk.
|
|
228
|
+
`c15t/react/components/consent-dialog` exports the dialog without the
|
|
229
|
+
deferral, plus each part as a named export (`Card`, `Header`, `Overlay`,
|
|
230
|
+
`Root` and the rest). Importing from that path puts the dialog's code in
|
|
231
|
+
the bundle of every page that imports it, so the first open needs no
|
|
232
|
+
download but every visitor downloads the dialog.
|
|
233
|
+
|
|
189
234
|
The compound parts import from `c15t/react` as properties of `ConsentDialog`,
|
|
190
235
|
and `ConsentWidget` from the same module. See
|
|
191
|
-
[
|
|
236
|
+
[customize](../customize.md) for slots and tokens.
|
|
192
237
|
|
|
193
238
|
## Verify
|
|
194
239
|
|
|
@@ -19,17 +19,17 @@ placeholder button opens.
|
|
|
19
19
|
import { ConsentGate } from 'c15t/react';
|
|
20
20
|
|
|
21
21
|
export function ProductVideo() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
57
|
-
provider
|
|
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,23 +70,26 @@ 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 `
|
|
74
|
-
consent to view this content
|
|
75
|
-
`consentTypes.<category>.title`, and a button labelled
|
|
76
|
-
|
|
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 `
|
|
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
|
|
|
86
87
|
When the provider starts from a snapshot prefetched on the server, the
|
|
87
|
-
server renders the
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
server renders the placeholder for a denied category. For a granted
|
|
89
|
+
category it renders the empty wrapper, and the children mount once
|
|
90
|
+
hydration completes, so an iframe loads once even when the page streams
|
|
91
|
+
inside a `Suspense` boundary. A grant in the request cookie allows the
|
|
92
|
+
embed only when policy and privacy signals permit it. Browser privacy signals detected during hydration can
|
|
90
93
|
withdraw that permission. In a browser-only setup the
|
|
91
94
|
provider has no permission until it resolves policy on the client, so
|
|
92
95
|
`ConsentGate` shows the placeholder first and swaps in the embed after resolution
|
|
@@ -124,8 +127,24 @@ translated text; pass children to either to replace it.
|
|
|
124
127
|
</ConsentGate>
|
|
125
128
|
```
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
|
|
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"`.
|
|
129
148
|
|
|
130
149
|
## Accessibility
|
|
131
150
|
|
|
@@ -142,7 +161,9 @@ center and is not restricted by policy or privacy signals such as GPC.
|
|
|
142
161
|
Load the page with the category denied. The placeholder text names the
|
|
143
162
|
category and the network panel shows no request to the embed's host. With a
|
|
144
163
|
prefetched snapshot, the server HTML contains the placeholder for a denied
|
|
145
|
-
category
|
|
164
|
+
category and neither placeholder nor embed for a granted category; the
|
|
165
|
+
embed appears after hydration and the network panel shows one request for
|
|
166
|
+
its document. Browser-only initialization
|
|
146
167
|
shows the placeholder before policy resolves, then replaces it with the
|
|
147
168
|
embed if the category is granted. After policy resolves, activate the
|
|
148
169
|
placeholder button: the preference center opens. Turn the category on and
|