@c15t/nextjs 2.0.0-rc.3 → 2.0.0-rc.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.
- package/client/components/consent-dialog-link.js +3 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/libs/browser-initial-data.cjs +1 -0
- package/dist/libs/browser-initial-data.js +1 -0
- package/dist/libs/initial-data.cjs +1 -1
- package/dist/libs/initial-data.js +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.js +1 -1
- package/dist-types/headless.d.ts +1 -0
- package/{dist → dist-types}/index.d.ts +4 -3
- package/dist-types/libs/browser-initial-data.d.ts +9 -0
- package/{dist → dist-types}/libs/initial-data.d.ts +1 -2
- package/dist-types/types.d.ts +38 -0
- package/dist-types/version.d.ts +1 -0
- package/docs/README.md +73 -0
- package/docs/building-headless-components.md +250 -0
- package/docs/callbacks.md +117 -0
- package/docs/components/consent-banner.md +174 -0
- package/docs/components/consent-dialog-link.md +59 -0
- package/docs/components/consent-dialog-trigger.md +103 -0
- package/docs/components/consent-dialog.md +137 -0
- package/docs/components/consent-manager-provider.md +422 -0
- package/docs/components/consent-widget.md +78 -0
- package/docs/components/dev-tools.md +63 -0
- package/docs/components/frame.md +73 -0
- package/docs/concepts/client-modes.md +163 -0
- package/docs/concepts/consent-categories.md +97 -0
- package/docs/concepts/consent-models.md +116 -0
- package/docs/concepts/cookie-management.md +122 -0
- package/docs/concepts/glossary.md +23 -0
- package/docs/concepts/initialization-flow.md +141 -0
- package/docs/concepts/policy-packs.md +229 -0
- package/docs/headless.md +184 -0
- package/docs/hooks/use-color-scheme.md +40 -0
- package/docs/hooks/use-consent-manager/checking-consent.md +94 -0
- package/docs/hooks/use-consent-manager/location-info.md +95 -0
- package/docs/hooks/use-consent-manager/overview.md +390 -0
- package/docs/hooks/use-consent-manager/setting-consent.md +92 -0
- package/docs/hooks/use-draggable.md +57 -0
- package/docs/hooks/use-focus-trap.md +41 -0
- package/docs/hooks/use-reduced-motion.md +35 -0
- package/docs/hooks/use-ssr-status.md +31 -0
- package/docs/hooks/use-text-direction.md +49 -0
- package/docs/hooks/use-translations.md +116 -0
- package/docs/iab/consent-banner.md +95 -0
- package/docs/iab/consent-dialog.md +135 -0
- package/docs/iab/overview.md +119 -0
- package/docs/iab/use-gvl-data.md +208 -0
- package/docs/iframe-blocking.md +107 -0
- package/docs/integrations/databuddy.md +186 -0
- package/docs/integrations/google-tag-manager.md +153 -0
- package/docs/integrations/google-tag.md +149 -0
- package/docs/integrations/linkedin-insights.md +109 -0
- package/docs/integrations/meta-pixel.md +342 -0
- package/docs/integrations/microsoft-uet.md +112 -0
- package/docs/integrations/overview.md +89 -0
- package/docs/integrations/posthog.md +177 -0
- package/docs/integrations/tiktok-pixel.md +113 -0
- package/docs/integrations/x-pixel.md +143 -0
- package/docs/internationalization.md +197 -0
- package/docs/network-blocker.md +178 -0
- package/docs/optimization.md +156 -0
- package/docs/policy-packs.md +246 -0
- package/docs/quickstart.md +145 -0
- package/docs/script-loader.md +300 -0
- package/docs/server-side.md +171 -0
- package/docs/styling/classnames.md +84 -0
- package/docs/styling/color-scheme.md +82 -0
- package/docs/styling/css-variables.md +92 -0
- package/docs/styling/overview.md +312 -0
- package/docs/styling/slots.md +93 -0
- package/docs/styling/tailwind.md +86 -0
- package/docs/styling/tokens.md +214 -0
- package/docs/troubleshooting.md +146 -0
- package/package.json +20 -10
- package/dist/headless.d.ts +0 -2
- package/dist/headless.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/libs/initial-data.d.ts.map +0 -1
- package/dist/types.d.ts +0 -16
- package/dist/types.d.ts.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const __rslib_import_meta_url__="undefined"==typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_modules__={"./libs/initial-data"(
|
|
1
|
+
"use strict";const __rslib_import_meta_url__="undefined"==typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_modules__={"./libs/browser-initial-data"(e){e.exports=require("./libs/browser-initial-data.cjs")},"./libs/initial-data"(e){e.exports=require("./libs/initial-data.cjs")},"@c15t/react"(e){e.exports=require("@c15t/react")},c15t(e){e.exports=require("c15t")}},__webpack_module_cache__={};function __webpack_require__(e){var _=__webpack_module_cache__[e];if(void 0!==_)return _.exports;var t=__webpack_module_cache__[e]={exports:{}};return __webpack_modules__[e](t,t.exports,__webpack_require__),t.exports}__webpack_require__.n=e=>{var _=e&&e.__esModule?()=>e.default:()=>e;return __webpack_require__.d(_,{a:_}),_},__webpack_require__.d=(e,_)=>{for(var t in _)__webpack_require__.o(_,t)&&!__webpack_require__.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:_[t]})},__webpack_require__.o=(e,_)=>Object.prototype.hasOwnProperty.call(e,_),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};for(var __rspack_i in(()=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{C15tPrefetch:()=>r.C15tPrefetch,buildPrefetchScript:()=>t.buildPrefetchScript,ensurePrefetchedInitialData:()=>t.ensurePrefetchedInitialData,fetchInitialData:()=>a.fetchInitialData,getPrefetchedInitialData:()=>t.getPrefetchedInitialData});var e=__webpack_require__("@c15t/react"),_={};for(let t in e)0>["C15tPrefetch","fetchInitialData","default","buildPrefetchScript","ensurePrefetchedInitialData","getPrefetchedInitialData"].indexOf(t)&&(_[t]=()=>e[t]);__webpack_require__.d(__webpack_exports__,_);var t=__webpack_require__("c15t"),r=__webpack_require__("./libs/browser-initial-data"),a=__webpack_require__("./libs/initial-data")})(),exports.C15tPrefetch=__webpack_exports__.C15tPrefetch,exports.buildPrefetchScript=__webpack_exports__.buildPrefetchScript,exports.ensurePrefetchedInitialData=__webpack_exports__.ensurePrefetchedInitialData,exports.fetchInitialData=__webpack_exports__.fetchInitialData,exports.getPrefetchedInitialData=__webpack_exports__.getPrefetchedInitialData,__webpack_exports__)-1===["C15tPrefetch","buildPrefetchScript","ensurePrefetchedInitialData","fetchInitialData","getPrefetchedInitialData"].indexOf(__rspack_i)&&(exports[__rspack_i]=__webpack_exports__[__rspack_i]);Object.defineProperty(exports,"__esModule",{value:!0});
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{buildPrefetchScript as t,ensurePrefetchedInitialData as i,getPrefetchedInitialData as e}from"c15t";import{C15tPrefetch as r}from"./libs/browser-initial-data.js";import{fetchInitialData as a}from"./libs/initial-data.js";export*from"@c15t/react";export{r as C15tPrefetch,t as buildPrefetchScript,i as ensurePrefetchedInitialData,a as fetchInitialData,e as getPrefetchedInitialData};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const __rslib_import_meta_url__="undefined"==typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.n=e=>{var _=e&&e.__esModule?()=>e.default:()=>e;return __webpack_require__.d(_,{a:_}),_},__webpack_require__.d=(e,_)=>{for(var r in _)__webpack_require__.o(_,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:_[r]})},__webpack_require__.o=(e,_)=>Object.prototype.hasOwnProperty.call(e,_),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{C15tPrefetch:()=>C15tPrefetch});const jsx_runtime_namespaceObject=require("react/jsx-runtime"),external_c15t_namespaceObject=require("c15t"),script_namespaceObject=require("next/script");var script_default=__webpack_require__.n(script_namespaceObject);const DEFAULT_SCRIPT_ID="c15t-initial-data-prefetch";function C15tPrefetch({id:e="c15t-initial-data-prefetch",..._}){return(0,jsx_runtime_namespaceObject.jsx)(script_default(),{id:e,strategy:"beforeInteractive",children:(0,external_c15t_namespaceObject.buildPrefetchScript)(_)})}for(var __rspack_i in exports.C15tPrefetch=__webpack_exports__.C15tPrefetch,__webpack_exports__)-1===["C15tPrefetch"].indexOf(__rspack_i)&&(exports[__rspack_i]=__webpack_exports__[__rspack_i]);Object.defineProperty(exports,"__esModule",{value:!0});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as t}from"react/jsx-runtime";import{buildPrefetchScript as r}from"c15t";import e from"next/script";function i({id:i="c15t-initial-data-prefetch",...c}){return t(e,{id:i,strategy:"beforeInteractive",children:r(c)})}export{i as C15tPrefetch};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const __rslib_import_meta_url__="undefined"==typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.d=(e,_)=>{for(var r in _)__webpack_require__.o(_,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:_[r]})},__webpack_require__.o=(e,_)=>Object.prototype.hasOwnProperty.call(e,_),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{fetchInitialData:()=>fetchInitialData});const server_namespaceObject=require("@c15t/react/server"),headers_namespaceObject=require("next/headers");async function fetchInitialData(e){let _=await (0,headers_namespaceObject.headers)();return(0,server_namespaceObject.fetchSSRData)({...e,headers:_})}for(var __rspack_i in exports.fetchInitialData=__webpack_exports__.fetchInitialData,__webpack_exports__)-1===["fetchInitialData"].indexOf(__rspack_i)&&(exports[__rspack_i]=__webpack_exports__[__rspack_i]);Object.defineProperty(exports,"__esModule",{value:!0});
|
|
1
|
+
"use strict";const __rslib_import_meta_url__="undefined"==typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.d=(e,_)=>{for(var r in _)__webpack_require__.o(_,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:_[r]})},__webpack_require__.o=(e,_)=>Object.prototype.hasOwnProperty.call(e,_),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{fetchInitialData:()=>fetchInitialData});const server_namespaceObject=require("@c15t/react/server"),cache_namespaceObject=require("next/cache"),headers_namespaceObject=require("next/headers"),DEFAULT_REVALIDATE_SECONDS=1;async function fetchInitialData(e){let _=await (0,headers_namespaceObject.headers)(),r=(0,server_namespaceObject.normalizeBackendURL)(e.backendURL,_);if(!r)return;let a=e.nextCache?.revalidateSeconds;if(!1===a)return(0,server_namespaceObject.fetchSSRData)({...e,backendURL:r,headers:_});let t=(0,server_namespaceObject.createSSRInitCacheKey)({normalizedURL:r,headers:_,overrides:e.overrides});return(0,cache_namespaceObject.unstable_cache)(()=>(0,server_namespaceObject.fetchSSRData)({...e,backendURL:r,headers:_}),["c15t:nextjs:fetchInitialData",t],{revalidate:a??1})()}for(var __rspack_i in exports.fetchInitialData=__webpack_exports__.fetchInitialData,__webpack_exports__)-1===["fetchInitialData"].indexOf(__rspack_i)&&(exports[__rspack_i]=__webpack_exports__[__rspack_i]);Object.defineProperty(exports,"__esModule",{value:!0});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fetchSSRData as t}from"@c15t/react/server";import{headers as
|
|
1
|
+
import{createSSRInitCacheKey as e,fetchSSRData as t,normalizeBackendURL as a}from"@c15t/react/server";import{unstable_cache as r}from"next/cache";import{headers as n}from"next/headers";async function c(c){let i=await n(),o=a(c.backendURL,i);if(!o)return;let d=c.nextCache?.revalidateSeconds;return!1===d?t({...c,backendURL:o,headers:i}):r(()=>t({...c,backendURL:o,headers:i}),["c15t:nextjs:fetchInitialData",e({normalizedURL:o,headers:i,overrides:c.overrides})],{revalidate:d??1})()}export{c as fetchInitialData};
|
package/dist/version.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const __rslib_import_meta_url__="undefined"==typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.d=(e,_)=>{for(var r in _)__webpack_require__.o(_,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:_[r]})},__webpack_require__.o=(e,_)=>Object.prototype.hasOwnProperty.call(e,_),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{version:()=>version});const version="2.0.0-rc.
|
|
1
|
+
"use strict";const __rslib_import_meta_url__="undefined"==typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.d=(e,_)=>{for(var r in _)__webpack_require__.o(_,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:_[r]})},__webpack_require__.o=(e,_)=>Object.prototype.hasOwnProperty.call(e,_),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{version:()=>version});const version="2.0.0-rc.5";for(var __rspack_i in exports.version=__webpack_exports__.version,__webpack_exports__)-1===["version"].indexOf(__rspack_i)&&(exports[__rspack_i]=__webpack_exports__[__rspack_i]);Object.defineProperty(exports,"__esModule",{value:!0});
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
let e="2.0.0-rc.
|
|
1
|
+
let e="2.0.0-rc.5";export{e as version};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../react/dist-types/headless.d';
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* @see {@link @c15t/react} for React components and hooks
|
|
8
8
|
* @see {@link ./middleware} for Next.js middleware integration
|
|
9
9
|
*/
|
|
10
|
-
export * from '
|
|
10
|
+
export * from '../../react/dist-types/index.d.ts';
|
|
11
|
+
export { buildPrefetchScript, ensurePrefetchedInitialData, getPrefetchedInitialData, type PrefetchOptions, } from '../../core/dist-types/index.d.ts';
|
|
12
|
+
export { C15tPrefetch } from './libs/browser-initial-data';
|
|
11
13
|
export { fetchInitialData } from './libs/initial-data';
|
|
12
|
-
export type { FetchInitialDataOptions, InitialDataPromise,
|
|
13
|
-
//# sourceMappingURL=index.d.ts.map
|
|
14
|
+
export type { C15tPrefetchProps, ConsentManagerProps, FetchInitialDataOptions, InitialDataPromise, } from './types';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { C15tPrefetchProps } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Next.js script component that starts `/init` prefetching before hydration.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Use in `app/layout.tsx` for static routes. Pair with
|
|
7
|
+
* `getPrefetchedInitialData()` or `ensurePrefetchedInitialData()` in your client provider.
|
|
8
|
+
*/
|
|
9
|
+
export declare function C15tPrefetch({ id, ...options }: C15tPrefetchProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SSRInitialData } from '
|
|
1
|
+
import type { SSRInitialData } from '../../../core/dist-types/index.d.ts';
|
|
2
2
|
import type { FetchInitialDataOptions } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Fetches initial consent data on the server for SSR hydration.
|
|
@@ -25,4 +25,3 @@ import type { FetchInitialDataOptions } from '../types';
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
export declare function fetchInitialData(options: FetchInitialDataOptions): Promise<SSRInitialData | undefined>;
|
|
28
|
-
//# sourceMappingURL=initial-data.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ConsentManagerProviderProps } from '../../react/dist-types/index.d.ts';
|
|
2
|
+
import type { FetchSSRDataOptionsBase } from '../../react/dist-types/server';
|
|
3
|
+
import type { PrefetchOptions } from '../../core/dist-types/index.d.ts';
|
|
4
|
+
export type InitialDataPromise = NonNullable<ConsentManagerProviderProps['options']['store']>['ssrData'];
|
|
5
|
+
export interface NextCacheOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Cache lifetime in seconds for the Next.js data cache.
|
|
8
|
+
* Set to false to disable Next.js caching for this call.
|
|
9
|
+
*
|
|
10
|
+
* @default 1
|
|
11
|
+
*/
|
|
12
|
+
revalidateSeconds?: number | false;
|
|
13
|
+
}
|
|
14
|
+
export interface C15tPrefetchProps extends PrefetchOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Optional script element ID.
|
|
17
|
+
*
|
|
18
|
+
* @default 'c15t-initial-data-prefetch'
|
|
19
|
+
*/
|
|
20
|
+
id?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Options for the fetchInitialData function.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Uses the base options from @c15t/react/server - headers are
|
|
27
|
+
* resolved automatically from Next.js.
|
|
28
|
+
*/
|
|
29
|
+
export interface FetchInitialDataOptions extends FetchSSRDataOptionsBase {
|
|
30
|
+
/**
|
|
31
|
+
* Optional Next.js cache controls for SSR init requests.
|
|
32
|
+
*/
|
|
33
|
+
nextCache?: NextCacheOptions;
|
|
34
|
+
}
|
|
35
|
+
export interface ConsentManagerProps {
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
ssrData?: InitialDataPromise;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const version = "2.0.0-rc.5";
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# c15t Next.js Docs
|
|
2
|
+
|
|
3
|
+
Next.js-focused c15t docs for consent UI, server-side behavior, callbacks, and integrations.
|
|
4
|
+
|
|
5
|
+
If you are changing consent flows, consent UI, script loading, server-side setup, or backend configuration in an app that uses this package, start here before editing code.
|
|
6
|
+
|
|
7
|
+
## Start Here
|
|
8
|
+
|
|
9
|
+
- [Quickstart](./quickstart.md)
|
|
10
|
+
- [Server Side](./server-side.md)
|
|
11
|
+
- [Callbacks](./callbacks.md)
|
|
12
|
+
- [Script Loader](./script-loader.md)
|
|
13
|
+
- [Google Tag Manager](./integrations/google-tag-manager.md)
|
|
14
|
+
|
|
15
|
+
## Workflow Rules
|
|
16
|
+
|
|
17
|
+
### Server-Side Setup
|
|
18
|
+
|
|
19
|
+
Use this when:
|
|
20
|
+
- working on App Router or SSR consent initialization
|
|
21
|
+
- connecting the provider to server-fetched consent data
|
|
22
|
+
- deciding whether client-only setup is sufficient
|
|
23
|
+
|
|
24
|
+
Prefer:
|
|
25
|
+
- Prefer the documented server helpers and provider setup.
|
|
26
|
+
- Use the server-side integration path before inventing custom cookie or init plumbing.
|
|
27
|
+
|
|
28
|
+
Avoid:
|
|
29
|
+
- Do not default to client-only initialization when the documented SSR flow already covers the use case.
|
|
30
|
+
|
|
31
|
+
Read next:
|
|
32
|
+
- [Quickstart](./quickstart.md)
|
|
33
|
+
- [Server Side](./server-side.md)
|
|
34
|
+
|
|
35
|
+
### Styling
|
|
36
|
+
|
|
37
|
+
Use this when:
|
|
38
|
+
- customizing component appearance in a Next.js app
|
|
39
|
+
- deciding how to override the default consent UI
|
|
40
|
+
|
|
41
|
+
Prefer:
|
|
42
|
+
- Prefer design tokens first.
|
|
43
|
+
- If tokens are not enough, use component slots.
|
|
44
|
+
- Then use CSS variables or `className` for targeted overrides.
|
|
45
|
+
|
|
46
|
+
If that is not enough:
|
|
47
|
+
- Use `noStyle` or headless only as a last resort.
|
|
48
|
+
|
|
49
|
+
Avoid:
|
|
50
|
+
- Do not rebuild the consent UI if the documented styling layers can express the design.
|
|
51
|
+
|
|
52
|
+
Read next:
|
|
53
|
+
- [Overview](./styling/overview.md)
|
|
54
|
+
- [Tokens](./styling/tokens.md)
|
|
55
|
+
- [Slots](./styling/slots.md)
|
|
56
|
+
|
|
57
|
+
### Scripts & Integrations
|
|
58
|
+
|
|
59
|
+
Use this when:
|
|
60
|
+
- wiring scripts through Next.js routes, layouts, or providers
|
|
61
|
+
- adding analytics or advertising behind consent
|
|
62
|
+
|
|
63
|
+
Prefer:
|
|
64
|
+
- Prefer premade scripts from `@c15t/scripts/*`.
|
|
65
|
+
- Start from the integration docs before wiring scripts through Next-specific code paths.
|
|
66
|
+
|
|
67
|
+
Avoid:
|
|
68
|
+
- Do not use custom script injection when a documented integration already exists.
|
|
69
|
+
|
|
70
|
+
Read next:
|
|
71
|
+
- [Script Loader](./script-loader.md)
|
|
72
|
+
- [Google Tag Manager](./integrations/google-tag-manager.md)
|
|
73
|
+
- [Meta Pixel](./integrations/meta-pixel.md)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Building Headless Components
|
|
3
|
+
description: Build policy-aware custom consent components in Next.js using the headless hooks and policy-pack tooling.
|
|
4
|
+
---
|
|
5
|
+
Building headless components is easier now because c15t exposes policy-aware primitives instead of forcing you to reconstruct banner rules by hand.
|
|
6
|
+
|
|
7
|
+
The headless stack is:
|
|
8
|
+
|
|
9
|
+
* `useHeadlessConsentUI()` for policy-aware banner/dialog actions, ordering, layout, and primary action hints
|
|
10
|
+
* `useConsentManager()` for runtime state, categories, selected consent state, and policy metadata
|
|
11
|
+
* `useTranslations()` for the resolved copy
|
|
12
|
+
* `offlinePolicy.policyPacks` for offline previews that behave like backend policy resolution
|
|
13
|
+
|
|
14
|
+
> ℹ️ **Info:**
|
|
15
|
+
> This guide is about building your own components while still respecting resolved policy-pack behavior. For the general headless overview, see Headless Mode.
|
|
16
|
+
|
|
17
|
+
## What the Headless Tooling Gives You
|
|
18
|
+
|
|
19
|
+
The main win is that your custom UI can stay aligned with policy packs without duplicating policy logic in your components.
|
|
20
|
+
|
|
21
|
+
`useHeadlessConsentUI()` already resolves:
|
|
22
|
+
|
|
23
|
+
* which actions are allowed
|
|
24
|
+
* the order those actions should render in
|
|
25
|
+
* grouped actions from policy `layout`
|
|
26
|
+
* layout `direction` (`row` or `column`)
|
|
27
|
+
* the primary action
|
|
28
|
+
* UI profile and scroll-lock hints
|
|
29
|
+
* whether the banner or dialog should currently be visible
|
|
30
|
+
|
|
31
|
+
That means your component mostly focuses on markup and design-system concerns instead of re-implementing policy interpretation.
|
|
32
|
+
|
|
33
|
+
## Provider Setup for Local Policy Testing
|
|
34
|
+
|
|
35
|
+
```tsx title="components/consent-manager/provider.tsx"
|
|
36
|
+
'use client';
|
|
37
|
+
|
|
38
|
+
import type { ReactNode } from 'react';
|
|
39
|
+
import {
|
|
40
|
+
ConsentManagerProvider,
|
|
41
|
+
policyPackPresets,
|
|
42
|
+
} from '@c15t/nextjs';
|
|
43
|
+
|
|
44
|
+
export function ConsentManager({ children }: { children: ReactNode }) {
|
|
45
|
+
return (
|
|
46
|
+
<ConsentManagerProvider
|
|
47
|
+
options={{
|
|
48
|
+
mode: 'offline',
|
|
49
|
+
offlinePolicy: {
|
|
50
|
+
policyPacks: [
|
|
51
|
+
policyPackPresets.californiaOptOut(),
|
|
52
|
+
policyPackPresets.europeOptIn(),
|
|
53
|
+
policyPackPresets.worldNoBanner(),
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
overrides: {
|
|
57
|
+
country: 'GB',
|
|
58
|
+
},
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</ConsentManagerProvider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Policy-Aware Banner Example
|
|
68
|
+
|
|
69
|
+
```tsx title="components/consent-manager/custom-banner.tsx"
|
|
70
|
+
'use client';
|
|
71
|
+
|
|
72
|
+
import { useHeadlessConsentUI, useTranslations } from '@c15t/nextjs/headless';
|
|
73
|
+
|
|
74
|
+
export function CustomConsentBanner() {
|
|
75
|
+
const { banner, openDialog, performAction } = useHeadlessConsentUI();
|
|
76
|
+
const translations = useTranslations();
|
|
77
|
+
|
|
78
|
+
if (!banner.isVisible) return null;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<aside className="rounded-xl border bg-white p-6 shadow-lg">
|
|
82
|
+
<h2 className="text-lg font-semibold">{translations.cookieBanner.title}</h2>
|
|
83
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
84
|
+
{translations.cookieBanner.description}
|
|
85
|
+
</p>
|
|
86
|
+
|
|
87
|
+
<div className="mt-4 space-y-2">
|
|
88
|
+
{banner.actionGroups.map((group, index) => (
|
|
89
|
+
<div key={`${group.join('-')}-${index}`} className="flex gap-2">
|
|
90
|
+
{group.map((action) => (
|
|
91
|
+
<button
|
|
92
|
+
key={action}
|
|
93
|
+
type="button"
|
|
94
|
+
className={action === banner.primaryAction ? 'btn-primary' : 'btn-secondary'}
|
|
95
|
+
onClick={() => {
|
|
96
|
+
if (action === 'customize') {
|
|
97
|
+
openDialog();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
void performAction(action, { surface: 'banner' });
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{action === 'accept'
|
|
104
|
+
? translations.common.acceptAll
|
|
105
|
+
: action === 'reject'
|
|
106
|
+
? translations.common.rejectAll
|
|
107
|
+
: translations.common.customize}
|
|
108
|
+
</button>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</aside>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Category List That Respects the Resolved Policy
|
|
119
|
+
|
|
120
|
+
```tsx title="components/consent-manager/custom-dialog.tsx"
|
|
121
|
+
'use client';
|
|
122
|
+
|
|
123
|
+
import {
|
|
124
|
+
useConsentManager,
|
|
125
|
+
useHeadlessConsentUI,
|
|
126
|
+
useTranslations,
|
|
127
|
+
} from '@c15t/nextjs/headless';
|
|
128
|
+
|
|
129
|
+
export function CustomConsentDialog() {
|
|
130
|
+
const { dialog, performDialogAction, saveCustomPreferences } = useHeadlessConsentUI();
|
|
131
|
+
const {
|
|
132
|
+
consentTypes,
|
|
133
|
+
consentCategories,
|
|
134
|
+
consents,
|
|
135
|
+
selectedConsents,
|
|
136
|
+
setSelectedConsent,
|
|
137
|
+
} = useConsentManager();
|
|
138
|
+
const translations = useTranslations();
|
|
139
|
+
|
|
140
|
+
if (!dialog.isVisible) return null;
|
|
141
|
+
|
|
142
|
+
const displayedTypes = consentTypes.filter(
|
|
143
|
+
(type) => type.display && consentCategories.includes(type.name)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<section className="rounded-xl border bg-white p-6 shadow-xl">
|
|
148
|
+
<h2 className="text-lg font-semibold">
|
|
149
|
+
{translations.consentManagerDialog.title}
|
|
150
|
+
</h2>
|
|
151
|
+
|
|
152
|
+
<div className="mt-4 space-y-3">
|
|
153
|
+
{displayedTypes.map((type) => (
|
|
154
|
+
<label key={type.name} className="flex items-start justify-between gap-4">
|
|
155
|
+
<div>
|
|
156
|
+
<p className="font-medium">
|
|
157
|
+
{translations.consentTypes[type.name]?.title ?? type.name}
|
|
158
|
+
</p>
|
|
159
|
+
<p className="text-sm text-gray-600">{type.description}</p>
|
|
160
|
+
</div>
|
|
161
|
+
<input
|
|
162
|
+
type="checkbox"
|
|
163
|
+
checked={selectedConsents[type.name] ?? consents[type.name] ?? false}
|
|
164
|
+
disabled={type.disabled}
|
|
165
|
+
onChange={(event) =>
|
|
166
|
+
setSelectedConsent(type.name, event.target.checked)
|
|
167
|
+
}
|
|
168
|
+
/>
|
|
169
|
+
</label>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="mt-4 flex gap-2">
|
|
174
|
+
{dialog.orderedActions.map((action) => (
|
|
175
|
+
<button
|
|
176
|
+
key={action}
|
|
177
|
+
type="button"
|
|
178
|
+
onClick={() => {
|
|
179
|
+
if (action === 'customize') {
|
|
180
|
+
void saveCustomPreferences();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
void performDialogAction(action);
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{action === 'accept'
|
|
187
|
+
? translations.common.acceptAll
|
|
188
|
+
: action === 'reject'
|
|
189
|
+
? translations.common.rejectAll
|
|
190
|
+
: translations.common.save}
|
|
191
|
+
</button>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
</section>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## What a Policy-Aware Headless Component Should Respect
|
|
200
|
+
|
|
201
|
+
When you build custom banner or dialog components, make sure they use:
|
|
202
|
+
|
|
203
|
+
* `activeUI` or `banner.isVisible` / `dialog.isVisible` for visibility
|
|
204
|
+
* `allowedActions`, `orderedActions`, or `actionGroups` instead of hard-coding buttons
|
|
205
|
+
* `primaryAction` for visual emphasis
|
|
206
|
+
* `consentCategories` when deciding which category toggles to render
|
|
207
|
+
* `policyDecision` when you want to debug why a specific UI state was chosen
|
|
208
|
+
|
|
209
|
+
If you ignore those values, your custom UI can drift away from the resolved policy pack even though the underlying consent engine is configured correctly.
|
|
210
|
+
|
|
211
|
+
## Test Custom UI Against the Resolved Policy
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import {
|
|
215
|
+
getEffectivePolicy,
|
|
216
|
+
type PolicyUIState,
|
|
217
|
+
validateUIAgainstPolicy,
|
|
218
|
+
} from 'c15t';
|
|
219
|
+
|
|
220
|
+
const policy = getEffectivePolicy(initData);
|
|
221
|
+
|
|
222
|
+
const dialogState: PolicyUIState = {
|
|
223
|
+
mode: 'dialog',
|
|
224
|
+
actions: ['accept', 'reject', 'customize'],
|
|
225
|
+
layout: 'split',
|
|
226
|
+
uiProfile: 'compact',
|
|
227
|
+
scrollLock: true,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const issues = validateUIAgainstPolicy({
|
|
231
|
+
policy,
|
|
232
|
+
state: dialogState,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(issues).toEqual([]);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Validation and Testing
|
|
239
|
+
|
|
240
|
+
If you are building a reusable headless component library, validate your rendered UI against the resolved runtime policy in tests.
|
|
241
|
+
|
|
242
|
+
The core package exposes:
|
|
243
|
+
|
|
244
|
+
* `getEffectivePolicy(initData)` to read the resolved policy from `/init`
|
|
245
|
+
* `validateUIAgainstPolicy({ policy, state })` to detect mismatches such as wrong actions, layout, or mode
|
|
246
|
+
|
|
247
|
+
This is useful when your design system renders custom button arrangements and you want tests to catch policy drift early.
|
|
248
|
+
|
|
249
|
+
> ℹ️ **Info:**
|
|
250
|
+
> Pair this with Policy Packs when you want to exercise multiple regional UI states locally before wiring a live backend.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Callbacks
|
|
3
|
+
description: React to consent lifecycle events - initialization, consent changes, errors, and revocation reloads.
|
|
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()`.
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { type ReactNode } from 'react';
|
|
11
|
+
import { ConsentManagerProvider } from '@c15t/nextjs';
|
|
12
|
+
|
|
13
|
+
export function ConsentManager({ children }: { children: ReactNode }) {
|
|
14
|
+
return (
|
|
15
|
+
<ConsentManagerProvider
|
|
16
|
+
options={{
|
|
17
|
+
mode: 'hosted',
|
|
18
|
+
backendURL: '/api/c15t',
|
|
19
|
+
callbacks: {
|
|
20
|
+
onBannerFetched: ({ jurisdiction, location, translations }) => {
|
|
21
|
+
console.log('Jurisdiction:', jurisdiction);
|
|
22
|
+
console.log('Country:', location.countryCode);
|
|
23
|
+
console.log('Language:', translations.language);
|
|
24
|
+
},
|
|
25
|
+
onConsentSet: ({ preferences }) => {
|
|
26
|
+
// Fire analytics event
|
|
27
|
+
analytics.track('consent_updated', { preferences });
|
|
28
|
+
},
|
|
29
|
+
onError: ({ error }) => {
|
|
30
|
+
errorReporter.captureMessage(error);
|
|
31
|
+
},
|
|
32
|
+
onBeforeConsentRevocationReload: ({ preferences }) => {
|
|
33
|
+
// Flush pending analytics before page reloads
|
|
34
|
+
analytics.flush();
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</ConsentManagerProvider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Available Callbacks
|
|
46
|
+
|
|
47
|
+
### `onBannerFetched`
|
|
48
|
+
|
|
49
|
+
Called when the consent banner data is fetched from the backend (or loaded from SSR data). The payload includes jurisdiction info, location data, and resolved translations.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
onBannerFetched: ({ jurisdiction, location, translations }) => {
|
|
53
|
+
// jurisdiction: 'GDPR' | 'CCPA' | { code: 'GDPR', message: '...' } | ...
|
|
54
|
+
// location: { countryCode: 'DE', regionCode: 'BY' }
|
|
55
|
+
// translations: { language: 'de', translations: {...} }
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `onConsentSet`
|
|
60
|
+
|
|
61
|
+
Called whenever consent preferences are saved - whether by user action (`saveConsents()`), automatic defaults, or on initialization when no jurisdiction is detected.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
onConsentSet: ({ preferences }) => {
|
|
65
|
+
// preferences: { necessary: true, measurement: true, marketing: false, ... }
|
|
66
|
+
if (preferences.measurement) {
|
|
67
|
+
loadAnalytics();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `onError`
|
|
73
|
+
|
|
74
|
+
Called when an error occurs during consent operations (e.g., API request failure). If no `onError` callback is provided, errors are logged to `console.error`.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
onError: ({ error }) => {
|
|
78
|
+
// error: string describing what went wrong
|
|
79
|
+
Sentry.captureMessage(`Consent error: ${error}`);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `onBeforeConsentRevocationReload`
|
|
84
|
+
|
|
85
|
+
Called synchronously before the page reloads due to consent revocation. This is your last chance to run cleanup before the reload. Keep this callback fast - avoid async operations.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
onBeforeConsentRevocationReload: ({ preferences }) => {
|
|
89
|
+
// Flush any pending data
|
|
90
|
+
navigator.sendBeacon('/api/flush', JSON.stringify({ session: sessionId }));
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Dynamic Registration
|
|
95
|
+
|
|
96
|
+
Register or update callbacks at runtime using `setCallback()`:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { useEffect } from 'react';
|
|
100
|
+
import { useConsentManager } from '@c15t/nextjs';
|
|
101
|
+
|
|
102
|
+
function ConsentAnalytics() {
|
|
103
|
+
const { setCallback } = useConsentManager();
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
setCallback('onConsentSet', ({ preferences }) => {
|
|
107
|
+
analytics.track('consent_changed', preferences);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
setCallback('onConsentSet', undefined);
|
|
112
|
+
};
|
|
113
|
+
}, [setCallback]);
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
```
|