@consenti/ui 0.3.0 → 0.5.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/vue.d.ts CHANGED
@@ -13,6 +13,32 @@ type DeepPartial<T> = T extends Function ? T : T extends Array<infer InferredArr
13
13
  [Key in keyof T]?: DeepPartial<T[Key]> | null;
14
14
  } : T;
15
15
 
16
+ /**
17
+ * Single source of truth for all compliance groups, regulations, and country mappings.
18
+ *
19
+ * This is DATA, not types. The `Compliance` and `ComplianceGroupId` literal types
20
+ * derived from the ID arrays below live in @consenti/types (packages/types/src/compliance.ts),
21
+ * via a type-only import — that keeps @consenti/types free of runtime code.
22
+ *
23
+ * ─── HOW TO UPDATE ────────────────────────────────────────────────────────────
24
+ *
25
+ * ADD A NEW REGULATION:
26
+ * 1. Add regulation ID string to `COMPLIANCE_REGULATION_IDS` below
27
+ * (the `Compliance` type in @consenti/types auto-updates from it).
28
+ * 2. Add it to the appropriate group's `compliances` array below.
29
+ * 3. Add / update the country entry in `EMBEDDED_COMPLIANCE_MAP` below.
30
+ * That is all — no other files need touching.
31
+ *
32
+ * ADD A NEW COMPLIANCE GROUP:
33
+ * 1. Add the kebab-case ID to `COMPLIANCE_GROUP_IDS` below.
34
+ * 2. Add a full group entry to `COMPLIANCE_GROUPS` below (label, compliances, flags).
35
+ * 3. Add a validation rule set to `COMPLIANCE_VALIDATION_RULES` below.
36
+ * 4. Map affected countries in `EMBEDDED_COMPLIANCE_MAP` to the new group.
37
+ *
38
+ * ADD OR UPDATE A COUNTRY:
39
+ * Edit the entry in `EMBEDDED_COMPLIANCE_MAP.countries` below. That is the ONLY
40
+ * place that needs changing for country-level jurisdiction updates.
41
+ */
16
42
  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"];
17
43
  declare const COOKIE_PURPOSE_IDS: readonly ["necessary", "functional", "preferences", "analytics", "marketing"];
18
44
 
@@ -44,11 +70,14 @@ interface ComplianceMapData {
44
70
  }
45
71
  interface CountryComplianceEntry {
46
72
  complianceGroup: ComplianceGroupId;
47
- default: ComplianceGroupId;
48
- description: string;
73
+ /** Fallback group used when only the country (not the region) is known. Defaults to
74
+ * `complianceGroup` when omitted — only set this explicitly when a region within this
75
+ * country can be stricter than the country-wide baseline (see `overriddenRegions`). */
76
+ default?: ComplianceGroupId;
77
+ description?: string;
49
78
  overriddenRegions?: Record<string, {
50
79
  complianceGroup: ComplianceGroupId;
51
- description: string;
80
+ description?: string;
52
81
  }>;
53
82
  }
54
83
  interface ConsentDbRecord {
@@ -63,9 +92,10 @@ interface ConsentDbRecord {
63
92
  ageVerified?: boolean;
64
93
  parentalConsentToken?: string;
65
94
  tcfString?: string;
66
- /** HMAC-SHA256 signature over the record's core fields, hex-encoded. Only present when
67
- * `consentSigningKey` is configured — opt-in tamper-evidence for server-stored records,
68
- * independent of the widget's own cookie-signing (`core.cookieSigningKey`). */
95
+ gppString?: string;
96
+ /** HMAC-SHA256 signature over the record's core fields, hex-encoded — tamper-evidence for
97
+ * server-stored records via `dataSigningHash` (auto-generated when unset), independent of the
98
+ * widget's own cookie-signing (`core.cookieSigningKey`). */
69
99
  signature?: string;
70
100
  createdAt: string;
71
101
  updatedAt: string;
@@ -76,15 +106,18 @@ type ConsentAction = 'accept_all' | 'reject_all' | 'custom' | 'update';
76
106
  type ConsentValue = Record<string, ConsentStatus>;
77
107
  /**
78
108
  * Output format for getConsent(type).
79
- * 'default' → raw ConsentValue (existing behaviour)
109
+ * 'default' → raw ConsentValue, keyed by every cookie parameter ID in the profile (existing behaviour)
80
110
  * 'google-gtm' → Google Consent Mode v2 keys + ads_data_redaction, url_passthrough
81
- * 'category' → consent per CookiePurpose category
111
+ * 'purpose' → consent per the fixed CookiePurpose taxonomy (necessary/functional/preferences/analytics/marketing)
112
+ * 'category' → consent per the tenant's own authored preference-modal category, keyed by category ID.
113
+ * 'granted' only when every member parameter is granted; 'denied'/'objected' otherwise
114
+ * (same ConsentStatus values as everything else — see getCategoryConsent for the exact rule)
82
115
  * 'adobe' → { analytics, target, manager, optimizer }
83
116
  * 'meta' → { pixel, api, plugins, facebookLogin }
84
117
  * 'microsoft-clarity' → { session, heatmaps, performance }
85
118
  * 'twilio-segment' → { identify, page, track, group, alias }
86
119
  */
87
- type ConsentType = 'default' | 'google-gtm' | 'category' | 'adobe' | 'meta' | 'microsoft-clarity' | 'twilio-segment';
120
+ type ConsentType = 'default' | 'google-gtm' | 'purpose' | 'category' | 'adobe' | 'meta' | 'microsoft-clarity' | 'twilio-segment';
88
121
  type GpcMode = 'ignore' | 'honor' | 'strict';
89
122
  type LegalBasis = 'mandatory' | 'consent' | 'legitimate_interest';
90
123
  type CookiePurpose = typeof COOKIE_PURPOSE_IDS[number];
@@ -172,12 +205,46 @@ interface PreferenceModal extends Omit<MainBanner, "position"> {
172
205
  receiptLabel?: string;
173
206
  /** Description shown beneath the consent-receipt checkbox. Falls back to a default when omitted. */
174
207
  receiptDescription?: string;
208
+ /**
209
+ * Renders a "Forget me" control in the modal body, below the categories — only shown to
210
+ * visitors who already have a stored consent decision (nothing to erase otherwise). Wired to
211
+ * the same `DELETE /consent/:visitorId` erasure endpoint as `widget.deleteConsent()`, giving
212
+ * visitors self-service access to GDPR Art. 17 / CCPA-CPRA / LGPD Art. 18 / UK GDPR / DPDPA /
213
+ * KVKK / POPIA / PDPA-TH / APPI erasure-equivalent rights without the host wiring a link
214
+ * themselves. See the "Right to erasure" guide for the full compliance mapping.
215
+ */
216
+ showForgetMe?: boolean;
217
+ /** Label for the "Forget me" button (shown when `showForgetMe` is true). Falls back to a default when omitted. */
218
+ forgetMeLabel?: string;
175
219
  }
176
220
  interface DpdpaConfig {
177
221
  dataFiduciary: string;
178
222
  grievanceEmail: string;
179
223
  purposeDescription?: string;
180
224
  }
225
+ /**
226
+ * Per-profile age gate — GDPR Art. 8's consent age varies 13–16 by EU member state and DPDPA has
227
+ * its own child-data age rules, so one global age (the old `ConsentiServerConfig.ageGate`) was
228
+ * wrong for a multi-region deployment. Mirrors `dpdpa` exactly: profile-scoped, not global.
229
+ */
230
+ interface AgeGateConfig {
231
+ enabled: boolean;
232
+ minimumAge: number;
233
+ requireParentalConsent?: boolean;
234
+ }
235
+ /** Per-locale age-gate modal text — translated the same way `mainBanner`/`preferenceModal`
236
+ * content is, unlike the widget's own hardcoded UI chrome (close button, locale switcher). */
237
+ interface AgeGateModalContent {
238
+ heading?: string;
239
+ htmlText: string;
240
+ confirmButtonLabel: string;
241
+ denyButtonLabel: string;
242
+ parentalConsent: {
243
+ heading?: string;
244
+ htmlText: string;
245
+ confirmButtonLabel: string;
246
+ };
247
+ }
181
248
  interface ResolvedProfile {
182
249
  id: string;
183
250
  /** Incremented in place on every save of this profile. Absent for embedded/local profiles that have no server-side version counter. */
@@ -193,9 +260,21 @@ interface ResolvedProfile {
193
260
  preferenceModal: PreferenceModal;
194
261
  cookieSigningKey?: string;
195
262
  dpdpa?: DpdpaConfig;
263
+ ageGate?: AgeGateConfig;
264
+ ageGateModal?: AgeGateModalContent;
196
265
  darkMode?: boolean;
197
266
  gpcMode?: GpcMode;
198
- complianceGroup?: ComplianceGroupId;
267
+ /** One of the 8 built-in groups, or a custom group id (via `customComplianceGroup` /
268
+ * `ComplianceType`'s string passthrough) — this is the join key `complianceGroupsOverride`
269
+ * looks up by, so it must reflect whichever group this profile actually resolved against,
270
+ * built-in or custom. */
271
+ complianceGroup?: ComplianceGroupId | (string & {});
272
+ /** Set when the geo-resolved region carries a `requiresSensitiveOptIn` carve-out (e.g.
273
+ * Colorado under `opt-out`) — cookies tagged `cpraCategory: 'sensitive'` default to denied
274
+ * even though the rest of `complianceGroup`'s cookies default to granted. Only ever set by
275
+ * server-side geo resolution (`api.enabled: true` + a geoip/maxmind provider); the
276
+ * client-only timezone heuristic never resolves a region, so this is always unset there. */
277
+ requiresSensitiveOptIn?: boolean;
199
278
  complianceConfig?: Record<string, string>;
200
279
  /** Falls back to `true` (hidden) if neither the widget config nor the profile set it. */
201
280
  hidePoweredBy?: boolean;
@@ -208,26 +287,66 @@ type WidgetCountryResolverFn = () => Promise<{
208
287
  country: string | null;
209
288
  region: string | null;
210
289
  confidence: number;
290
+ /** Optional escape hatch: when set, profile resolution uses this compliance group directly
291
+ * instead of computing one from `country`/`region` — mirrors the server-side `GeoResult`'s
292
+ * `complianceGroup` field (`@consenti/types`'s `api.ts`). `country`/`region` are still used for
293
+ * anything that reads them independently of group selection. Optional — omitting it preserves
294
+ * the existing timezone/language-based resolution exactly as before. */
295
+ complianceGroup?: string;
211
296
  }>;
212
- interface AgeGateWidgetConfig {
213
- enabled: boolean;
214
- minimumAge: number;
215
- requireParentalConsent?: boolean;
216
- }
217
297
  /** Mirrors the server-side `TcfConfig` shape (`@consenti/api`) — `cmpId`/`cmpVersion` must match
218
- * what the backend uses to encode `tcfString`, since both feed the same simplified TC string. */
298
+ * what the backend uses to encode `tcfString`, since both feed the same simplified TC string.
299
+ *
300
+ * The client-side `__tcfapi` stub never fetches the Global Vendor List itself (multi-megabyte,
301
+ * and IAB policy requires CMPs to cache their own copy rather than have every visitor's browser
302
+ * fetch it) — `publisherCC`/`vendorListVersion`/`gvlVersion` are therefore operator-supplied
303
+ * config here, not auto-derived. Spec-correct binary TC-string encoding happens server-side in
304
+ * `@consenti/api` (see `compliance.tcf.publisherCC` there); this stub's `tcString` stays the
305
+ * simplified format documented in `@consenti/utils`'s `encodeTcString`. */
219
306
  interface TcfWidgetConfig {
220
307
  enabled: boolean;
221
308
  cmpId: number;
222
309
  cmpVersion: number;
310
+ /** ISO 3166-1 alpha-2 publisher country code. No honest default exists — leave unset only if
311
+ * you don't rely on this field downstream; ad-tech vendors that read it will see 'AA'
312
+ * (an explicit "not configured" placeholder, not a real country). */
313
+ publisherCC?: string;
314
+ /** Known Global Vendor List version, if you track it. Defaults to 0 ("unknown"). */
315
+ vendorListVersion?: number;
316
+ /** Known GVL specification version, if you track it. Defaults to 0 ("unknown"). */
317
+ gvlVersion?: number;
318
+ }
319
+ /** Mirrors the server-side `GppConfig` shape (`@consenti/api`) — `cmpId`/`cmpVersion` must match
320
+ * what the backend uses to encode `gppString`. Unlike TCF, the GPP US National section needs no
321
+ * Global Vendor List, so both the widget's `window.__gpp` and the server's `gppString` use the
322
+ * same real, spec-correct encoder (the optional `@iabgpp/cmpapi` peer dependency) — there's no
323
+ * "simplified fallback" format the way TCF has, since a non-spec GPP string has no consumer that
324
+ * would accept it. When `@iabgpp/cmpapi` isn't installed, `window.__gpp` is simply not exposed. */
325
+ interface GppWidgetConfig {
326
+ enabled: boolean;
327
+ cmpId: number;
328
+ cmpVersion: number;
329
+ /** MSPA "covered transaction" — whether this deployment's data transactions fall under MSPA
330
+ * signatory obligations. No honest default, so it's required rather than optional. */
331
+ mspaCoveredTransaction: boolean;
332
+ /** IAB's tri-state encoding for both MSPA fields: 0 = not applicable, 1 = yes, 2 = no. */
333
+ mspaOptOutOptionMode: 0 | 1 | 2;
334
+ mspaServiceProviderMode: 0 | 1 | 2;
223
335
  }
224
336
  interface ComplianceWidgetConfig {
225
337
  type?: ComplianceType;
226
338
  geoDataProvider?: 'default' | WidgetCountryResolverFn;
227
- autoComplianceMap?: 'default' | ComplianceMapData | 'auto';
228
- complianceMapUrl?: string;
229
- ageGate?: AgeGateWidgetConfig;
339
+ /** Only meaningful in standalone mode (no `api.enabled`) — ignored (with a warning) when
340
+ * `api.enabled: true`, since the server resolves the compliance group in that mode.
341
+ * `'default'` (embedded map, the default), a URL to fetch a JSON `ComplianceMapData`
342
+ * document from (the browser's own HTTP cache honors whatever `Cache-Control`/`ETag` the
343
+ * response sends — no custom caching here), or an inline operator-supplied `ComplianceMapData`
344
+ * object. Invalid data from either a URL or an object logs a warning and falls back to
345
+ * `'default'`. Only overrides the country→group mapping — country/region detection itself
346
+ * always uses the embedded geo data. */
347
+ complianceMap?: 'default' | string | ComplianceMapData;
230
348
  tcf?: TcfWidgetConfig;
349
+ gpp?: GppWidgetConfig;
231
350
  }
232
351
  interface GtmConfig {
233
352
  containerId?: string;
@@ -248,37 +367,67 @@ interface ApiConfig {
248
367
  complianceGroup?: ComplianceGroupId;
249
368
  trustDomain?: boolean;
250
369
  }
370
+ /** Field names mirror their `--consenti-*` CSS variable literally (`--consenti-color-primary`
371
+ * → `colorPrimary`) — see `applyTheme()` in `consenti-setup.ts` for the full mapping. */
251
372
  interface ThemeConfig {
252
- bgColor?: string;
253
- textColor?: string;
254
- primaryColor?: string;
255
- primaryTextColor?: string;
256
- secondaryColor?: string;
257
- secondaryTextColor?: string;
258
- borderColor?: string;
373
+ colorBg?: string;
374
+ colorText?: string;
375
+ colorTextMuted?: string;
376
+ colorPrimary?: string;
377
+ colorPrimaryText?: string;
378
+ colorSecondary?: string;
379
+ colorSecondaryText?: string;
380
+ colorBorder?: string;
381
+ colorSecondaryBorder?: string;
382
+ colorOverlay?: string;
383
+ colorAccent?: string;
384
+ colorAccentText?: string;
259
385
  fontFamily?: string;
386
+ fontFamilyMono?: string;
260
387
  fontSizeBase?: string;
261
388
  fontSizeHeading?: string;
262
389
  fontSizeMultiplier?: string;
390
+ fontWeightHeading?: string;
391
+ lineHeight?: string;
392
+ spacingXs?: string;
393
+ spacingSm?: string;
394
+ spacingMd?: string;
395
+ spacingLg?: string;
263
396
  borderRadius?: string;
264
- buttonBorderRadius?: string;
397
+ borderRadiusBtn?: string;
398
+ shadow?: string;
265
399
  toggleBgOn?: string;
400
+ toggleBgPartial?: string;
266
401
  toggleBgOff?: string;
267
- accentColor?: string;
268
- accentTextColor?: string;
402
+ toggleKnob?: string;
403
+ toggleWidth?: string;
404
+ toggleHeight?: string;
405
+ zBanner?: string;
406
+ zOverlay?: string;
407
+ zModal?: string;
269
408
  }
270
409
  interface CoreConfig {
271
410
  tenantId?: string;
272
411
  locale?: string;
273
- /** Text direction for the banner/modal root. `'auto'` (default) derives it from `locale`
274
- * (Arabic, Hebrew, Persian, Urdu, ... → `rtl`); set explicitly to override. */
412
+ /** Text direction for the banner/modal root. `'auto'` (default) derives it from `locale` via
413
+ * `Intl.Locale(...).getTextInfo().direction` (falling back to a hand-maintained RTL-language
414
+ * list on engines without `getTextInfo()` support); set explicitly to override. */
275
415
  dir?: 'ltr' | 'rtl' | 'auto';
276
416
  disableCssTemplate?: boolean;
277
417
  cookieSigningKey?: string;
278
418
  allowReceipt?: boolean;
279
419
  cookieDomains?: string;
420
+ /** Name of the consent cookie/localStorage key. Default: `'consenti_data'`. Not switched
421
+ * automatically by detected region — set explicitly if you want a different name (e.g.
422
+ * `'euconsent-v2'`, the IAB TCF convention, but only meaningful for operators using the
423
+ * spec-correct binary encoder — `@consenti/api` + the optional `@iabtechlabtcf/core` peer
424
+ * dependency — since the IAB name implies IAB's binary format, not Consenti's own encoding). */
425
+ cookieName?: string;
280
426
  storage?: 'cookie' | 'localStorage';
281
427
  theme?: ThemeConfig;
428
+ /** Initial logged-in application user ID; empty/anonymous when unset. Prefer `getUserId()`/
429
+ * `setUserId()` (or the `consenti:listener:identify` event) to change it after init — those
430
+ * also handle reconsent when the identity changes on a shared device. */
282
431
  userId?: string;
283
432
  usePrebuiltProfiles?: 'all' | NonEmptyArray<ComplianceGroupId>;
284
433
  cacheResolvedProfiles?: boolean;
@@ -298,10 +447,25 @@ interface ConsentiConfig {
298
447
  utils?: UtilsConfig;
299
448
  plugins?: ConsentiPlugin[];
300
449
  profileOverride?: DeepPartial<ResolvedProfile>;
450
+ /** Per-compliance-group text/button/cookie overrides — applied on top of whichever profile the
451
+ * visitor's resolved group loads, keyed by that group's id (one of the 8 built-in
452
+ * `ComplianceGroupId`s, or any custom group id). Lets you author different copy per country
453
+ * without creating a separate profile (and separate `compliance.type` wiring) for each one —
454
+ * e.g. `{ 'opt-in': { mainBanner: { htmlText: '...' } }, 'my-custom-group': { ... } }`.
455
+ * Applied before `profileOverride`, which still always applies last as the universal catch-all.
456
+ * Works identically in standalone and server (`api.enabled`) mode. */
457
+ complianceGroupsOverride?: Partial<Record<ComplianceGroupId | (string & {}), DeepPartial<ResolvedProfile>>>;
301
458
  /** When true, suppresses the "Powered by Consenti" footer link in the banner and modal. */
302
459
  hidePoweredBy?: boolean;
303
460
  }
304
- type ConsentiEventName = 'bannerInitialized' | 'consenti:bannerInitialized' | 'bannerVisibility' | 'consenti:bannerVisibility' | 'modalVisibility' | 'consenti:modalVisibility' | 'consentBeingSubmitted' | 'consenti:consentBeingSubmitted' | 'consentSubmitted' | 'consenti:consentSubmitted' | 'parentalConsentRequired' | 'consenti:parentalConsentRequired';
461
+ type ConsentiEventName = 'bannerInitialized' | 'consenti:bannerInitialized' | 'bannerVisibility' | 'consenti:bannerVisibility' | 'modalVisibility' | 'consenti:modalVisibility' | 'consentBeingSubmitted' | 'consenti:consentBeingSubmitted' | 'consentSubmitted' | 'consenti:consentSubmitted' | 'parentalConsentRequired' | 'consenti:parentalConsentRequired' | 'forgetMeRequested' | 'consenti:forgetMeRequested' | 'forgotten' | 'consenti:forgotten';
462
+ /** Snapshot of the current visitor's identity. `visitorId` is `null` until a consent decision
463
+ * has actually happened — it's never minted just to answer this call, see `getVisitor()`. */
464
+ interface VisitorIdentity {
465
+ visitorId: string | null;
466
+ type: 'authenticated' | 'anonymous';
467
+ userId: string | null;
468
+ }
305
469
  interface ConsentiWidgetAPI {
306
470
  hasConsent(): boolean;
307
471
  getConsent(): ConsentValue | null;
@@ -332,10 +496,25 @@ interface ConsentiWidgetAPI {
332
496
  submitConsent(consent: Partial<ConsentValue>): Promise<ConsentDbRecord | void>;
333
497
  deleteConsent(): Promise<void>;
334
498
  reConsent(): Promise<void>;
499
+ /** Erases the stored consent record (same underlying call as `deleteConsent()`) and
500
+ * re-prompts, dispatching `consenti:forgetMeRequested` / `consenti:forgotten` around it so a
501
+ * host app can hook its own identity-verified erasure workflow. Powers the preference modal's
502
+ * "Forget me" button; also callable directly for a custom placement of the same action. */
503
+ forgetMe(resetAgeGate?: boolean): Promise<void>;
335
504
  getRootElement(): HTMLElement | null;
336
505
  getBannerElement(): HTMLElement | null;
337
506
  getModalElement(): HTMLElement | null;
338
507
  switchLocale(locale: string): void;
508
+ getUserId(): string | null;
509
+ /** Sets the logged-in application user ID (`null` for anonymous/logout). When the stored
510
+ * consent record's user differs from the new value, reconsents by default — pass
511
+ * `reConsent: false` to just update the identity without prompting again. No-ops (no
512
+ * reconsent, no warning) when there's no prior consent record to compare against. */
513
+ setUserId(userId: string | null, reConsent?: boolean): Promise<void>;
514
+ /** Snapshot of the current visitor's identity: the stable per-browser `visitorId` (`null` if
515
+ * no consent decision has happened yet — this never creates one), whether they're
516
+ * `authenticated` or `anonymous`, and the app `userId` (same as `getUserId()`). */
517
+ getVisitor(): VisitorIdentity;
339
518
  setDarkMode(enable?: boolean): void;
340
519
  setTheme(theme: Partial<ThemeConfig>): void;
341
520
  setConfig(config: DeepPartial<ConsentiConfig>): void;
@@ -383,17 +562,45 @@ interface ConsentBeingSubmitted {
383
562
  interface ConsentSubmittedDetail extends ConsentBeingSubmitted {
384
563
  apiResponse: ConsentDbRecord;
385
564
  }
386
- /** Dispatched when the age gate is declined and `requireParentalConsent` is set — see
387
- * `AgeGateWidgetConfig`. A deny-all consent (mandatory cookies only) has already been
565
+ /** Dispatched when the age gate is declined and `requireParentalConsent` is set — see the
566
+ * profile's `AgeGateConfig`. A deny-all consent (mandatory cookies only) has already been
388
567
  * submitted with `parentalConsentToken` attached; this event is the hook for the site
389
- * owner to run their own out-of-band parental-consent verification. */
568
+ * owner to run their own out-of-band parental-consent verification. `parentalConsentToken` is
569
+ * server-issued (and signed, when `compliance.dataSigningHash` is configured) when `api.enabled`,
570
+ * otherwise a client-generated, unsigned fallback — see `POST /consent/:visitorId/parental-consent-request`. */
390
571
  interface ParentalConsentRequiredDetail {
391
572
  parentalConsentToken: string;
392
573
  profileId: string;
393
574
  visitorId: string;
394
575
  timestamp: number;
395
576
  }
396
- type ConsentEvent = BannerInitializedDetail | BannerVisibilityDetail | ModalVisibilityDetail | ConsentBeingSubmitted | ConsentSubmittedDetail | ParentalConsentRequiredDetail;
577
+ /** Dispatched by the standalone `resolveParentalConsent()` util (`apps/ui/src/utils/parental-consent.ts`)
578
+ * after `POST /consent/parental-consent-resolve` succeeds — typically from a fresh page/session
579
+ * with no live `ConsentiSetup` instance (the parent's own device, not the child's). A page that
580
+ * does have a live widget instance can listen for this to react (e.g. re-show the banner/modal for
581
+ * the real consent choice); what "resolved" should actually do to consent state is host-defined. */
582
+ interface ParentalConsentGrantedDetail {
583
+ visitorId: string;
584
+ profileId: string;
585
+ }
586
+ /** Dispatched by `forgetMe()` right before the erasure call goes out — the hook for a host app
587
+ * to kick off its own identity-verified erasure workflow across other systems (CRM, DMP,
588
+ * analytics). The CMP itself only ever erases its own consent record; see the server-side
589
+ * `consent.erased` eventBus event (fired by `DELETE /consent/:visitorId`) for the same hook on
590
+ * the backend. See the "Right to erasure" guide for the full picture. */
591
+ interface ForgetMeRequestedDetail {
592
+ visitorId: string;
593
+ profileId: string;
594
+ timestamp: number;
595
+ }
596
+ /** Dispatched by `forgetMe()` after the consent record has been erased and the banner/age-gate
597
+ * has been re-prompted (see `reConsent()`, which `forgetMe()` wraps). */
598
+ interface ForgottenDetail {
599
+ visitorId: string;
600
+ profileId: string;
601
+ timestamp: number;
602
+ }
603
+ type ConsentEvent = BannerInitializedDetail | BannerVisibilityDetail | ModalVisibilityDetail | ConsentBeingSubmitted | ConsentSubmittedDetail | ParentalConsentRequiredDetail | ParentalConsentGrantedDetail | ForgetMeRequestedDetail | ForgottenDetail;
397
604
 
398
605
  /**
399
606
  * Main entry point for the Consenti widget.
@@ -417,6 +624,7 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
417
624
  private initializing;
418
625
  private profile;
419
626
  private resolvedBase;
627
+ private profilePatches;
420
628
  /** cookieId → Category lookup, rebuilt whenever `profile` changes. Avoids rescanning categories on every consent action. */
421
629
  private categoryIndex;
422
630
  /** Watchers created by the declarative `data-consenti-*` DOM scan — destroyed alongside the widget since they have no other owner. */
@@ -449,6 +657,8 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
449
657
  private _bannerMounted;
450
658
  /** Active focus trap keydown listener — removed when modal closes. */
451
659
  private _focusTrapListener;
660
+ /** Inbound `consenti:listener:identify` window listener — installed once per init(), removed on destroy(). */
661
+ private _identifyListener;
452
662
  /** Maps original handler functions to their DOM-wrapped listeners for on/off bookkeeping. */
453
663
  private _eventHandlers;
454
664
  /** `core` defaulted to `{}` in the constructor below — every one of its own fields is
@@ -465,6 +675,29 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
465
675
  */
466
676
  constructor(config: ConsentiConfig);
467
677
  private profileId;
678
+ /** Patches `base` with `complianceGroupsOverride[base.complianceGroup]`, if both exist — the
679
+ * per-group counterpart to `profileOverride`. Lets an author supply different text/buttons/
680
+ * cookies per compliance group (built-in or custom) without authoring a separate profile for
681
+ * each one. Applied before `profileOverride`, which still always wins as the final catch-all. */
682
+ private applyComplianceGroupOverride;
683
+ /**
684
+ * Applied in order: compliance-group override → `config.profileOverride` → each `setProfile()`
685
+ * patch. The `setProfile()` patches are kept as a list and replayed, not folded into one
686
+ * object, because folding runs `deepMerge` patch-onto-patch, which consumes `null`/`'*': null`
687
+ * delete markers before they ever reach the profile they were meant to delete from.
688
+ */
689
+ private applyProfileOverrides;
690
+ private warnIfAcceptOnly;
691
+ /**
692
+ * Whether `window.__tcfapi`/`window.__gpp` should stay uninstalled because the corresponding
693
+ * server-side registration (`compliance.tcf`/`compliance.gpp`) is unconfirmed (cmpId/cmpVersion
694
+ * changed since the dashboard's confirmation checkbox was last checked, or never confirmed).
695
+ * Standalone (no-backend) deployments have no governance layer to check against, so this always
696
+ * resolves `false` there — the client-side config is used as configured, same as before this
697
+ * feature existed. A network failure fails closed (`true`) rather than assuming the
698
+ * unverifiable state is fine.
699
+ */
700
+ private isRegistrationBlocked;
468
701
  /**
469
702
  * Starts widget initialisation. Called automatically unless `autoInit: false` is set.
470
703
  * Can be called explicitly after construction when `autoInit: false`, or after `destroy()`
@@ -478,7 +711,7 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
478
711
  * `resolveAgeGate()`, then resume here with `hasExisting` fixed at `false`.
479
712
  */
480
713
  private continueInit;
481
- /** Mounts the age-gate prompt (`compliance.ageGate`) in place of the normal banner flow. */
714
+ /** Mounts the age-gate prompt (`profile.ageGate`) in place of the normal banner flow. */
482
715
  private showAgeGate;
483
716
  /**
484
717
  * Handles the visitor's age-gate answer.
@@ -501,13 +734,35 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
501
734
  * a resolved profile to read consent from.
502
735
  */
503
736
  private finishInit;
737
+ /** Installs the inbound `consenti:listener:identify` window listener — the event-driven
738
+ * counterpart to `setUserId()`. Removes any previously-installed listener first so repeated
739
+ * `init()` calls (e.g. from `switchLocale()`) don't stack up duplicate listeners. */
740
+ private _installIdentifyListener;
741
+ /** Returns the current logged-in application user ID, or `null` for an anonymous visitor. */
742
+ getUserId(): string | null;
743
+ /**
744
+ * Sets the logged-in application user ID (`null` for anonymous/logout). If a stored consent
745
+ * record exists and its user differs from `userId`, this is treated as a different data
746
+ * subject on a shared device: by default it reconsents (deletes the existing record and
747
+ * re-shows the banner) and logs a warning. Pass `reConsent: false` to just update the identity
748
+ * without prompting again (still logs the warning). No-ops entirely — no warning, no
749
+ * reconsent — when there is no prior consent record to compare against (true first-time visitor).
750
+ */
751
+ setUserId(userId: string | null, reConsent?: boolean): Promise<void>;
752
+ /**
753
+ * Snapshot of the current visitor's identity. `visitorId` is the stable per-browser ID —
754
+ * `null` until a consent decision has actually happened, since this never mints one just to
755
+ * answer the call (see `getOrCreateVisitorId()`). `type` is `'authenticated'` whenever a
756
+ * `userId` is set (via `setUserId()` or `core.userId`), `'anonymous'` otherwise.
757
+ */
758
+ getVisitor(): VisitorIdentity;
504
759
  /** Sets the `dir` attribute on the root element from `core.dir` (default `'auto'`, derived from `core.locale`). */
505
760
  private applyDirection;
506
761
  /** Applies the `consenti-root--dark` class based on config or profile flag. */
507
762
  private applyDarkMode;
508
763
  /** Applies `consenti--enhanced-a11y` class when `profile.enhanceAccessibility` is set. */
509
764
  private applyAccessibility;
510
- /** Builds and appends the metadata footer strip to a banner or modal container. */
765
+ /** Builds and appends the metadata footer strip to modal container. */
511
766
  private _appendMetaFooter;
512
767
  /**
513
768
  * Injects a `<style>` element into the root for `stackButtonsOnBreakpoint`.
@@ -551,8 +806,16 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
551
806
  * Pass `type` to get a vendor-ready object instead of the raw Consenti format.
552
807
  * Always returns `null` during SSR or when no consent has been saved.
553
808
  *
554
- * @param type - Vendor format: 'google-gtm', 'category', 'adobe', 'meta',
555
- * 'microsoft-clarity', 'twilio-segment', or omit for raw format.
809
+ * @param type - Output format:
810
+ * - omit / `'default'` → raw ConsentValue, keyed by every cookie parameter ID in the profile
811
+ * - `'purpose'` → consent per the fixed CookiePurpose taxonomy (necessary/functional/preferences/analytics/marketing)
812
+ * - `'category'` → consent per the tenant's own authored preference-modal category, keyed by category ID
813
+ * ('granted' only when every member parameter is granted; 'denied'/'objected' otherwise)
814
+ * - `'google-gtm'` → Google Consent Mode v2 keys + ads_data_redaction, url_passthrough
815
+ * - `'adobe'` → { analytics, target, manager, optimizer }
816
+ * - `'meta'` → { pixel, api, plugins, facebookLogin }
817
+ * - `'microsoft-clarity'` → { session, heatmaps, performance }
818
+ * - `'twilio-segment'` → { identify, page, track, group, alias }
556
819
  */
557
820
  getConsent(): ConsentValue | null;
558
821
  getConsent(type: ConsentType): Record<string, string> | null;
@@ -644,8 +907,30 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
644
907
  * method so callers do not need to coordinate the async deletion with the banner display.
645
908
  *
646
909
  * Use this to power "Review my consent" links in footers or privacy dashboards.
910
+ *
911
+ * @param resetAgeGate - When `true` (default) and the profile has `ageGate.enabled`, the
912
+ * age-gate prompt is shown again instead of jumping straight to the
913
+ * banner — mirroring the original first-visit flow. Pass `false` to
914
+ * skip it and go straight to the banner, keeping the visitor's prior
915
+ * age-gate answer.
647
916
  */
648
- reConsent(): Promise<void>;
917
+ reConsent(resetAgeGate?: boolean): Promise<void>;
918
+ /**
919
+ * Self-service erasure entry point — GDPR Art. 17 / CCPA-CPRA / LGPD Art. 18 and equivalent
920
+ * "right to be forgotten" rights, exercised from the widget itself. Wraps `reConsent()` (same
921
+ * erase-then-reprompt behavior) with `consenti:forgetMeRequested` / `consenti:forgotten`
922
+ * dispatched around it, so a host app can hook its own identity-verified erasure workflow
923
+ * across other systems (CRM, DMP, analytics) — the CMP itself only ever erases its own consent
924
+ * record. See the "Right to erasure" guide for the full picture, including the server-side
925
+ * `consent:erased` eventBus event fired by `DELETE /consent/:visitorId`.
926
+ *
927
+ * Powers the preference modal's "Forget me" button (`preferenceModal.showForgetMe`, only
928
+ * rendered when the visitor already has a stored consent decision). Also callable directly —
929
+ * e.g. `widget.forgetMe()` from a custom footer link or privacy-settings page.
930
+ *
931
+ * @param resetAgeGate - Forwarded to `reConsent()` — see its docs.
932
+ */
933
+ forgetMe(resetAgeGate?: boolean): Promise<void>;
649
934
  /**
650
935
  * Returns the fully resolved profile for the current session, or `null` during the
651
936
  * async init phase (before the profile has been fetched/resolved).
@@ -824,14 +1109,21 @@ declare function useConsent(): {
824
1109
  denyAll: (includingMandatory?: boolean) => Promise<void>;
825
1110
  /** Saves the given partial consent map. */
826
1111
  submitConsent: (c: Partial<ConsentValue>) => Promise<void | ConsentDbRecord>;
827
- /** Deletes the current consent record and re-shows the banner. */
828
- reConsent: () => Promise<void>;
1112
+ /** Deletes the current consent record and re-shows the banner. Pass `false` to skip
1113
+ * re-showing the age-gate prompt (if the profile has one) and go straight to the banner. */
1114
+ reConsent: (resetAgeGate?: boolean) => Promise<void>;
829
1115
  /** Returns `true` if the given cookie ID is `'granted'`. */
830
1116
  isCookieGranted: (id: string) => boolean;
831
1117
  /** Returns `true` if every cookie in the given category is `'granted'`. */
832
1118
  isCategoryGranted: (id: string) => boolean;
833
1119
  /** Switches the active locale and re-renders the banner and modal. */
834
1120
  switchLocale: (locale: string) => void | undefined;
1121
+ /** Returns the current logged-in application user ID, or `null` if anonymous. */
1122
+ getUserId: () => string | null;
1123
+ /** Sets the logged-in application user ID; reconsents by default when it changes. */
1124
+ setUserId: (userId: string | null, reConsent?: boolean) => Promise<void>;
1125
+ /** Snapshot of the current visitor's identity (`visitorId`, `type`, `userId`). */
1126
+ getVisitor: () => VisitorIdentity;
835
1127
  };
836
1128
 
837
1129
  export { setConsentiWidget, useConsent };
package/dist/vue.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var r=Object.defineProperty;var f=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var w=(o,t)=>{for(var a in t)r(o,a,{get:t[a],enumerable:!0})},b=(o,t,a,l)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of C(t))!m.call(o,i)&&i!==a&&r(o,i,{get:()=>t[i],enumerable:!(l=f(t,i))||l.enumerable});return o};var v=o=>b(r({},"__esModule",{value:!0}),o);var g={};w(g,{setConsentiWidget:()=>p,useConsent:()=>y});module.exports=v(g);var s=require("vue"),e=null;function p(o){e=o}function y(){let o=(0,s.ref)(e?.hasConsent()??!1),t=(0,s.ref)(e?.getConsent()??null),a=(0,s.ref)(e?.getConsentDate()??!1),l=(0,s.ref)(!1),i=(0,s.ref)(!1),d=()=>{o.value=e?.hasConsent()??!1,t.value=e?.getConsent()??null,a.value=e?.getConsentDate()??!1},u=()=>{l.value=e?.bannerVisibility()??!1},c=()=>{i.value=e?.modalVisibility()??!1};return(0,s.onMounted)(()=>{typeof window>"u"||(window.addEventListener("consenti:consentSubmitted",d),window.addEventListener("consenti:bannerVisibility",u),window.addEventListener("consenti:modalVisibility",c))}),(0,s.onUnmounted)(()=>{typeof window>"u"||(window.removeEventListener("consenti:consentSubmitted",d),window.removeEventListener("consenti:bannerVisibility",u),window.removeEventListener("consenti:modalVisibility",c))}),{hasConsent:o,consent:t,consentDate:a,bannerVisible:l,modalVisible:i,showBanner:n=>e?.showBanner(n),hideBanner:()=>e?.hideBanner(),showModal:()=>e?.showModal(),hideModal:()=>e?.hideModal(),grantAll:n=>e?.grantAll(n)??Promise.resolve(),denyAll:n=>e?.denyAll(n)??Promise.resolve(),submitConsent:n=>e?.submitConsent(n)??Promise.resolve(),reConsent:()=>e?.reConsent()??Promise.resolve(),isCookieGranted:n=>e?.isCookieGranted(n)??!1,isCategoryGranted:n=>e?.isCategoryGranted(n)??!1,switchLocale:n=>e?.switchLocale(n)}}
1
+ "use strict";var r=Object.defineProperty;var m=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var w=(t,o)=>{for(var l in o)r(t,l,{get:o[l],enumerable:!0})},v=(t,o,l,a)=>{if(o&&typeof o=="object"||typeof o=="function")for(let i of b(o))!C.call(t,i)&&i!==l&&r(t,i,{get:()=>o[i],enumerable:!(a=m(o,i))||a.enumerable});return t};var g=t=>v(r({},"__esModule",{value:!0}),t);var V={};w(V,{setConsentiWidget:()=>y,useConsent:()=>p});module.exports=g(V);var s=require("vue"),e=null;function y(t){e=t}function p(){let t=(0,s.ref)(e?.hasConsent()??!1),o=(0,s.ref)(e?.getConsent()??null),l=(0,s.ref)(e?.getConsentDate()??!1),a=(0,s.ref)(!1),i=(0,s.ref)(!1),d=()=>{t.value=e?.hasConsent()??!1,o.value=e?.getConsent()??null,l.value=e?.getConsentDate()??!1},u=()=>{a.value=e?.bannerVisibility()??!1},c=()=>{i.value=e?.modalVisibility()??!1};return(0,s.onMounted)(()=>{typeof window>"u"||(window.addEventListener("consenti:consentSubmitted",d),window.addEventListener("consenti:bannerVisibility",u),window.addEventListener("consenti:modalVisibility",c))}),(0,s.onUnmounted)(()=>{typeof window>"u"||(window.removeEventListener("consenti:consentSubmitted",d),window.removeEventListener("consenti:bannerVisibility",u),window.removeEventListener("consenti:modalVisibility",c))}),{hasConsent:t,consent:o,consentDate:l,bannerVisible:a,modalVisible:i,showBanner:n=>e?.showBanner(n),hideBanner:()=>e?.hideBanner(),showModal:()=>e?.showModal(),hideModal:()=>e?.hideModal(),grantAll:n=>e?.grantAll(n)??Promise.resolve(),denyAll:n=>e?.denyAll(n)??Promise.resolve(),submitConsent:n=>e?.submitConsent(n)??Promise.resolve(),reConsent:n=>e?.reConsent(n)??Promise.resolve(),isCookieGranted:n=>e?.isCookieGranted(n)??!1,isCategoryGranted:n=>e?.isCategoryGranted(n)??!1,switchLocale:n=>e?.switchLocale(n),getUserId:()=>e?.getUserId()??null,setUserId:(n,f)=>e?.setUserId(n,f)??Promise.resolve(),getVisitor:()=>e?.getVisitor()??{visitorId:null,type:"anonymous",userId:null}}}
2
2
  //# sourceMappingURL=vue.js.map
package/dist/vue.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vue.ts"],"sourcesContent":["/**\n * Vue 3 composable integration for `@consenti/ui`.\n *\n * Import from the `@consenti/ui/vue` subpath to keep this module\n * tree-shaken out of vanilla JS or React bundles.\n *\n * Intended for **Nuxt 3** and any Vue 3 project where you manage the `ConsentiSetup`\n * instance yourself (typically in a `.client.ts` plugin) and want `useConsent()`\n * to be reactive in components.\n *\n * @example\n * ```ts\n * // plugins/consenti.client.ts (Nuxt 3)\n * import { ConsentiSetup } from '@consenti/ui'\n * import { setConsentiWidget } from '@consenti/ui/vue'\n *\n * export default defineNuxtPlugin(() => {\n * const widget = new ConsentiSetup({\n * api: { enabled: true, baseUrl: useRuntimeConfig().public.apiUrl },\n * })\n * setConsentiWidget(widget)\n * })\n *\n * // Any component\n * import { useConsent } from '@consenti/ui/vue'\n *\n * const { hasConsent, consent, showModal } = useConsent()\n * // hasConsent, consent, bannerVisible, modalVisible are Refs — reactive in templates\n * ```\n */\n\nimport { ref, onMounted, onUnmounted } from 'vue'\nimport type { ConsentValue } from './types'\nimport type { ConsentiSetup } from './core/consenti-setup'\n\n/**\n * Module-level singleton — the active `ConsentiSetup` instance.\n * Using a module-level variable (rather than `provide`/`inject`) keeps the integration\n * zero-configuration: no wrapper component is required.\n */\nlet _widget: ConsentiSetup | null = null\n\n/**\n * Registers the active `ConsentiSetup` instance so `useConsent()` can read from it.\n * Call this once after creating your widget, typically in a `.client.ts` Nuxt plugin.\n *\n * @param widget - The `ConsentiSetup` instance to register.\n */\nexport function setConsentiWidget(widget: ConsentiSetup): void {\n _widget = widget\n}\n\n/**\n * Vue 3 composable that exposes consent state, banner/modal visibility, and widget actions.\n *\n * State values (`hasConsent`, `consent`, `consentDate`, `bannerVisible`, `modalVisible`) are\n * Vue `Ref`s — they are reactive in templates and `watch`/`computed` without calling `.value`\n * in the template (Vue auto-unwraps top-level refs).\n *\n * Event listeners are registered in `onMounted` and removed in `onUnmounted`, so this\n * composable is safe to call in components that render on the server (Nuxt SSR): the refs\n * simply hold their initial values (`false` / `null`) during SSR and hydrate correctly.\n *\n * @returns Reactive refs for consent state and plain functions for widget actions.\n */\nexport function useConsent() {\n const hasConsent = ref<boolean>(_widget?.hasConsent() ?? false)\n const consent = ref<ConsentValue | null>(_widget?.getConsent() ?? null)\n const consentDate = ref<Date | false>(_widget?.getConsentDate() ?? false)\n const bannerVisible = ref<'main' | 'gpc' | false>(false)\n const modalVisible = ref<'preference' | false>(false)\n\n const onConsent = () => {\n hasConsent.value = _widget?.hasConsent() ?? false\n consent.value = _widget?.getConsent() ?? null\n consentDate.value = _widget?.getConsentDate() ?? false\n }\n const onBanner = () => { bannerVisible.value = _widget?.bannerVisibility() ?? false }\n const onModal = () => { modalVisible.value = _widget?.modalVisibility() ?? false }\n\n onMounted(() => {\n if (typeof window === 'undefined') return\n window.addEventListener('consenti:consentSubmitted', onConsent)\n window.addEventListener('consenti:bannerVisibility', onBanner)\n window.addEventListener('consenti:modalVisibility', onModal)\n })\n\n onUnmounted(() => {\n if (typeof window === 'undefined') return\n window.removeEventListener('consenti:consentSubmitted', onConsent)\n window.removeEventListener('consenti:bannerVisibility', onBanner)\n window.removeEventListener('consenti:modalVisibility', onModal)\n })\n\n return {\n /** Reactive — `true` once the visitor has saved a valid consent record. */\n hasConsent,\n /** Reactive — the stored consent map (`Record<cookieId, status>`), or `null`. */\n consent,\n /** Reactive — `Date` of the last consent submission, or `false`. */\n consentDate,\n /** Reactive — `'main'`, `'gpc'`, or `false` (banner hidden). */\n bannerVisible,\n /** Reactive — `'preference'` or `false` (modal hidden). */\n modalVisible,\n\n /** Shows the consent banner. Pass `true` to force the GPC variant. */\n showBanner: (gpc?: boolean) => _widget?.showBanner(gpc),\n /** Hides the consent banner. */\n hideBanner: () => _widget?.hideBanner(),\n /** Opens the preference modal. */\n showModal: () => _widget?.showModal(),\n /** Closes the preference modal. */\n hideModal: () => _widget?.hideModal(),\n\n /** Grants all non-mandatory cookies. Pass `true` to grant only mandatory ones and deny the rest. */\n grantAll: (onlyMandatory?: boolean) => _widget?.grantAll(onlyMandatory) ?? Promise.resolve(),\n /** Denies all non-mandatory cookies. Pass `true` to deny mandatory ones too (use with care). */\n denyAll: (includingMandatory?: boolean) => _widget?.denyAll(includingMandatory) ?? Promise.resolve(),\n /** Saves the given partial consent map. */\n submitConsent: (c: Partial<ConsentValue>) => _widget?.submitConsent(c) ?? Promise.resolve(),\n /** Deletes the current consent record and re-shows the banner. */\n reConsent: () => _widget?.reConsent() ?? Promise.resolve(),\n\n /** Returns `true` if the given cookie ID is `'granted'`. */\n isCookieGranted: (id: string) => (_widget?.isCookieGranted(id) ?? false) as boolean,\n /** Returns `true` if every cookie in the given category is `'granted'`. */\n isCategoryGranted: (id: string) => (_widget?.isCategoryGranted(id) ?? false) as boolean,\n /** Switches the active locale and re-renders the banner and modal. */\n switchLocale: (locale: string) => _widget?.switchLocale(locale),\n }\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,uBAAAE,EAAA,eAAAC,IAAA,eAAAC,EAAAJ,GA+BA,IAAAK,EAA4C,eASxCC,EAAgC,KAQ7B,SAASJ,EAAkBK,EAA6B,CAC7DD,EAAUC,CACZ,CAeO,SAASJ,GAAa,CAC3B,IAAMK,KAAe,OAAaF,GAAS,WAAW,GAAK,EAAK,EAC1DG,KAAe,OAAyBH,GAAS,WAAW,GAAK,IAAI,EACrEI,KAAe,OAAkBJ,GAAS,eAAe,GAAK,EAAK,EACnEK,KAAgB,OAA4B,EAAK,EACjDC,KAAgB,OAA0B,EAAK,EAE/CC,EAAY,IAAM,CACtBL,EAAW,MAASF,GAAS,WAAW,GAAK,GAC7CG,EAAQ,MAAYH,GAAS,WAAW,GAAK,KAC7CI,EAAY,MAAQJ,GAAS,eAAe,GAAK,EACnD,EACMQ,EAAW,IAAM,CAAEH,EAAc,MAAQL,GAAS,iBAAiB,GAAK,EAAM,EAC9ES,EAAW,IAAM,CAAEH,EAAa,MAASN,GAAS,gBAAgB,GAAK,EAAM,EAEnF,sBAAU,IAAM,CACV,OAAO,OAAW,MACtB,OAAO,iBAAiB,4BAA6BO,CAAS,EAC9D,OAAO,iBAAiB,4BAA8BC,CAAQ,EAC9D,OAAO,iBAAiB,2BAA8BC,CAAO,EAC/D,CAAC,KAED,eAAY,IAAM,CACZ,OAAO,OAAW,MACtB,OAAO,oBAAoB,4BAA6BF,CAAS,EACjE,OAAO,oBAAoB,4BAA8BC,CAAQ,EACjE,OAAO,oBAAoB,2BAA8BC,CAAO,EAClE,CAAC,EAEM,CAEL,WAAAP,EAEA,QAAAC,EAEA,YAAAC,EAEA,cAAAC,EAEA,aAAAC,EAGA,WAAaI,GAAkBV,GAAS,WAAWU,CAAG,EAEtD,WAAY,IAAMV,GAAS,WAAW,EAEtC,UAAW,IAAMA,GAAS,UAAU,EAEpC,UAAW,IAAMA,GAAS,UAAU,EAGpC,SAAWW,GAA4BX,GAAS,SAASW,CAAa,GAAK,QAAQ,QAAQ,EAE3F,QAAUC,GAAiCZ,GAAS,QAAQY,CAAkB,GAAK,QAAQ,QAAQ,EAEnG,cAAgBC,GAA6Bb,GAAS,cAAca,CAAC,GAAK,QAAQ,QAAQ,EAE1F,UAAW,IAAMb,GAAS,UAAU,GAAK,QAAQ,QAAQ,EAGzD,gBAAkBc,GAAgBd,GAAS,gBAAgBc,CAAE,GAAK,GAElE,kBAAoBA,GAAgBd,GAAS,kBAAkBc,CAAE,GAAK,GAEtE,aAAeC,GAAmBf,GAAS,aAAae,CAAM,CAChE,CACF","names":["vue_exports","__export","setConsentiWidget","useConsent","__toCommonJS","import_vue","_widget","widget","hasConsent","consent","consentDate","bannerVisible","modalVisible","onConsent","onBanner","onModal","gpc","onlyMandatory","includingMandatory","c","id","locale"]}
1
+ {"version":3,"sources":["../src/vue.ts"],"sourcesContent":["/**\n * Vue 3 composable integration for `@consenti/ui`.\n *\n * Import from the `@consenti/ui/vue` subpath to keep this module\n * tree-shaken out of vanilla JS or React bundles.\n *\n * Intended for **Nuxt 3** and any Vue 3 project where you manage the `ConsentiSetup`\n * instance yourself (typically in a `.client.ts` plugin) and want `useConsent()`\n * to be reactive in components.\n *\n * @example\n * ```ts\n * // plugins/consenti.client.ts (Nuxt 3)\n * import { ConsentiSetup } from '@consenti/ui'\n * import { setConsentiWidget } from '@consenti/ui/vue'\n *\n * export default defineNuxtPlugin(() => {\n * const widget = new ConsentiSetup({\n * api: { enabled: true, baseUrl: useRuntimeConfig().public.apiUrl },\n * })\n * setConsentiWidget(widget)\n * })\n *\n * // Any component\n * import { useConsent } from '@consenti/ui/vue'\n *\n * const { hasConsent, consent, showModal } = useConsent()\n * // hasConsent, consent, bannerVisible, modalVisible are Refs — reactive in templates\n * ```\n */\n\nimport { ref, onMounted, onUnmounted } from 'vue'\nimport type { ConsentValue, VisitorIdentity } from './types'\nimport type { ConsentiSetup } from './core/consenti-setup'\n\n/**\n * Module-level singleton — the active `ConsentiSetup` instance.\n * Using a module-level variable (rather than `provide`/`inject`) keeps the integration\n * zero-configuration: no wrapper component is required.\n */\nlet _widget: ConsentiSetup | null = null\n\n/**\n * Registers the active `ConsentiSetup` instance so `useConsent()` can read from it.\n * Call this once after creating your widget, typically in a `.client.ts` Nuxt plugin.\n *\n * @param widget - The `ConsentiSetup` instance to register.\n */\nexport function setConsentiWidget(widget: ConsentiSetup): void {\n _widget = widget\n}\n\n/**\n * Vue 3 composable that exposes consent state, banner/modal visibility, and widget actions.\n *\n * State values (`hasConsent`, `consent`, `consentDate`, `bannerVisible`, `modalVisible`) are\n * Vue `Ref`s — they are reactive in templates and `watch`/`computed` without calling `.value`\n * in the template (Vue auto-unwraps top-level refs).\n *\n * Event listeners are registered in `onMounted` and removed in `onUnmounted`, so this\n * composable is safe to call in components that render on the server (Nuxt SSR): the refs\n * simply hold their initial values (`false` / `null`) during SSR and hydrate correctly.\n *\n * @returns Reactive refs for consent state and plain functions for widget actions.\n */\nexport function useConsent() {\n const hasConsent = ref<boolean>(_widget?.hasConsent() ?? false)\n const consent = ref<ConsentValue | null>(_widget?.getConsent() ?? null)\n const consentDate = ref<Date | false>(_widget?.getConsentDate() ?? false)\n const bannerVisible = ref<'main' | 'gpc' | false>(false)\n const modalVisible = ref<'preference' | false>(false)\n\n const onConsent = () => {\n hasConsent.value = _widget?.hasConsent() ?? false\n consent.value = _widget?.getConsent() ?? null\n consentDate.value = _widget?.getConsentDate() ?? false\n }\n const onBanner = () => { bannerVisible.value = _widget?.bannerVisibility() ?? false }\n const onModal = () => { modalVisible.value = _widget?.modalVisibility() ?? false }\n\n onMounted(() => {\n if (typeof window === 'undefined') return\n window.addEventListener('consenti:consentSubmitted', onConsent)\n window.addEventListener('consenti:bannerVisibility', onBanner)\n window.addEventListener('consenti:modalVisibility', onModal)\n })\n\n onUnmounted(() => {\n if (typeof window === 'undefined') return\n window.removeEventListener('consenti:consentSubmitted', onConsent)\n window.removeEventListener('consenti:bannerVisibility', onBanner)\n window.removeEventListener('consenti:modalVisibility', onModal)\n })\n\n return {\n /** Reactive — `true` once the visitor has saved a valid consent record. */\n hasConsent,\n /** Reactive — the stored consent map (`Record<cookieId, status>`), or `null`. */\n consent,\n /** Reactive — `Date` of the last consent submission, or `false`. */\n consentDate,\n /** Reactive — `'main'`, `'gpc'`, or `false` (banner hidden). */\n bannerVisible,\n /** Reactive — `'preference'` or `false` (modal hidden). */\n modalVisible,\n\n /** Shows the consent banner. Pass `true` to force the GPC variant. */\n showBanner: (gpc?: boolean) => _widget?.showBanner(gpc),\n /** Hides the consent banner. */\n hideBanner: () => _widget?.hideBanner(),\n /** Opens the preference modal. */\n showModal: () => _widget?.showModal(),\n /** Closes the preference modal. */\n hideModal: () => _widget?.hideModal(),\n\n /** Grants all non-mandatory cookies. Pass `true` to grant only mandatory ones and deny the rest. */\n grantAll: (onlyMandatory?: boolean) => _widget?.grantAll(onlyMandatory) ?? Promise.resolve(),\n /** Denies all non-mandatory cookies. Pass `true` to deny mandatory ones too (use with care). */\n denyAll: (includingMandatory?: boolean) => _widget?.denyAll(includingMandatory) ?? Promise.resolve(),\n /** Saves the given partial consent map. */\n submitConsent: (c: Partial<ConsentValue>) => _widget?.submitConsent(c) ?? Promise.resolve(),\n /** Deletes the current consent record and re-shows the banner. Pass `false` to skip\n * re-showing the age-gate prompt (if the profile has one) and go straight to the banner. */\n reConsent: (resetAgeGate?: boolean) => _widget?.reConsent(resetAgeGate) ?? Promise.resolve(),\n\n /** Returns `true` if the given cookie ID is `'granted'`. */\n isCookieGranted: (id: string) => (_widget?.isCookieGranted(id) ?? false) as boolean,\n /** Returns `true` if every cookie in the given category is `'granted'`. */\n isCategoryGranted: (id: string) => (_widget?.isCategoryGranted(id) ?? false) as boolean,\n /** Switches the active locale and re-renders the banner and modal. */\n switchLocale: (locale: string) => _widget?.switchLocale(locale),\n /** Returns the current logged-in application user ID, or `null` if anonymous. */\n getUserId: () => _widget?.getUserId() ?? null,\n /** Sets the logged-in application user ID; reconsents by default when it changes. */\n setUserId: (userId: string | null, reConsent?: boolean) => _widget?.setUserId(userId, reConsent) ?? Promise.resolve(),\n /** Snapshot of the current visitor's identity (`visitorId`, `type`, `userId`). */\n getVisitor: () => _widget?.getVisitor() ?? ({ visitorId: null, type: 'anonymous', userId: null } as VisitorIdentity),\n }\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,uBAAAE,EAAA,eAAAC,IAAA,eAAAC,EAAAJ,GA+BA,IAAAK,EAA4C,eASxCC,EAAgC,KAQ7B,SAASJ,EAAkBK,EAA6B,CAC7DD,EAAUC,CACZ,CAeO,SAASJ,GAAa,CAC3B,IAAMK,KAAe,OAAaF,GAAS,WAAW,GAAK,EAAK,EAC1DG,KAAe,OAAyBH,GAAS,WAAW,GAAK,IAAI,EACrEI,KAAe,OAAkBJ,GAAS,eAAe,GAAK,EAAK,EACnEK,KAAgB,OAA4B,EAAK,EACjDC,KAAgB,OAA0B,EAAK,EAE/CC,EAAY,IAAM,CACtBL,EAAW,MAASF,GAAS,WAAW,GAAK,GAC7CG,EAAQ,MAAYH,GAAS,WAAW,GAAK,KAC7CI,EAAY,MAAQJ,GAAS,eAAe,GAAK,EACnD,EACMQ,EAAW,IAAM,CAAEH,EAAc,MAAQL,GAAS,iBAAiB,GAAK,EAAM,EAC9ES,EAAW,IAAM,CAAEH,EAAa,MAASN,GAAS,gBAAgB,GAAK,EAAM,EAEnF,sBAAU,IAAM,CACV,OAAO,OAAW,MACtB,OAAO,iBAAiB,4BAA6BO,CAAS,EAC9D,OAAO,iBAAiB,4BAA8BC,CAAQ,EAC9D,OAAO,iBAAiB,2BAA8BC,CAAO,EAC/D,CAAC,KAED,eAAY,IAAM,CACZ,OAAO,OAAW,MACtB,OAAO,oBAAoB,4BAA6BF,CAAS,EACjE,OAAO,oBAAoB,4BAA8BC,CAAQ,EACjE,OAAO,oBAAoB,2BAA8BC,CAAO,EAClE,CAAC,EAEM,CAEL,WAAAP,EAEA,QAAAC,EAEA,YAAAC,EAEA,cAAAC,EAEA,aAAAC,EAGA,WAAaI,GAAkBV,GAAS,WAAWU,CAAG,EAEtD,WAAY,IAAMV,GAAS,WAAW,EAEtC,UAAW,IAAMA,GAAS,UAAU,EAEpC,UAAW,IAAMA,GAAS,UAAU,EAGpC,SAAWW,GAA4BX,GAAS,SAASW,CAAa,GAAK,QAAQ,QAAQ,EAE3F,QAAUC,GAAiCZ,GAAS,QAAQY,CAAkB,GAAK,QAAQ,QAAQ,EAEnG,cAAgBC,GAA6Bb,GAAS,cAAca,CAAC,GAAK,QAAQ,QAAQ,EAG1F,UAAYC,GAA2Bd,GAAS,UAAUc,CAAY,GAAK,QAAQ,QAAQ,EAG3F,gBAAkBC,GAAgBf,GAAS,gBAAgBe,CAAE,GAAK,GAElE,kBAAoBA,GAAgBf,GAAS,kBAAkBe,CAAE,GAAK,GAEtE,aAAeC,GAAmBhB,GAAS,aAAagB,CAAM,EAE9D,UAAW,IAAMhB,GAAS,UAAU,GAAK,KAEzC,UAAW,CAACiB,EAAuBC,IAAwBlB,GAAS,UAAUiB,EAAQC,CAAS,GAAK,QAAQ,QAAQ,EAEpH,WAAY,IAAMlB,GAAS,WAAW,GAAM,CAAE,UAAW,KAAM,KAAM,YAAa,OAAQ,IAAK,CACjG,CACF","names":["vue_exports","__export","setConsentiWidget","useConsent","__toCommonJS","import_vue","_widget","widget","hasConsent","consent","consentDate","bannerVisible","modalVisible","onConsent","onBanner","onModal","gpc","onlyMandatory","includingMandatory","c","resetAgeGate","id","locale","userId","reConsent"]}