@c15t/react 3.0.0-alpha.7 → 3.0.0-alpha.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.
@@ -1,2 +1,2 @@
1
1
  "use client";
2
- import{Fragment as o,jsx as t,jsxs as e}from"react/jsx-runtime";import r from"@c15t/ui/styles/components/consent-banner";import{forwardRef as n,useMemo as i}from"react";import{useHeadlessConsentUI as a}from"../../../component-hooks/use-headless-consent-ui.js";import{ConsentTrackingContext as s}from"../../../context/consent-tracking-context.js";import{LocalThemeContext as l}from"../../../context/theme-context.js";import{useActiveUI as c,useHasConsentUI as m,useModel as p,usePolicyRule as d,useTranslations as u}from"../../../hooks.js";import{useLateEntry as f}from"../../../hooks/use-late-entry.js";import{useTextDirection as v}from"../../../hooks/use-text-direction.js";import{useUIConfig as b}from"../../../ui-config-context.js";import{defaultTranslationConfig as g}from"../../../utils/default-translation-config.js";import{mergeSlotProps as h}from"../../../utils/merge-slot-props.js";import{ConsentBannerSurfaceContext as j,mirrorDefaultPosition as k,useConsentBannerSurface as x}from"../surface-context.js";import{Overlay as N}from"./overlay.js";let y=["opt-in","opt-out"],S=n(({asChild:n,children:i,className:a,style:s,className:l,disableAnimation:j,noStyle:S,models:C=y,...$},L)=>{let P=c(),{components:R}=b(),B=m(),F=p()??"opt-in",A=d(),E=x(),V=u(),q=v(V?.language??g.defaultLanguage),w=k(E.position,E.positionSource,E.variant,q),z=B&&"banner"===P&&C.includes(F),D=f(z)&&!j,G=h(R?.banner?.root,{baseClassName:r.root,className:a||l,noStyle:S,style:s,...$}),H=S?G.className||"":`${G.className||""} ${j?"":`${r.bannerVisible} ${r.bannerEntering}`}`;return z?e(o,{children:[t(N,{}),t("div",{ref:L,...G,className:H,"data-variant":E.variant,"data-position":w,"data-blocking":E.blocking?"true":void 0,"data-prompt":A.prompt,"data-model":A.model,"data-entry":D?"late":void 0,"data-testid":"consent-banner-root",dir:q,children:i})]}):null}),C=({children:o,className:e,noStyle:r,disableAnimation:n,scrollLock:c,trapFocus:m,models:p,uiSource:d,variant:u,position:f,blocking:v,...b})=>{let{banner:g}=a({prompt:{blocking:v,position:f,scrollLock:c,trapFocus:m,variant:u}}),h=g.scrollLock,k=g.trapFocus,x=i(()=>({disableAnimation:n,noStyle:r,scrollLock:h,trapFocus:k}),[n,r,h,k]),N=i(()=>({blocking:g.blocking,position:g.position,positionSource:g.positionSource,variant:g.variant}),[g.blocking,g.position,g.positionSource,g.variant]),y=i(()=>({uiSource:d??"banner"}),[d]);return t(s.Provider,{value:y,children:t(l.Provider,{value:x,children:t(j.Provider,{value:N,children:t(S,{disableAnimation:n,className:e,noStyle:r,models:p,...b,children:o})})})})};S.displayName="ConsentBannerRootChildren";let $=C;export{C as ConsentBannerRoot,$ as Root};
2
+ import{Fragment as o,jsx as t,jsxs as e}from"react/jsx-runtime";import r from"@c15t/ui/styles/components/consent-banner";import{forwardRef as n,useMemo as i}from"react";import{useHeadlessConsentUI as a}from"../../../component-hooks/use-headless-consent-ui.js";import{ConsentTrackingContext as s}from"../../../context/consent-tracking-context.js";import{LocalThemeContext as l}from"../../../context/theme-context.js";import{useActiveUI as c,useHasConsentUI as m,useModel as p,usePolicyRule as d,useTranslations as u}from"../../../hooks.js";import{useLateEntry as f}from"../../../hooks/use-late-entry.js";import{useTextDirection as h}from"../../../hooks/use-text-direction.js";import{useUIConfig as v}from"../../../ui-config-context.js";import{defaultTranslationConfig as b}from"../../../utils/default-translation-config.js";import{mergeSlotProps as g}from"../../../utils/merge-slot-props.js";import{StreamedSurface as j}from"../../shared/streamed-surface.js";import{ConsentBannerSurfaceContext as k,mirrorDefaultPosition as x,useConsentBannerSurface as N}from"../surface-context.js";import{Overlay as y}from"./overlay.js";let S=["opt-in","opt-out"],C=n(({asChild:n,children:i,className:a,style:s,className:l,disableAnimation:j,noStyle:k,models:C=S,...$},L)=>{let P=c(),{components:R}=v(),B=m(),F=p()??"opt-in",A=d(),E=N(),V=u(),q=h(V?.language??b.defaultLanguage),w=x(E.position,E.positionSource,E.variant,q),z=B&&"banner"===P&&C.includes(F),D=f(z)&&!j,G=g(R?.banner?.root,{baseClassName:r.root,className:a||l,noStyle:k,style:s,...$}),H=k?G.className||"":`${G.className||""} ${j?"":`${r.bannerVisible} ${r.bannerEntering}`}`;return z?e(o,{children:[t(y,{}),t("div",{ref:L,...G,className:H,"data-variant":E.variant,"data-position":w,"data-blocking":E.blocking?"true":void 0,"data-prompt":A.prompt,"data-model":A.model,"data-entry":D?"late":void 0,"data-testid":"consent-banner-root",dir:q,children:i})]}):null}),$=({children:o,className:e,noStyle:r,disableAnimation:n,scrollLock:c,trapFocus:m,models:p,uiSource:d,variant:u,position:f,blocking:h,...v})=>{let{banner:b}=a({prompt:{blocking:h,position:f,scrollLock:c,trapFocus:m,variant:u}}),g=b.scrollLock,j=b.trapFocus,x=i(()=>({disableAnimation:n,noStyle:r,scrollLock:g,trapFocus:j}),[n,r,g,j]),N=i(()=>({blocking:b.blocking,position:b.position,positionSource:b.positionSource,variant:b.variant}),[b.blocking,b.position,b.positionSource,b.variant]),y=i(()=>({uiSource:d??"banner"}),[d]);return t(s.Provider,{value:y,children:t(l.Provider,{value:x,children:t(k.Provider,{value:N,children:t(C,{disableAnimation:n,className:e,noStyle:r,models:p,...v,children:o})})})})};C.displayName="ConsentBannerRootChildren";let L=o=>t(j,{children:t($,{...o})}),P=L;export{L as ConsentBannerRoot,P as Root};
@@ -1,2 +1,2 @@
1
1
  "use client";
2
- import{jsx as t,jsxs as n}from"react/jsx-runtime";import e from"@c15t/ui/styles/components/consent-actions";import r from"@c15t/ui/styles/components/consent-banner";import{Fragment as o}from"react";import{useHeadlessConsentUI as i}from"../../component-hooks/use-headless-consent-ui.js";import{Box as a}from"../shared/primitives/box.js";import{BrandingLink as s}from"../shared/ui/branding.js";import{useComponentConfig as c}from"../../hooks/use-component-config.js";import{ConsentBannerRoot as l}from"./atoms/root.js";import{ConsentBannerAcceptButton as d,ConsentBannerCard as m,ConsentBannerCustomizeButton as p,ConsentBannerDescription as u,ConsentBannerDismissButton as h,ConsentBannerFooter as b,ConsentBannerFooterSubGroup as f,ConsentBannerHeader as y,ConsentBannerRejectButton as g,ConsentBannerRights as j,ConsentBannerTitle as A}from"./components.js";import{ErrorBoundary as k}from"./error-boundary.js";import{resolveBannerPrimaryActions as v}from"./resolve-banner-primary-actions.js";let N=({noStyle:N,disableAnimation:S,scrollLock:w,trapFocus:C,title:L,description:x,rejectButtonText:z,customizeButtonText:F,acceptButtonText:P,dismissButtonText:B,legalLinks:$,hideBranding:G=!1,layout:E,direction:K,primaryButton:R,models:U,uiSource:q,variant:D,position:H,blocking:I})=>{let{banner:J}=i({prompt:{blocking:I,direction:K,layout:E,position:H,primaryActions:"string"==typeof R?[R]:R,scrollLock:w,trapFocus:C,variant:D}}),M=c({disableAnimation:S,noStyle:N,scrollLock:J.scrollLock,trapFocus:J.trapFocus}),{orderedActions:O}=J,Q=new Set(O),T=v(J.primaryActions,O),V=J.actionGroups,W=J.direction,{shouldFillActions:X}=J,Y=(n,e)=>{if(!Q.has(n))return null;let r=T.includes(n);switch(n){case"reject":return t(g,{consentAction:"reject",isPrimary:r,className:e,"data-action":"reject","data-testid":"consent-banner-reject-button",children:z});case"accept":return t(d,{consentAction:"accept",isPrimary:r,className:e,"data-action":"accept","data-testid":"consent-banner-accept-button",children:P});case"customize":return t(p,{consentAction:"customize",isPrimary:r,className:e,"data-action":"customize","data-testid":"consent-banner-customize-button",children:F});case"dismiss":return t(h,{consentAction:"dismiss",isPrimary:r,className:e,"data-action":"dismiss","data-testid":"consent-banner-dismiss-button",children:B});case"save":return null;default:throw Error(`Unhandled consent banner button type: ${n}`)}};return t(k,{fallback:t("div",{children:"Something went wrong with the Consent Banner."}),children:t(l,{...M,models:U,uiSource:q,variant:D,position:H,blocking:I,children:n(a,{baseClassName:r.cardShell,slotKey:"banner.cardShell",children:[t(s,{hideBranding:G,variant:"banner-tag",slotContext:"banner","data-testid":"consent-banner-branding"}),n(m,{children:[n(y,{children:[t(A,{children:L}),t(u,{legalLinks:$,children:x})]}),n(b,{className:e.actionRoot,"data-direction":W,"data-fill":!!X||void 0,"data-split":V.length>1&&!X||void 0,children:[t(j,{rights:J.preferenceControls}),V.map((n,r)=>{if(Array.isArray(n)){let i=n.filter(t=>Q.has(t));if(0===i.length)return null;let a=n.join("-");return t(f,{className:e.actionGroup,"data-direction":W,"data-fill":!!X||void 0,children:i.map(n=>t(o,{children:Y(n)},n))},a?`group-${a}`:`group-${r}`)}return Q.has(n)?t(o,{children:Y(n)},n):null})]})]})]})})})};export{N as ConsentBanner};
2
+ import{jsx as t,jsxs as n}from"react/jsx-runtime";import e from"@c15t/ui/styles/components/consent-actions";import r from"@c15t/ui/styles/components/consent-banner";import{Fragment as o}from"react";import{useHeadlessConsentUI as i}from"../../component-hooks/use-headless-consent-ui.js";import{Box as a}from"../shared/primitives/box.js";import{BrandingLink as s}from"../shared/ui/branding.js";import{useComponentConfig as c}from"../../hooks/use-component-config.js";import{StreamedSurface as l}from"../shared/streamed-surface.js";import{ConsentBannerRoot as d}from"./atoms/root.js";import{ConsentBannerAcceptButton as m,ConsentBannerCard as p,ConsentBannerCustomizeButton as u,ConsentBannerDescription as h,ConsentBannerDismissButton as b,ConsentBannerFooter as f,ConsentBannerFooterSubGroup as y,ConsentBannerHeader as g,ConsentBannerRejectButton as j,ConsentBannerRights as A,ConsentBannerTitle as k}from"./components.js";import{ErrorBoundary as v}from"./error-boundary.js";import{resolveBannerPrimaryActions as N}from"./resolve-banner-primary-actions.js";let S=({noStyle:l,disableAnimation:S,scrollLock:w,trapFocus:C,title:L,description:x,rejectButtonText:z,customizeButtonText:F,acceptButtonText:P,dismissButtonText:B,legalLinks:$,hideBranding:G=!1,layout:E,direction:K,primaryButton:R,models:U,uiSource:q,variant:D,position:H,blocking:I})=>{let{banner:J}=i({prompt:{blocking:I,direction:K,layout:E,position:H,primaryActions:"string"==typeof R?[R]:R,scrollLock:w,trapFocus:C,variant:D}}),M=c({disableAnimation:S,noStyle:l,scrollLock:J.scrollLock,trapFocus:J.trapFocus}),{orderedActions:O}=J,Q=new Set(O),T=N(J.primaryActions,O),V=J.actionGroups,W=J.direction,{shouldFillActions:X}=J,Y=(n,e)=>{if(!Q.has(n))return null;let r=T.includes(n);switch(n){case"reject":return t(j,{consentAction:"reject",isPrimary:r,className:e,"data-action":"reject","data-testid":"consent-banner-reject-button",children:z});case"accept":return t(m,{consentAction:"accept",isPrimary:r,className:e,"data-action":"accept","data-testid":"consent-banner-accept-button",children:P});case"customize":return t(u,{consentAction:"customize",isPrimary:r,className:e,"data-action":"customize","data-testid":"consent-banner-customize-button",children:F});case"dismiss":return t(b,{consentAction:"dismiss",isPrimary:r,className:e,"data-action":"dismiss","data-testid":"consent-banner-dismiss-button",children:B});case"save":return null;default:throw Error(`Unhandled consent banner button type: ${n}`)}};return t(v,{fallback:t("div",{children:"Something went wrong with the Consent Banner."}),children:t(d,{...M,models:U,uiSource:q,variant:D,position:H,blocking:I,children:n(a,{baseClassName:r.cardShell,slotKey:"banner.cardShell",children:[t(s,{hideBranding:G,variant:"banner-tag",slotContext:"banner","data-testid":"consent-banner-branding"}),n(p,{children:[n(g,{children:[t(k,{children:L}),t(h,{legalLinks:$,children:x})]}),n(f,{className:e.actionRoot,"data-direction":W,"data-fill":!!X||void 0,"data-split":V.length>1&&!X||void 0,children:[t(A,{rights:J.preferenceControls}),V.map((n,r)=>{if(Array.isArray(n)){let i=n.filter(t=>Q.has(t));if(0===i.length)return null;let a=n.join("-");return t(y,{className:e.actionGroup,"data-direction":W,"data-fill":!!X||void 0,children:i.map(n=>t(o,{children:Y(n)},n))},a?`group-${a}`:`group-${r}`)}return Q.has(n)?t(o,{children:Y(n)},n):null})]})]})]})})})},w=n=>t(l,{children:t(S,{...n})});export{w as ConsentBanner};
@@ -1 +1 @@
1
- import{jsxs as e}from"react/jsx-runtime";import t from"@c15t/ui/styles/components/legal-links";import{resolveTranslations as l}from"@c15t/ui/utils";import{useContext as n,useMemo as r,useSyncExternalStore as i}from"react";import{KernelContext as s}from"../../../../context.js";import{V3UIConfigContext as o,useUIConfig as a}from"../../../../ui-config-context.js";import{defaultTranslationConfig as u}from"../../../../utils/default-translation-config.js";import{mergeSlotProps as g}from"../../../../utils/merge-slot-props.js";let m=()=>()=>void 0,c=function(){let e=n(s),t=i(e?t=>e.subscribe(t):m,()=>e?.getSnapshot().translations??null,()=>e?.getServerSnapshot().translations??null);return r(()=>t?.translations?t.translations.legalLinks:l({},u).legalLinks,[t])},f=function(e){let t=n(o).legalLinks;return null==e?null:Object.fromEntries(Object.entries(t??{}).filter(([t])=>e.includes(t)))},p=({links:l,context:n,testIdPrefix:r})=>{let i=f(l),s=c(),{components:o}=a(),u=g(o?.["legal-links"]?.root,{}),m=g(o?.link?.[n],{baseClassName:t.legalLink});return i&&0!==Object.keys(i).length?e("span",{...u,children:[" ",Object.entries(i).map(([t,l],n,i)=>l?e("span",{children:[e("a",{href:l.href,target:l.target||"_blank",rel:l.rel||("_blank"===l.target?"noopener noreferrer":void 0),...m,"data-testid":r?`${r}-${t}`:void 0,children:[l.label??s?.[t],n<i.length-1&&","]}),n<i.length-1&&" "]},String(t)):null)]}):null};export{p as InlineLegalLinks,f as useFilteredLegalLinks};
1
+ import{jsxs as e}from"react/jsx-runtime";import t from"@c15t/ui/styles/components/legal-links";import{resolveTranslations as l}from"@c15t/ui/utils";import{useContext as n,useMemo as r,useSyncExternalStore as i}from"react";import{KernelContext as s}from"../../../../context.js";import{useServerSnapshot as o}from"../../../../kernel-selector.js";import{V3UIConfigContext as a,useUIConfig as m}from"../../../../ui-config-context.js";import{defaultTranslationConfig as u}from"../../../../utils/default-translation-config.js";import{mergeSlotProps as c}from"../../../../utils/merge-slot-props.js";let g=()=>()=>void 0,f=function(){let e=n(s),t=o(e),a=i(e?t=>e.subscribe(t):g,()=>e?.getSnapshot().translations??null,()=>t()?.translations??null);return r(()=>a?.translations?a.translations.legalLinks:l({},u).legalLinks,[a])},p=function(e){let t=n(a).legalLinks;return null==e?null:Object.fromEntries(Object.entries(t??{}).filter(([t])=>e.includes(t)))},k=({links:l,context:n,testIdPrefix:r})=>{let i=p(l),s=f(),{components:o}=m(),a=c(o?.["legal-links"]?.root,{}),u=c(o?.link?.[n],{baseClassName:t.legalLink});return i&&0!==Object.keys(i).length?e("span",{...a,children:[" ",Object.entries(i).map(([t,l],n,i)=>l?e("span",{children:[e("a",{href:l.href,target:l.target||"_blank",rel:l.rel||("_blank"===l.target?"noopener noreferrer":void 0),...u,"data-testid":r?`${r}-${t}`:void 0,children:[l.label??s?.[t],n<i.length-1&&","]}),n<i.length-1&&" "]},String(t)):null)]}):null};export{k as InlineLegalLinks,p as useFilteredLegalLinks};
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ import{jsx as e}from"react/jsx-runtime";import{HydrationSnapshotContext as r,StreamedPrefetchContext as t}from"../../context.js";import{useKernel as o}from"../../kernel-selector.js";import*as l from"react";let u="u"<typeof document?l.useEffect:l.useLayoutEffect,n=function(e){if("fulfilled"===e.status)return e.value;throw e},a=function(e){let{use:r}=l;return r?r(e):n(e)},d=({children:l,streamed:n})=>{let d=o(),s=a(n.settled),c=n.snapshotFor(s);return u(()=>{n.adopt(s)},[n,s]),e(r.Provider,{value:c===d.getServerSnapshot()?null:c,children:e(t.Provider,{value:void 0,children:l})})},s=({children:r})=>{let o=(0,l.useContext)(t);return o?e(l.Suspense,{fallback:null,children:e(d,{streamed:o,children:r})}):r};export{s as StreamedSurface,n as readRecorded};
package/dist/context.js CHANGED
@@ -1 +1 @@
1
- import{createContext as e}from"react";let t=e(null);t.displayName="C15tKernelContext";let l=e(null);export{t as KernelContext,l as ProviderServicesContext};
1
+ import{createContext as e}from"react";let t=e(null);t.displayName="C15tKernelContext";let n=e(null),o=e(void 0),r=e(null);export{r as HydrationSnapshotContext,t as KernelContext,n as ProviderServicesContext,o as StreamedPrefetchContext};
package/dist/hooks.js CHANGED
@@ -1 +1 @@
1
- import{applyExperimentAssignment as e,applyExperimentTheme as s,isVendorAllowed as o}from"@c15t/core";import{hasConsentPreferences as n,hasConsentUI as t,showConsentSurface as u}from"@c15t/core/surface-actions";import{useCallback as r,useContext as i,useMemo as a,useSyncExternalStore as c}from"react";import{ProviderServicesContext as f}from"./context.js";import{useGateSelector as l,useKernel as h,useKernelSelector as d}from"./kernel-selector.js";import{useUIConfig as m}from"./ui-config-context.js";let p=function(){let e=h();return c(s=>e.subscribe(s),()=>e.getSnapshot(),()=>e.getServerSnapshot())},k=function(e){return d(s=>s.effectivePermissions[e])},_=function(){return d(e=>e.effectivePermissions)},C=function(){return d(e=>e.overrides)},g=function(){return d(e=>e.user)},P=function(){return d(e=>e.location)},v=function(){return d(e=>e.translations)},S=function(){return d(e=>e.branding)},y=function(){return d(e=>"matched"===e.resolution.status)},I=function(){return d(t)},R=function(){return d(n)},b=function(){return d(e=>"matched"===e.resolution.status?e.model:null)},A=function(){return d(e=>e.activeUI)},x=function(){return d(e=>e.policyRule.scope)},U=function(){return d(e=>e.policyRule.scopeMode)},E=function(){return d(e=>e.iab)},T=function(){return d(e=>e.iab?.enabled??!1)},V=function(e){let s=String(e);return d(e=>e.iab?.vendorConsents[s]??!1)},D=function(e){return d(s=>s.iab?.purposeConsents[e]??!1)},O=function(e){return d(s=>s.iab?.specialFeatureOptIns[e]??!1)},B=function(){return d(e=>e.iab?.tcString??null)},H=[],L=function(){return d(e=>"iab"===e.model?H:e.vendors?.declared??H)},M=function(){return d(e=>e.vendorChoice)},N=function(e){return l((s,n)=>o(s,e,n))},j=function(){return h().set.registerConsentCategories},q=function(){return h().set.overrides},F=function(){let e=h(),s=i(f);return r(o=>{s?s.setLanguage(o):o!==e.getSnapshot().overrides.language&&(e.set.language(o),e.commands.init())},[e,s])},w=function(){let e=h();return r(s=>u(e,s),[e])},z=function(){return h().commands.save},G=function(){let e=h();return r(s=>e.events.on("permissions:changed",({snapshot:e})=>s(e.effectivePermissions)),[e])},J=function(){return h().commands.identify},K=function(){return h().commands.init},Q=function(){return d(e=>e.explicitChoice)},W=function(){return d(e=>e.effectivePermissions)},X=function(){return d(e=>e.promptRequirement)},Y=function(){return d(e=>e.noticeDismissal)},Z=function(){return d(e=>e.privacySignals)},$=function(){return d(e=>e.resolution)},ee=function(){return d(e=>e.policyRule)},es=function(){return d(e=>e.restrictions)},eo=function(){return h().commands.dismissNotice},en=function(){return d(e=>e.experiment)},et=function(){let{experiment:s,presentation:o}=m(),n=en();return a(()=>e(o,s,n),[o,s,n])},eu=function(){let{experiment:e,theme:o}=m(),n=en();return a(()=>s(o,e,n),[o,e,n])},er={},ei={},ea=function(){return et()?.prompt??er},ec=function(){return et()?.preferences??ei};export{A as useActiveUI,S as useBranding,k as useConsent,_ as useConsents,L as useDeclaredVendors,eo as useDismissNotice,W as useEffectivePermissions,en as useExperiment,Q as useExplicitChoice,y as useHasConsentPolicy,R as useHasConsentPreferences,I as useHasConsentUI,T as useIABEnabled,E as useIABSnapshot,J as useIdentify,K as useInit,P as useLocation,b as useModel,Y as useNoticeDismissal,C as useOverrides,x as usePolicyCategories,$ as usePolicyResolution,ee as usePolicyRule,U as usePolicyScopeMode,ec as usePreferencesPresentation,Z as usePrivacySignals,ea as usePromptPresentation,X as usePromptRequirement,D as usePurposeConsent,j as useRegisterConsentCategories,et as useResolvedPresentation,eu as useResolvedTheme,es as useRestrictions,z as useSaveConsents,w as useSetActiveUI,F as useSetLanguage,q as useSetOverrides,p as useSnapshot,O as useSpecialFeatureOptIn,G as useSubscribeToConsentChanges,B as useTCString,v as useTranslations,g as useUser,N as useVendorAllowed,M as useVendorChoice,V as useVendorConsent};
1
+ import{applyExperimentAssignment as e,applyExperimentTheme as s,isVendorAllowed as o}from"@c15t/core";import{hasConsentPreferences as n,hasConsentUI as t,showConsentSurface as u}from"@c15t/core/surface-actions";import{useCallback as r,useContext as i,useMemo as a,useSyncExternalStore as c}from"react";import{ProviderServicesContext as f}from"./context.js";import{useGateSelector as l,useKernel as h,useKernelSelector as d,useServerSnapshot as m}from"./kernel-selector.js";import{useUIConfig as k}from"./ui-config-context.js";let p=function(){let e=h();return c(s=>e.subscribe(s),()=>e.getSnapshot(),m(e))},_=function(e){return d(s=>s.effectivePermissions[e])},C=function(){return d(e=>e.effectivePermissions)},g=function(){return d(e=>e.overrides)},P=function(){return d(e=>e.user)},v=function(){return d(e=>e.location)},S=function(){return d(e=>e.translations)},y=function(){return d(e=>e.branding)},I=function(){return d(e=>"matched"===e.resolution.status)},R=function(){return d(t)},b=function(){return d(n)},A=function(){return d(e=>"matched"===e.resolution.status?e.model:null)},x=function(){return d(e=>e.activeUI)},U=function(){return d(e=>e.policyRule.scope)},E=function(){return d(e=>e.policyRule.scopeMode)},T=function(){return d(e=>e.iab)},V=function(){return d(e=>e.iab?.enabled??!1)},D=function(e){let s=String(e);return d(e=>e.iab?.vendorConsents[s]??!1)},O=function(e){return d(s=>s.iab?.purposeConsents[e]??!1)},B=function(e){return d(s=>s.iab?.specialFeatureOptIns[e]??!1)},H=function(){return d(e=>e.iab?.tcString??null)},L=[],M=function(){return d(e=>"iab"===e.model?L:e.vendors?.declared??L)},N=function(){return d(e=>e.vendorChoice)},j=function(e){return l((s,n)=>o(s,e,n))},q=function(){return h().set.registerConsentCategories},F=function(){return h().set.overrides},w=function(){let e=h(),s=i(f);return r(o=>{s?s.setLanguage(o):o!==e.getSnapshot().overrides.language&&(e.set.language(o),e.commands.init())},[e,s])},z=function(){let e=h();return r(s=>u(e,s),[e])},G=function(){return h().commands.save},J=function(){let e=h();return r(s=>e.events.on("permissions:changed",({snapshot:e})=>s(e.effectivePermissions)),[e])},K=function(){return h().commands.identify},Q=function(){return h().commands.init},W=function(){return d(e=>e.explicitChoice)},X=function(){return d(e=>e.effectivePermissions)},Y=function(){return d(e=>e.promptRequirement)},Z=function(){return d(e=>e.noticeDismissal)},$=function(){return d(e=>e.privacySignals)},ee=function(){return d(e=>e.resolution)},es=function(){return d(e=>e.policyRule)},eo=function(){return d(e=>e.restrictions)},en=function(){return h().commands.dismissNotice},et=function(){return d(e=>e.experiment)},eu=function(){let{experiment:s,presentation:o}=k(),n=et();return a(()=>e(o,s,n),[o,s,n])},er=function(){let{experiment:e,theme:o}=k(),n=et();return a(()=>s(o,e,n),[o,e,n])},ei={},ea={},ec=function(){return eu()?.prompt??ei},ef=function(){return eu()?.preferences??ea};export{x as useActiveUI,y as useBranding,_ as useConsent,C as useConsents,M as useDeclaredVendors,en as useDismissNotice,X as useEffectivePermissions,et as useExperiment,W as useExplicitChoice,I as useHasConsentPolicy,b as useHasConsentPreferences,R as useHasConsentUI,V as useIABEnabled,T as useIABSnapshot,K as useIdentify,Q as useInit,v as useLocation,A as useModel,Z as useNoticeDismissal,g as useOverrides,U as usePolicyCategories,ee as usePolicyResolution,es as usePolicyRule,E as usePolicyScopeMode,ef as usePreferencesPresentation,$ as usePrivacySignals,ec as usePromptPresentation,Y as usePromptRequirement,O as usePurposeConsent,q as useRegisterConsentCategories,eu as useResolvedPresentation,er as useResolvedTheme,eo as useRestrictions,G as useSaveConsents,z as useSetActiveUI,w as useSetLanguage,F as useSetOverrides,p as useSnapshot,B as useSpecialFeatureOptIn,J as useSubscribeToConsentChanges,H as useTCString,S as useTranslations,P as useUser,j as useVendorAllowed,N as useVendorChoice,D as useVendorConsent};
@@ -1 +1 @@
1
- import{evaluateConsent as e}from"@c15t/core";import{useContext as t,useSyncExternalStore as r}from"react";import{KernelContext as n}from"./context.js";import{defaultTranslationConfig as o}from"./utils/default-translation-config.js";let a=function(){let e=t(n);if(!e)throw Error("c15t: no kernel in context. Wrap your app with <ConsentProvider options={...}> from @c15t/react.");return e},s=function(e){let t=a();return r(e=>t.subscribe(e),()=>e(t.getSnapshot()),()=>e(t.getServerSnapshot()))},l=function(e){let t=a();return r(e=>t.subscribe(e),()=>e(t.getSnapshot(),Date.now()),()=>{let r=t.getServerSnapshot();return e(r,r.evaluatedAt)})},u=function(t){return l((r,n)=>e({category:t},r,n))},c=function(){return s(e=>e.translations?.language??o.defaultLanguage)};export{u as useCategoryAllowed,l as useGateSelector,a as useKernel,s as useKernelSelector,c as useTranslationLanguage};
1
+ import{evaluateConsent as e}from"@c15t/core";import{useContext as t,useSyncExternalStore as r}from"react";import{HydrationSnapshotContext as n,KernelContext as o}from"./context.js";import{defaultTranslationConfig as s}from"./utils/default-translation-config.js";let a=function(){let e=t(o);if(!e)throw Error("c15t: no kernel in context. Wrap your app with <ConsentProvider options={...}> from @c15t/react.");return e},l=function(e){let r=t(n);return()=>r??e?.getServerSnapshot()},u=function(e){let t=a(),n=l(t);return r(e=>t.subscribe(e),()=>e(t.getSnapshot()),()=>e(n()))},c=function(e){let t=a(),n=l(t);return r(e=>t.subscribe(e),()=>e(t.getSnapshot(),Date.now()),()=>{let t=n();return e(t,t.evaluatedAt)})},i=function(t){return c((r,n)=>e({category:t},r,n))},f=function(){return u(e=>e.translations?.language??s.defaultLanguage)};export{i as useCategoryAllowed,c as useGateSelector,a as useKernel,u as useKernelSelector,l as useServerSnapshot,f as useTranslationLanguage};
package/dist/provider.js CHANGED
@@ -1,2 +1,2 @@
1
1
  "use client";
2
- let e,t;import{jsx as o,jsxs as r}from"react/jsx-runtime";import{applyExperimentAssignment as n,applyExperimentTheme as i,hostExperiment as s,watchRevocationReload as c}from"@c15t/core";import{createPersistence as l,readStoredRecords as a}from"@c15t/core/modules/persistence";import{createWindowDebug as u}from"@c15t/core/modules/window-debug";import{claimEarlyJourney as d,createConsentProviderRuntime as m,hostedModes as p,lazyRuntimeModule as f,lazyStreamPrefetch as h,streamPrefetchWith as g}from"@c15t/core/runtime/provider";import{applyThemeSlots as y}from"@c15t/ui/utils";import{useCallback as v,useEffect as k,useLayoutEffect as S,useMemo as b,useReducer as w,useState as C,useSyncExternalStore as x}from"react";import{KernelContext as j,ProviderServicesContext as L}from"./context.js";import{ExternalIABProvider as N}from"./external-iab-context.js";import{useColorScheme as O}from"./hooks/use-color-scheme.js";import{useIsHydrated as D}from"./hooks/use-is-hydrated.js";import{createIframeBlockerOnDemand as P}from"./module-hooks/iframe-blocker.js";import{UNCOMMITTED_HOLD_MS as R}from"./module-hooks/network-hold.js";import{V3ThemeProvider as _}from"./theme-provider.js";let E=(e,t)=>{let o,r=!1;return(async()=>{try{let{connectConsentSource:n}=await import("@c15t/core/runtime/controls");r||(o=n(e,t))}catch{}})(),()=>{r=!0,o?.()}},T=async()=>(await import("@c15t/core/modules/script-loader")).createScriptLoader,q=function(e){let t=e.__resolveStreamedInit;return{connectConsentSource:E,createClearOnRevocation:f(async()=>(await import("@c15t/core/modules/clear-on-revocation")).createClearOnRevocation),createIframeBlocker:P,createNetworkBlocker:f(async()=>(await import("@c15t/core/modules/network-blocker")).createNetworkBlocker),createPersistence:l,createScriptLoader:f(T),createWindowDebug:u,preloadScriptLoader:e.__preloadScriptLoader?.(T),streamPrefetch:t?g(t):h,watchRevocationReload:c}},B=new WeakMap,J=new Set,K=(e,t)=>e.fetch===t.fetch&&e.initialData===t.initialData&&JSON.stringify(e)===JSON.stringify(t),A=function(o){if("function"!=typeof o)return o;let r=B.get(o);return r||(r=Object.assign(r=>e=(t=()=>o(Object.create(r,{iabEnabled:{value:void 0}})))(),{kind:o.kind}),B.set(o,r)),r},F=function(e){return{...e,mode:A(e.mode),pkg:e.__debugPkg??"@c15t/react"}},I=function(e){let{persistence:t}=e;if(!1===t||e.consentSource)return!1;let o="object"==typeof t?t:{};if(o.skipHydration)return!1;let r=o.now?o.now():Date.now(),{records:n}=a(o.storageConfig??e.storageConfig,r);return!!(n.choice||n.noticeDismissal)},M=function(e){let{journey:t}=e;return JSON.stringify({journey:t?[t.id,t.scope,t.storedChoice]:null,overrides:e.overrides,user:e.user})},V=new Set,W=0,z=function(o,r){let n,i,s,{mode:c,networkBlocker:l}=o,a=m(F(o),q(o)),u=o,f=!1,h=!1,g=0,y=e;W+=1;let v={expire(){J.delete(s),V.delete(v),clearTimeout(n),f=!0,a.dispose()},holdKey:!1!==o.enabled&&l&&!1!==l.enabled?JSON.stringify(l.rules):null,sequence:W};if("u"<typeof window)h=!0;else{V.add(v),n=setTimeout(v.expire,R);let e=a.kernel.getSnapshot(),l=p.get(c),u=l&&{...l,fetch:l.fetch??globalThis.fetch};if(r&&u&&e.policyPending&&!(o.prefetch||o.experiment)){let r=d({option:o.journey,storedChoice:I(o)}),n=r?{journey:r,overrides:e.overrides,user:e.user}:{overrides:e.overrides,user:e.user},c=M(n);for(let e of J)e.key===c&&K(e.options,u)&&(i=e);if(!i){let e=t(),o=e.init,r=o(n);r.catch(()=>void 0);let l=!1;e.init=e=>{let t=!l&&M(e)===c;return l=!0,t?r:o(e)},i=s={key:c,options:u,transport:e},J.add(s)}}}let k=function(){if(f)return!1;if(h)return!0;if(h=!0,J.delete(i)&&Object.assign(y,i?.transport),V.delete(v),clearTimeout(n),null!==v.holdKey)for(let e of V)e.sequence<v.sequence&&e.holdKey===v.holdKey&&e.expire();return!0};return{apply(e){e!==u&&(u=e,a.update(F(e)).catch(()=>void 0))},mount(){if(!k())return null;let e=g+=1;return a.start(),()=>{queueMicrotask(()=>{g===e&&a.dispose()})}},runtime:a}},H=function(){let e=null;return{get:(t,o)=>e??=z(t,o),reset(){e=null}}},G=e=>e+1,Q=()=>()=>void 0,U=function(e){let[t]=C(H),[,o]=w(G,0),r=D(),n=v(()=>{t.reset(),o()},[t]);return e?{entry:t.get(e,r),rebuild:n}:void 0},X="u"<typeof document?k:S,Y=({entry:e,options:t,rebuild:o})=>(X(()=>e.apply(t),[e,t]),k(()=>{let t=e.mount();return t||void o()},[e,o]),null),Z=e=>{let{children:t}=e,c=e.options??{},[l]=C(()=>e.runtime),a=U(l?void 0:c),u=a?.entry.runtime,d=l?e.runtime??l:void 0,m=u??d,p=x(u?u.subscribe:Q,()=>m.kernel,()=>m.kernel),[f]=C(()=>{var e;return l?l.experiment??s(c.experiment,(e=c.prefetch,"function"==typeof e?.then)?void 0:c.prefetch):void 0}),h=u?u.experiment:f,g=d?c.consentCategories:void 0;k(()=>{d&&void 0!==g&&d.setConsentCategories(g)},[d,g]);let{presentation:v}=c,S=b(()=>({clearRecords:()=>m.clearRecords(),getConsentCategories:()=>m.consentCategories,getPresentation:()=>n(v,h,p.getSnapshot().experiment),setLanguage:e=>m.setLanguage(e)}),[h,p,v,m]),w=!!d&&void 0!==c.callbacks;k(()=>{"production"!==process.env.NODE_ENV&&w&&console.warn("c15t ConsentProvider: `options.callbacks` is ignored when you pass `runtime`. Pass them to createConsentRuntime({ callbacks }) instead.")},[w]);let D=x(e=>p.subscribe(e),()=>p.getSnapshot().experiment,()=>p.getServerSnapshot().experiment),P=b(()=>i(c.theme,h,D),[c.theme,h,D]);k(()=>{"production"===process.env.NODE_ENV||!P||!["colors","dark","motion","radius","shadows","spacing","typography"].some(e=>e in P)||document.getElementById("c15t-theme")||console.warn("c15t: `theme` tokens are no longer turned into CSS in the browser. Render <ConsentTheme theme={theme} /> on the server, or put the CSS from generateThemeCSS() in your stylesheet. See https://c15t.com/docs/frameworks/react/customize")},[P]);let R=b(()=>({colorScheme:c.colorScheme,disableAnimation:c.disableAnimation,noStyle:c.noStyle,scrollLock:c.scrollLock,theme:P,trapFocus:c.trapFocus}),[P,c.noStyle,c.disableAnimation,c.scrollLock,c.trapFocus,c.colorScheme]),E=b(()=>({components:y(P?.slots,c.components,"className"),experiment:h,legalLinks:c.legalLinks,preloadDialog:c.preloadDialog,presentation:c.presentation,theme:c.theme}),[P?.slots,c.components,h,c.legalLinks,c.preloadDialog,c.presentation,c.theme]);return O(c.colorScheme),o(j.Provider,{value:p,children:o(L.Provider,{value:S,children:r(_,{themeConfig:R,uiConfig:E,children:[a?o(Y,{entry:a.entry,options:c,rebuild:a.rebuild}):null,d?o(N,{runtime:d,children:t}):t]})})})};export{Z as ConsentProvider};
2
+ let e,t;import{jsx as o,jsxs as r}from"react/jsx-runtime";import{applyExperimentAssignment as n,applyExperimentTheme as i,hostExperiment as s,watchRevocationReload as c}from"@c15t/core";import{createPersistence as l,readStoredRecords as a}from"@c15t/core/modules/persistence";import{createWindowDebug as u}from"@c15t/core/modules/window-debug";import{claimEarlyJourney as d,createConsentProviderRuntime as m,hostedModes as p,lazyRuntimeModule as f,lazyStreamPrefetch as h,streamPrefetchWith as g}from"@c15t/core/runtime/provider";import{applyThemeSlots as v}from"@c15t/ui/utils";import{useCallback as y,useEffect as k,useLayoutEffect as S,useMemo as b,useReducer as w,useState as C,useSyncExternalStore as x}from"react";import{KernelContext as j,ProviderServicesContext as L,StreamedPrefetchContext as N}from"./context.js";import{ExternalIABProvider as O}from"./external-iab-context.js";import{useColorScheme as P}from"./hooks/use-color-scheme.js";import{useIsHydrated as D}from"./hooks/use-is-hydrated.js";import{createIframeBlockerOnDemand as R}from"./module-hooks/iframe-blocker.js";import{UNCOMMITTED_HOLD_MS as _}from"./module-hooks/network-hold.js";import{V3ThemeProvider as E}from"./theme-provider.js";let T=(e,t)=>{let o,r=!1;return(async()=>{try{let{connectConsentSource:n}=await import("@c15t/core/runtime/controls");r||(o=n(e,t))}catch{}})(),()=>{r=!0,o?.()}},B=async()=>(await import("@c15t/core/modules/script-loader")).createScriptLoader,q=function(e){let t=e.__resolveStreamedInit;return{connectConsentSource:T,createClearOnRevocation:f(async()=>(await import("@c15t/core/modules/clear-on-revocation")).createClearOnRevocation),createIframeBlocker:R,createNetworkBlocker:f(async()=>(await import("@c15t/core/modules/network-blocker")).createNetworkBlocker),createPersistence:l,createScriptLoader:f(B),createWindowDebug:u,preloadScriptLoader:e.__preloadScriptLoader?.(B),streamPrefetch:t?g(t):h,watchRevocationReload:c}},J=new WeakMap,K=new Set,A=(e,t)=>e.fetch===t.fetch&&e.initialData===t.initialData&&JSON.stringify(e)===JSON.stringify(t),F=function(o){if("function"!=typeof o)return o;let r=J.get(o);return r||(r=Object.assign(r=>e=(t=()=>o(Object.create(r,{iabEnabled:{value:void 0}})))(),{kind:o.kind}),J.set(o,r)),r},I=function(e){return{...e,mode:F(e.mode),pkg:e.__debugPkg??"@c15t/react"}},M=function(e){let{persistence:t}=e;if(!1===t||e.consentSource)return!1;let o="object"==typeof t?t:{};if(o.skipHydration)return!1;let r=o.now?o.now():Date.now(),{records:n}=a(o.storageConfig??e.storageConfig,r);return!!(n.choice||n.noticeDismissal)},V=function(e){let{journey:t}=e;return JSON.stringify({journey:t?[t.id,t.scope,t.storedChoice]:null,overrides:e.overrides,user:e.user})},W=new Set,z=0,H=function(o,r){let n,i,s,{mode:c,networkBlocker:l}=o,a=m(I(o),q(o)),u=o,f=!1,h=!1,g=0,v=e;z+=1;let y={expire(){K.delete(s),W.delete(y),clearTimeout(n),f=!0,a.dispose()},holdKey:!1!==o.enabled&&l&&!1!==l.enabled?JSON.stringify(l.rules):null,sequence:z};if("u"<typeof window)h=!0;else{W.add(y),n=setTimeout(y.expire,_);let e=a.kernel.getSnapshot(),l=p.get(c),u=l&&{...l,fetch:l.fetch??globalThis.fetch};if(r&&u&&e.policyPending&&!(o.prefetch||o.experiment)){let r=d({option:o.journey,storedChoice:M(o)}),n=r?{journey:r,overrides:e.overrides,user:e.user}:{overrides:e.overrides,user:e.user},c=V(n);for(let e of K)e.key===c&&A(e.options,u)&&(i=e);if(!i){let e=t(),o=e.init,r=o(n);r.catch(()=>void 0);let l=!1;e.init=e=>{let t=!l&&V(e)===c;return l=!0,t?r:o(e)},i=s={key:c,options:u,transport:e},K.add(s)}}}let k=function(){if(f)return!1;if(h)return!0;if(h=!0,K.delete(i)&&Object.assign(v,i?.transport),W.delete(y),clearTimeout(n),null!==y.holdKey)for(let e of W)e.sequence<y.sequence&&e.holdKey===y.holdKey&&e.expire();return!0};return{apply(e){e!==u&&(u=e,a.update(I(e)).catch(()=>void 0))},mount(){if(!k())return null;let e=g+=1;return a.start(),()=>{queueMicrotask(()=>{g===e&&a.dispose()})}},runtime:a}},G=function(){let e=null;return{get:(t,o)=>e??=H(t,o),reset(){e=null}}},Q=e=>e+1,U=()=>()=>void 0,X=function(e){let[t]=C(G),[,o]=w(Q,0),r=D(),n=y(()=>{t.reset(),o()},[t]);return e?{entry:t.get(e,r),rebuild:n}:void 0},Y="u"<typeof document?k:S,Z=({entry:e,options:t,rebuild:o})=>(Y(()=>e.apply(t),[e,t]),k(()=>{let t=e.mount();return t||void o()},[e,o]),null),$=e=>{let{children:t}=e,c=e.options??{},[l]=C(()=>e.runtime),a=X(l?void 0:c),u=a?.entry.runtime,d=l?e.runtime??l:void 0,m=u??d,p=x(u?u.subscribe:U,()=>m.kernel,()=>m.kernel),[f]=C(()=>{var e;return l?l.experiment??s(c.experiment,(e=c.prefetch,"function"==typeof e?.then)?void 0:c.prefetch):void 0}),h=u?u.experiment:f,g=d?c.consentCategories:void 0;k(()=>{d&&void 0!==g&&d.setConsentCategories(g)},[d,g]);let{presentation:y}=c,S=b(()=>({clearRecords:()=>m.clearRecords(),getConsentCategories:()=>m.consentCategories,getPresentation:()=>n(y,h,p.getSnapshot().experiment),setLanguage:e=>m.setLanguage(e)}),[h,p,y,m]),w=!!d&&void 0!==c.callbacks;k(()=>{"production"!==process.env.NODE_ENV&&w&&console.warn("c15t ConsentProvider: `options.callbacks` is ignored when you pass `runtime`. Pass them to createConsentRuntime({ callbacks }) instead.")},[w]);let D=x(e=>p.subscribe(e),()=>p.getSnapshot().experiment,()=>p.getServerSnapshot().experiment),R=b(()=>i(c.theme,h,D),[c.theme,h,D]);k(()=>{"production"===process.env.NODE_ENV||!R||!["colors","dark","motion","radius","shadows","spacing","typography"].some(e=>e in R)||document.getElementById("c15t-theme")||console.warn("c15t: `theme` tokens are no longer turned into CSS in the browser. Render <ConsentTheme theme={theme} /> on the server, or put the CSS from generateThemeCSS() in your stylesheet. See https://c15t.com/docs/frameworks/react/customize")},[R]);let _=b(()=>({colorScheme:c.colorScheme,disableAnimation:c.disableAnimation,noStyle:c.noStyle,scrollLock:c.scrollLock,theme:R,trapFocus:c.trapFocus}),[R,c.noStyle,c.disableAnimation,c.scrollLock,c.trapFocus,c.colorScheme]),T=b(()=>({components:v(R?.slots,c.components,"className"),experiment:h,legalLinks:c.legalLinks,preloadDialog:c.preloadDialog,presentation:c.presentation,theme:c.theme}),[R?.slots,c.components,h,c.legalLinks,c.preloadDialog,c.presentation,c.theme]);return P(c.colorScheme),o(j.Provider,{value:p,children:o(L.Provider,{value:S,children:r(E,{themeConfig:_,uiConfig:T,children:[a?o(Z,{entry:a.entry,options:c,rebuild:a.rebuild}):null,d?o(O,{runtime:d,children:t}):o(N.Provider,{value:!1===c.streamBanner?void 0:u?.streamed,children:t})]})})})};export{$ as ConsentProvider};
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- let e="3.0.0-alpha.7";export{e as version};
1
+ let e="3.0.0-alpha.8";export{e as version};
@@ -73,6 +73,10 @@ interface ConsentBannerRootProps extends HTMLAttributes<HTMLDivElement> {
73
73
  */
74
74
  blocking?: boolean;
75
75
  }
76
+ /**
77
+ * The banner, rendered where the server can stream it: see
78
+ * `StreamedSurface`.
79
+ */
76
80
  declare const ConsentBannerRoot: FC<ConsentBannerRootProps>;
77
81
  declare const Root: FC<ConsentBannerRootProps>;
78
82
  export { ConsentBannerRoot, Root };
@@ -162,6 +162,10 @@ export interface ConsentBannerProps {
162
162
  */
163
163
  blocking?: boolean;
164
164
  }
165
+ /**
166
+ * The stock banner. It reads the policy to choose its buttons, so all of
167
+ * it renders where the server can stream it: see `StreamedSurface`.
168
+ */
165
169
  export declare const ConsentBanner: FC<ConsentBannerProps>;
166
170
  /**
167
171
  * Component type definition for the ConsentBanner with its compound components.
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+ import type { ReactNode } from 'react';
3
+ /**
4
+ * React 18's way to suspend: throw the promise, and read its recorded
5
+ * result on the retry. The runtime's `settled` records its own result.
6
+ * Exported for tests.
7
+ *
8
+ * @internal
9
+ */
10
+ export declare const readRecorded: <Value>(thenable: PromiseLike<Value>) => Value;
11
+ /**
12
+ * Renders a consent surface the server can stream. While the provider's
13
+ * `prefetch` is a promise, the surface waits for it in its own Suspense
14
+ * boundary: the rest of the page streams at once, and the surface follows
15
+ * in a later chunk of the same response, rendered from the resolved
16
+ * config. It shows only when that config carries a policy that shows it.
17
+ * Hydration reads the same config, so the surface hydrates in place.
18
+ *
19
+ * @internal
20
+ */
21
+ export declare const StreamedSurface: ({ children }: {
22
+ children: ReactNode;
23
+ }) => string | number | bigint | boolean | React.JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | React.ReactPortal | null | undefined> | null | undefined;
@@ -1,4 +1,5 @@
1
- import type { AllConsentNames, ConsentKernel, ConsentPresentation } from '@c15t/core';
1
+ import type { AllConsentNames, ConsentKernel, ConsentPresentation, ConsentSnapshot } from '@c15t/core';
2
+ import type { StreamedPrefetch } from '@c15t/core/runtime';
2
3
  /**
3
4
  * React context carrying the kernel instance.
4
5
  *
@@ -21,3 +22,18 @@ export interface ProviderServices {
21
22
  setLanguage: (code: string) => void;
22
23
  }
23
24
  export declare const ProviderServicesContext: import("react").Context<ProviderServices | null>;
25
+ /**
26
+ * The provider runtime's streamed `prefetch`, while `prefetch` is a promise.
27
+ * Consent surfaces render inside a boundary that waits for it.
28
+ *
29
+ * @internal
30
+ */
31
+ export declare const StreamedPrefetchContext: import("react").Context<StreamedPrefetch | undefined>;
32
+ /**
33
+ * The snapshot a streamed consent surface was server-rendered from, which
34
+ * its hydration must read in place of the kernel's own server snapshot.
35
+ * `null` everywhere else.
36
+ *
37
+ * @internal
38
+ */
39
+ export declare const HydrationSnapshotContext: import("react").Context<ConsentSnapshot | null>;
@@ -16,6 +16,16 @@ import type { AllConsentNames, ConsentKernel, ConsentSnapshot } from '@c15t/core
16
16
  * @internal
17
17
  */
18
18
  export declare const useKernel: () => ConsentKernel;
19
+ /**
20
+ * Reads the snapshot hydration renders: the kernel's server snapshot, or,
21
+ * inside a streamed consent surface, the snapshot the server rendered that
22
+ * surface from.
23
+ *
24
+ * @param kernel - The provider's kernel.
25
+ * @returns A `getServerSnapshot` for `useSyncExternalStore`.
26
+ * @internal
27
+ */
28
+ export declare const useServerSnapshot: (kernel: ConsentKernel | null) => () => ConsentSnapshot | undefined;
19
29
  /**
20
30
  * Subscribes to one slice of the kernel snapshot. React re-renders the
21
31
  * caller only when the selected value `Object.is`-differs, so a selector
@@ -85,9 +85,12 @@ export interface ConsentProviderOptions extends ConsentControlOptions, Pick<Reac
85
85
  * Pass the pending `Promise<KernelConfig>` instead and the provider
86
86
  * mounts at once with a provisional policy, so `children` render (and
87
87
  * the static shell can prerender) while the consent data streams in.
88
- * Consent surfaces stay hidden until the promise resolves; its first
89
- * `init()` then applies the resolved config in place of the transport's
90
- * network init. A config that resolves without a policy (persisted
88
+ * Consent surfaces stay hidden until the promise resolves. On the
89
+ * server, `ConsentBanner` waits for it in its own Suspense boundary and
90
+ * follows the page in a later chunk of the same response, before
91
+ * hydration, when the resolved policy shows a banner; see
92
+ * `streamBanner`. The resolved config then answers the first `init()`
93
+ * in place of the transport's network init. A config that resolves without a policy (persisted
91
94
  * consents, geo, language only) is applied to the kernel and the
92
95
  * transport init runs as usual; a rejected promise is logged outside
93
96
  * production and falls through to the transport init. Initial-only:
@@ -105,6 +108,17 @@ export interface ConsentProviderOptions extends ConsentControlOptions, Pick<Reac
105
108
  * ```
106
109
  */
107
110
  prefetch?: ConsentProviderPrefetch | Promise<ConsentProviderPrefetch>;
111
+ /**
112
+ * Whether `ConsentBanner` renders on the server once a `prefetch`
113
+ * promise resolves, so it arrives in the streamed response before the
114
+ * page hydrates. It shows only when the resolved policy shows a banner
115
+ * for this visitor. Set `false` to mount the banner after hydration
116
+ * instead, as with a client-only provider. No effect when `prefetch` is
117
+ * not a promise. Use the same value on the server and in the browser.
118
+ *
119
+ * @default true
120
+ */
121
+ streamBanner?: boolean;
108
122
  callbacks?: ConsentProviderCallbacks;
109
123
  /**
110
124
  * Remove configured browser data when its consent permission is revoked.
@@ -1 +1 @@
1
- export declare const version = "3.0.0-alpha.7";
1
+ export declare const version = "3.0.0-alpha.8";
@@ -13,7 +13,7 @@ to the alternatives it rules out.
13
13
  | Your app | Setup | Guide |
14
14
  | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
15
15
  | Next.js App Router with a Next.js server | Server resolves consent from a build-time manifest and streams it to the page | [App Router](https://c15t.com/docs/frameworks/next/quickstart) |
16
- | Next.js App Router, banner must be in the first HTML | Await consent inside `Suspense` | [Stream or await](https://c15t.com/docs/frameworks/next/rendering#stream-or-await-consent-in-the-app-router) |
16
+ | Next.js App Router, page must arrive with its banner | Await consent inside `Suspense` | [Stream or await](https://c15t.com/docs/frameworks/next/rendering#stream-or-await-consent-in-the-app-router) |
17
17
  | Next.js with static, ISR, `'use cache'` or `ensureStatic = 'navigation'` pages | Browser resolves consent on those pages | [Static, ISR and cached pages](https://c15t.com/docs/frameworks/next/rendering#render-static-isr-and-cached-pages) |
18
18
  | Next.js Pages Router | Resolve consent in `getServerSideProps` | [Pages Router](https://c15t.com/docs/frameworks/next/rendering#pick-your-rendering-path) |
19
19
  | Next.js with `output: 'export'` | Browser resolves consent | [Static export](https://c15t.com/docs/frameworks/next/static-export) |
@@ -12,20 +12,25 @@ The [quickstart](../quickstart.md) mounts both in
12
12
  `src/consent.tsx`:
13
13
 
14
14
  ```tsx title="src/consent.tsx"
15
+ import { manifest } from '@c15t/browser/headless';
15
16
  import {
16
17
  ConsentBanner,
17
18
  ConsentDialog,
18
19
  ConsentDialogLink,
19
20
  ConsentProvider,
20
- hosted,
21
21
  } from 'c15t/react';
22
22
  import type { ReactNode } from 'react';
23
23
 
24
+ import { consentManifest } from './c15t-manifest';
24
25
  import { scripts } from './scripts';
25
26
 
26
27
  import 'c15t/react/styles.css';
27
28
 
28
- const mode = hosted({ url: 'https://your-project.inth.app' });
29
+ const mode = manifest({
30
+ backendURL:
31
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
32
+ manifest: consentManifest,
33
+ });
29
34
 
30
35
  export const Consent = ({ children }: { children: ReactNode }) => (
31
36
  <ConsentProvider options={{ mode, scripts }}>
@@ -14,20 +14,25 @@ group: frameworks
14
14
  code.
15
15
 
16
16
  ```tsx title="src/consent.tsx"
17
+ import { manifest } from '@c15t/browser/headless';
17
18
  import {
18
19
  ConsentBanner,
19
20
  ConsentDialog,
20
21
  ConsentDialogLink,
21
22
  ConsentProvider,
22
- hosted,
23
23
  } from 'c15t/react';
24
24
  import type { ReactNode } from 'react';
25
25
 
26
+ import { consentManifest } from './c15t-manifest';
26
27
  import { scripts } from './scripts';
27
28
 
28
29
  import 'c15t/react/styles.css';
29
30
 
30
- const mode = hosted({ url: 'https://your-project.inth.app' });
31
+ const mode = manifest({
32
+ backendURL:
33
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
34
+ manifest: consentManifest,
35
+ });
31
36
 
32
37
  export const Consent = ({ children }: { children: ReactNode }) => (
33
38
  <ConsentProvider options={{ mode, scripts }}>
@@ -14,20 +14,25 @@ The [quickstart](../quickstart.md) does this in
14
14
  `src/consent.tsx`:
15
15
 
16
16
  ```tsx title="src/consent.tsx"
17
+ import { manifest } from '@c15t/browser/headless';
17
18
  import {
18
19
  ConsentBanner,
19
20
  ConsentDialog,
20
21
  ConsentDialogLink,
21
22
  ConsentProvider,
22
- hosted,
23
23
  } from 'c15t/react';
24
24
  import type { ReactNode } from 'react';
25
25
 
26
+ import { consentManifest } from './c15t-manifest';
26
27
  import { scripts } from './scripts';
27
28
 
28
29
  import 'c15t/react/styles.css';
29
30
 
30
- const mode = hosted({ url: 'https://your-project.inth.app' });
31
+ const mode = manifest({
32
+ backendURL:
33
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
34
+ manifest: consentManifest,
35
+ });
31
36
 
32
37
  export const Consent = ({ children }: { children: ReactNode }) => (
33
38
  <ConsentProvider options={{ mode, scripts }}>
@@ -41,9 +46,11 @@ export const Consent = ({ children }: { children: ReactNode }) => (
41
46
  );
42
47
  ```
43
48
 
44
- `hosted({ url })` points the provider at your Inth backend. Replace
45
- `https://your-project.inth.app` with the URL from your Inth project. The
46
- Next.js and TanStack Start adapters export `ConsentRoot` instead, which wraps
49
+ `manifest()` from `@c15t/browser/headless` resolves the policy from the
50
+ snapshot the build bundled, and sends consent choices to `backendURL`. Replace
51
+ the demo project URL with the URL from your Inth project, or set
52
+ `VITE_C15T_BACKEND_URL`. To fetch the policy from the backend on every page
53
+ load instead, pass `hosted({ url })` from `c15t/react`. The Next.js and TanStack Start adapters export `ConsentRoot` instead, which wraps
47
54
  this provider and passes it server-resolved state.
48
55
 
49
56
  ## Provider options
@@ -56,35 +63,35 @@ the provider to change them. `scripts`, `vendors`, `networkBlocker`,
56
63
  `iframeBlocker`, `consentCategories`, `overrides`, `user`, `callbacks` and
57
64
  `enabled` apply when they change.
58
65
 
59
- | Option | Purpose |
60
- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
61
- | `mode` | Required. `hosted({ url })` for a backend, `offline()` for local policies, or `custom()` for your own transport |
62
- | `prefetch` | Server-resolved state, or a promise of it, from a framework adapter's server helper |
63
- | `scripts` | Consent-gated vendor scripts. See [scripts and embeds](../scripts.md) |
64
- | `vendors` | Vendors listed under their category with their own switch. See [vendor consent](../vendor-consent.md) |
65
- | `scriptLoader` | Options for the script loader module |
66
- | `networkBlocker` | Rules that hold `fetch` and `XMLHttpRequest` calls until their category is allowed, or `false` |
67
- | `iframeBlocker` | Gates DOM iframes that carry `data-category`. On by default; `false` turns it off |
68
- | `persistence` | Stores the visitor's choice in a cookie and localStorage. On by default |
69
- | `storageConfig` | Storage key and cookie settings for the stored choice |
70
- | `clearOnRevocation` | Cookies and storage keys to delete when a category is denied |
71
- | `reloadOnConsentRevoked` | Reload the page after the visitor turns off a granted category or vendor. Defaults to `true` |
72
- | `consentCategories` | Categories the preference dialog offers alongside those your scripts use, within the policy's scope |
73
- | `overrides` | Force the country, region or language the policy is resolved for, for testing |
74
- | `user` | Link consent records to a signed-in user |
75
- | `callbacks` | Choice, permission and error events |
76
- | `i18n` | Languages and message overrides |
77
- | `legalLinks` | Privacy policy and other links shown in the banner and dialog |
78
- | `theme` | `consentActions` and slot styles. Tokens go through `ConsentTheme`; see [customize](../customize.md) |
79
- | `components` | Slot attributes per component part |
80
- | `presentation` | Banner and dialog shape, position and blocking |
81
- | `experiment` | A/B test of `presentation`, read once at mount. See [banner experiments](../../../guides/banner-experiments.md) |
82
- | `journey` | Random id that links each `/init` to the save that follows. See [session reports](../../../concepts/data-fetching.md#link-an-init-to-the-save-that-follows) |
83
- | `colorScheme` | `'light'`, `'dark'`, `'system'`, or `null` to leave the `c15t-dark` class alone |
84
- | `noStyle`, `disableAnimation` | Drop the built-in styles, or turn off animations |
85
- | `preloadDialog` | When the deferred `ConsentDialog` starts loading: `'idle'` (default) or `'intent'`; see [ConsentDialog](./consent-dialog.md#behavior) |
86
- | `nonce` | Content Security Policy nonce for script elements c15t inserts |
87
- | `enabled` | Set `false` only when you deliberately bypass consent enforcement |
66
+ | Option | Purpose |
67
+ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `mode` | Required. `hosted({ url })` for a backend, `manifest()` from `@c15t/browser/headless` for a bundled policy, `offline()` for local policies, or `custom()` for your own transport |
69
+ | `prefetch` | Server-resolved state, or a promise of it, from a framework adapter's server helper |
70
+ | `scripts` | Consent-gated vendor scripts. See [scripts and embeds](../scripts.md) |
71
+ | `vendors` | Vendors listed under their category with their own switch. See [vendor consent](../vendor-consent.md) |
72
+ | `scriptLoader` | Options for the script loader module |
73
+ | `networkBlocker` | Rules that hold `fetch` and `XMLHttpRequest` calls until their category is allowed, or `false` |
74
+ | `iframeBlocker` | Gates DOM iframes that carry `data-category`. On by default; `false` turns it off |
75
+ | `persistence` | Stores the visitor's choice in a cookie and localStorage. On by default |
76
+ | `storageConfig` | Storage key and cookie settings for the stored choice |
77
+ | `clearOnRevocation` | Cookies and storage keys to delete when a category is denied |
78
+ | `reloadOnConsentRevoked` | Reload the page after the visitor turns off a granted category or vendor. Defaults to `true` |
79
+ | `consentCategories` | Categories the preference dialog offers alongside those your scripts use, within the policy's scope |
80
+ | `overrides` | Force the country, region or language the policy is resolved for, for testing |
81
+ | `user` | Link consent records to a signed-in user |
82
+ | `callbacks` | Choice, permission and error events |
83
+ | `i18n` | Languages and message overrides |
84
+ | `legalLinks` | Privacy policy and other links shown in the banner and dialog |
85
+ | `theme` | `consentActions` and slot styles. Tokens go through `ConsentTheme`; see [customize](../customize.md) |
86
+ | `components` | Slot attributes per component part |
87
+ | `presentation` | Banner and dialog shape, position and blocking |
88
+ | `experiment` | A/B test of `presentation`, read once at mount. See [banner experiments](../../../guides/banner-experiments.md) |
89
+ | `journey` | Random id that links each `/init` to the save that follows. See [session reports](../../../concepts/data-fetching.md#link-an-init-to-the-save-that-follows) |
90
+ | `colorScheme` | `'light'`, `'dark'`, `'system'`, or `null` to leave the `c15t-dark` class alone |
91
+ | `noStyle`, `disableAnimation` | Drop the built-in styles, or turn off animations |
92
+ | `preloadDialog` | When the deferred `ConsentDialog` starts loading: `'idle'` (default) or `'intent'`; see [ConsentDialog](./consent-dialog.md#behavior) |
93
+ | `nonce` | Content Security Policy nonce for script elements c15t inserts |
94
+ | `enabled` | Set `false` only when you deliberately bypass consent enforcement |
88
95
 
89
96
  `enabled: false` grants categories and allows gated loading while suppressing
90
97
  consent UI. It is not a way to fix failed initialization or hide a banner in
@@ -36,20 +36,25 @@ Most apps start with four components. `ConsentProvider` holds the runtime,
36
36
  one file:
37
37
 
38
38
  ```tsx title="src/consent.tsx"
39
+ import { manifest } from '@c15t/browser/headless';
39
40
  import {
40
41
  ConsentBanner,
41
42
  ConsentDialog,
42
43
  ConsentDialogLink,
43
44
  ConsentProvider,
44
- hosted,
45
45
  } from 'c15t/react';
46
46
  import type { ReactNode } from 'react';
47
47
 
48
+ import { consentManifest } from './c15t-manifest';
48
49
  import { scripts } from './scripts';
49
50
 
50
51
  import 'c15t/react/styles.css';
51
52
 
52
- const mode = hosted({ url: 'https://your-project.inth.app' });
53
+ const mode = manifest({
54
+ backendURL:
55
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
56
+ manifest: consentManifest,
57
+ });
53
58
 
54
59
  export const Consent = ({ children }: { children: ReactNode }) => (
55
60
  <ConsentProvider options={{ mode, scripts }}>
@@ -7,10 +7,11 @@ group: frameworks
7
7
 
8
8
  ## Before you start
9
9
 
10
- This guide sets up a client-rendered React app built with Vite. The browser
11
- loads the page, resolves the visitor's policy, then shows the banner or runs
12
- the scripts the visitor allowed. The build-time step bundles the public
13
- policy; regional policies may still need a backend location request.
10
+ This guide sets up a client-rendered React app built with Vite. The build
11
+ bundles your project's public policy. The browser loads the page, resolves the
12
+ visitor's policy from that bundle, then shows the banner or runs the scripts
13
+ the visitor allowed. Regional policies may still need a backend location
14
+ request.
14
15
 
15
16
  * **Next.js or TanStack Start?** Use that framework's guide from the
16
17
  [framework list](https://c15t.com/docs/frameworks). Their adapters resolve consent on the
@@ -27,21 +28,65 @@ the backend URL.
27
28
  [Choose your setup](../../concepts/choose-your-setup.md#decide-who-runs-the-consent-backend)
28
29
  covers self-hosting and offline mode.
29
30
 
30
- Build-time manifests are the recommended production setup. Complete the
31
- [build-time manifest step](#bundle-the-manifest-during-builds) before
32
- deploying, and rebuild after changing policies, translations or vendors.
31
+ Bundling the policy at build time is the recommended production setup.
32
+ Rebuild after changing policies, translations or vendors.
33
33
 
34
34
  ## Install c15t
35
35
 
36
- | Package manager | Command |
37
- | :-------------- | :------------------------------------------------ |
38
- | npm | `npm install c15t@alpha @c15t/integrations@alpha` |
39
- | pnpm | `pnpm add c15t@alpha @c15t/integrations@alpha` |
40
- | yarn | `yarn add c15t@alpha @c15t/integrations@alpha` |
41
- | bun | `bun add c15t@alpha @c15t/integrations@alpha` |
36
+ | Package manager | Command |
37
+ | :-------------- | :-------------------------------------------------------------------- |
38
+ | npm | `npm install c15t@alpha @c15t/browser@alpha @c15t/integrations@alpha` |
39
+ | pnpm | `pnpm add c15t@alpha @c15t/browser@alpha @c15t/integrations@alpha` |
40
+ | yarn | `yarn add c15t@alpha @c15t/browser@alpha @c15t/integrations@alpha` |
41
+ | bun | `bun add c15t@alpha @c15t/browser@alpha @c15t/integrations@alpha` |
42
42
 
43
- `c15t` contains the React adapter at `c15t/react`. `@c15t/integrations` holds the
44
- vendor loaders used below.
43
+ `c15t` contains the React adapter at `c15t/react` and the Vite plugin at
44
+ `c15t/build`. `@c15t/browser` resolves the bundled policy in the browser.
45
+ `@c15t/integrations` holds the vendor loaders used below.
46
+
47
+ ## Bundle your policy
48
+
49
+ Add `consentManifest` to `vite.config.ts`:
50
+
51
+ ```ts title="vite.config.ts"
52
+ import react from '@vitejs/plugin-react';
53
+ import { consentManifest } from 'c15t/build';
54
+ import { defineConfig, loadEnv } from 'vite';
55
+
56
+ export default defineConfig(({ mode }) => {
57
+ const env = loadEnv(mode, process.cwd(), 'VITE_');
58
+ return {
59
+ plugins: [
60
+ react(),
61
+ // Downloads the policy and writes src/c15t-manifest.ts.
62
+ consentManifest({
63
+ backendURL:
64
+ env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
65
+ }),
66
+ ],
67
+ };
68
+ });
69
+ ```
70
+
71
+ `https://benchmarks-inth.inth.app` is a demo project, so the snippet runs as
72
+ is. Replace it with your project's backend URL, exactly as Inth shows it,
73
+ including any path prefix, or set `VITE_C15T_BACKEND_URL` when you build.
74
+
75
+ `consentManifest` downloads your project's public policy from `/manifest` when
76
+ `vite build` or `vite dev` starts, and writes it to `src/c15t-manifest.ts`.
77
+ Add that file to `.gitignore`.
78
+
79
+ The snapshot is fixed at build time:
80
+
81
+ * Rebuild after changing policies, translations or vendors. If your CI caches
82
+ build output, force a fresh build.
83
+ * The build must reach the backend. A failed fetch fails the build.
84
+ * Consent choices still go to the backend.
85
+
86
+ Run `vite build` or `vite dev` before a standalone `tsc`, `vue-tsc` or
87
+ `svelte-check`; the generated file doesn't exist on a fresh checkout. Both
88
+ commands fetch the manifest when they start, so both need the backend.
89
+ `vite preview` serves the last build without fetching.
45
90
 
46
91
  ## Register your vendor scripts
47
92
 
@@ -77,20 +122,25 @@ asks for them, and `ConsentDialogLink` keeps preferences reachable after the
77
122
  banner closes.
78
123
 
79
124
  ```tsx title="src/consent.tsx"
125
+ import { manifest } from '@c15t/browser/headless';
80
126
  import {
81
127
  ConsentBanner,
82
128
  ConsentDialog,
83
129
  ConsentDialogLink,
84
130
  ConsentProvider,
85
- hosted,
86
131
  } from 'c15t/react';
87
132
  import type { ReactNode } from 'react';
88
133
 
134
+ import { consentManifest } from './c15t-manifest';
89
135
  import { scripts } from './scripts';
90
136
 
91
137
  import 'c15t/react/styles.css';
92
138
 
93
- const mode = hosted({ url: 'https://your-project.inth.app' });
139
+ const mode = manifest({
140
+ backendURL:
141
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
142
+ manifest: consentManifest,
143
+ });
94
144
 
95
145
  export const Consent = ({ children }: { children: ReactNode }) => (
96
146
  <ConsentProvider options={{ mode, scripts }}>
@@ -104,8 +154,17 @@ export const Consent = ({ children }: { children: ReactNode }) => (
104
154
  );
105
155
  ```
106
156
 
107
- Replace `https://your-project.inth.app` with your project's backend URL,
108
- exactly as Inth shows it, including any path prefix.
157
+ `manifest()` resolves the policy from the bundled snapshot in the browser,
158
+ with no `/manifest` request. Use the same backend URL as in
159
+ `vite.config.ts`: consent choices go to it, and so does an `/init` request
160
+ when the policy depends on the visitor's country or region and the browser
161
+ does not know it. That request returns the backend's current policy, which can
162
+ be newer than the snapshot. Vite reads `VITE_C15T_BACKEND_URL` at build time
163
+ for both files, so set it before you build, not when you serve the files.
164
+
165
+ To apply policy edits without rebuilding, remove `consentManifest` and use
166
+ `hosted({ url })` from `c15t/react` as the mode. The browser then requests the
167
+ policy from `/init` on every page load.
109
168
 
110
169
  Wrap your app with it in `src/main.tsx`:
111
170
 
@@ -131,66 +190,6 @@ router, render the router inside `Consent` so navigation does not remount the
131
190
  provider. The stylesheet import in `src/consent.tsx` styles the banner and
132
191
  dialog; import it once.
133
192
 
134
- ## Bundle the manifest during builds
135
-
136
- The build fetches your public policy once and bundles it, so the server never
137
- fetches it at runtime.
138
-
139
- Add `consentManifest` to your existing Vite plugin list:
140
-
141
- ```ts title="vite.config.ts (plugins option)"
142
- import { consentManifest } from 'c15t/build';
143
-
144
- export default {
145
- plugins: [
146
- // Keep your existing framework plugins here.
147
- consentManifest({ backendURL: 'https://your-project.inth.app' }),
148
- ],
149
- };
150
- ```
151
-
152
- Vite writes `src/c15t-manifest.ts` before compiling, including at development
153
- startup. Keep the generated file out of Git. Install `@c15t/browser@alpha` for
154
- its browser manifest transport. In the quickstart's `src/consent.tsx`, replace
155
- the `hosted()` mode with this factory:
156
-
157
- ```tsx title="src/consent.tsx"
158
- import { manifest } from '@c15t/browser/headless';
159
-
160
- import { consentManifest } from './c15t-manifest';
161
-
162
- const mode = manifest({
163
- backendURL: 'https://your-project.inth.app',
164
- manifest: consentManifest,
165
- });
166
- ```
167
-
168
- Keep passing `mode` to `ConsentProvider`. The browser resolves the snapshot
169
- without `/manifest` when it has the geography the policy needs, or the policy
170
- does not depend on geography. Supply known country and region through `inputs`
171
- or overrides to keep regional resolution on the snapshot.
172
-
173
- When the required country or region is missing, the transport calls backend
174
- `/init` and uses its resolved policy and translations. That response can use
175
- newer backend policy than the build snapshot. This is a full consent resolution,
176
- so these visitors still wait for a backend round trip.
177
-
178
- Policy and resolver code are part of the browser bundle.
179
-
180
- The snapshot is fixed at build time:
181
-
182
- * Rebuild after changing policies, translations or vendors. If your CI caches
183
- build output, force a fresh build.
184
- * The build must reach the backend. A failed fetch fails the build.
185
- * Consent choices still go to the backend.
186
-
187
- Run `vite build` or `vite dev` before a standalone `tsc`, `vue-tsc` or
188
- `svelte-check`; the generated file doesn't exist on a fresh checkout.
189
-
190
- For policy updates without a rebuild, keep the original `hosted()` factory
191
- and omit the build plugin. That setup resolves policy through the backend
192
- `/init` on each page load.
193
-
194
193
  ## Gate your own features
195
194
 
196
195
  Read a category's permission with `useConsent` inside the provider:
@@ -215,8 +214,8 @@ Build and preview the production bundle with `vite build` and `vite preview`,
215
214
  then open the site in a private window with DevTools open on the Network tab.
216
215
  Test under a policy that asks for a choice, such as an EU opt-in policy.
217
216
 
218
- 1. **Before a choice.** The banner shows. There are no requests to
219
- `posthog.com`.
217
+ 1. **Before a choice.** The banner shows, with no `/manifest` request. There
218
+ are no requests to `posthog.com`.
220
219
  2. **Reject All.** The banner closes and the vendor requests stay absent.
221
220
  3. **Reload.** The banner stays closed and the vendor requests stay absent.
222
221
  4. **Privacy settings.** The footer link opens the dialog with **Analytics** (the
@@ -25,6 +25,9 @@ explains the difference in general terms.
25
25
 
26
26
  ## Bundle the manifest during Vite builds
27
27
 
28
+ The [quickstart](./quickstart.md) and `examples/react` in
29
+ the c15t repository use this setup.
30
+
28
31
  The build fetches your public policy once and bundles it, so the server never
29
32
  fetches it at runtime.
30
33
 
@@ -43,8 +46,8 @@ export default {
43
46
 
44
47
  Vite writes `src/c15t-manifest.ts` before compiling, including at development
45
48
  startup. Keep the generated file out of Git. Install `@c15t/browser@alpha` for
46
- its browser manifest transport. In the quickstart's `src/consent.tsx`, replace
47
- the `hosted()` mode with this factory:
49
+ its browser manifest transport. In `src/consent.tsx`, create the provider's
50
+ mode with `manifest()` instead of `hosted()`:
48
51
 
49
52
  ```tsx title="src/consent.tsx"
50
53
  import { manifest } from '@c15t/browser/headless';
@@ -77,19 +80,21 @@ The snapshot is fixed at build time:
77
80
  * Consent choices still go to the backend.
78
81
 
79
82
  Run `vite build` or `vite dev` before a standalone `tsc`, `vue-tsc` or
80
- `svelte-check`; the generated file doesn't exist on a fresh checkout.
83
+ `svelte-check`; the generated file doesn't exist on a fresh checkout. Both
84
+ commands fetch the manifest when they start, so both need the backend.
85
+ `vite preview` serves the last build without fetching.
81
86
 
82
- For policy updates without a rebuild, keep the original `hosted()` factory
83
- and omit the build plugin. That setup resolves policy through the backend
84
- `/init` on each page load.
87
+ For policy updates without a rebuild, use `hosted({ url })` from `c15t/react`
88
+ as the mode and omit the build plugin. That setup resolves policy through the
89
+ backend `/init` on each page load.
85
90
 
86
91
  ## Client-rendered apps
87
92
 
88
93
  The recommended [quickstart](./quickstart.md) bundles the
89
94
  manifest and resolves it in the browser. See [build-time manifests](#bundle-the-manifest-during-vite-builds).
90
95
 
91
- For policy updates without rebuilding, keep `hosted({ url })`. That runtime
92
- path works as follows:
96
+ For policy updates without rebuilding, use `hosted({ url })` from
97
+ `c15t/react` as the mode. That runtime path works as follows:
93
98
 
94
99
  1. The page loads with no consent state. Every optional category is denied.
95
100
  2. `ConsentProvider` requests `GET /init` from your backend URL while it first
@@ -182,7 +187,12 @@ With `hosted({ url })`, the browser calls two backend endpoints:
182
187
  | `GET {url}/init` | Once per page load, to resolve the policy |
183
188
  | `POST {url}/subjects` | When the visitor accepts, rejects or saves |
184
189
 
185
- Both go to the backend's origin, so it must list your site as a trusted origin.
190
+ With a bundled manifest, `manifest({ backendURL })` sends the same
191
+ `/subjects` request, and requests `/init` only when the policy depends on a
192
+ country or region the browser does not know.
193
+
194
+ These requests go to the backend's origin, so it must list your site as a
195
+ trusted origin.
186
196
  [Data fetching](../../concepts/data-fetching.md) compares this with proxies and
187
197
  custom transports.
188
198
 
@@ -28,20 +28,25 @@ export const scripts = [
28
28
  The provider in `src/consent.tsx` receives them:
29
29
 
30
30
  ```tsx title="src/consent.tsx"
31
+ import { manifest } from '@c15t/browser/headless';
31
32
  import {
32
33
  ConsentBanner,
33
34
  ConsentDialog,
34
35
  ConsentDialogLink,
35
36
  ConsentProvider,
36
- hosted,
37
37
  } from 'c15t/react';
38
38
  import type { ReactNode } from 'react';
39
39
 
40
+ import { consentManifest } from './c15t-manifest';
40
41
  import { scripts } from './scripts';
41
42
 
42
43
  import 'c15t/react/styles.css';
43
44
 
44
- const mode = hosted({ url: 'https://your-project.inth.app' });
45
+ const mode = manifest({
46
+ backendURL:
47
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
48
+ manifest: consentManifest,
49
+ });
45
50
 
46
51
  export const Consent = ({ children }: { children: ReactNode }) => (
47
52
  <ConsentProvider options={{ mode, scripts }}>
@@ -5,25 +5,40 @@ description: Fix a missing banner, early vendor requests, unstyled components
5
5
  group: frameworks
6
6
  ---
7
7
 
8
- ## The /init request goes to your-project.inth.app
8
+ ## Why does the build fail to download the manifest?
9
9
 
10
- **Check.** In DevTools Network, the `GET` request to `/init` goes to
11
- `your-project.inth.app` and fails.
10
+ **Check.** `vite build` or `vite dev` stops before compiling, with an error
11
+ from `consentManifest` that starts with `@c15t/core/build:`.
12
12
 
13
- **Fix.** Replace the placeholder URL in `hosted({ url })` in `src/consent.tsx`
14
- with the backend URL from your Inth project, exactly as Inth shows it, then
15
- rebuild.
13
+ | Error | Cause | Fix |
14
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
15
+ | `build-time manifests require an absolute upstream URL.` | `backendURL` is empty or relative, such as `/api/c15t` | Pass the absolute backend URL from your Inth project |
16
+ | `build-time manifests require an http(s) upstream URL.` | `backendURL` uses another protocol | Use the `https://` URL Inth shows |
17
+ | `/manifest responded 404` | The URL is not your project's backend, such as the `https://your-project.inth.app` placeholder or a URL missing its path prefix | Copy the backend URL exactly as Inth shows it, or set `VITE_C15T_BACKEND_URL` |
18
+ | `/manifest returned an invalid consent manifest.` | The URL answered with something other than a consent manifest, such as an HTML page | Point `backendURL` at the backend itself, not your site or a dashboard page |
19
+ | `fetch failed` | The build cannot reach the backend | Build where the backend is reachable. A failed download never reuses an old snapshot |
20
+
21
+ ## Why can't TypeScript find ./c15t-manifest?
22
+
23
+ **Check.** `tsc`, `vue-tsc` or your editor reports that it cannot find the
24
+ module `./c15t-manifest`, on a fresh checkout or in CI.
25
+
26
+ **Fix.** `consentManifest` writes `src/c15t-manifest.ts` when `vite build` or
27
+ `vite dev` starts, and the file is not in Git. Run one of them before the type
28
+ check. In CI, run the build first, or cache the generated file with the build
29
+ output.
16
30
 
17
31
  ## There is no banner
18
32
 
19
33
  **Check.** In DevTools Network, find the `GET` request to `/init` on your
20
- backend URL.
21
-
22
- | What you see | Cause | Fix |
23
- | ------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
24
- | No `/init` request | `ConsentProvider` is not mounted, or an error stopped `src/consent.tsx` from rendering | Check the console for errors and render one provider at the app root, as in the [quickstart](./quickstart.md) |
25
- | `/init` fails with a CORS error | Your site's origin is not trusted | Add the exact origin, including port in development, to the trusted origins in your Inth project |
26
- | `/init` returns 200 | The policy for this visitor asks for no banner, or a stored choice is still valid | Inspect the active policy with the component below, or clear site data |
34
+ backend URL. With the bundled manifest, the browser makes no request before
35
+ the banner unless the policy depends on a country or region it does not know.
36
+
37
+ | What you see | Cause | Fix |
38
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39
+ | No request and no banner | `ConsentProvider` is not mounted, an error stopped `src/consent.tsx` from rendering, or the bundled policy asks for no banner | Check the console for errors, render one provider at the app root as in the [quickstart](./quickstart.md), and inspect the active policy with the component below |
40
+ | `/init` fails with a CORS error | Your site's origin is not trusted | Add the exact origin, including port in development, to the trusted origins in your Inth project |
41
+ | `/init` returns 200 | The policy for this visitor asks for no banner, or a stored choice is still valid | Inspect the active policy with the component below, or clear site data |
27
42
 
28
43
  A policy with `prompt: 'none'` shows no banner by design, and so does a
29
44
  returning visitor with a valid stored choice. [Policies](../../concepts/policies.md)
@@ -258,7 +258,7 @@ import { resolveConsent } from 'c15t/next/server';
258
258
  import { Suspense } from 'react';
259
259
  import type { ReactNode } from 'react';
260
260
 
261
- import { consentConfig } from '@/c15t.config';
261
+ import { consentOptions } from '@/c15t.server';
262
262
  import { ExperimentConsent } from '@/components/experiment-consent';
263
263
  import { bannerExperiment } from '@/lib/experiment';
264
264
  import { bannerExperimentFlag } from '@/lib/flags';
@@ -268,7 +268,7 @@ import '@/styles/globals.css';
268
268
  const ResolvedConsent = async ({ children }: { children: ReactNode }) => {
269
269
  const arm = await bannerExperimentFlag();
270
270
  const state = await resolveConsent({
271
- config: consentConfig,
271
+ ...consentOptions,
272
272
  // `off` keeps this visitor out of the experiment.
273
273
  experiment: arm === 'off' ? undefined : { ...bannerExperiment, arm },
274
274
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c15t/react",
3
- "version": "3.0.0-alpha.7",
3
+ "version": "3.0.0-alpha.8",
4
4
  "description": "React cookie banner, consent manager, preference center, and headless CMP components with IAB TCF and Consent Mode support.",
5
5
  "keywords": [
6
6
  "ccpa",
@@ -254,16 +254,16 @@
254
254
  "test:watch": "bun prebuild && vitest"
255
255
  },
256
256
  "dependencies": {
257
- "@c15t/core": "3.0.0-alpha.7",
258
- "@c15t/dev-tools": "3.0.0-alpha.7",
259
- "@c15t/iab": "3.0.0-alpha.7",
260
- "@c15t/schema": "3.0.0-alpha.6",
257
+ "@c15t/core": "3.0.0-alpha.8",
258
+ "@c15t/dev-tools": "3.0.0-alpha.8",
259
+ "@c15t/iab": "3.0.0-alpha.8",
260
+ "@c15t/schema": "3.0.0-alpha.7",
261
261
  "@c15t/translations": "3.0.0-alpha.6",
262
- "@c15t/ui": "3.0.0-alpha.7",
262
+ "@c15t/ui": "3.0.0-alpha.8",
263
263
  "@types/google.maps": "3.65.3"
264
264
  },
265
265
  "devDependencies": {
266
- "@c15t/backend": "3.0.0-alpha.7",
266
+ "@c15t/backend": "3.0.0-alpha.8",
267
267
  "@c15t/conformance": "0.0.1",
268
268
  "@c15t/typescript-config": "0.0.1",
269
269
  "@c15t/vitest-config": "1.0.0",