@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/index.d.ts CHANGED
@@ -1,16 +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';
2
- type ConsentAction = 'accept_all' | 'reject_all' | 'custom' | 'update';
73
+ type ConsentAction$1 = 'accept_all' | 'reject_all' | 'custom' | 'update';
3
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
+ */
4
94
  interface Cookie {
5
- id: string;
6
- type?: 'consent' | 'legitimate_interest';
7
- mandatory?: boolean;
95
+ purpose?: CookiePurpose;
8
96
  listenGpc?: boolean;
9
- 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;
10
106
  tcfVendorId?: number;
11
107
  tcfPurposes?: number[];
108
+ tcfSpecialFeatures?: number[];
12
109
  cpraCategory?: 'sale' | 'sharing' | 'sensitive';
13
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>;
14
113
  type ButtonStyle = 'primary' | 'secondary' | 'text' | 'accent';
15
114
  type ButtonAction = 'submit' | 'manage' | 'close' | 'custom' | 'link';
16
115
  interface Button {
@@ -20,6 +119,10 @@ interface Button {
20
119
  cookies?: string[] | '*' | '!';
21
120
  url?: string;
22
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>;
23
126
  interface MainBanner {
24
127
  position: 'top' | 'bottom' | 'middle' | 'left-bottom' | 'right-bottom';
25
128
  overlayOpacity?: number;
@@ -28,39 +131,50 @@ interface MainBanner {
28
131
  heading?: string;
29
132
  headingTag?: string;
30
133
  htmlText: string;
31
- 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;
32
139
  }
33
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
+ */
34
150
  interface Category {
35
- id: string;
36
151
  heading: string;
37
152
  headingTag?: string;
38
153
  htmlText: string;
39
- mandatory?: boolean;
40
- type?: 'consent' | 'legitimate_interest';
41
- legitimateInterest?: {
42
- enabled?: boolean;
43
- description?: string;
44
- };
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. */
45
158
  cookies: string[];
46
159
  }
47
- 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"> {
48
163
  position?: 'left' | 'right' | 'center';
49
- overlayOpacity?: number;
50
- showClose?: boolean;
51
- showLocaleSwitcher?: boolean;
52
- heading?: string;
53
- headingTag?: string;
54
164
  subheading?: string;
55
- htmlText?: string;
56
- buttons: Button[];
57
- categories: Category[];
165
+ categories: CategoryMap;
58
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;
59
173
  }
60
174
  interface LocaleTranslations {
61
- mainBanner: MainBanner;
62
- gpcBanner?: GpcBanner;
63
- preferenceModal: PreferenceModal;
175
+ mainBanner: DeepPartial<MainBanner>;
176
+ gpcBanner?: DeepPartial<GpcBanner>;
177
+ preferenceModal: DeepPartial<PreferenceModal>;
64
178
  }
65
179
  interface ProfileTranslations {
66
180
  [locale: string]: LocaleTranslations;
@@ -72,53 +186,154 @@ interface DpdpaConfig {
72
186
  }
73
187
  interface LocaleTextContent {
74
188
  mainBanner: {
75
- heading: string;
189
+ heading?: string;
76
190
  htmlText: string;
77
- buttonLabels: string[];
191
+ /** Keyed by the linked UI template's button id — was a positional array matched to
192
+ * `buttons[i]` by index; now matched by key, robust against button reordering. */
193
+ buttonLabels?: Record<string, string>;
78
194
  };
79
195
  gpcBanner: {
80
- heading: string;
196
+ heading?: string;
81
197
  htmlText: string;
82
- buttonLabels: string[];
198
+ buttonLabels?: Record<string, string>;
83
199
  };
84
200
  preferenceModal: {
85
201
  heading: string;
86
202
  subheading?: string;
87
203
  htmlText?: string;
88
- buttonLabels: string[];
89
- categories: Array<{
90
- id: string;
204
+ buttonLabels?: Record<string, string>;
205
+ /** `legitimateInterestDescription` is only meaningful for categories whose legalBasis is 'legitimate_interest'; optional GDPR balancing-test text, translated per locale like heading/htmlText. */
206
+ categories: Record<string, {
91
207
  heading: string;
92
208
  htmlText: string;
209
+ legitimateInterestDescription?: string;
93
210
  }>;
211
+ receiptLabel?: string;
212
+ receiptDescription?: string;
94
213
  };
95
214
  }
96
215
  interface ProfileConfig {
97
- cookies?: Cookie[];
216
+ id: string;
217
+ cookies?: CookieMap;
98
218
  defaultLocale: string;
219
+ /** Days until consent expires and the visitor is asked again (profile-wide). Default: 365. */
220
+ expiryDays?: number;
99
221
  translations?: ProfileTranslations;
100
- gpcBanner?: boolean;
222
+ mainBanner: MainBanner;
223
+ gpcBanner?: GpcBanner;
224
+ preferenceModal: PreferenceModal;
101
225
  darkMode?: boolean;
102
226
  allowedOrigins?: string[];
103
227
  dpdpa?: DpdpaConfig;
104
228
  regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
105
229
  regulations?: string[];
106
- cookieTemplateId?: string;
230
+ consentTemplateId?: string;
107
231
  uiTemplateId?: string;
108
232
  localeContents?: Record<string, LocaleTextContent>;
233
+ complianceGroup?: ComplianceGroupId;
234
+ /**
235
+ * Free-form identifier (lower-kebab-case) for profiles that don't map to one of the
236
+ * built-in `complianceGroup` values — the identifier the widget's `compliance.type`
237
+ * config targets to fetch this profile. Participates in activation, deactivation, and
238
+ * "one active profile per group" conflict detection the same way `complianceGroup` does;
239
+ * no `COMPLIANCE_GROUPS` validation rules or GPC defaults apply to it, since none exist
240
+ * for a free-form name.
241
+ */
242
+ customComplianceGroup?: string;
243
+ /**
244
+ * Per-profile deltas applied on top of the consent template's authored `Cookie` values
245
+ * (e.g. an overridden `preGrant`) when resolving this profile — see the dashboard's Step 2
246
+ * pre-grant override UX. Keyed by cookie id; only the overridden fields need to be present.
247
+ */
248
+ cookiesOverride?: Record<string, Partial<Cookie>>;
249
+ /**
250
+ * Reserved for a future phase — stored but not yet applied when resolving a profile.
251
+ * Intended to mirror `cookiesOverride` for per-category deltas.
252
+ */
253
+ categoriesOverride?: Record<string, Partial<Category>>;
254
+ /**
255
+ * Reserved for a future phase — stored but not yet applied when resolving a profile.
256
+ * Intended to hold per-profile UI-template deltas.
257
+ */
258
+ uiOverride?: Record<string, unknown>;
259
+ /**
260
+ * When registered via `registerProfile()` and `complianceGroup` matches one of
261
+ * Consenti's built-in groups: `false` (default) fully replaces the built-in
262
+ * embedded profile for that group; `true` deep-merges this config onto the
263
+ * built-in profile instead (only meaningful together with `complianceGroup`).
264
+ */
265
+ deepMerge?: boolean;
266
+ gpcMode?: GpcMode;
267
+ isActive?: boolean;
268
+ hidePoweredBy?: boolean;
269
+ allowReceipt?: boolean;
270
+ /** Per-compliance extra config (e.g. DPDPA data fiduciary name). */
271
+ complianceConfig?: Record<string, string>;
272
+ /** Show a metadata footer strip in the banner/modal with Consent ID, Date, Version, Privacy Settings link. */
273
+ showFooterMetadata?: boolean;
274
+ /** Apply WCAG 2.1 AA button sizing (44px min-height), visible focus rings, and screen-reader labels. */
275
+ enhanceAccessibility?: boolean;
109
276
  }
277
+ /**
278
+ * Input accepted by `registerProfile()`. Either a complete `ProfileConfig`
279
+ * (used as-is or, if `complianceGroup` matches a built-in group, fully replaces
280
+ * it), or a partial overlay explicitly opted into `deepMerge: true` — merged
281
+ * onto the built-in embedded profile for `complianceGroup` at resolution time.
282
+ */
283
+ type RegisterableProfileConfig = ProfileConfig | (DeepPartial<ProfileConfig> & {
284
+ complianceGroup: ComplianceGroupId;
285
+ deepMerge: true;
286
+ });
110
287
  interface ResolvedProfile {
111
- id: number;
112
- version: number;
288
+ id: string;
289
+ /** Incremented in place on every save of this profile. Absent for embedded/local profiles that have no server-side version counter. */
290
+ version?: number;
113
291
  defaultLocale: string;
114
292
  locales?: string[];
115
- cookies: Cookie[];
293
+ cookies: CookieMap;
294
+ /** Days until consent expires and the visitor is asked again (profile-wide). Default: 365. */
295
+ expiryDays?: number;
296
+ allowReceipt?: boolean;
116
297
  mainBanner: MainBanner;
117
298
  gpcBanner?: GpcBanner;
118
299
  preferenceModal: PreferenceModal;
119
300
  cookieSigningKey?: string;
120
301
  dpdpa?: DpdpaConfig;
121
302
  darkMode?: boolean;
303
+ gpcMode?: GpcMode;
304
+ complianceGroup?: ComplianceGroupId;
305
+ complianceConfig?: Record<string, string>;
306
+ /** Falls back to `true` (hidden) if neither the widget config nor the profile set it. */
307
+ hidePoweredBy?: boolean;
308
+ /** Show metadata footer strip (Consent ID, Date, Version, Privacy Settings link). */
309
+ showFooterMetadata?: boolean;
310
+ /** Apply WCAG 2.1 AA accessibility enhancements to the widget. */
311
+ enhanceAccessibility?: boolean;
312
+ }
313
+ type WidgetCountryResolverFn = () => Promise<{
314
+ country: string | null;
315
+ region: string | null;
316
+ confidence: number;
317
+ }>;
318
+ interface AgeGateWidgetConfig {
319
+ enabled: boolean;
320
+ minimumAge: number;
321
+ requireParentalConsent?: boolean;
322
+ }
323
+ /** Mirrors the server-side `TcfConfig` shape (`@consenti/api`) — `cmpId`/`cmpVersion` must match
324
+ * what the backend uses to encode `tcfString`, since both feed the same simplified TC string. */
325
+ interface TcfWidgetConfig {
326
+ enabled: boolean;
327
+ cmpId: number;
328
+ cmpVersion: number;
329
+ }
330
+ interface ComplianceWidgetConfig {
331
+ type?: ComplianceType;
332
+ geoDataProvider?: 'default' | WidgetCountryResolverFn;
333
+ autoComplianceMap?: 'default' | ComplianceMapData | 'auto';
334
+ complianceMapUrl?: string;
335
+ ageGate?: AgeGateWidgetConfig;
336
+ tcf?: TcfWidgetConfig;
122
337
  }
123
338
  interface GtmConfig {
124
339
  containerId?: string;
@@ -126,6 +341,7 @@ interface GtmConfig {
126
341
  dataLayer?: string;
127
342
  urlPassthrough?: boolean;
128
343
  adsDataRedaction?: boolean;
344
+ verbose?: boolean;
129
345
  }
130
346
  interface UtilsConfig {
131
347
  gtm?: GtmConfig;
@@ -134,6 +350,9 @@ interface ApiConfig {
134
350
  enabled?: boolean;
135
351
  baseUrl?: string;
136
352
  authToken?: string;
353
+ tenantId?: string;
354
+ complianceGroup?: ComplianceGroupId;
355
+ trustDomain?: boolean;
137
356
  }
138
357
  interface ThemeConfig {
139
358
  bgColor?: string;
@@ -155,35 +374,60 @@ interface ThemeConfig {
155
374
  accentTextColor?: string;
156
375
  }
157
376
  interface CoreConfig {
158
- profileId?: number;
159
- autoHonorGPC?: boolean | 'strict';
377
+ tenantId?: string;
160
378
  locale?: string;
379
+ /** Text direction for the banner/modal root. `'auto'` (default) derives it from `locale`
380
+ * (Arabic, Hebrew, Persian, Urdu, ... → `rtl`); set explicitly to override. */
381
+ dir?: 'ltr' | 'rtl' | 'auto';
161
382
  disableCssTemplate?: boolean;
162
- signCookies?: boolean;
163
383
  cookieSigningKey?: string;
164
384
  allowReceipt?: boolean;
165
385
  cookieDomains?: string;
166
386
  storage?: 'cookie' | 'localStorage';
167
387
  theme?: ThemeConfig;
168
- privacyPolicyUrl?: string;
169
- regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
170
388
  userId?: string;
389
+ usePrebuiltProfiles?: 'all' | NonEmptyArray<ComplianceGroupId>;
390
+ cacheResolvedProfiles?: boolean;
391
+ console?: Array<'info' | 'log' | 'warning' | 'error'>;
171
392
  }
172
393
  interface ConsentiConfig {
173
- core: CoreConfig;
394
+ verbose?: boolean;
395
+ /** Optional — every field of `CoreConfig` is itself optional, so `new ConsentiSetup({})` and
396
+ * `new ConsentiSetup({ compliance: { type: 'opt-in' } })` (omitting `core` entirely) both work,
397
+ * matching the widget's own "Minimal config" documentation. */
398
+ core?: CoreConfig;
399
+ compliance?: ComplianceWidgetConfig;
174
400
  rootEl?: string | HTMLElement;
175
401
  darkMode?: boolean;
176
402
  autoInit?: boolean;
177
403
  api?: ApiConfig;
178
404
  utils?: UtilsConfig;
179
405
  plugins?: ConsentiPlugin$1[];
180
- profileOverride?: Partial<ResolvedProfile>;
406
+ profileOverride?: DeepPartial<ResolvedProfile>;
407
+ /** When true, suppresses the "Powered by Consenti" footer link in the banner and modal. */
408
+ hidePoweredBy?: boolean;
181
409
  }
410
+ type ConsentiEventName = 'bannerInitialized' | 'consenti:bannerInitialized' | 'bannerVisibility' | 'consenti:bannerVisibility' | 'modalVisibility' | 'consenti:modalVisibility' | 'consentBeingSubmitted' | 'consenti:consentBeingSubmitted' | 'consentSubmitted' | 'consenti:consentSubmitted' | 'parentalConsentRequired' | 'consenti:parentalConsentRequired';
182
411
  interface ConsentiWidgetAPI {
183
412
  hasConsent(): boolean;
184
413
  getConsent(): ConsentValue | null;
414
+ getConsent(type: ConsentType): Record<string, string> | null;
415
+ getConsent(type?: ConsentType): ConsentValue | Record<string, string> | null;
185
416
  getConsentDate(): Date | false;
186
417
  getGTMConsent(): Record<string, string> | null;
418
+ isCookieGranted(cookieId: string, requestValue?: boolean): boolean | ConsentStatus;
419
+ isCategoryGranted(categoryId: string, requestValue?: boolean): boolean | {
420
+ [cookieId: string]: ConsentStatus;
421
+ }[];
422
+ grantAll(onlyMandatory?: boolean): Promise<void>;
423
+ denyAll(includingMandatory?: boolean): Promise<void>;
424
+ on(event: ConsentiEventName, handler: (data: ConsentEvent) => void): void;
425
+ off(event: ConsentiEventName, handler: (data: ConsentEvent) => void): void;
426
+ version(): {
427
+ package: string;
428
+ profileVersion: string | null;
429
+ consentVersion: string | null;
430
+ };
187
431
  bannerVisibility(): 'main' | 'gpc' | false;
188
432
  modalVisibility(): 'preference' | false;
189
433
  getProfile(): ResolvedProfile | null;
@@ -191,13 +435,17 @@ interface ConsentiWidgetAPI {
191
435
  hideBanner(): void;
192
436
  showModal(triggerEl?: HTMLElement): void;
193
437
  hideModal(): void;
194
- submitConsent(consent: Partial<ConsentValue>): Promise<void>;
438
+ submitConsent(consent: Partial<ConsentValue>): Promise<ConsentDbRecord | void>;
195
439
  deleteConsent(): Promise<void>;
196
440
  reConsent(): Promise<void>;
197
441
  getRootElement(): HTMLElement | null;
198
442
  getBannerElement(): HTMLElement | null;
199
443
  getModalElement(): HTMLElement | null;
200
444
  switchLocale(locale: string): void;
445
+ setDarkMode(enable?: boolean): void;
446
+ setTheme(theme: Partial<ThemeConfig>): void;
447
+ setConfig(config: DeepPartial<ConsentiConfig>): void;
448
+ setProfile(override: DeepPartial<ResolvedProfile>): void;
201
449
  init(): Promise<void>;
202
450
  onReady(callback: () => void): void;
203
451
  destroy(): void;
@@ -226,41 +474,67 @@ interface ParsedConsent {
226
474
  interface ConsentRecord {
227
475
  visitorId: string;
228
476
  consentId?: string;
229
- profileId: number;
230
- profileVersion: number;
477
+ profileId: string;
231
478
  consent: ConsentValue;
232
479
  timestamp: string;
233
480
  gpc?: boolean;
234
- action?: ConsentAction;
481
+ action?: ConsentAction$1;
235
482
  pageUrl?: string;
236
483
  }
237
484
  interface ConsentReceipt {
238
- version: '1.0';
485
+ version: string;
239
486
  issuedAt: string;
240
487
  visitorId: string;
241
- profileId: number;
242
- profileVersion: number;
488
+ profileId: string;
243
489
  locale: string;
244
490
  consent: ConsentValue;
245
491
  signature?: string;
246
492
  }
247
- interface ConsentEvent {
248
- profileId?: number;
249
- consent?: ConsentValue;
250
- hasExistingConsent?: boolean;
251
- show?: boolean;
252
- action?: boolean;
253
- consentAction?: ConsentAction;
254
- consentId?: string;
255
- pageUrl?: string;
256
- apiResponse?: unknown;
493
+ interface BannerInitializedDetail {
494
+ profileId: string;
495
+ complianceGroup?: ComplianceType;
496
+ hasExistingConsent: boolean;
497
+ gpcDetected: boolean;
498
+ willShow: boolean;
499
+ }
500
+ interface BannerVisibilityDetail {
501
+ visible: boolean;
502
+ variant: 'main' | 'gpc';
503
+ action: boolean;
504
+ }
505
+ interface ModalVisibilityDetail {
506
+ visible: boolean;
507
+ action: boolean;
508
+ }
509
+ interface ConsentBeingSubmitted {
510
+ consentId: string;
511
+ visitorId: string;
512
+ profileId: string;
513
+ consentJson: ConsentValue;
514
+ consentAction: ConsentAction$1;
515
+ gpcDetected: boolean;
516
+ pageUrl: string;
517
+ timestamp: number;
257
518
  fromBroadcast?: boolean;
258
- gpcDetected?: boolean;
259
519
  }
520
+ interface ConsentSubmittedDetail extends ConsentBeingSubmitted {
521
+ apiResponse: ConsentDbRecord;
522
+ }
523
+ /** Dispatched when the age gate is declined and `requireParentalConsent` is set — see
524
+ * `AgeGateWidgetConfig`. A deny-all consent (mandatory cookies only) has already been
525
+ * submitted with `parentalConsentToken` attached; this event is the hook for the site
526
+ * owner to run their own out-of-band parental-consent verification. */
527
+ interface ParentalConsentRequiredDetail {
528
+ parentalConsentToken: string;
529
+ profileId: string;
530
+ visitorId: string;
531
+ timestamp: number;
532
+ }
533
+ type ConsentEvent = BannerInitializedDetail | BannerVisibilityDetail | ModalVisibilityDetail | ConsentBeingSubmitted | ConsentSubmittedDetail | ParentalConsentRequiredDetail;
260
534
  type ConsentiMessage = {
261
535
  type: 'consentUpdated';
262
536
  consent: ConsentValue;
263
- profileId: number;
537
+ profileId: string;
264
538
  } | {
265
539
  type: 'bannerClosed';
266
540
  } | {
@@ -277,7 +551,7 @@ type ConsentiMessage = {
277
551
  * @example
278
552
  * ```ts
279
553
  * const widget = new ConsentiSetup({
280
- * core: { profileId: 0 },
554
+ * compliance: { type: 'opt-in' },
281
555
  * })
282
556
  * await widget.ready
283
557
  * console.log(widget.hasConsent())
@@ -285,15 +559,30 @@ type ConsentiMessage = {
285
559
  */
286
560
 
287
561
  declare class ConsentiSetup implements ConsentiWidgetAPI {
288
- private config;
289
562
  private initialized;
290
563
  private initializing;
291
564
  private profile;
565
+ private resolvedBase;
566
+ /** cookieId → Category lookup, rebuilt whenever `profile` changes. Avoids rescanning categories on every consent action. */
567
+ private categoryIndex;
568
+ /** Watchers created by the declarative `data-consenti-*` DOM scan — destroyed alongside the widget since they have no other owner. */
569
+ private scannedScripts;
292
570
  private consentStore;
571
+ /**
572
+ * Stable per-browser identifier. Created lazily the first time a consent decision actually
573
+ * happens (manual submit, GPC auto-response, or relaying a decision already made in another
574
+ * tab) — never minted before that, so an undecided visitor leaves nothing in storage. See
575
+ * `getOrCreateVisitorId()`.
576
+ */
577
+ private visitorId;
293
578
  private eventBus;
294
579
  private banner;
295
580
  private modal;
581
+ private ageGate;
296
582
  private renderer;
583
+ /** Set once the age gate (if enabled) resolves; threaded onto every `submitConsent` call thereafter. */
584
+ private _ageVerified;
585
+ private _parentalConsentToken;
297
586
  private broadcastChannel;
298
587
  private readyResolve;
299
588
  private readyPromise;
@@ -302,13 +591,23 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
302
591
  private lastSignature;
303
592
  /** Tracks which banner variant is currently visible, or null when hidden. */
304
593
  private _bannerVariant;
594
+ /** Tracks which banner variant is currently mounted in the DOM (persists when banner is hidden). */
595
+ private _bannerMounted;
596
+ /** Active focus trap keydown listener — removed when modal closes. */
597
+ private _focusTrapListener;
598
+ /** Maps original handler functions to their DOM-wrapped listeners for on/off bookkeeping. */
599
+ private _eventHandlers;
600
+ /** `core` defaulted to `{}` in the constructor below — every one of its own fields is
601
+ * optional, so this narrows back to non-optional for every other method in this class. */
602
+ private config;
305
603
  /**
306
604
  * Creates and initialises the consent widget.
307
605
  *
308
606
  * Construction is synchronous; the async `init()` is launched in the background.
309
607
  * Use `await widget.ready` or `widget.onReady(cb)` to run code after init completes.
310
608
  *
311
- * @param config - Widget configuration. See `ConsentiConfig` for all options.
609
+ * @param config - Widget configuration. See `ConsentiConfig` for all options. `core` may be
610
+ * omitted entirely — every one of its own fields is optional too.
312
611
  */
313
612
  constructor(config: ConsentiConfig);
314
613
  private profileId;
@@ -318,8 +617,53 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
318
617
  * to re-initialise the same instance.
319
618
  */
320
619
  init(): Promise<void>;
620
+ /**
621
+ * Everything after the "does consent already exist" check — GPC/CCPA/CPRA silent paths,
622
+ * then the normal opt-in banner. Extracted from `init()` so the age gate (when enabled and
623
+ * no consent exists yet) can defer this whole sequence until the visitor answers it via
624
+ * `resolveAgeGate()`, then resume here with `hasExisting` fixed at `false`.
625
+ */
626
+ private continueInit;
627
+ /** Mounts the age-gate prompt (`compliance.ageGate`) in place of the normal banner flow. */
628
+ private showAgeGate;
629
+ /**
630
+ * Handles the visitor's age-gate answer.
631
+ * - Confirmed → records `ageVerified: true` for all future `submitConsent` calls and
632
+ * resumes the normal init flow via `continueInit()`.
633
+ * - Declined, no parental consent required → immediately submits a deny-all consent
634
+ * (mandatory cookies still granted), mirroring the GPC-strict silent-write pattern.
635
+ * - Declined, parental consent required → same deny-all submission, plus a
636
+ * `parentalConsentToken` and a `consenti:parentalConsentRequired` event so the site
637
+ * owner can wire their own out-of-band verification flow (no email infra exists in
638
+ * this package to do that here).
639
+ */
640
+ private resolveAgeGate;
641
+ /**
642
+ * Marks the widget ready and runs the declarative DOM-scan for
643
+ * `data-consenti-consent-script`/`data-consenti-category-script` tags. Called from
644
+ * every exit path of `init()` so the scan always runs exactly once per init cycle,
645
+ * whether `init()` was triggered automatically (`autoInit !== false`) or explicitly
646
+ * by the caller (`autoInit: false`) — it never runs before the widget actually has
647
+ * a resolved profile to read consent from.
648
+ */
649
+ private finishInit;
650
+ /** Sets the `dir` attribute on the root element from `core.dir` (default `'auto'`, derived from `core.locale`). */
651
+ private applyDirection;
321
652
  /** Applies the `consenti-root--dark` class based on config or profile flag. */
322
653
  private applyDarkMode;
654
+ /** Applies `consenti--enhanced-a11y` class when `profile.enhanceAccessibility` is set. */
655
+ private applyAccessibility;
656
+ /** Builds and appends the metadata footer strip to a banner or modal container. */
657
+ private _appendMetaFooter;
658
+ /**
659
+ * Injects a `<style>` element into the root for `stackButtonsOnBreakpoint`.
660
+ * Called once per profile load; a new style replaces the previous one.
661
+ */
662
+ private applyStackButtonsBreakpoint;
663
+ /** Installs a keyboard focus trap on `container` while the modal is open. */
664
+ private _installFocusTrap;
665
+ /** Removes the active focus trap keyboard listener. */
666
+ private _removeFocusTrap;
323
667
  /**
324
668
  * Injects CSS custom property overrides supplied via `core.theme` into the
325
669
  * Consenti root element so they cascade down to all widget elements.
@@ -334,6 +678,8 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
334
678
  * @param invoke - Function that calls the desired hook method on a single plugin.
335
679
  */
336
680
  private callPluginHook;
681
+ private _buildGrantAllConsent;
682
+ private _buildDenyAllConsent;
337
683
  private buildHandlers;
338
684
  /**
339
685
  * Classifies a consent submission as `accept_all`, `reject_all`, `custom`, or `update`.
@@ -347,22 +693,23 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
347
693
  */
348
694
  hasConsent(): boolean;
349
695
  /**
350
- * Returns the stored consent map, or `null` if no consent has been saved.
351
- * Always returns `null` during SSR.
696
+ * Returns the stored consent map, optionally transformed for a specific vendor.
697
+ * Pass `type` to get a vendor-ready object instead of the raw Consenti format.
698
+ * Always returns `null` during SSR or when no consent has been saved.
699
+ *
700
+ * @param type - Vendor format: 'google-gtm', 'category', 'adobe', 'meta',
701
+ * 'microsoft-clarity', 'twilio-segment', or omit for raw format.
352
702
  */
353
703
  getConsent(): ConsentValue | null;
704
+ getConsent(type: ConsentType): Record<string, string> | null;
354
705
  /**
355
706
  * Returns the `Date` when consent was last saved, or `false` if no consent exists.
356
707
  * Always returns `false` during SSR.
357
708
  */
358
709
  getConsentDate(): Date | false;
359
710
  /**
360
- * Returns the consent map in Google Consent Mode v2 format, ready to pass to
361
- * `gtag('consent', 'update', ...)`. Returns `null` if no consent has been saved.
362
- *
363
- * Maps Consenti cookie IDs to the seven Google Consent Mode v2 parameters.
364
- * Cookies whose IDs exactly match a GCM v2 key are forwarded directly.
365
- * Non-GCM cookies are ignored by this method (they appear in `getConsent()` only).
711
+ * @deprecated Use `getConsent('google-gtm')` instead.
712
+ * Returns the consent map in Google Consent Mode v2 format.
366
713
  */
367
714
  getGTMConsent(): Record<string, string> | null;
368
715
  /**
@@ -374,7 +721,7 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
374
721
  * @param consent - Partial or full map of cookie ID → status.
375
722
  * @param gpcDerived - Internal flag set to `true` when called from the GPC strict path.
376
723
  */
377
- submitConsent(consent: Partial<ConsentValue>, gpcDerived?: boolean): Promise<void>;
724
+ submitConsent(consent: Partial<ConsentValue>, gpcDerived?: boolean, source?: number): Promise<ConsentDbRecord | void>;
378
725
  /**
379
726
  * Returns the currently visible banner variant, or `false` if no banner is shown.
380
727
  * Useful for programmatically checking state before calling `showBanner` / `hideBanner`.
@@ -482,6 +829,93 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
482
829
  * @param locale - BCP 47 locale code to switch to (e.g. `'fr'`, `'de-AT'`).
483
830
  */
484
831
  switchLocale(locale: string): void;
832
+ /**
833
+ * Returns `true` if the named cookie's consent is `'granted'`.
834
+ * When `requestValue` is `true`, returns the raw `ConsentStatus` string instead of a boolean,
835
+ * or `false` if the cookie is not in the consent map.
836
+ */
837
+ isCookieGranted(cookieId: string, requestValue?: boolean): boolean | 'granted' | 'denied' | 'objected';
838
+ /**
839
+ * Checks consent for all cookies belonging to the given category.
840
+ *
841
+ * With `requestValue = false` (default): returns `true` only when every cookie in the
842
+ * category is `'granted'`, `false` otherwise.
843
+ *
844
+ * With `requestValue = true`: returns an array of one-key records — one per cookie in the
845
+ * category — e.g. `[{ analytics: 'granted' }, { pixel: 'denied' }]`.
846
+ *
847
+ * Returns `false` / `[]` if the category is not found or the widget is not yet initialized.
848
+ */
849
+ isCategoryGranted(categoryId: string, requestValue?: boolean): boolean | {
850
+ [cookieId: string]: 'granted' | 'denied' | 'objected';
851
+ }[];
852
+ /**
853
+ * Programmatically accepts cookies and hides the banner.
854
+ *
855
+ * @param onlyMandatory - When `true`, only mandatory cookies are granted and all others
856
+ * are set to `'denied'`. Defaults to `false` (grant everything).
857
+ */
858
+ grantAll(onlyMandatory?: boolean): Promise<void>;
859
+ /**
860
+ * Programmatically denies cookies and hides the banner.
861
+ *
862
+ * @param includingMandatory - When `true`, mandatory cookies are also denied. Defaults to
863
+ * `false` (mandatory cookies remain `'granted'`). Passing `true`
864
+ * logs a warning — ensure this is intentional.
865
+ */
866
+ denyAll(includingMandatory?: boolean): Promise<void>;
867
+ /**
868
+ * Subscribes to a Consenti widget event. The `consenti:` prefix is optional —
869
+ * both `'consentSubmitted'` and `'consenti:consentSubmitted'` are accepted.
870
+ *
871
+ * The same handler reference must be passed to `off()` to unsubscribe.
872
+ *
873
+ * @example
874
+ * ```ts
875
+ * const handler = (data) => console.log(data)
876
+ * widget.on('consentSubmitted', handler)
877
+ * // later:
878
+ * widget.off('consentSubmitted', handler)
879
+ * ```
880
+ */
881
+ on(event: ConsentiEventName, handler: (data: ConsentEvent) => void): void;
882
+ /**
883
+ * Removes a previously registered event handler. The same function reference passed to
884
+ * `on()` must be used here.
885
+ */
886
+ off(event: ConsentiEventName, handler: (data: ConsentEvent) => void): void;
887
+ /**
888
+ * Returns the current version of the package and the active profile/consent versions.
889
+ * Useful for debugging and support diagnostics.
890
+ */
891
+ version(): {
892
+ package: string;
893
+ profileVersion: string;
894
+ consentVersion: string | null;
895
+ };
896
+ /**
897
+ * Toggles or sets dark mode at runtime without re-initialising the widget.
898
+ * When `enable` is omitted, the current dark mode state is toggled.
899
+ */
900
+ setDarkMode(enable?: boolean): void;
901
+ /**
902
+ * Applies partial theme token overrides at runtime, merging them into the current theme.
903
+ * CSS custom properties on the root element are updated immediately.
904
+ */
905
+ setTheme(theme: Partial<ThemeConfig>): void;
906
+ /**
907
+ * Deep-merges a partial config into the current widget config.
908
+ * Theme and dark mode side-effects are re-applied immediately.
909
+ * Changes to locale or profile require an explicit `switchLocale()` or `init()` call.
910
+ */
911
+ setConfig(config: DeepPartial<ConsentiConfig>): void;
912
+ /**
913
+ * Merges a partial profile override into the current profile and re-renders any visible UI.
914
+ * Does not make a network request — the resolved base profile from the last `init()` is reused.
915
+ *
916
+ * No-op before `init()` completes (while `resolvedBase` is null).
917
+ */
918
+ setProfile(override: DeepPartial<ResolvedProfile>): void;
485
919
  /**
486
920
  * Destroys the widget — removes all DOM elements, disconnects the BroadcastChannel,
487
921
  * and calls `destroy()` on all registered plugins.
@@ -580,28 +1014,36 @@ declare abstract class ConsentiPlugin {
580
1014
  * import { ConsentiProfile, ConsentiSetup } from '@consenti/ui'
581
1015
  *
582
1016
  * const profile = new ConsentiProfile({
583
- * cookies: [
584
- * { id: 'necessary', mandatory: true },
585
- * { id: 'analytics' },
586
- * ],
1017
+ * cookies: {
1018
+ * necessary: {},
1019
+ * analytics: {},
1020
+ * },
587
1021
  * defaultLocale: 'en',
588
1022
  * translations: {
589
1023
  * en: {
590
1024
  * mainBanner: {
591
1025
  * position: 'bottom',
592
1026
  * htmlText: 'We use cookies.',
593
- * buttons: [{ text: 'Accept All', type: 'primary', cookies: '*' }],
1027
+ * buttons: { 'accept-all': { text: 'Accept All', style: 'primary', action: 'submit', cookies: '*' } },
594
1028
  * },
595
1029
  * preferenceModal: {
596
- * categories: [],
597
- * buttons: [{ text: 'Save', type: 'submit' }],
1030
+ * categories: {
1031
+ * necessary: { heading: 'Necessary', htmlText: '...', legalBasis: 'mandatory', cookies: ['necessary'] },
1032
+ * },
1033
+ * buttons: { save: { text: 'Save', style: 'primary', action: 'submit' } },
598
1034
  * },
599
1035
  * },
600
1036
  * },
601
1037
  * })
602
1038
  *
603
- * const widget = new ConsentiSetup({ core: { profileId: profile.getId() } })
1039
+ * const widget = new ConsentiSetup({ core: { profileId: profile.getType() } })
604
1040
  * ```
1041
+ *
1042
+ * To instead patch one of Consenti's built-in profiles for a given `complianceGroup`
1043
+ * (rather than defining a full standalone profile), pass `complianceGroup` and
1044
+ * `deepMerge: true` with only the fields you want to override — see
1045
+ * `RegisterableProfileConfig` and the `complianceGroup`/`deepMerge` resolver behavior
1046
+ * in `profile-resolver.ts`.
605
1047
  */
606
1048
 
607
1049
  /**
@@ -609,15 +1051,17 @@ declare abstract class ConsentiPlugin {
609
1051
  */
610
1052
  declare class ConsentiProfile {
611
1053
  private config;
612
- private id;
1054
+ private type;
613
1055
  /**
614
- * @param config - Full profile configuration including cookies, locales, and UI text.
1056
+ * @param config - Full profile configuration including cookies, locales, and UI text,
1057
+ * or a `{ complianceGroup, deepMerge: true, ... }` partial overlay to
1058
+ * patch a built-in profile instead of defining a standalone one.
615
1059
  */
616
- constructor(config: ProfileConfig);
1060
+ constructor(config: RegisterableProfileConfig);
617
1061
  /** Returns the auto-assigned numeric profile ID. Pass this to `core.profileId`. */
618
- getId(): number;
619
- /** Returns the raw `ProfileConfig` passed to the constructor. */
620
- toJSON(): ProfileConfig;
1062
+ getType(): Symbol;
1063
+ /** Returns the raw config passed to the constructor. */
1064
+ toJSON(): RegisterableProfileConfig;
621
1065
  }
622
1066
 
623
1067
  /**
@@ -655,8 +1099,12 @@ interface ConsentScriptOptions {
655
1099
  widget: ConsentiSetup;
656
1100
  /** Remote script `src` URL to inject when consent is granted. */
657
1101
  src?: string;
658
- /** Inline JavaScript content to execute when consent is granted. */
659
- innerHTML?: string;
1102
+ /**
1103
+ * Inline JavaScript to inject when consent is granted (e.g. a GTM snippet).
1104
+ * WARNING: this string is assigned directly to `script.innerHTML` — only
1105
+ * pass a static, trusted literal. Never pass user-supplied or remote content.
1106
+ */
1107
+ unsafeInnerHTML?: string;
660
1108
  /**
661
1109
  * Additional HTML attributes applied to the injected `<script>` element.
662
1110
  * Set a key to an empty string for boolean attributes (e.g. `{ async: '', defer: '' }`).
@@ -666,6 +1114,12 @@ interface ConsentScriptOptions {
666
1114
  onLoad?: () => void;
667
1115
  /** Called after the `<script>` element has been removed from the DOM. */
668
1116
  onRevoke?: () => void;
1117
+ /**
1118
+ * When `true` (default), the script is automatically removed when consent is revoked and
1119
+ * re-injected when consent is re-granted. Set to `false` to evaluate consent only once at
1120
+ * construction time — no event listener is attached and the script is never auto-removed.
1121
+ */
1122
+ bind?: boolean;
669
1123
  }
670
1124
  /**
671
1125
  * Conditionally injects and removes a `<script>` element based on a specific
@@ -695,37 +1149,248 @@ declare class ConsentScript {
695
1149
  }
696
1150
 
697
1151
  /**
698
- * CookieTrigger — a pre-built accessible UI element that reopens the Consenti
699
- * preference panel when clicked.
1152
+ * ConsentAction — runs a callback based on a specific consent parameter's grant status.
1153
+ *
1154
+ * Same lifecycle as `ConsentScript` (evaluates immediately on construction so
1155
+ * existing consent is honoured, re-evaluates on `consenti:consentSubmitted`,
1156
+ * `destroy()` to unbind) but fires `onGrant`/`onDeny` callbacks instead of
1157
+ * injecting a `<script>` element — for integrations that expose their own
1158
+ * `sdk.optIn()`/`sdk.optOut()` API (Segment, Mixpanel, Amplitude, Sentry, etc.)
1159
+ * rather than needing a script tag toggled. Use `ConsentScript` for that case instead.
1160
+ *
1161
+ * @example
1162
+ * ```ts
1163
+ * import { ConsentiSetup, ConsentAction } from '@consenti/ui'
1164
+ *
1165
+ * const widget = new ConsentiSetup({ core: { profileId: 0 } })
1166
+ * await widget.ready
1167
+ *
1168
+ * new ConsentAction({
1169
+ * id: 'analytics_storage',
1170
+ * widget,
1171
+ * onGrant: () => analyticsSdk.optIn(),
1172
+ * onDeny: () => analyticsSdk.optOut(),
1173
+ * })
1174
+ * ```
1175
+ */
1176
+
1177
+ /** Passed to `onGrant`/`onDeny` — the resolved parameter's metadata plus the full current consent map. */
1178
+ interface ConsentActionParams {
1179
+ cookieId: string;
1180
+ status: ConsentStatus;
1181
+ cookie: Cookie | undefined;
1182
+ consent: ConsentValue | null;
1183
+ }
1184
+ /** Options for constructing a {@link ConsentAction} instance. */
1185
+ interface ConsentActionOptions {
1186
+ /** The consent parameter ID to watch (e.g. `'analytics_storage'`). */
1187
+ id: string;
1188
+ /** The `ConsentiSetup` widget instance to read consent from. */
1189
+ widget: ConsentiSetup;
1190
+ /**
1191
+ * When `true` (default), the callbacks re-fire automatically on every future consent
1192
+ * change. Set to `false` to evaluate consent only once at construction time — no
1193
+ * event listener is attached.
1194
+ */
1195
+ bind?: boolean;
1196
+ /** Called when the parameter's status transitions to `'granted'`. */
1197
+ onGrant?: (params: ConsentActionParams) => void;
1198
+ /** Called when the parameter's status transitions away from `'granted'` (`'denied'` or `'objected'`). */
1199
+ onDeny?: (params: ConsentActionParams) => void;
1200
+ }
1201
+ /**
1202
+ * Watches a single consent parameter's grant status and fires `onGrant`/`onDeny`
1203
+ * callbacks on transitions. Call {@link ConsentAction.destroy} when no longer
1204
+ * needed to remove the event listener.
1205
+ */
1206
+ declare class ConsentAction {
1207
+ private readonly options;
1208
+ private granted;
1209
+ private readonly handler;
1210
+ constructor(options: ConsentActionOptions);
1211
+ private evaluate;
1212
+ /** Removes the event listener (if bound). */
1213
+ destroy(): void;
1214
+ }
1215
+
1216
+ /**
1217
+ * CategoryAction — runs a callback based on a category's rollup consent state.
1218
+ *
1219
+ * A category is considered "granted" only when *every* parameter it contains is
1220
+ * `'granted'` — matching the preference modal's tri-state rollup rule. `onDeny`
1221
+ * fires for both a fully-denied category and a partially-granted ("mixed") one,
1222
+ * since neither represents full consent for the category.
1223
+ *
1224
+ * Same lifecycle as `ConsentAction`: evaluates immediately on construction,
1225
+ * re-evaluates on `consenti:consentSubmitted`, `destroy()` to unbind.
1226
+ *
1227
+ * @example
1228
+ * ```ts
1229
+ * import { ConsentiSetup, CategoryAction } from '@consenti/ui'
1230
+ *
1231
+ * const widget = new ConsentiSetup({ core: { profileId: 0 } })
1232
+ * await widget.ready
1233
+ *
1234
+ * new CategoryAction({
1235
+ * id: 'marketing',
1236
+ * widget,
1237
+ * onGrant: () => adSdk.enableAll(),
1238
+ * onDeny: () => adSdk.disableAll(),
1239
+ * })
1240
+ * ```
1241
+ */
1242
+
1243
+ /** Passed to `onGrant`/`onDeny` — the category's member parameter IDs plus the full current consent map. */
1244
+ interface CategoryActionParams {
1245
+ categoryId: string;
1246
+ cookieIds: string[];
1247
+ consent: ConsentValue | null;
1248
+ }
1249
+ /** Options for constructing a {@link CategoryAction} instance. */
1250
+ interface CategoryActionOptions {
1251
+ /** The category ID to watch (e.g. `'marketing'`). */
1252
+ id: string;
1253
+ /** The `ConsentiSetup` widget instance to read consent from. */
1254
+ widget: ConsentiSetup;
1255
+ /**
1256
+ * When `true` (default), the callbacks re-fire automatically on every future consent
1257
+ * change. Set to `false` to evaluate consent only once at construction time — no
1258
+ * event listener is attached.
1259
+ */
1260
+ bind?: boolean;
1261
+ /** Called when every parameter in the category transitions to `'granted'`. */
1262
+ onGrant?: (params: CategoryActionParams) => void;
1263
+ /** Called when the category transitions away from fully-granted (any parameter denied/objected). */
1264
+ onDeny?: (params: CategoryActionParams) => void;
1265
+ }
1266
+ /**
1267
+ * Watches a category's rollup grant status and fires `onGrant`/`onDeny` callbacks
1268
+ * on transitions. Call {@link CategoryAction.destroy} when no longer needed to
1269
+ * remove the event listener.
1270
+ */
1271
+ declare class CategoryAction {
1272
+ private readonly options;
1273
+ private granted;
1274
+ private readonly handler;
1275
+ constructor(options: CategoryActionOptions);
1276
+ private evaluate;
1277
+ /** Removes the event listener (if bound). */
1278
+ destroy(): void;
1279
+ }
1280
+
1281
+ /**
1282
+ * CategoryScript — conditionally loads/unloads a third-party `<script>` tag
1283
+ * based on a whole category's rollup consent state, instead of one parameter.
1284
+ *
1285
+ * A category is considered "granted" only when *every* parameter it contains is
1286
+ * `'granted'` — matching the preference modal's tri-state rollup rule and
1287
+ * `CategoryAction`'s behavior. Injection mechanics (src validation, inline-script
1288
+ * escape hatch, attribute filtering) are identical to `ConsentScript` — both share
1289
+ * the same underlying `injectScript`/`revokeScript` helpers.
1290
+ *
1291
+ * @example
1292
+ * ```ts
1293
+ * import { ConsentiSetup, CategoryScript } from '@consenti/ui'
1294
+ *
1295
+ * const widget = new ConsentiSetup({ core: { profileId: 0 } })
1296
+ * await widget.ready
1297
+ *
1298
+ * new CategoryScript({
1299
+ * categoryId: 'marketing',
1300
+ * widget,
1301
+ * src: 'https://example.com/ad-pixel.js',
1302
+ * })
1303
+ * ```
1304
+ */
1305
+
1306
+ /** Options for constructing a {@link CategoryScript} instance. */
1307
+ interface CategoryScriptOptions {
1308
+ /** The category ID to watch (e.g. `'marketing'`). */
1309
+ categoryId: string;
1310
+ /** The `ConsentiSetup` widget instance to read consent from. */
1311
+ widget: ConsentiSetup;
1312
+ /** Remote script `src` URL to inject when every parameter in the category is granted. */
1313
+ src?: string;
1314
+ /**
1315
+ * Inline JavaScript to inject when granted (e.g. a GTM snippet).
1316
+ * WARNING: this string is assigned directly to `script.innerHTML` — only
1317
+ * pass a static, trusted literal. Never pass user-supplied or remote content.
1318
+ */
1319
+ unsafeInnerHTML?: string;
1320
+ /**
1321
+ * Additional HTML attributes applied to the injected `<script>` element.
1322
+ * Set a key to an empty string for boolean attributes (e.g. `{ async: '', defer: '' }`).
1323
+ */
1324
+ attributes?: Record<string, string>;
1325
+ /** Called after the `<script>` element has been appended to `<head>`. */
1326
+ onLoad?: () => void;
1327
+ /** Called after the `<script>` element has been removed from the DOM. */
1328
+ onRevoke?: () => void;
1329
+ /**
1330
+ * When `true` (default), the script is automatically removed when the category is no
1331
+ * longer fully granted and re-injected when it becomes fully granted again. Set to
1332
+ * `false` to evaluate consent only once at construction time — no event listener is
1333
+ * attached and the script is never auto-removed.
1334
+ */
1335
+ bind?: boolean;
1336
+ }
1337
+ /**
1338
+ * Conditionally injects and removes a `<script>` element based on a category's
1339
+ * rollup consent status (every member parameter granted).
1340
+ *
1341
+ * Call {@link CategoryScript.destroy} when the trigger is no longer needed
1342
+ * (e.g. on SPA route unmount) to remove the event listener and the injected element.
1343
+ */
1344
+ declare class CategoryScript {
1345
+ private readonly options;
1346
+ private scriptEl;
1347
+ private readonly handler;
1348
+ constructor(options: CategoryScriptOptions);
1349
+ private evaluate;
1350
+ private inject;
1351
+ private revoke;
1352
+ /**
1353
+ * Removes the event listener and the injected `<script>` element (if present).
1354
+ * Call this when the widget is destroyed or the script is no longer needed.
1355
+ */
1356
+ destroy(): void;
1357
+ }
1358
+
1359
+ /**
1360
+ * BannerTrigger — a pre-built accessible UI element that reopens the Consenti
1361
+ * banner or preference modal when clicked.
700
1362
  *
701
1363
  * Use this to add a "Cookie Settings" link in your site footer or cookie policy
702
1364
  * page without wiring up the click handler manually or importing the widget module
703
1365
  * into your layout component.
704
1366
  *
1367
+ * (Renamed from `CookieTrigger` — it was never scoped to a specific cookie, it
1368
+ * just opens the banner or modal.)
1369
+ *
705
1370
  * @example
706
1371
  * ```ts
707
- * import { ConsentiSetup, CookieTrigger } from '@consenti/ui'
1372
+ * import { ConsentiSetup, BannerTrigger } from '@consenti/ui'
708
1373
  *
709
1374
  * const widget = new ConsentiSetup({ core: { profileId: 0 } })
710
1375
  *
711
1376
  * // Attach to an existing element
712
- * new CookieTrigger({ widget, el: '#footer-cookie-settings' })
1377
+ * new BannerTrigger({ widget, el: '#footer-cookie-settings' })
713
1378
  *
714
1379
  * // — or — create a new button and place it yourself
715
- * const trigger = new CookieTrigger({ widget, label: 'Cookie Settings' })
1380
+ * const trigger = new BannerTrigger({ widget, label: 'Cookie Settings' })
716
1381
  * document.querySelector('.site-footer')?.appendChild(trigger.getElement())
717
1382
  * ```
718
1383
  */
719
1384
 
720
- /** Options for constructing a {@link CookieTrigger} instance. */
721
- interface CookieTriggerOptions {
1385
+ /** Options for constructing a {@link BannerTrigger} instance. */
1386
+ interface BannerTriggerOptions {
722
1387
  /** The widget instance to control when the trigger is clicked. */
723
1388
  widget: ConsentiSetup;
724
1389
  /**
725
1390
  * Element to attach the click handler to.
726
1391
  * - `HTMLElement` — used directly.
727
1392
  * - `string` — treated as a CSS selector; the first matching element is used.
728
- * - Omitted — a new `<button>` is created. Retrieve it with {@link CookieTrigger.getElement}
1393
+ * - Omitted — a new `<button>` is created. Retrieve it with {@link BannerTrigger.getElement}
729
1394
  * and append it to the DOM at the desired location.
730
1395
  */
731
1396
  el?: HTMLElement | string;
@@ -748,12 +1413,12 @@ interface CookieTriggerOptions {
748
1413
  * The created button uses the `.consenti-btn` and `.consenti-btn--text` classes so
749
1414
  * it inherits the widget's theme automatically.
750
1415
  */
751
- declare class CookieTrigger {
1416
+ declare class BannerTrigger {
752
1417
  private readonly el;
753
1418
  private readonly clickHandler;
754
1419
  /** Whether this instance created the element (used to decide if `destroy()` removes it). */
755
1420
  private readonly created;
756
- constructor(options: CookieTriggerOptions);
1421
+ constructor(options: BannerTriggerOptions);
757
1422
  /**
758
1423
  * Returns the trigger element.
759
1424
  * When no `el` was provided, append the returned element to the desired location
@@ -767,4 +1432,42 @@ declare class CookieTrigger {
767
1432
  destroy(): void;
768
1433
  }
769
1434
 
770
- export { type ApiConfig, type Button, type ButtonAction, type ButtonStyle, type Category, type ConsentAction, type ConsentEvent, type ConsentReceipt, type ConsentRecord, ConsentScript, type ConsentScriptOptions, type ConsentStatus, type ConsentValue, type ConsentiConfig, type ConsentiMessage, ConsentiPlugin, ConsentiProfile, ConsentiSetup, type ConsentiWidgetAPI, type Cookie, type CookieOptions, CookieTrigger, type CookieTriggerOptions, type CoreConfig, type GpcBanner, type GtmConfig, type LocaleTranslations, type MainBanner, type ParsedConsent, type PreferenceModal, type ProfileConfig, type ProfileTranslations, type ResolvedProfile, type ThemeConfig, type UtilsConfig };
1435
+ /**
1436
+ * Declarative DOM-scan bindings — a zero-JS integration path for gating
1437
+ * third-party scripts via `data-*` attributes instead of hand-written
1438
+ * `ConsentScript`/`CategoryScript` calls.
1439
+ *
1440
+ * ```html
1441
+ * <script type="text/plain" data-consenti-category-script="marketing" src="https://example.com/pixel.js"></script>
1442
+ * <script type="text/plain" data-consenti-consent-script="analytics_storage">/* inline snippet *\/</script>
1443
+ * <script type="text/plain" data-consenti-consent-script="ad_storage" data-consenti-bind="false" src="..."></script>
1444
+ * ```
1445
+ *
1446
+ * Scanned automatically once per `ConsentiSetup.init()` cycle (after `init()`
1447
+ * completes — so it respects `autoInit: false`, only running once the widget is
1448
+ * actually initialized, however `init()` was triggered). The scanned tags are
1449
+ * read-only config sources and stay inert (`type="text/plain"`) in the DOM
1450
+ * permanently — `ConsentScript`/`CategoryScript` inject their own separate
1451
+ * `<script>` element exactly as they do when constructed by hand.
1452
+ */
1453
+
1454
+ /**
1455
+ * Scans the document for `<script type="text/plain">` tags carrying
1456
+ * `data-consenti-consent-script` or `data-consenti-category-script`, and
1457
+ * instantiates `ConsentScript`/`CategoryScript` for each. Called automatically
1458
+ * by `ConsentiSetup` at the end of every `init()` cycle.
1459
+ *
1460
+ * v1 scans once automatically; scripts injected into the DOM after that point
1461
+ * (e.g. by a late-loading CMS widget) are not picked up automatically — call this
1462
+ * function again manually after adding more tags dynamically if needed. Each element
1463
+ * is only ever scanned once (marked with `data-consenti-scanned`), so calling this
1464
+ * again is safe — it only processes newly-added tags, never re-instantiates a watcher
1465
+ * for one already scanned.
1466
+ *
1467
+ * Returns the created watchers — `ConsentiSetup` holds onto these and calls
1468
+ * `.destroy()` on each when the widget itself is destroyed, since a scanner-created
1469
+ * watcher has no other owner to release its `consenti:consentSubmitted` listener.
1470
+ */
1471
+ declare function scanConsentScripts(widget: ConsentiSetup): Array<ConsentScript | CategoryScript>;
1472
+
1473
+ export { type AgeGateWidgetConfig, type ApiConfig, BannerTrigger, type BannerTriggerOptions, type Button, type ButtonAction, type ButtonStyle, type Category, CategoryAction, type CategoryActionOptions, type CategoryActionParams, type CategoryMap, CategoryScript, type CategoryScriptOptions, type ComplianceWidgetConfig, ConsentAction, type ConsentActionOptions, type ConsentActionParams, type ConsentAction$1 as ConsentActionType, type ConsentEvent, type ConsentReceipt, type ConsentRecord, ConsentScript, type ConsentScriptOptions, type ConsentStatus, type ConsentValue, type ConsentiConfig, type ConsentiEventName, type ConsentiMessage, ConsentiPlugin, ConsentiProfile, ConsentiSetup, type ConsentiWidgetAPI, type Cookie, type CookieMap, type CookieOptions, type CoreConfig, type GpcBanner, type GtmConfig, type LocaleTranslations, type MainBanner, type ParentalConsentRequiredDetail, type ParsedConsent, type PreferenceModal, type ProfileConfig, type ProfileTranslations, type RegisterableProfileConfig, type ResolvedProfile, type TcfWidgetConfig, type ThemeConfig, type UtilsConfig, scanConsentScripts };