@consenti/ui 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +548 -151
  2. package/dist/angular.d.cts +486 -111
  3. package/dist/angular.d.ts +486 -111
  4. package/dist/angular.js +1 -73
  5. package/dist/angular.js.map +1 -1
  6. package/dist/angular.mjs +1 -52
  7. package/dist/angular.mjs.map +1 -1
  8. package/dist/chunk-3EXCD24H.mjs +2 -0
  9. package/dist/{chunk-ARALKXMB.mjs.map → chunk-3EXCD24H.mjs.map} +1 -1
  10. package/dist/chunk-J4BKXUD6.mjs +2 -0
  11. package/dist/chunk-J4BKXUD6.mjs.map +1 -0
  12. package/dist/hmac-UKMRMKN6.mjs +2 -0
  13. package/dist/{hmac-N6H7F2K2.mjs.map → hmac-UKMRMKN6.mjs.map} +1 -1
  14. package/dist/http-AQVVUCX6.mjs +2 -0
  15. package/dist/index.css +1 -1
  16. package/dist/index.d.ts +806 -103
  17. package/dist/index.mjs +1 -2220
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/index.umd.js +1 -2304
  20. package/dist/index.umd.js.map +1 -1
  21. package/dist/profiles-OEZAKEWL.mjs +2 -0
  22. package/dist/profiles-OEZAKEWL.mjs.map +1 -0
  23. package/dist/react.d.cts +470 -89
  24. package/dist/react.d.ts +470 -89
  25. package/dist/react.js +1 -60
  26. package/dist/react.js.map +1 -1
  27. package/dist/react.mjs +1 -39
  28. package/dist/react.mjs.map +1 -1
  29. package/dist/testing.js +1 -94
  30. package/dist/testing.js.map +1 -1
  31. package/dist/testing.mjs +1 -73
  32. package/dist/testing.mjs.map +1 -1
  33. package/dist/vue.d.cts +473 -82
  34. package/dist/vue.d.ts +473 -82
  35. package/dist/vue.js +1 -72
  36. package/dist/vue.js.map +1 -1
  37. package/dist/vue.mjs +1 -51
  38. package/dist/vue.mjs.map +1 -1
  39. package/package.json +15 -6
  40. package/dist/chunk-ARALKXMB.mjs +0 -20
  41. package/dist/hmac-N6H7F2K2.mjs +0 -26
  42. package/dist/http-VHOGZ6ZJ.mjs +0 -7
  43. /package/dist/{http-VHOGZ6ZJ.mjs.map → http-AQVVUCX6.mjs.map} +0 -0
package/dist/react.d.ts CHANGED
@@ -1,15 +1,115 @@
1
+ type NonEmptyArray<T> = [T, ...T[]];
2
+ /**
3
+ * Deep-partial with JSON Merge Patch (RFC 7396) delete semantics: any key may be
4
+ * set to `null` instead of its usual type to signal "delete this key" when the
5
+ * partial is applied with `deepMerge()` — `undefined`/omitted keeps the base
6
+ * value, `null` removes it. Only meaningful for object/map keys (e.g. removing
7
+ * a `CategoryMap`/`CookieMap` entry via `profileOverride`); array elements are
8
+ * merged positionally and don't support deletion this way.
9
+ */
10
+ type DeepPartial<T> = T extends Function ? T : T extends Array<infer InferredArrayType> ? Array<DeepPartial<InferredArrayType>> : T extends ReadonlyArray<infer InferredReadonlyArrayType> ? ReadonlyArray<DeepPartial<InferredReadonlyArrayType>> : T extends object ? {
11
+ [Key in keyof T]?: DeepPartial<T[Key]> | null;
12
+ } : T;
13
+
14
+ declare const COMPLIANCE_GROUP_IDS: readonly ["opt-in", "opt-out", "opt-out-strict", "opt-in-dpdpa", "opt-in-china", "opt-in-brazil", "general-privacy-consent", "notice-only"];
15
+ declare const COOKIE_PURPOSE_IDS: readonly ["necessary", "functional", "preferences", "analytics", "marketing"];
16
+
17
+ /**
18
+ * Compliance-related literal types, derived from the regulation/group ID data
19
+ * that lives in @consenti/utils (packages/utils/src/compliance.ts).
20
+ *
21
+ * This import is type-only and fully erased at compile time — @consenti/types
22
+ * has no runtime dependency on @consenti/utils. The actual data (COMPLIANCE_GROUPS,
23
+ * EMBEDDED_COMPLIANCE_MAP, COMPLIANCE_VALIDATION_RULES, GPC_OPTIONS, etc.) lives in
24
+ * @consenti/utils; import it from there.
25
+ *
26
+ * HOW TO ADD A NEW REGULATION OR COMPLIANCE GROUP: see the header comment in
27
+ * packages/utils/src/compliance.ts — these types auto-update from that data.
28
+ */
29
+
30
+ type ComplianceGroupId = typeof COMPLIANCE_GROUP_IDS[number];
31
+ /**
32
+ * `(string & {})` (rather than plain `string`) keeps IDE autocomplete for the
33
+ * known `ComplianceGroupId` literals while still allowing any other string —
34
+ * used to target a profile authored against a custom (non-built-in)
35
+ * `customComplianceGroup` instead of one of the 8 built-in groups.
36
+ */
37
+ type ComplianceType = 'auto' | ComplianceGroupId | (string & {}) | Symbol;
38
+
39
+ interface ComplianceMapData {
40
+ version: string;
41
+ countries: Record<string, CountryComplianceEntry>;
42
+ }
43
+ interface CountryComplianceEntry {
44
+ complianceGroup: ComplianceGroupId;
45
+ default: ComplianceGroupId;
46
+ description: string;
47
+ overriddenRegions?: Record<string, {
48
+ complianceGroup: ComplianceGroupId;
49
+ description: string;
50
+ }>;
51
+ }
52
+ interface ConsentDbRecord {
53
+ id: string;
54
+ tenantId: string;
55
+ visitorId: string;
56
+ profileId: string;
57
+ locale: string;
58
+ consentJson: ConsentValue;
59
+ gpcDetected: boolean;
60
+ source: 'banner' | 'api' | 'import';
61
+ ageVerified?: boolean;
62
+ parentalConsentToken?: string;
63
+ tcfString?: string;
64
+ /** HMAC-SHA256 signature over the record's core fields, hex-encoded. Only present when
65
+ * `consentSigningKey` is configured — opt-in tamper-evidence for server-stored records,
66
+ * independent of the widget's own cookie-signing (`core.cookieSigningKey`). */
67
+ signature?: string;
68
+ createdAt: string;
69
+ updatedAt: string;
70
+ }
71
+
1
72
  type ConsentStatus = 'granted' | 'denied' | 'objected';
73
+ type ConsentAction = 'accept_all' | 'reject_all' | 'custom' | 'update';
2
74
  type ConsentValue = Record<string, ConsentStatus>;
75
+ /**
76
+ * Output format for getConsent(type).
77
+ * 'default' → raw ConsentValue (existing behaviour)
78
+ * 'google-gtm' → Google Consent Mode v2 keys + ads_data_redaction, url_passthrough
79
+ * 'category' → consent per CookiePurpose category
80
+ * 'adobe' → { analytics, target, manager, optimizer }
81
+ * 'meta' → { pixel, api, plugins, facebookLogin }
82
+ * 'microsoft-clarity' → { session, heatmaps, performance }
83
+ * 'twilio-segment' → { identify, page, track, group, alias }
84
+ */
85
+ type ConsentType = 'default' | 'google-gtm' | 'category' | 'adobe' | 'meta' | 'microsoft-clarity' | 'twilio-segment';
86
+ type GpcMode = 'ignore' | 'honor' | 'strict';
87
+ type LegalBasis = 'mandatory' | 'consent' | 'legitimate_interest';
88
+ type CookiePurpose = typeof COOKIE_PURPOSE_IDS[number];
89
+ /**
90
+ * A single consent parameter (tracker/cookie definition).
91
+ * Keyed by its own id inside `Record<string, Cookie>` — the map key IS the id,
92
+ * there is no `id` field on the value itself.
93
+ */
3
94
  interface Cookie {
4
- id: string;
5
- type?: 'consent' | 'legitimate_interest';
6
- mandatory?: boolean;
95
+ purpose?: CookiePurpose;
7
96
  listenGpc?: boolean;
8
- expiry?: number;
97
+ /**
98
+ * Pre-grant this parameter's default consent to 'granted' (instead of the
99
+ * compliance-group-driven default) when no stored decision exists yet.
100
+ * Only meaningful when the owning category's `legalBasis === 'consent'` —
101
+ * `mandatory`/`legitimate_interest` categories are already effectively
102
+ * pre-granted regardless of this flag. Never overrides an active GPC signal.
103
+ * Default: false.
104
+ */
105
+ preGrant?: boolean;
9
106
  tcfVendorId?: number;
10
107
  tcfPurposes?: number[];
108
+ tcfSpecialFeatures?: number[];
11
109
  cpraCategory?: 'sale' | 'sharing' | 'sensitive';
12
110
  }
111
+ /** `Cookie` keyed by its own id — the standard shape used everywhere a parameter list is passed around. */
112
+ type CookieMap = Record<string, Cookie>;
13
113
  type ButtonStyle = 'primary' | 'secondary' | 'text' | 'accent';
14
114
  type ButtonAction = 'submit' | 'manage' | 'close' | 'custom' | 'link';
15
115
  interface Button {
@@ -19,6 +119,10 @@ interface Button {
19
119
  cookies?: string[] | '*' | '!';
20
120
  url?: string;
21
121
  }
122
+ /** `Button` keyed by its own id — e.g. `'accept-all'`. The map key IS the id, rendered as the DOM
123
+ * `id` (`consenti-btn-{id}`) so integrators can target specific buttons, and is what
124
+ * `profileOverride`'s `deepMerge` deletes by (`{ 'reject-optional': null }` removes one button). */
125
+ type ButtonMap = Record<string, Button>;
22
126
  interface MainBanner {
23
127
  position: 'top' | 'bottom' | 'middle' | 'left-bottom' | 'right-bottom';
24
128
  overlayOpacity?: number;
@@ -27,34 +131,45 @@ interface MainBanner {
27
131
  heading?: string;
28
132
  headingTag?: string;
29
133
  htmlText: string;
30
- buttons: Button[];
134
+ buttons: ButtonMap;
135
+ /** Stack buttons vertically below this viewport width in px. 0 or undefined = disabled. Default: 576. */
136
+ stackButtonsOnBreakpoint?: number;
137
+ /** Trap keyboard Tab focus within the consent root while this banner is visible. */
138
+ trapFocus?: boolean;
31
139
  }
32
140
  type GpcBanner = MainBanner;
141
+ /**
142
+ * A consent category — the single source of legal basis for every parameter
143
+ * it lists in `cookies`. Keyed by its own id inside `Record<string, Category>` —
144
+ * the map key IS the id, there is no `id` field on the value itself.
145
+ *
146
+ * A parameter must belong to exactly one category (enforced at authoring time,
147
+ * not by this type) — that relationship is what `getCookieLegalBasis`/
148
+ * `isMandatoryCookie` (`@consenti/utils`) resolve at runtime.
149
+ */
33
150
  interface Category {
34
- id: string;
35
151
  heading: string;
36
152
  headingTag?: string;
37
153
  htmlText: string;
38
- mandatory?: boolean;
39
- type?: 'consent' | 'legitimate_interest';
40
- legitimateInterest?: {
41
- enabled?: boolean;
42
- description?: string;
43
- };
154
+ legalBasis: LegalBasis;
155
+ /** Only meaningful when legalBasis === 'legitimate_interest' — the GDPR balancing-test justification shown to visitors. */
156
+ legitimateInterestDescription?: string;
157
+ /** IDs of the parameters (from the profile's `cookies` map) that belong to this category. */
44
158
  cookies: string[];
45
159
  }
46
- interface PreferenceModal {
160
+ /** `Category` keyed by its own id — the standard shape used everywhere a category list is passed around. */
161
+ type CategoryMap = Record<string, Category>;
162
+ interface PreferenceModal extends Omit<MainBanner, "position"> {
47
163
  position?: 'left' | 'right' | 'center';
48
- overlayOpacity?: number;
49
- showClose?: boolean;
50
- showLocaleSwitcher?: boolean;
51
- heading?: string;
52
- headingTag?: string;
53
164
  subheading?: string;
54
- htmlText?: string;
55
- buttons: Button[];
56
- categories: Category[];
165
+ categories: CategoryMap;
57
166
  persistent?: boolean;
167
+ /** Screen width in px below which the modal expands to full screen. Default: 576. Set to 0 to disable. */
168
+ mobileFullScreenBreakpoint?: number;
169
+ /** Label for the optional consent-receipt download checkbox (shown when `allowReceipt` is true). Falls back to a default when omitted. */
170
+ receiptLabel?: string;
171
+ /** Description shown beneath the consent-receipt checkbox. Falls back to a default when omitted. */
172
+ receiptDescription?: string;
58
173
  }
59
174
  interface DpdpaConfig {
60
175
  dataFiduciary: string;
@@ -62,17 +177,55 @@ interface DpdpaConfig {
62
177
  purposeDescription?: string;
63
178
  }
64
179
  interface ResolvedProfile {
65
- id: number;
66
- version: number;
180
+ id: string;
181
+ /** Incremented in place on every save of this profile. Absent for embedded/local profiles that have no server-side version counter. */
182
+ version?: number;
67
183
  defaultLocale: string;
68
184
  locales?: string[];
69
- cookies: Cookie[];
185
+ cookies: CookieMap;
186
+ /** Days until consent expires and the visitor is asked again (profile-wide). Default: 365. */
187
+ expiryDays?: number;
188
+ allowReceipt?: boolean;
70
189
  mainBanner: MainBanner;
71
190
  gpcBanner?: GpcBanner;
72
191
  preferenceModal: PreferenceModal;
73
192
  cookieSigningKey?: string;
74
193
  dpdpa?: DpdpaConfig;
75
194
  darkMode?: boolean;
195
+ gpcMode?: GpcMode;
196
+ complianceGroup?: ComplianceGroupId;
197
+ complianceConfig?: Record<string, string>;
198
+ /** Falls back to `true` (hidden) if neither the widget config nor the profile set it. */
199
+ hidePoweredBy?: boolean;
200
+ /** Show metadata footer strip (Consent ID, Date, Version, Privacy Settings link). */
201
+ showFooterMetadata?: boolean;
202
+ /** Apply WCAG 2.1 AA accessibility enhancements to the widget. */
203
+ enhanceAccessibility?: boolean;
204
+ }
205
+ type WidgetCountryResolverFn = () => Promise<{
206
+ country: string | null;
207
+ region: string | null;
208
+ confidence: number;
209
+ }>;
210
+ interface AgeGateWidgetConfig {
211
+ enabled: boolean;
212
+ minimumAge: number;
213
+ requireParentalConsent?: boolean;
214
+ }
215
+ /** Mirrors the server-side `TcfConfig` shape (`@consenti/api`) — `cmpId`/`cmpVersion` must match
216
+ * what the backend uses to encode `tcfString`, since both feed the same simplified TC string. */
217
+ interface TcfWidgetConfig {
218
+ enabled: boolean;
219
+ cmpId: number;
220
+ cmpVersion: number;
221
+ }
222
+ interface ComplianceWidgetConfig {
223
+ type?: ComplianceType;
224
+ geoDataProvider?: 'default' | WidgetCountryResolverFn;
225
+ autoComplianceMap?: 'default' | ComplianceMapData | 'auto';
226
+ complianceMapUrl?: string;
227
+ ageGate?: AgeGateWidgetConfig;
228
+ tcf?: TcfWidgetConfig;
76
229
  }
77
230
  interface GtmConfig {
78
231
  containerId?: string;
@@ -80,6 +233,7 @@ interface GtmConfig {
80
233
  dataLayer?: string;
81
234
  urlPassthrough?: boolean;
82
235
  adsDataRedaction?: boolean;
236
+ verbose?: boolean;
83
237
  }
84
238
  interface UtilsConfig {
85
239
  gtm?: GtmConfig;
@@ -88,6 +242,9 @@ interface ApiConfig {
88
242
  enabled?: boolean;
89
243
  baseUrl?: string;
90
244
  authToken?: string;
245
+ tenantId?: string;
246
+ complianceGroup?: ComplianceGroupId;
247
+ trustDomain?: boolean;
91
248
  }
92
249
  interface ThemeConfig {
93
250
  bgColor?: string;
@@ -109,35 +266,60 @@ interface ThemeConfig {
109
266
  accentTextColor?: string;
110
267
  }
111
268
  interface CoreConfig {
112
- profileId?: number;
113
- autoHonorGPC?: boolean | 'strict';
269
+ tenantId?: string;
114
270
  locale?: string;
271
+ /** Text direction for the banner/modal root. `'auto'` (default) derives it from `locale`
272
+ * (Arabic, Hebrew, Persian, Urdu, ... → `rtl`); set explicitly to override. */
273
+ dir?: 'ltr' | 'rtl' | 'auto';
115
274
  disableCssTemplate?: boolean;
116
- signCookies?: boolean;
117
275
  cookieSigningKey?: string;
118
276
  allowReceipt?: boolean;
119
277
  cookieDomains?: string;
120
278
  storage?: 'cookie' | 'localStorage';
121
279
  theme?: ThemeConfig;
122
- privacyPolicyUrl?: string;
123
- regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
124
280
  userId?: string;
281
+ usePrebuiltProfiles?: 'all' | NonEmptyArray<ComplianceGroupId>;
282
+ cacheResolvedProfiles?: boolean;
283
+ console?: Array<'info' | 'log' | 'warning' | 'error'>;
125
284
  }
126
285
  interface ConsentiConfig {
127
- core: CoreConfig;
286
+ verbose?: boolean;
287
+ /** Optional — every field of `CoreConfig` is itself optional, so `new ConsentiSetup({})` and
288
+ * `new ConsentiSetup({ compliance: { type: 'opt-in' } })` (omitting `core` entirely) both work,
289
+ * matching the widget's own "Minimal config" documentation. */
290
+ core?: CoreConfig;
291
+ compliance?: ComplianceWidgetConfig;
128
292
  rootEl?: string | HTMLElement;
129
293
  darkMode?: boolean;
130
294
  autoInit?: boolean;
131
295
  api?: ApiConfig;
132
296
  utils?: UtilsConfig;
133
297
  plugins?: ConsentiPlugin[];
134
- profileOverride?: Partial<ResolvedProfile>;
298
+ profileOverride?: DeepPartial<ResolvedProfile>;
299
+ /** When true, suppresses the "Powered by Consenti" footer link in the banner and modal. */
300
+ hidePoweredBy?: boolean;
135
301
  }
302
+ type ConsentiEventName = 'bannerInitialized' | 'consenti:bannerInitialized' | 'bannerVisibility' | 'consenti:bannerVisibility' | 'modalVisibility' | 'consenti:modalVisibility' | 'consentBeingSubmitted' | 'consenti:consentBeingSubmitted' | 'consentSubmitted' | 'consenti:consentSubmitted' | 'parentalConsentRequired' | 'consenti:parentalConsentRequired';
136
303
  interface ConsentiWidgetAPI {
137
304
  hasConsent(): boolean;
138
305
  getConsent(): ConsentValue | null;
306
+ getConsent(type: ConsentType): Record<string, string> | null;
307
+ getConsent(type?: ConsentType): ConsentValue | Record<string, string> | null;
139
308
  getConsentDate(): Date | false;
140
309
  getGTMConsent(): Record<string, string> | null;
310
+ isCookieGranted(cookieId: string, requestValue?: boolean): boolean | ConsentStatus;
311
+ isCategoryGranted(categoryId: string, requestValue?: boolean): boolean | {
312
+ [cookieId: string]: ConsentStatus;
313
+ }[];
314
+ grantAll(onlyMandatory?: boolean): Promise<void>;
315
+ denyAll(includingMandatory?: boolean): Promise<void>;
316
+ on(event: ConsentiEventName, handler: (data: ConsentEvent) => void): void;
317
+ off(event: ConsentiEventName, handler: (data: ConsentEvent) => void): void;
318
+ version(): {
319
+ package: string;
320
+ profileVersion: string | null;
321
+ consentVersion: string | null;
322
+ };
141
323
  bannerVisibility(): 'main' | 'gpc' | false;
142
324
  modalVisibility(): 'preference' | false;
143
325
  getProfile(): ResolvedProfile | null;
@@ -145,13 +327,17 @@ interface ConsentiWidgetAPI {
145
327
  hideBanner(): void;
146
328
  showModal(triggerEl?: HTMLElement): void;
147
329
  hideModal(): void;
148
- submitConsent(consent: Partial<ConsentValue>): Promise<void>;
330
+ submitConsent(consent: Partial<ConsentValue>): Promise<ConsentDbRecord | void>;
149
331
  deleteConsent(): Promise<void>;
150
332
  reConsent(): Promise<void>;
151
333
  getRootElement(): HTMLElement | null;
152
334
  getBannerElement(): HTMLElement | null;
153
335
  getModalElement(): HTMLElement | null;
154
336
  switchLocale(locale: string): void;
337
+ setDarkMode(enable?: boolean): void;
338
+ setTheme(theme: Partial<ThemeConfig>): void;
339
+ setConfig(config: DeepPartial<ConsentiConfig>): void;
340
+ setProfile(override: DeepPartial<ResolvedProfile>): void;
155
341
  init(): Promise<void>;
156
342
  onReady(callback: () => void): void;
157
343
  destroy(): void;
@@ -165,6 +351,47 @@ declare abstract class ConsentiPlugin {
165
351
  onModalShow?(): void | Promise<void>;
166
352
  onModalHide?(): void | Promise<void>;
167
353
  }
354
+ interface BannerInitializedDetail {
355
+ profileId: string;
356
+ complianceGroup?: ComplianceType;
357
+ hasExistingConsent: boolean;
358
+ gpcDetected: boolean;
359
+ willShow: boolean;
360
+ }
361
+ interface BannerVisibilityDetail {
362
+ visible: boolean;
363
+ variant: 'main' | 'gpc';
364
+ action: boolean;
365
+ }
366
+ interface ModalVisibilityDetail {
367
+ visible: boolean;
368
+ action: boolean;
369
+ }
370
+ interface ConsentBeingSubmitted {
371
+ consentId: string;
372
+ visitorId: string;
373
+ profileId: string;
374
+ consentJson: ConsentValue;
375
+ consentAction: ConsentAction;
376
+ gpcDetected: boolean;
377
+ pageUrl: string;
378
+ timestamp: number;
379
+ fromBroadcast?: boolean;
380
+ }
381
+ interface ConsentSubmittedDetail extends ConsentBeingSubmitted {
382
+ apiResponse: ConsentDbRecord;
383
+ }
384
+ /** Dispatched when the age gate is declined and `requireParentalConsent` is set — see
385
+ * `AgeGateWidgetConfig`. A deny-all consent (mandatory cookies only) has already been
386
+ * submitted with `parentalConsentToken` attached; this event is the hook for the site
387
+ * owner to run their own out-of-band parental-consent verification. */
388
+ interface ParentalConsentRequiredDetail {
389
+ parentalConsentToken: string;
390
+ profileId: string;
391
+ visitorId: string;
392
+ timestamp: number;
393
+ }
394
+ type ConsentEvent = BannerInitializedDetail | BannerVisibilityDetail | ModalVisibilityDetail | ConsentBeingSubmitted | ConsentSubmittedDetail | ParentalConsentRequiredDetail;
168
395
 
169
396
  /**
170
397
  * Main entry point for the Consenti widget.
@@ -176,7 +403,7 @@ declare abstract class ConsentiPlugin {
176
403
  * @example
177
404
  * ```ts
178
405
  * const widget = new ConsentiSetup({
179
- * core: { profileId: 0 },
406
+ * compliance: { type: 'opt-in' },
180
407
  * })
181
408
  * await widget.ready
182
409
  * console.log(widget.hasConsent())
@@ -184,15 +411,30 @@ declare abstract class ConsentiPlugin {
184
411
  */
185
412
 
186
413
  declare class ConsentiSetup implements ConsentiWidgetAPI {
187
- private config;
188
414
  private initialized;
189
415
  private initializing;
190
416
  private profile;
417
+ private resolvedBase;
418
+ /** cookieId → Category lookup, rebuilt whenever `profile` changes. Avoids rescanning categories on every consent action. */
419
+ private categoryIndex;
420
+ /** Watchers created by the declarative `data-consenti-*` DOM scan — destroyed alongside the widget since they have no other owner. */
421
+ private scannedScripts;
191
422
  private consentStore;
423
+ /**
424
+ * Stable per-browser identifier. Created lazily the first time a consent decision actually
425
+ * happens (manual submit, GPC auto-response, or relaying a decision already made in another
426
+ * tab) — never minted before that, so an undecided visitor leaves nothing in storage. See
427
+ * `getOrCreateVisitorId()`.
428
+ */
429
+ private visitorId;
192
430
  private eventBus;
193
431
  private banner;
194
432
  private modal;
433
+ private ageGate;
195
434
  private renderer;
435
+ /** Set once the age gate (if enabled) resolves; threaded onto every `submitConsent` call thereafter. */
436
+ private _ageVerified;
437
+ private _parentalConsentToken;
196
438
  private broadcastChannel;
197
439
  private readyResolve;
198
440
  private readyPromise;
@@ -201,13 +443,23 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
201
443
  private lastSignature;
202
444
  /** Tracks which banner variant is currently visible, or null when hidden. */
203
445
  private _bannerVariant;
446
+ /** Tracks which banner variant is currently mounted in the DOM (persists when banner is hidden). */
447
+ private _bannerMounted;
448
+ /** Active focus trap keydown listener — removed when modal closes. */
449
+ private _focusTrapListener;
450
+ /** Maps original handler functions to their DOM-wrapped listeners for on/off bookkeeping. */
451
+ private _eventHandlers;
452
+ /** `core` defaulted to `{}` in the constructor below — every one of its own fields is
453
+ * optional, so this narrows back to non-optional for every other method in this class. */
454
+ private config;
204
455
  /**
205
456
  * Creates and initialises the consent widget.
206
457
  *
207
458
  * Construction is synchronous; the async `init()` is launched in the background.
208
459
  * Use `await widget.ready` or `widget.onReady(cb)` to run code after init completes.
209
460
  *
210
- * @param config - Widget configuration. See `ConsentiConfig` for all options.
461
+ * @param config - Widget configuration. See `ConsentiConfig` for all options. `core` may be
462
+ * omitted entirely — every one of its own fields is optional too.
211
463
  */
212
464
  constructor(config: ConsentiConfig);
213
465
  private profileId;
@@ -217,8 +469,53 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
217
469
  * to re-initialise the same instance.
218
470
  */
219
471
  init(): Promise<void>;
472
+ /**
473
+ * Everything after the "does consent already exist" check — GPC/CCPA/CPRA silent paths,
474
+ * then the normal opt-in banner. Extracted from `init()` so the age gate (when enabled and
475
+ * no consent exists yet) can defer this whole sequence until the visitor answers it via
476
+ * `resolveAgeGate()`, then resume here with `hasExisting` fixed at `false`.
477
+ */
478
+ private continueInit;
479
+ /** Mounts the age-gate prompt (`compliance.ageGate`) in place of the normal banner flow. */
480
+ private showAgeGate;
481
+ /**
482
+ * Handles the visitor's age-gate answer.
483
+ * - Confirmed → records `ageVerified: true` for all future `submitConsent` calls and
484
+ * resumes the normal init flow via `continueInit()`.
485
+ * - Declined, no parental consent required → immediately submits a deny-all consent
486
+ * (mandatory cookies still granted), mirroring the GPC-strict silent-write pattern.
487
+ * - Declined, parental consent required → same deny-all submission, plus a
488
+ * `parentalConsentToken` and a `consenti:parentalConsentRequired` event so the site
489
+ * owner can wire their own out-of-band verification flow (no email infra exists in
490
+ * this package to do that here).
491
+ */
492
+ private resolveAgeGate;
493
+ /**
494
+ * Marks the widget ready and runs the declarative DOM-scan for
495
+ * `data-consenti-consent-script`/`data-consenti-category-script` tags. Called from
496
+ * every exit path of `init()` so the scan always runs exactly once per init cycle,
497
+ * whether `init()` was triggered automatically (`autoInit !== false`) or explicitly
498
+ * by the caller (`autoInit: false`) — it never runs before the widget actually has
499
+ * a resolved profile to read consent from.
500
+ */
501
+ private finishInit;
502
+ /** Sets the `dir` attribute on the root element from `core.dir` (default `'auto'`, derived from `core.locale`). */
503
+ private applyDirection;
220
504
  /** Applies the `consenti-root--dark` class based on config or profile flag. */
221
505
  private applyDarkMode;
506
+ /** Applies `consenti--enhanced-a11y` class when `profile.enhanceAccessibility` is set. */
507
+ private applyAccessibility;
508
+ /** Builds and appends the metadata footer strip to a banner or modal container. */
509
+ private _appendMetaFooter;
510
+ /**
511
+ * Injects a `<style>` element into the root for `stackButtonsOnBreakpoint`.
512
+ * Called once per profile load; a new style replaces the previous one.
513
+ */
514
+ private applyStackButtonsBreakpoint;
515
+ /** Installs a keyboard focus trap on `container` while the modal is open. */
516
+ private _installFocusTrap;
517
+ /** Removes the active focus trap keyboard listener. */
518
+ private _removeFocusTrap;
222
519
  /**
223
520
  * Injects CSS custom property overrides supplied via `core.theme` into the
224
521
  * Consenti root element so they cascade down to all widget elements.
@@ -233,6 +530,8 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
233
530
  * @param invoke - Function that calls the desired hook method on a single plugin.
234
531
  */
235
532
  private callPluginHook;
533
+ private _buildGrantAllConsent;
534
+ private _buildDenyAllConsent;
236
535
  private buildHandlers;
237
536
  /**
238
537
  * Classifies a consent submission as `accept_all`, `reject_all`, `custom`, or `update`.
@@ -246,22 +545,23 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
246
545
  */
247
546
  hasConsent(): boolean;
248
547
  /**
249
- * Returns the stored consent map, or `null` if no consent has been saved.
250
- * Always returns `null` during SSR.
548
+ * Returns the stored consent map, optionally transformed for a specific vendor.
549
+ * Pass `type` to get a vendor-ready object instead of the raw Consenti format.
550
+ * Always returns `null` during SSR or when no consent has been saved.
551
+ *
552
+ * @param type - Vendor format: 'google-gtm', 'category', 'adobe', 'meta',
553
+ * 'microsoft-clarity', 'twilio-segment', or omit for raw format.
251
554
  */
252
555
  getConsent(): ConsentValue | null;
556
+ getConsent(type: ConsentType): Record<string, string> | null;
253
557
  /**
254
558
  * Returns the `Date` when consent was last saved, or `false` if no consent exists.
255
559
  * Always returns `false` during SSR.
256
560
  */
257
561
  getConsentDate(): Date | false;
258
562
  /**
259
- * Returns the consent map in Google Consent Mode v2 format, ready to pass to
260
- * `gtag('consent', 'update', ...)`. Returns `null` if no consent has been saved.
261
- *
262
- * Maps Consenti cookie IDs to the seven Google Consent Mode v2 parameters.
263
- * Cookies whose IDs exactly match a GCM v2 key are forwarded directly.
264
- * Non-GCM cookies are ignored by this method (they appear in `getConsent()` only).
563
+ * @deprecated Use `getConsent('google-gtm')` instead.
564
+ * Returns the consent map in Google Consent Mode v2 format.
265
565
  */
266
566
  getGTMConsent(): Record<string, string> | null;
267
567
  /**
@@ -273,7 +573,7 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
273
573
  * @param consent - Partial or full map of cookie ID → status.
274
574
  * @param gpcDerived - Internal flag set to `true` when called from the GPC strict path.
275
575
  */
276
- submitConsent(consent: Partial<ConsentValue>, gpcDerived?: boolean): Promise<void>;
576
+ submitConsent(consent: Partial<ConsentValue>, gpcDerived?: boolean, source?: number): Promise<ConsentDbRecord | void>;
277
577
  /**
278
578
  * Returns the currently visible banner variant, or `false` if no banner is shown.
279
579
  * Useful for programmatically checking state before calling `showBanner` / `hideBanner`.
@@ -381,6 +681,93 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
381
681
  * @param locale - BCP 47 locale code to switch to (e.g. `'fr'`, `'de-AT'`).
382
682
  */
383
683
  switchLocale(locale: string): void;
684
+ /**
685
+ * Returns `true` if the named cookie's consent is `'granted'`.
686
+ * When `requestValue` is `true`, returns the raw `ConsentStatus` string instead of a boolean,
687
+ * or `false` if the cookie is not in the consent map.
688
+ */
689
+ isCookieGranted(cookieId: string, requestValue?: boolean): boolean | 'granted' | 'denied' | 'objected';
690
+ /**
691
+ * Checks consent for all cookies belonging to the given category.
692
+ *
693
+ * With `requestValue = false` (default): returns `true` only when every cookie in the
694
+ * category is `'granted'`, `false` otherwise.
695
+ *
696
+ * With `requestValue = true`: returns an array of one-key records — one per cookie in the
697
+ * category — e.g. `[{ analytics: 'granted' }, { pixel: 'denied' }]`.
698
+ *
699
+ * Returns `false` / `[]` if the category is not found or the widget is not yet initialized.
700
+ */
701
+ isCategoryGranted(categoryId: string, requestValue?: boolean): boolean | {
702
+ [cookieId: string]: 'granted' | 'denied' | 'objected';
703
+ }[];
704
+ /**
705
+ * Programmatically accepts cookies and hides the banner.
706
+ *
707
+ * @param onlyMandatory - When `true`, only mandatory cookies are granted and all others
708
+ * are set to `'denied'`. Defaults to `false` (grant everything).
709
+ */
710
+ grantAll(onlyMandatory?: boolean): Promise<void>;
711
+ /**
712
+ * Programmatically denies cookies and hides the banner.
713
+ *
714
+ * @param includingMandatory - When `true`, mandatory cookies are also denied. Defaults to
715
+ * `false` (mandatory cookies remain `'granted'`). Passing `true`
716
+ * logs a warning — ensure this is intentional.
717
+ */
718
+ denyAll(includingMandatory?: boolean): Promise<void>;
719
+ /**
720
+ * Subscribes to a Consenti widget event. The `consenti:` prefix is optional —
721
+ * both `'consentSubmitted'` and `'consenti:consentSubmitted'` are accepted.
722
+ *
723
+ * The same handler reference must be passed to `off()` to unsubscribe.
724
+ *
725
+ * @example
726
+ * ```ts
727
+ * const handler = (data) => console.log(data)
728
+ * widget.on('consentSubmitted', handler)
729
+ * // later:
730
+ * widget.off('consentSubmitted', handler)
731
+ * ```
732
+ */
733
+ on(event: ConsentiEventName, handler: (data: ConsentEvent) => void): void;
734
+ /**
735
+ * Removes a previously registered event handler. The same function reference passed to
736
+ * `on()` must be used here.
737
+ */
738
+ off(event: ConsentiEventName, handler: (data: ConsentEvent) => void): void;
739
+ /**
740
+ * Returns the current version of the package and the active profile/consent versions.
741
+ * Useful for debugging and support diagnostics.
742
+ */
743
+ version(): {
744
+ package: string;
745
+ profileVersion: string;
746
+ consentVersion: string | null;
747
+ };
748
+ /**
749
+ * Toggles or sets dark mode at runtime without re-initialising the widget.
750
+ * When `enable` is omitted, the current dark mode state is toggled.
751
+ */
752
+ setDarkMode(enable?: boolean): void;
753
+ /**
754
+ * Applies partial theme token overrides at runtime, merging them into the current theme.
755
+ * CSS custom properties on the root element are updated immediately.
756
+ */
757
+ setTheme(theme: Partial<ThemeConfig>): void;
758
+ /**
759
+ * Deep-merges a partial config into the current widget config.
760
+ * Theme and dark mode side-effects are re-applied immediately.
761
+ * Changes to locale or profile require an explicit `switchLocale()` or `init()` call.
762
+ */
763
+ setConfig(config: DeepPartial<ConsentiConfig>): void;
764
+ /**
765
+ * Merges a partial profile override into the current profile and re-renders any visible UI.
766
+ * Does not make a network request — the resolved base profile from the last `init()` is reused.
767
+ *
768
+ * No-op before `init()` completes (while `resolvedBase` is null).
769
+ */
770
+ setProfile(override: DeepPartial<ResolvedProfile>): void;
384
771
  /**
385
772
  * Destroys the widget — removes all DOM elements, disconnects the BroadcastChannel,
386
773
  * and calls `destroy()` on all registered plugins.
@@ -390,43 +777,6 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
390
777
  destroy(): void;
391
778
  }
392
779
 
393
- /**
394
- * React integration for `@consenti/ui`.
395
- *
396
- * Import from the `@consenti/ui/react` subpath to keep this module
397
- * tree-shaken out of vanilla JS or Vue bundles.
398
- *
399
- * Intended for **Next.js** (App Router and Pages Router) and any other React 18+
400
- * project where you manage the `ConsentiSetup` instance yourself and want the
401
- * `useConsent()` hook to re-render when consent changes.
402
- *
403
- * @example
404
- * ```tsx
405
- * // app/providers.tsx (Next.js App Router)
406
- * 'use client'
407
- * import { useEffect } from 'react'
408
- * import { ConsentiSetup } from '@consenti/ui'
409
- * import { setConsentiWidget } from '@consenti/ui/react'
410
- *
411
- * export function ConsentiProvider({ children }: { children: React.ReactNode }) {
412
- * useEffect(() => {
413
- * const widget = new ConsentiSetup({ core: { profileId: 0 } })
414
- * setConsentiWidget(widget)
415
- * }, [])
416
- * return <>{children}</>
417
- * }
418
- *
419
- * // Any component
420
- * import { useConsent } from '@consenti/ui/react'
421
- *
422
- * function CookieBanner() {
423
- * const { hasConsent, submitConsent } = useConsent()
424
- * if (hasConsent()) return null
425
- * return <button onClick={() => submitConsent({ analytics: 'granted' })}>Accept</button>
426
- * }
427
- * ```
428
- */
429
-
430
780
  /**
431
781
  * Registers the active `ConsentiSetup` instance so `useConsent()` can read from it.
432
782
  * Call this once after creating your widget, typically inside a `useEffect`.
@@ -435,18 +785,49 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
435
785
  */
436
786
  declare function setConsentiWidget(widget: ConsentiSetup): void;
437
787
  /**
438
- * React hook that exposes consent state and lets components react to consent changes.
788
+ * React hook that exposes consent state, banner/modal visibility, and widget actions.
789
+ *
790
+ * Subscribes to `consenti:consentSubmitted`, `consenti:bannerVisibility`, and
791
+ * `consenti:modalVisibility` on `window` and triggers re-renders on each event.
792
+ * Returns SSR-safe initial values during server render.
439
793
  *
440
- * Subscribes to `consenti:consentSubmitted` on `window` and triggers a re-render
441
- * each time consent is updated. Returns SSR-safe fallback values during server render.
794
+ * Destructure only what you need — unused values do not cause extra renders.
442
795
  *
443
- * @returns An object with `hasConsent`, `getConsent`, `submitConsent`, and `getConsentDate`.
796
+ * @returns Consent state values and widget action functions.
444
797
  */
445
798
  declare function useConsent(): {
446
- hasConsent: () => boolean;
447
- getConsent: () => ConsentValue | null;
448
- submitConsent: (_consent: Partial<ConsentValue>) => Promise<void>;
449
- getConsentDate: () => Date | false;
799
+ /** `true` if the visitor has saved a valid consent record. */
800
+ hasConsent: boolean;
801
+ /** The stored consent map (`Record<cookieId, status>`), or `null` before first submission. */
802
+ consent: ConsentValue | null;
803
+ /** `Date` of the last consent submission, or `false` if no consent exists. */
804
+ consentDate: false | Date;
805
+ /** Current banner state: `'main'`, `'gpc'`, or `false` (hidden). */
806
+ bannerVisible: false | "main" | "gpc";
807
+ /** Current modal state: `'preference'` or `false` (hidden). */
808
+ modalVisible: false | "preference";
809
+ /** Shows the consent banner. Pass `true` to force the GPC variant. */
810
+ showBanner: (gpc?: boolean) => void | undefined;
811
+ /** Hides the consent banner. */
812
+ hideBanner: () => void | undefined;
813
+ /** Opens the preference modal. */
814
+ showModal: () => void | undefined;
815
+ /** Closes the preference modal. */
816
+ hideModal: () => void | undefined;
817
+ /** Grants all non-mandatory cookies. Pass `true` to grant only mandatory ones and deny the rest. */
818
+ grantAll: (onlyMandatory?: boolean) => Promise<void>;
819
+ /** Denies all non-mandatory cookies. Pass `true` to deny mandatory ones too (use with care). */
820
+ denyAll: (includingMandatory?: boolean) => Promise<void>;
821
+ /** Saves the given partial consent map. */
822
+ submitConsent: (c: Partial<ConsentValue>) => Promise<void | ConsentDbRecord>;
823
+ /** Deletes the current consent record and re-shows the banner. */
824
+ reConsent: () => Promise<void>;
825
+ /** Returns `true` if the given cookie ID is `'granted'`. */
826
+ isCookieGranted: (id: string) => boolean;
827
+ /** Returns `true` if every cookie in the given category is `'granted'`. */
828
+ isCategoryGranted: (id: string) => boolean;
829
+ /** Switches the active locale and re-renders the banner and modal. */
830
+ switchLocale: (locale: string) => void | undefined;
450
831
  };
451
832
 
452
833
  export { setConsentiWidget, useConsent };