@consenti/ui 0.2.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.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,18 @@
1
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
+ */
2
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 ? {
3
- [Key in keyof T]?: DeepPartial<T[Key]>;
11
+ [Key in keyof T]?: DeepPartial<T[Key]> | null;
4
12
  } : T;
5
13
 
6
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"];
7
16
 
8
17
  /**
9
18
  * Compliance-related literal types, derived from the regulation/group ID data
@@ -19,6 +28,13 @@ declare const COMPLIANCE_GROUP_IDS: readonly ["opt-in", "opt-out", "opt-out-stri
19
28
  */
20
29
 
21
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;
22
38
 
23
39
  interface ComplianceMapData {
24
40
  version: string;
@@ -33,24 +49,67 @@ interface CountryComplianceEntry {
33
49
  description: string;
34
50
  }>;
35
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
+ }
36
71
 
37
72
  type ConsentStatus = 'granted' | 'denied' | 'objected';
38
- type ConsentAction = 'accept_all' | 'reject_all' | 'custom' | 'update';
73
+ type ConsentAction$1 = 'accept_all' | 'reject_all' | 'custom' | 'update';
39
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
+ */
40
94
  interface Cookie {
41
- id: string;
42
- legalBasis?: 'mandatory' | 'consent' | 'legitimate_interest';
43
- /** @deprecated use legalBasis: 'mandatory' */
44
- mandatory?: boolean;
45
- /** @deprecated use legalBasis */
46
- type?: 'consent' | 'legitimate_interest';
95
+ purpose?: CookiePurpose;
47
96
  listenGpc?: boolean;
48
- 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;
49
106
  tcfVendorId?: number;
50
107
  tcfPurposes?: number[];
51
108
  tcfSpecialFeatures?: number[];
52
109
  cpraCategory?: 'sale' | 'sharing' | 'sensitive';
53
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>;
54
113
  type ButtonStyle = 'primary' | 'secondary' | 'text' | 'accent';
55
114
  type ButtonAction = 'submit' | 'manage' | 'close' | 'custom' | 'link';
56
115
  interface Button {
@@ -59,8 +118,11 @@ interface Button {
59
118
  action: ButtonAction;
60
119
  cookies?: string[] | '*' | '!';
61
120
  url?: string;
62
- type?: string;
63
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>;
64
126
  interface MainBanner {
65
127
  position: 'top' | 'bottom' | 'middle' | 'left-bottom' | 'right-bottom';
66
128
  overlayOpacity?: number;
@@ -69,34 +131,50 @@ interface MainBanner {
69
131
  heading?: string;
70
132
  headingTag?: string;
71
133
  htmlText: string;
72
- 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;
73
139
  }
74
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
+ */
75
150
  interface Category {
76
- id: string;
77
151
  heading: string;
78
152
  headingTag?: string;
79
153
  htmlText: string;
80
- mandatory?: boolean;
81
- type?: 'consent' | 'legitimate_interest';
82
- legitimateInterest?: {
83
- enabled?: boolean;
84
- description?: string;
85
- };
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. */
86
158
  cookies: string[];
87
159
  }
160
+ /** `Category` keyed by its own id — the standard shape used everywhere a category list is passed around. */
161
+ type CategoryMap = Record<string, Category>;
88
162
  interface PreferenceModal extends Omit<MainBanner, "position"> {
89
163
  position?: 'left' | 'right' | 'center';
90
164
  subheading?: string;
91
- categories: Category[];
165
+ categories: CategoryMap;
92
166
  persistent?: boolean;
93
167
  /** Screen width in px below which the modal expands to full screen. Default: 576. Set to 0 to disable. */
94
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;
95
173
  }
96
174
  interface LocaleTranslations {
97
- mainBanner: MainBanner;
98
- gpcBanner?: GpcBanner;
99
- preferenceModal: PreferenceModal;
175
+ mainBanner: DeepPartial<MainBanner>;
176
+ gpcBanner?: DeepPartial<GpcBanner>;
177
+ preferenceModal: DeepPartial<PreferenceModal>;
100
178
  }
101
179
  interface ProfileTranslations {
102
180
  [locale: string]: LocaleTranslations;
@@ -110,64 +188,127 @@ interface LocaleTextContent {
110
188
  mainBanner: {
111
189
  heading?: string;
112
190
  htmlText: string;
113
- 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>;
114
194
  };
115
195
  gpcBanner: {
116
196
  heading?: string;
117
197
  htmlText: string;
118
- buttonLabels?: string[];
198
+ buttonLabels?: Record<string, string>;
119
199
  };
120
200
  preferenceModal: {
121
201
  heading: string;
122
202
  subheading?: string;
123
203
  htmlText?: string;
124
- buttonLabels?: string[];
125
- categories: {
126
- 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, {
127
207
  heading: string;
128
208
  htmlText: string;
129
- }[];
209
+ legitimateInterestDescription?: string;
210
+ }>;
211
+ receiptLabel?: string;
212
+ receiptDescription?: string;
130
213
  };
131
214
  }
132
215
  interface ProfileConfig {
133
- cookies?: Cookie[];
216
+ id: string;
217
+ cookies?: CookieMap;
134
218
  defaultLocale: string;
219
+ /** Days until consent expires and the visitor is asked again (profile-wide). Default: 365. */
220
+ expiryDays?: number;
135
221
  translations?: ProfileTranslations;
136
- gpcBanner?: boolean;
222
+ mainBanner: MainBanner;
223
+ gpcBanner?: GpcBanner;
224
+ preferenceModal: PreferenceModal;
137
225
  darkMode?: boolean;
138
226
  allowedOrigins?: string[];
139
227
  dpdpa?: DpdpaConfig;
140
228
  regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
141
229
  regulations?: string[];
142
- cookieTemplateId?: string;
230
+ consentTemplateId?: string;
143
231
  uiTemplateId?: string;
144
232
  localeContents?: Record<string, LocaleTextContent>;
145
- complianceGroup?: string;
146
- /** @deprecated use gpcMode */
147
- gpcCompliance?: 'strict' | 'honor' | 'ignore';
148
- gpcMode?: 'ignore' | 'honor' | 'strict';
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;
149
267
  isActive?: boolean;
150
268
  hidePoweredBy?: boolean;
151
269
  allowReceipt?: boolean;
152
270
  /** Per-compliance extra config (e.g. DPDPA data fiduciary name). */
153
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;
154
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
+ });
155
287
  interface ResolvedProfile {
156
- id: number;
157
- profileUuid?: string;
158
- 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;
159
291
  defaultLocale: string;
160
292
  locales?: string[];
161
- 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;
162
297
  mainBanner: MainBanner;
163
298
  gpcBanner?: GpcBanner;
164
299
  preferenceModal: PreferenceModal;
165
300
  cookieSigningKey?: string;
166
301
  dpdpa?: DpdpaConfig;
167
302
  darkMode?: boolean;
168
- gpcMode?: 'ignore' | 'honor' | 'strict';
169
- complianceGroup?: string;
303
+ gpcMode?: GpcMode;
304
+ complianceGroup?: ComplianceGroupId;
170
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;
171
312
  }
172
313
  type WidgetCountryResolverFn = () => Promise<{
173
314
  country: string | null;
@@ -179,12 +320,20 @@ interface AgeGateWidgetConfig {
179
320
  minimumAge: number;
180
321
  requireParentalConsent?: boolean;
181
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
+ }
182
330
  interface ComplianceWidgetConfig {
183
- type?: 'auto' | ComplianceGroupId | string;
331
+ type?: ComplianceType;
184
332
  geoDataProvider?: 'default' | WidgetCountryResolverFn;
185
333
  autoComplianceMap?: 'default' | ComplianceMapData | 'auto';
186
334
  complianceMapUrl?: string;
187
335
  ageGate?: AgeGateWidgetConfig;
336
+ tcf?: TcfWidgetConfig;
188
337
  }
189
338
  interface GtmConfig {
190
339
  containerId?: string;
@@ -192,6 +341,7 @@ interface GtmConfig {
192
341
  dataLayer?: string;
193
342
  urlPassthrough?: boolean;
194
343
  adsDataRedaction?: boolean;
344
+ verbose?: boolean;
195
345
  }
196
346
  interface UtilsConfig {
197
347
  gtm?: GtmConfig;
@@ -201,7 +351,7 @@ interface ApiConfig {
201
351
  baseUrl?: string;
202
352
  authToken?: string;
203
353
  tenantId?: string;
204
- complianceGroup?: string;
354
+ complianceGroup?: ComplianceGroupId;
205
355
  trustDomain?: boolean;
206
356
  }
207
357
  interface ThemeConfig {
@@ -225,8 +375,10 @@ interface ThemeConfig {
225
375
  }
226
376
  interface CoreConfig {
227
377
  tenantId?: string;
228
- autoHonorGPC?: boolean | 'strict';
229
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';
230
382
  disableCssTemplate?: boolean;
231
383
  cookieSigningKey?: string;
232
384
  allowReceipt?: boolean;
@@ -239,7 +391,11 @@ interface CoreConfig {
239
391
  console?: Array<'info' | 'log' | 'warning' | 'error'>;
240
392
  }
241
393
  interface ConsentiConfig {
242
- 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;
243
399
  compliance?: ComplianceWidgetConfig;
244
400
  rootEl?: string | HTMLElement;
245
401
  darkMode?: boolean;
@@ -251,10 +407,12 @@ interface ConsentiConfig {
251
407
  /** When true, suppresses the "Powered by Consenti" footer link in the banner and modal. */
252
408
  hidePoweredBy?: boolean;
253
409
  }
254
- type ConsentiEventName = 'bannerInitialized' | 'consenti:bannerInitialized' | 'bannerVisibility' | 'consenti:bannerVisibility' | 'modalVisibility' | 'consenti:modalVisibility' | 'consentBeingSubmitted' | 'consenti:consentBeingSubmitted' | 'consentSubmitted' | 'consenti:consentSubmitted';
410
+ type ConsentiEventName = 'bannerInitialized' | 'consenti:bannerInitialized' | 'bannerVisibility' | 'consenti:bannerVisibility' | 'modalVisibility' | 'consenti:modalVisibility' | 'consentBeingSubmitted' | 'consenti:consentBeingSubmitted' | 'consentSubmitted' | 'consenti:consentSubmitted' | 'parentalConsentRequired' | 'consenti:parentalConsentRequired';
255
411
  interface ConsentiWidgetAPI {
256
412
  hasConsent(): boolean;
257
413
  getConsent(): ConsentValue | null;
414
+ getConsent(type: ConsentType): Record<string, string> | null;
415
+ getConsent(type?: ConsentType): ConsentValue | Record<string, string> | null;
258
416
  getConsentDate(): Date | false;
259
417
  getGTMConsent(): Record<string, string> | null;
260
418
  isCookieGranted(cookieId: string, requestValue?: boolean): boolean | ConsentStatus;
@@ -267,8 +425,8 @@ interface ConsentiWidgetAPI {
267
425
  off(event: ConsentiEventName, handler: (data: ConsentEvent) => void): void;
268
426
  version(): {
269
427
  package: string;
270
- profileVersion: number | null;
271
- consentVersion: number;
428
+ profileVersion: string | null;
429
+ consentVersion: string | null;
272
430
  };
273
431
  bannerVisibility(): 'main' | 'gpc' | false;
274
432
  modalVisibility(): 'preference' | false;
@@ -277,7 +435,7 @@ interface ConsentiWidgetAPI {
277
435
  hideBanner(): void;
278
436
  showModal(triggerEl?: HTMLElement): void;
279
437
  hideModal(): void;
280
- submitConsent(consent: Partial<ConsentValue>): Promise<void>;
438
+ submitConsent(consent: Partial<ConsentValue>): Promise<ConsentDbRecord | void>;
281
439
  deleteConsent(): Promise<void>;
282
440
  reConsent(): Promise<void>;
283
441
  getRootElement(): HTMLElement | null;
@@ -316,41 +474,67 @@ interface ParsedConsent {
316
474
  interface ConsentRecord {
317
475
  visitorId: string;
318
476
  consentId?: string;
319
- profileId: number;
320
- profileVersion: number;
477
+ profileId: string;
321
478
  consent: ConsentValue;
322
479
  timestamp: string;
323
480
  gpc?: boolean;
324
- action?: ConsentAction;
481
+ action?: ConsentAction$1;
325
482
  pageUrl?: string;
326
483
  }
327
484
  interface ConsentReceipt {
328
- version: '1.0';
485
+ version: string;
329
486
  issuedAt: string;
330
487
  visitorId: string;
331
- profileId: number;
332
- profileVersion: number;
488
+ profileId: string;
333
489
  locale: string;
334
490
  consent: ConsentValue;
335
491
  signature?: string;
336
492
  }
337
- interface ConsentEvent {
338
- profileId?: number;
339
- consent?: ConsentValue;
340
- hasExistingConsent?: boolean;
341
- show?: boolean;
342
- action?: boolean;
343
- consentAction?: ConsentAction;
344
- consentId?: string;
345
- pageUrl?: string;
346
- 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;
347
518
  fromBroadcast?: boolean;
348
- gpcDetected?: boolean;
349
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;
350
534
  type ConsentiMessage = {
351
535
  type: 'consentUpdated';
352
536
  consent: ConsentValue;
353
- profileId: number;
537
+ profileId: string;
354
538
  } | {
355
539
  type: 'bannerClosed';
356
540
  } | {
@@ -375,16 +559,30 @@ type ConsentiMessage = {
375
559
  */
376
560
 
377
561
  declare class ConsentiSetup implements ConsentiWidgetAPI {
378
- private config;
379
562
  private initialized;
380
563
  private initializing;
381
564
  private profile;
382
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;
383
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;
384
578
  private eventBus;
385
579
  private banner;
386
580
  private modal;
581
+ private ageGate;
387
582
  private renderer;
583
+ /** Set once the age gate (if enabled) resolves; threaded onto every `submitConsent` call thereafter. */
584
+ private _ageVerified;
585
+ private _parentalConsentToken;
388
586
  private broadcastChannel;
389
587
  private readyResolve;
390
588
  private readyPromise;
@@ -393,15 +591,23 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
393
591
  private lastSignature;
394
592
  /** Tracks which banner variant is currently visible, or null when hidden. */
395
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;
396
598
  /** Maps original handler functions to their DOM-wrapped listeners for on/off bookkeeping. */
397
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;
398
603
  /**
399
604
  * Creates and initialises the consent widget.
400
605
  *
401
606
  * Construction is synchronous; the async `init()` is launched in the background.
402
607
  * Use `await widget.ready` or `widget.onReady(cb)` to run code after init completes.
403
608
  *
404
- * @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.
405
611
  */
406
612
  constructor(config: ConsentiConfig);
407
613
  private profileId;
@@ -411,8 +617,53 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
411
617
  * to re-initialise the same instance.
412
618
  */
413
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;
414
652
  /** Applies the `consenti-root--dark` class based on config or profile flag. */
415
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;
416
667
  /**
417
668
  * Injects CSS custom property overrides supplied via `core.theme` into the
418
669
  * Consenti root element so they cascade down to all widget elements.
@@ -442,22 +693,23 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
442
693
  */
443
694
  hasConsent(): boolean;
444
695
  /**
445
- * Returns the stored consent map, or `null` if no consent has been saved.
446
- * 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.
447
702
  */
448
703
  getConsent(): ConsentValue | null;
704
+ getConsent(type: ConsentType): Record<string, string> | null;
449
705
  /**
450
706
  * Returns the `Date` when consent was last saved, or `false` if no consent exists.
451
707
  * Always returns `false` during SSR.
452
708
  */
453
709
  getConsentDate(): Date | false;
454
710
  /**
455
- * Returns the consent map in Google Consent Mode v2 format, ready to pass to
456
- * `gtag('consent', 'update', ...)`. Returns `null` if no consent has been saved.
457
- *
458
- * Maps Consenti cookie IDs to the seven Google Consent Mode v2 parameters.
459
- * Cookies whose IDs exactly match a GCM v2 key are forwarded directly.
460
- * 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.
461
713
  */
462
714
  getGTMConsent(): Record<string, string> | null;
463
715
  /**
@@ -469,7 +721,7 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
469
721
  * @param consent - Partial or full map of cookie ID → status.
470
722
  * @param gpcDerived - Internal flag set to `true` when called from the GPC strict path.
471
723
  */
472
- submitConsent(consent: Partial<ConsentValue>, gpcDerived?: boolean): Promise<void>;
724
+ submitConsent(consent: Partial<ConsentValue>, gpcDerived?: boolean, source?: number): Promise<ConsentDbRecord | void>;
473
725
  /**
474
726
  * Returns the currently visible banner variant, or `false` if no banner is shown.
475
727
  * Useful for programmatically checking state before calling `showBanner` / `hideBanner`.
@@ -638,8 +890,8 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
638
890
  */
639
891
  version(): {
640
892
  package: string;
641
- profileVersion: number | null;
642
- consentVersion: number;
893
+ profileVersion: string;
894
+ consentVersion: string | null;
643
895
  };
644
896
  /**
645
897
  * Toggles or sets dark mode at runtime without re-initialising the widget.
@@ -762,28 +1014,36 @@ declare abstract class ConsentiPlugin {
762
1014
  * import { ConsentiProfile, ConsentiSetup } from '@consenti/ui'
763
1015
  *
764
1016
  * const profile = new ConsentiProfile({
765
- * cookies: [
766
- * { id: 'necessary', mandatory: true },
767
- * { id: 'analytics' },
768
- * ],
1017
+ * cookies: {
1018
+ * necessary: {},
1019
+ * analytics: {},
1020
+ * },
769
1021
  * defaultLocale: 'en',
770
1022
  * translations: {
771
1023
  * en: {
772
1024
  * mainBanner: {
773
1025
  * position: 'bottom',
774
1026
  * htmlText: 'We use cookies.',
775
- * buttons: [{ text: 'Accept All', type: 'primary', cookies: '*' }],
1027
+ * buttons: { 'accept-all': { text: 'Accept All', style: 'primary', action: 'submit', cookies: '*' } },
776
1028
  * },
777
1029
  * preferenceModal: {
778
- * categories: [],
779
- * 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' } },
780
1034
  * },
781
1035
  * },
782
1036
  * },
783
1037
  * })
784
1038
  *
785
- * const widget = new ConsentiSetup({ core: { profileId: profile.getId() } })
1039
+ * const widget = new ConsentiSetup({ core: { profileId: profile.getType() } })
786
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`.
787
1047
  */
788
1048
 
789
1049
  /**
@@ -791,15 +1051,17 @@ declare abstract class ConsentiPlugin {
791
1051
  */
792
1052
  declare class ConsentiProfile {
793
1053
  private config;
794
- private id;
1054
+ private type;
795
1055
  /**
796
- * @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.
797
1059
  */
798
- constructor(config: ProfileConfig);
1060
+ constructor(config: RegisterableProfileConfig);
799
1061
  /** Returns the auto-assigned numeric profile ID. Pass this to `core.profileId`. */
800
- getId(): number;
801
- /** Returns the raw `ProfileConfig` passed to the constructor. */
802
- toJSON(): ProfileConfig;
1062
+ getType(): Symbol;
1063
+ /** Returns the raw config passed to the constructor. */
1064
+ toJSON(): RegisterableProfileConfig;
803
1065
  }
804
1066
 
805
1067
  /**
@@ -887,37 +1149,248 @@ declare class ConsentScript {
887
1149
  }
888
1150
 
889
1151
  /**
890
- * CookieTrigger — a pre-built accessible UI element that reopens the Consenti
891
- * 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.
892
1362
  *
893
1363
  * Use this to add a "Cookie Settings" link in your site footer or cookie policy
894
1364
  * page without wiring up the click handler manually or importing the widget module
895
1365
  * into your layout component.
896
1366
  *
1367
+ * (Renamed from `CookieTrigger` — it was never scoped to a specific cookie, it
1368
+ * just opens the banner or modal.)
1369
+ *
897
1370
  * @example
898
1371
  * ```ts
899
- * import { ConsentiSetup, CookieTrigger } from '@consenti/ui'
1372
+ * import { ConsentiSetup, BannerTrigger } from '@consenti/ui'
900
1373
  *
901
1374
  * const widget = new ConsentiSetup({ core: { profileId: 0 } })
902
1375
  *
903
1376
  * // Attach to an existing element
904
- * new CookieTrigger({ widget, el: '#footer-cookie-settings' })
1377
+ * new BannerTrigger({ widget, el: '#footer-cookie-settings' })
905
1378
  *
906
1379
  * // — or — create a new button and place it yourself
907
- * const trigger = new CookieTrigger({ widget, label: 'Cookie Settings' })
1380
+ * const trigger = new BannerTrigger({ widget, label: 'Cookie Settings' })
908
1381
  * document.querySelector('.site-footer')?.appendChild(trigger.getElement())
909
1382
  * ```
910
1383
  */
911
1384
 
912
- /** Options for constructing a {@link CookieTrigger} instance. */
913
- interface CookieTriggerOptions {
1385
+ /** Options for constructing a {@link BannerTrigger} instance. */
1386
+ interface BannerTriggerOptions {
914
1387
  /** The widget instance to control when the trigger is clicked. */
915
1388
  widget: ConsentiSetup;
916
1389
  /**
917
1390
  * Element to attach the click handler to.
918
1391
  * - `HTMLElement` — used directly.
919
1392
  * - `string` — treated as a CSS selector; the first matching element is used.
920
- * - 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}
921
1394
  * and append it to the DOM at the desired location.
922
1395
  */
923
1396
  el?: HTMLElement | string;
@@ -940,12 +1413,12 @@ interface CookieTriggerOptions {
940
1413
  * The created button uses the `.consenti-btn` and `.consenti-btn--text` classes so
941
1414
  * it inherits the widget's theme automatically.
942
1415
  */
943
- declare class CookieTrigger {
1416
+ declare class BannerTrigger {
944
1417
  private readonly el;
945
1418
  private readonly clickHandler;
946
1419
  /** Whether this instance created the element (used to decide if `destroy()` removes it). */
947
1420
  private readonly created;
948
- constructor(options: CookieTriggerOptions);
1421
+ constructor(options: BannerTriggerOptions);
949
1422
  /**
950
1423
  * Returns the trigger element.
951
1424
  * When no `el` was provided, append the returned element to the desired location
@@ -959,4 +1432,42 @@ declare class CookieTrigger {
959
1432
  destroy(): void;
960
1433
  }
961
1434
 
962
- 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 ConsentiEventName, 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 };