@consenti/ui 0.1.0 → 0.2.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/README.md CHANGED
@@ -8,12 +8,13 @@ Zero-dependency, GDPR-compliant cookie consent widget for any web framework.
8
8
 
9
9
  ---
10
10
 
11
- ## Why this package?
11
+ ## Why Consenti?
12
12
 
13
13
  - **Zero runtime dependencies** — browser built-ins only (`fetch`, `crypto.subtle`, `BroadcastChannel`, `CustomEvent`, `document.cookie`, `localStorage`)
14
- - **Fully standalone or API-connected** — works offline with a local profile, or connects to `@consenti/api` for dashboard-managed profiles
15
- - **Multi-regulation** — GDPR, UK-GDPR, CCPA, CPRA, LGPD, DPDPA, PIPEDA, POPIA, PDPA-TH, APPI, KVKK
16
- - **GPC aware** — Global Privacy Control signal detection with `true` / `'strict'` modes
14
+ - **Fully standalone or API-connected** — works offline with pre-built profiles, or connects to `@consenti/api` for dashboard-managed profiles
15
+ - **Multi-regulation** — GDPR, CCPA, CPRA, LGPD, DPDPA, PIPL, and more via 8 built-in compliance groups
16
+ - **Automatic geo-resolution** — detects visitor jurisdiction client-side (timezone + language) or server-side (`/resolve-profile`)
17
+ - **GPC aware** — Global Privacy Control signal detection with `'honor'` / `'strict'` / `'ignore'` modes per profile
17
18
  - **GTM / Google Consent Mode v2** — built-in `dataLayer` integration
18
19
  - **SSR-safe** — all browser API access is guarded; `new ConsentiSetup()` during SSR is a silent no-op
19
20
  - **Fully typed** — ships TypeScript definitions; no `@types` needed
@@ -48,7 +49,7 @@ import '@consenti/ui/dist/index.css'
48
49
  <script src="https://cdn.jsdelivr.net/npm/@consenti/ui/dist/index.umd.js"></script>
49
50
  <script>
50
51
  const { ConsentiSetup } = ConsentiUI
51
- new ConsentiSetup({ core: { regulation: 'gdpr' } })
52
+ new ConsentiSetup({ compliance: { type: 'opt-in' } })
52
53
  </script>
53
54
  ```
54
55
 
@@ -57,7 +58,7 @@ import '@consenti/ui/dist/index.css'
57
58
  ```html
58
59
  <script type="module">
59
60
  import { ConsentiSetup } from 'https://esm.sh/@consenti/ui'
60
- new ConsentiSetup({ core: { regulation: 'gdpr' } })
61
+ new ConsentiSetup({ compliance: { type: 'opt-in' } })
61
62
  </script>
62
63
  ```
63
64
 
@@ -74,8 +75,16 @@ import '@consenti/ui/dist/index.css'
74
75
  ## Quick Start
75
76
 
76
77
  ```ts
77
- // Simplest possible — built-in GDPR profile, no backend required
78
- new ConsentiSetup({ core: { regulation: 'gdpr' } })
78
+ // Simplest possible — auto-detects jurisdiction from browser timezone + language
79
+ new ConsentiSetup({})
80
+
81
+ // Fixed GDPR (EU opt-in) profile, no backend required
82
+ new ConsentiSetup({ compliance: { type: 'opt-in' } })
83
+
84
+ // API-backed — server geo-resolves the right profile per visitor
85
+ new ConsentiSetup({
86
+ api: { enabled: true, baseUrl: 'https://your-backend.com' },
87
+ })
79
88
  ```
80
89
 
81
90
  A consent banner appears on first visit. Everything below is optional.
@@ -101,20 +110,27 @@ import { ConsentiSetup } from '@consenti/ui'
101
110
  import '@consenti/ui/dist/index.css'
102
111
 
103
112
  const widget = new ConsentiSetup({
104
- // ── Required ────────────────────────────────────────────────────────────────
113
+ // ── Core (optional) ──────────────────────────────────────────────────────────
105
114
  core: {
106
- profileId: 1, // 0 = built-in default; >0 = local or API profile
107
- regulation: 'gdpr', // controls opt-in / opt-out behaviour
108
- locale: 'en', // BCP 47; falls back to language prefix, then 'en'
109
- autoHonorGPC: true, // false | true | 'strict'
110
- storage: 'cookie', // 'cookie' | 'localStorage'
111
- cookieDomains: '.example.com',
112
- privacyPolicyUrl: '/privacy',
113
- signCookies: true,
114
- cookieSigningKey: 'min-32-char-secret', // client-only mode; use API mode in production
115
- allowReceipt: true,
116
- disableCssTemplate: false,
115
+ tenantId: 'acme', // identifies your tenant's profiles; default: 'default'
116
+ locale: 'en', // BCP 47; 'auto' = navigator.language; default: 'auto'
117
+ autoHonorGPC: true, // explicit operator override: false | true | 'strict'
118
+ // overrides profile.gpcMode when set
119
+ storage: 'cookie', // 'cookie' | 'localStorage'; default: 'cookie'
120
+ cookieDomains: '.example.com', // comma-separated; first entry used as Domain attribute
121
+ cookieSigningKey: 'min-32-char-secret', // HMAC-SHA256 signing; implicit from presence
122
+ allowReceipt: true, // allow consent receipt download; default: false
123
+ disableCssTemplate: false, // skip all style injection; default: false
117
124
  userId: 'server-assigned-uuid', // authenticated users — enables cross-device sync
125
+
126
+ // Pre-built profile fallback
127
+ usePrebuiltProfiles: 'all', // 'all' (default) | ['opt-in', 'opt-out', ...] (non-empty)
128
+ // controls which of the 8 built-in profiles are available
129
+ // as fallbacks when the API is unavailable
130
+ cacheResolvedProfiles: true, // cache /resolve-profile response in sessionStorage; default: true
131
+ console: ['error'], // log levels emitted: 'info' | 'log' | 'warning' | 'error'
132
+ // default: ['error'] only — no noise in production
133
+
118
134
  theme: {
119
135
  bgColor: '#ffffff',
120
136
  textColor: '#1a1a1a',
@@ -136,6 +152,28 @@ const widget = new ConsentiSetup({
136
152
  },
137
153
  },
138
154
 
155
+ // ── Compliance routing (optional) ─────────────────────────────────────────
156
+ compliance: {
157
+ // 'auto' → geo-resolve per visitor (default)
158
+ // api.enabled=true → server resolves via /resolve-profile
159
+ // api.enabled=false → resolves client-side from timezone + language
160
+ // ComplianceGroupId → fixed group for all visitors (skip geo)
161
+ // localProfileType → use a locally registered ConsentiProfile
162
+ type: 'auto',
163
+
164
+ // Client-side geo resolver (used only when api.enabled = false)
165
+ // 'default' → browser timezone + navigator.language heuristic (built-in, zero deps)
166
+ // WidgetCountryResolverFn → custom async function: () => Promise<{ country, region, confidence }>
167
+ geoDataProvider: 'default',
168
+
169
+ // Age gate (optional)
170
+ ageGate: {
171
+ enabled: false,
172
+ minimumAge: 13,
173
+ requireParentalConsent: false,
174
+ },
175
+ },
176
+
139
177
  // ── Mount point (optional) ────────────────────────────────────────────────
140
178
  rootEl: '#my-consent-wrapper', // CSS selector or HTMLElement; default: appends to body
141
179
 
@@ -147,6 +185,9 @@ const widget = new ConsentiSetup({
147
185
  enabled: true,
148
186
  baseUrl: 'https://your-site.com', // default: window.location.origin
149
187
  authToken: '', // sent as Authorization: Bearer <token>
188
+ tenantId: 'acme', // overrides core.tenantId for API calls
189
+ complianceGroup: 'opt-in', // pin a specific group on the API side (Scenario 2B)
190
+ trustDomain: false, // true = bypass allowedOrigins check (use in dev only)
150
191
  },
151
192
 
152
193
  // ── GTM / Google Consent Mode v2 (optional) ───────────────────────────────
@@ -172,45 +213,109 @@ const widget = new ConsentiSetup({
172
213
 
173
214
  ---
174
215
 
175
- ## Regulations
176
-
177
- | Value | Region | Model | Notes |
178
- |--------------|---------------------|---------------------|------------------------------------------------------------------------|
179
- | `'gdpr'` | EU / EEA | Opt-in | Default. Banner on first visit; all non-mandatory cookies denied until granted. |
180
- | `'uk-gdpr'` | UK | Opt-in | Same model as GDPR; ICO-enforced; age gate at 13. |
181
- | `'ccpa'` | California (US) | Opt-out | All cookies default to `'granted'`; consent written silently; no banner. |
182
- | `'cpra'` | California (US) | Opt-out / Opt-in | Supersedes CCPA. Opt-out for sale/sharing; opt-in for sensitive data. GPC triggers Do Not Sell + Do Not Share. |
183
- | `'lgpd'` | Brazil | Opt-in | 10 lawful bases; ANPD-enforced; parental consent gate under-12. |
184
- | `'dpdpa'` | India | Opt-in | Fiduciary name + grievance officer in modal; GPC ignored. |
185
- | `'pipeda'` | Canada | Opt-in | PIPEDA + Quebec Law 25 (GDPR-aligned). |
186
- | `'popia'` | South Africa | Opt-in | 8 processing conditions; Information Regulator-enforced. |
187
- | `'pdpa-th'` | Thailand | Opt-in | Cross-border transfer rules; PDPC-enforced. |
188
- | `'appi'` | Japan | Opt-in / Opt-out | Opt-in for sensitive data and overseas transfers; opt-out for general third-party sharing. |
189
- | `'kvkk'` | Turkey | Opt-in | Explicit consent for sensitive personal data; KVK Board-enforced. |
216
+ ## Compliance Groups
190
217
 
191
- ---
218
+ Consenti ships 8 pre-built English profiles, one per compliance group. The active group is resolved automatically per visitor (API or client-side) or fixed globally via `compliance.type`.
192
219
 
193
- ## GPC — Global Privacy Control
220
+ | Group | Region / Law | Model | GPC Default |
221
+ |----------------------------|-------------------------------|------------|-------------|
222
+ | `'opt-in'` | EU / EEA — GDPR | Opt-in | `'honor'` |
223
+ | `'opt-out'` | California — CCPA | Opt-out | `'honor'` |
224
+ | `'opt-out-strict'` | California — CPRA | Strict opt-out | `'strict'` |
225
+ | `'opt-in-dpdpa'` | India — DPDPA | Opt-in | `'honor'` |
226
+ | `'opt-in-china'` | China — PIPL | Opt-in | `'ignore'` |
227
+ | `'opt-in-brazil'` | Brazil — LGPD | Opt-in | `'honor'` |
228
+ | `'general-privacy-consent'`| Global / general | Opt-in | `'honor'` |
229
+ | `'notice-only'` | Informational | Notice | `'ignore'` |
230
+
231
+ ### Fixed group (no geo-routing)
194
232
 
195
233
  ```ts
234
+ // All visitors see the GDPR opt-in banner
235
+ new ConsentiSetup({ compliance: { type: 'opt-in' } })
236
+
237
+ // All visitors see the CCPA opt-out notice
238
+ new ConsentiSetup({ compliance: { type: 'opt-out' } })
239
+ ```
240
+
241
+ ### Auto geo-routing (default)
242
+
243
+ ```ts
244
+ // Client-side: timezone + navigator.language → compliance group
245
+ new ConsentiSetup({ compliance: { type: 'auto' } })
246
+
247
+ // Server-side: /resolve-profile → right profile per visitor country
196
248
  new ConsentiSetup({
197
- core: {
198
- regulation: 'gdpr',
199
- autoHonorGPC: true, // pre-deny 'listenGpc' cookies and show GPC banner variant
200
- // autoHonorGPC: 'strict' // pre-deny silently — no banner shown at all
201
- },
249
+ api: { enabled: true, baseUrl: 'https://consent.example.com' },
250
+ compliance: { type: 'auto' },
202
251
  })
203
252
  ```
204
253
 
205
- Cookies with `listenGpc: true` in the profile are automatically denied when the signal is detected.
254
+ ---
255
+
256
+ ## Profile Resolution Scenarios
257
+
258
+ | Scenario | When | How |
259
+ |---|---|---|
260
+ | **1A** | `api.enabled = false`, `compliance.type = 'auto'` | Timezone + language → group → pre-built profile |
261
+ | **1B** | `api.enabled = false`, `compliance.type = ComplianceGroupId` | Direct pre-built profile load |
262
+ | **2A** | `api.enabled = true`, `compliance.type = 'auto'` | `GET /resolve-profile?tz&lang&locale&tenantId` → `filePath` → static JSON |
263
+ | **2B** | `api.enabled = true`, `compliance.type = ComplianceGroupId` | `GET /profiles/:tenantId/:group/:locale.json` |
264
+ | **Local** | `compliance.type = localKey` + registered `ConsentiProfile` | Locally registered profile |
265
+ | **Fallback** | Any fetch failure | Pre-built profile → `DEFAULT_PROFILE` |
266
+
267
+ The `/resolve-profile` response URL is cached in `sessionStorage` for 1 hour per tab (`core.cacheResolvedProfiles`). The profile JSON itself is HTTP-cached via `Cache-Control: public, max-age=3600` on the server.
268
+
269
+ ### Domain allowlist
270
+
271
+ If a profile's `allowedOrigins` list is configured on the server, the widget checks `window.location.origin` against it before using the profile. A mismatch falls back to the pre-built profile for that group.
272
+
273
+ Set `api.trustDomain: true` to bypass this check (useful for localhost / dev environments where `allowedOrigins` contains only production domains).
274
+
275
+ ---
276
+
277
+ ## GPC — Global Privacy Control
278
+
279
+ GPC mode is set per-profile on the server (`gpcMode: 'honor' | 'strict' | 'ignore'`). Widget config (`core.autoHonorGPC`) overrides the profile value when explicitly set.
280
+
281
+ ```ts
282
+ // Respect profile's gpcMode setting (default)
283
+ new ConsentiSetup({ compliance: { type: 'opt-in' } })
284
+
285
+ // Force honor GPC regardless of profile setting
286
+ new ConsentiSetup({ core: { autoHonorGPC: true } })
287
+
288
+ // Force strict GPC — pre-deny silently, no banner
289
+ new ConsentiSetup({ core: { autoHonorGPC: 'strict' } })
290
+ ```
291
+
292
+ | `core.autoHonorGPC` | Effect |
293
+ |---|---|
294
+ | `undefined` (default) | Profile's `gpcMode` takes effect |
295
+ | `false` | GPC signal always ignored |
296
+ | `true` | `'honor'` — pre-deny GPC cookies; show GPC banner variant |
297
+ | `'strict'` | Pre-deny silently — no banner shown |
206
298
 
207
299
  ---
208
300
 
209
301
  ## Profiles
210
302
 
211
- ### Built-in default profile
303
+ ### Pre-built profiles (no backend)
304
+
305
+ The widget includes 8 pre-built English profiles as dynamic-import chunks. Only the matched chunk downloads at runtime.
306
+
307
+ ```ts
308
+ // GDPR banner — no server needed
309
+ new ConsentiSetup({ compliance: { type: 'opt-in' } })
310
+
311
+ // CCPA notice — no server needed
312
+ new ConsentiSetup({ compliance: { type: 'opt-out' } })
212
313
 
213
- When `core.profileId` is `0` or omitted, the widget uses a built-in English GDPR profile covering the five Google Consent Mode v2 purposes. No configuration needed.
314
+ // Restrict which pre-built profiles are available as fallbacks
315
+ new ConsentiSetup({
316
+ core: { usePrebuiltProfiles: ['opt-in', 'opt-out'] },
317
+ })
318
+ ```
214
319
 
215
320
  ### Local profile (no backend)
216
321
 
@@ -221,11 +326,12 @@ import { ConsentiProfile, ConsentiSetup } from '@consenti/ui'
221
326
 
222
327
  const profile = new ConsentiProfile({
223
328
  defaultLocale: 'en',
329
+ complianceGroup: 'opt-in',
224
330
  cookies: [
225
331
  { id: 'necessary', mandatory: true, expiry: 365 },
226
332
  { id: 'analytics', listenGpc: true, expiry: 365 },
227
333
  { id: 'marketing', listenGpc: true, expiry: 365, cpraCategory: 'sale' },
228
- // cpraCategory: 'sale' | 'sharing' | 'sensitive' — used when regulation: 'cpra'
334
+ // cpraCategory: 'sale' | 'sharing' | 'sensitive' — used by CPRA group
229
335
  ],
230
336
  translations: {
231
337
  en: {
@@ -240,10 +346,9 @@ const profile = new ConsentiProfile({
240
346
  buttons: [
241
347
  { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
242
348
  // cookies: '*' = grant all | '!' = deny all | ['id1','id2'] = grant specific
243
- { text: 'Reject All', style: 'secondary', action: 'custom', cookies: '!' },
244
- { text: 'Manage Preferences', style: 'secondary', action: 'manage' },
349
+ { text: 'Reject Optional', style: 'secondary', action: 'custom', cookies: '!' },
350
+ { text: 'Customize', style: 'secondary', action: 'manage' },
245
351
  { text: 'Privacy Policy', style: 'text', action: 'link', url: '/privacy' },
246
- // action: 'link' + url — renders as a text link below the banner
247
352
  ],
248
353
  },
249
354
  gpcBanner: { // shown instead of mainBanner when GPC detected
@@ -253,13 +358,13 @@ const profile = new ConsentiProfile({
253
358
  showLocaleSwitcher: false,
254
359
  htmlText: "Your browser's GPC signal was detected. Ad cookies have been pre-denied.",
255
360
  buttons: [
256
- { text: 'Understood', style: 'primary', action: 'custom', cookies: '!' },
257
- { text: 'Manage Preferences', style: 'secondary', action: 'manage' },
361
+ { text: 'Understood', style: 'primary', action: 'custom', cookies: '!' },
362
+ { text: 'Customize', style: 'secondary', action: 'manage' },
258
363
  ],
259
364
  },
260
365
  preferenceModal: {
261
366
  heading: 'Cookie Preferences',
262
- headingTag: 'h2', // HTML tag for the modal heading; default 'h2'
367
+ headingTag: 'h2',
263
368
  subheading: 'Choose which cookies you allow.',
264
369
  htmlText: 'We use different types of cookies. You can enable or disable each category below.',
265
370
  position: 'center', // 'left' | 'right' | 'center'
@@ -267,16 +372,17 @@ const profile = new ConsentiProfile({
267
372
  showLocaleSwitcher: false,
268
373
  persistent: false, // true = cannot dismiss by clicking outside
269
374
  overlayOpacity: 50,
375
+ mobileFullScreenBreakpoint: 576,
270
376
  buttons: [
271
377
  { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
272
378
  { text: 'Save Preferences', style: 'primary', action: 'submit' },
273
- { text: 'Reject All', style: 'text', action: 'custom', cookies: '!' },
379
+ { text: 'Reject Optional', style: 'text', action: 'custom', cookies: '!' },
274
380
  ],
275
381
  categories: [
276
382
  {
277
383
  id: 'cat-necessary',
278
384
  heading: 'Strictly Necessary',
279
- headingTag: 'h3', // HTML tag for the category heading; default 'h3'
385
+ headingTag: 'h3',
280
386
  htmlText: 'Required for the site to function. <strong>Cannot be disabled.</strong>',
281
387
  mandatory: true,
282
388
  cookies: ['necessary'],
@@ -286,7 +392,6 @@ const profile = new ConsentiProfile({
286
392
  heading: 'Analytics',
287
393
  htmlText: 'Helps us understand how visitors use the site.',
288
394
  type: 'consent', // 'consent' | 'legitimate_interest'
289
- // type: 'legitimate_interest' — refusal stored as 'objected' instead of 'denied'
290
395
  cookies: ['analytics'],
291
396
  },
292
397
  {
@@ -307,35 +412,30 @@ const profile = new ConsentiProfile({
307
412
  })
308
413
 
309
414
  new ConsentiSetup({
310
- core: {
311
- profileId: profile.getId(), // always use .getId() — never hardcode the ID
312
- regulation: 'gdpr',
313
- locale: 'fr',
314
- },
415
+ compliance: { type: profile.getComplianceGroup() },
315
416
  })
316
417
  ```
317
418
 
318
- > **Note:** Local profile IDs are auto-assigned starting from `1000` to avoid collisions with API profile IDs (which start from `1`). Always call `profile.getId()`.
319
-
320
419
  ### API profile (with backend)
321
420
 
322
421
  ```ts
323
422
  new ConsentiSetup({
324
- core: { profileId: 3, regulation: 'gdpr', locale: 'de' },
325
423
  api: {
326
424
  enabled: true,
327
425
  baseUrl: 'https://consent.example.com',
426
+ tenantId: 'acme',
328
427
  },
428
+ // compliance.type: 'auto' resolves the right profile per visitor country
329
429
  })
330
430
  ```
331
431
 
332
- The widget fetches the profile from the API and falls back to a local profile (or the built-in default) if the request fails.
432
+ The widget calls `GET /resolve-profile?tz&lang&locale&tenantId`, receives the profile file path, then fetches that static JSON directly. Falls back to pre-built profile if the API is unavailable.
333
433
 
334
434
  ---
335
435
 
336
436
  ## Cookie Format
337
437
 
338
- Consent is persisted as a single cookie named `consenti_{profileId}`.
438
+ Consent is persisted as a single cookie named `consenti_{userId}_{profileId}`.
339
439
 
340
440
  ```
341
441
  t:{unixTimestamp}::{cookieId}:{status}|{cookieId}:{status}|…[::sig:{hmac}]
@@ -346,18 +446,38 @@ Example:
346
446
  t:1782133054::analytics:granted|marketing:denied|necessary:granted::sig:abc123
347
447
  ```
348
448
 
449
+ The consent cookie lifetime equals the shortest `cookie.expiry` value across all profile cookies (in days, converted to `max-age` seconds). When that cookie expires the browser deletes it, the widget sees no consent, and re-shows the banner.
450
+
451
+ ---
452
+
453
+ ## Debugging
454
+
455
+ ```ts
456
+ new ConsentiSetup({
457
+ core: {
458
+ console: ['error', 'warning', 'info'], // add 'log' for verbose output
459
+ },
460
+ })
461
+ ```
462
+
463
+ The default is `['error']` only — no noise in production. Add levels when investigating profile resolution, GPC behavior, or domain allowlist failures. All Consenti log lines are prefixed `[Consenti]`.
464
+
349
465
  ---
350
466
 
351
467
  ## Widget API Methods
352
468
 
353
469
  ```ts
354
- const widget = new ConsentiSetup({ core: { regulation: 'gdpr' } })
470
+ const widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
355
471
 
356
472
  // Consent state
357
- widget.hasConsent() // boolean — true if valid consent record exists
358
- widget.getConsent() // Record<string, 'granted'|'denied'|'objected'> | null
359
- widget.getGTMConsent() // consent in GTM / Google Consent Mode v2 format
360
- widget.getConsentDate() // Date | false — time of last submission
473
+ widget.hasConsent() // boolean — true if valid consent record exists
474
+ widget.getConsent() // Record<string, 'granted'|'denied'|'objected'> | null
475
+ widget.getGTMConsent() // consent in GTM / Google Consent Mode v2 format
476
+ widget.getConsentDate() // Date | false — time of last submission
477
+ widget.isCookieGranted('analytics') // boolean — true if that cookie is 'granted'
478
+ widget.isCookieGranted('analytics', true)// 'granted' | 'denied' | 'objected' | false
479
+ widget.isCategoryGranted('cat-analytics') // boolean — true if ALL cookies in category are granted
480
+ widget.isCategoryGranted('cat-analytics', true) // [{ analytics: 'granted' }, { pixel: 'denied' }]
361
481
 
362
482
  // Visibility
363
483
  widget.showBanner(gpc?) // show main banner (or GPC variant)
@@ -367,6 +487,12 @@ widget.hideModal() // close preference modal
367
487
  widget.bannerVisibility() // 'main' | 'gpc' | false
368
488
  widget.modalVisibility() // 'preference' | false
369
489
 
490
+ // Bulk consent actions
491
+ widget.grantAll() // grant all cookies and dismiss banner
492
+ widget.grantAll(true) // grant only mandatory; deny everything else
493
+ widget.denyAll() // deny non-mandatory; mandatory stay 'granted'
494
+ widget.denyAll(true) // deny all including mandatory (logs a warning)
495
+
370
496
  // Actions
371
497
  widget.init() // manually start init (when autoInit: false)
372
498
  widget.onReady(callback) // called once widget is fully initialised
@@ -374,15 +500,61 @@ widget.switchLocale(locale) // switch locale and re-render (e.g. 'fr', 'de-
374
500
  widget.submitConsent(consent) // programmatically submit consent values
375
501
  widget.deleteConsent() // delete consent record (cookie + backend if enabled)
376
502
  widget.reConsent() // delete consent and re-show banner
377
- widget.downloadReceipt() // trigger JSON consent receipt download
378
503
  widget.destroy() // unmount DOM elements and remove all event listeners
504
+
505
+ // Runtime configuration
506
+ widget.setDarkMode() // toggle dark mode
507
+ widget.setDarkMode(true) // force dark on
508
+ widget.setDarkMode(false) // force light
509
+ widget.setTheme({ primaryColor: '#ff0000' }) // hot-swap CSS tokens (merges into current theme)
510
+ widget.setConfig({ darkMode: true }) // deep-merge partial config (no re-init)
511
+ widget.setProfile({ mainBanner: { heading: 'Updated' } }) // re-render UI with new profile data
512
+
513
+ // Diagnostics
514
+ widget.version() // { package: '1.0.0', profileVersion: 2, consentVersion: 1 }
515
+ ```
516
+
517
+ ### Consent checks
518
+
519
+ ```ts
520
+ // Gate analytics code on a single cookie
521
+ if (widget.isCookieGranted('analytics_storage')) {
522
+ initAnalytics()
523
+ }
524
+
525
+ // Read the raw status string when you need it
526
+ const status = widget.isCookieGranted('marketing', true) // 'granted' | 'denied' | 'objected' | false
527
+
528
+ // Gate a feature on an entire category (all cookies in category must be granted)
529
+ if (widget.isCategoryGranted('cat-analytics')) {
530
+ loadHeatmaps()
531
+ }
532
+
533
+ // Inspect each cookie's status in a category
534
+ const statuses = widget.isCategoryGranted('cat-marketing', true)
535
+ // [{ ad_storage: 'granted' }, { ad_personalization: 'denied' }]
536
+ ```
537
+
538
+ ### Typed event subscriptions
539
+
540
+ ```ts
541
+ // Both forms are accepted — 'consenti:' prefix is optional
542
+ const handler = (data) => console.log('Consent saved:', data.consent)
543
+ widget.on('consentSubmitted', handler)
544
+
545
+ // Remove later (must pass the same function reference)
546
+ widget.off('consentSubmitted', handler)
547
+
548
+ // Available event names:
549
+ // 'bannerInitialized' | 'bannerVisibility' | 'modalVisibility'
550
+ // 'consentBeingSubmitted' | 'consentSubmitted'
379
551
  ```
380
552
 
381
553
  ### Manual init
382
554
 
383
555
  ```ts
384
556
  const widget = new ConsentiSetup({
385
- core: {},
557
+ compliance: { type: 'opt-in' },
386
558
  rootEl: '#consent-mount',
387
559
  autoInit: false,
388
560
  })
@@ -400,32 +572,39 @@ await widget.submitConsent({
400
572
  marketing: 'denied',
401
573
  necessary: 'granted', // mandatory cookies are always 'granted' regardless
402
574
  })
575
+
576
+ // Or use the convenience methods:
577
+ await widget.grantAll() // accept all
578
+ await widget.denyAll() // reject all non-mandatory
403
579
  ```
404
580
 
405
581
  ---
406
582
 
407
583
  ## Events
408
584
 
409
- Custom DOM events fire on `document` at every lifecycle step. All are prefixed `consenti:`.
410
-
411
- | Event | Fired when | Detail type |
412
- |--------------------------------|----------------------------------------------------------|--------------------------------|
413
- | `consenti:bannerInitialized` | Widget initialises and determines banner visibility | `BannerInitializedDetail` |
414
- | `consenti:bannerVisibility` | Banner shows or hides | `BannerVisibilityDetail` |
415
- | `consenti:modalVisibility` | Preference modal shows or hides | `ModalVisibilityDetail` |
416
- | `consenti:consentBeingSubmitted` | User clicked a consent button (before API call) | `ConsentBeingSubmittedDetail` |
417
- | `consenti:consentSubmitted` | Consent saved (cookie written + API call if configured) | `ConsentSubmittedDetail` |
418
-
419
- ```ts
420
- document.addEventListener('consenti:consentSubmitted', (e: Event) => {
421
- const detail = (e as CustomEvent<ConsentSubmittedDetail>).detail
422
- // detail.consentAction — 'accept_all' | 'reject_all' | 'custom' | 'update'
423
- // detail.consentJson — { analytics: 'granted', marketing: 'denied', ... }
424
- // detail.visitorId — stable UUID
425
- // detail.pageUrl — window.location.href at submission time
426
- // detail.gpcDetected — boolean
427
- // detail.timestamp — ISO 8601
428
- // detail.apiResponse — backend response (if api.enabled: true)
585
+ Custom DOM events fire on `window` at every lifecycle step. All are prefixed `consenti:`.
586
+
587
+ | Event | Fired when |
588
+ |--------------------------------|----------------------------------------------------------|
589
+ | `consenti:bannerInitialized` | Widget initialises and determines banner visibility |
590
+ | `consenti:bannerVisibility` | Banner shows or hides |
591
+ | `consenti:modalVisibility` | Preference modal shows or hides |
592
+ | `consenti:consentBeingSubmitted` | User clicked a consent button (before API call) |
593
+ | `consenti:consentSubmitted` | Consent saved (cookie written + API call if configured) |
594
+
595
+ **Recommended:** use `widget.on()` / `widget.off()` for typed subscriptions (the `consenti:` prefix is optional):
596
+
597
+ ```ts
598
+ const handler = (data: ConsentEvent) => console.log(data.consent)
599
+ widget.on('consentSubmitted', handler)
600
+ widget.off('consentSubmitted', handler) // same reference to unsubscribe
601
+ ```
602
+
603
+ **Raw DOM listeners** (equivalent):
604
+
605
+ ```ts
606
+ window.addEventListener('consenti:consentSubmitted', (e: Event) => {
607
+ const detail = (e as CustomEvent<ConsentEvent>).detail
429
608
  })
430
609
  ```
431
610
 
@@ -435,7 +614,7 @@ document.addEventListener('consenti:consentSubmitted', (e: Event) => {
435
614
 
436
615
  ```ts
437
616
  new ConsentiSetup({
438
- core: { regulation: 'gdpr' },
617
+ compliance: { type: 'opt-in' },
439
618
  utils: {
440
619
  gtm: {
441
620
  containerId: 'GTM-XXXXXX',
@@ -483,6 +662,8 @@ new ConsentScript({
483
662
  src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXX',
484
663
  onLoad: () => console.log('Analytics loaded'),
485
664
  onRevoke: () => console.log('Analytics removed'),
665
+ // bind: true (default) — auto-removes script on consent revoke, re-injects on re-grant
666
+ // bind: false — check consent once at construction; never auto-remove
486
667
  })
487
668
  ```
488
669
 
@@ -555,8 +736,8 @@ Override any token in your own stylesheet:
555
736
 
556
737
  ```ts
557
738
  new ConsentiSetup({
739
+ compliance: { type: 'opt-in' },
558
740
  core: {
559
- regulation: 'gdpr',
560
741
  theme: {
561
742
  primaryColor: '#7c3aed', // purple brand
562
743
  primaryTextColor: '#ffffff',
@@ -572,7 +753,7 @@ new ConsentiSetup({
572
753
 
573
754
  ```ts
574
755
  new ConsentiSetup({
575
- core: { profileId: 1 },
756
+ compliance: { type: 'opt-in' },
576
757
  darkMode: true, // or: window.matchMedia('(prefers-color-scheme: dark)').matches
577
758
  })
578
759
  ```
@@ -631,7 +812,8 @@ import '@consenti/ui/dist/index.css'
631
812
  export function ConsentSetup() {
632
813
  useEffect(() => {
633
814
  const widget = new ConsentiSetup({
634
- core: { regulation: 'gdpr', locale: 'en' },
815
+ compliance: { type: 'opt-in' },
816
+ core: { locale: 'en' },
635
817
  })
636
818
  return () => widget.destroy()
637
819
  }, [])
@@ -687,8 +869,8 @@ export function ConsentSetup() {
687
869
  let widget: WidgetType
688
870
  import('@consenti/ui').then(({ ConsentiSetup }) => {
689
871
  widget = new ConsentiSetup({
690
- core: { regulation: 'gdpr', locale: 'en', autoHonorGPC: true },
691
872
  api: { enabled: true, baseUrl: process.env.NEXT_PUBLIC_API_URL },
873
+ core: { autoHonorGPC: true },
692
874
  })
693
875
  widgetRef.current = widget
694
876
  })
@@ -710,7 +892,7 @@ let widget: unknown = null
710
892
  onMounted(async () => {
711
893
  const { ConsentiSetup } = await import('@consenti/ui')
712
894
  await import('@consenti/ui/dist/index.css')
713
- widget = new ConsentiSetup({ core: { regulation: 'gdpr' } })
895
+ widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
714
896
  })
715
897
 
716
898
  onBeforeUnmount(() => (widget as { destroy?: () => void })?.destroy?.())
@@ -739,7 +921,7 @@ export class ConsentService implements OnDestroy {
739
921
  async init() {
740
922
  if (!isPlatformBrowser(this.platformId)) return
741
923
  const { ConsentiSetup } = await import('@consenti/ui')
742
- this.widget = new ConsentiSetup({ core: { regulation: 'gdpr' } })
924
+ this.widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
743
925
  }
744
926
 
745
927
  ngOnDestroy() {
@@ -766,7 +948,7 @@ export class MyComponent {
766
948
  import { ConsentiSetup } from '@consenti/ui'
767
949
  import '@consenti/ui/dist/index.css'
768
950
 
769
- const widget = new ConsentiSetup({ core: { regulation: 'gdpr', locale: 'en' } })
951
+ const widget = new ConsentiSetup({ compliance: { type: 'opt-in' }, core: { locale: 'en' } })
770
952
 
771
953
  document.querySelector('#cookie-settings')?.addEventListener('click', () => {
772
954
  widget.showModal()
@@ -798,7 +980,7 @@ class MyPlugin extends ConsentiPlugin {
798
980
  }
799
981
 
800
982
  new ConsentiSetup({
801
- core: { regulation: 'gdpr' },
983
+ compliance: { type: 'opt-in' },
802
984
  plugins: [new MyPlugin()],
803
985
  })
804
986
  ```
@@ -809,7 +991,7 @@ new ConsentiSetup({
809
991
 
810
992
  ```ts
811
993
  // Safe to call during SSR — silently no-ops, never touches the DOM:
812
- const widget = new ConsentiSetup({ core: { regulation: 'gdpr' } })
994
+ const widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
813
995
  // All browser API access is guarded by isClient() internally.
814
996
  ```
815
997
 
@@ -830,8 +1012,8 @@ import {
830
1012
  simulateConsentSubmitted,
831
1013
  } from '@consenti/ui/testing'
832
1014
 
833
- mockAllGranted() // fake-grant all cookies for unit tests
834
- mockAllDenied() // fake-deny all
1015
+ mockAllGranted(['analytics', 'marketing']) // fake-grant these cookies for unit tests
1016
+ mockAllDenied(['analytics', 'marketing']) // fake-deny these cookies
835
1017
  simulateConsentSubmitted({ analytics: 'granted' })
836
1018
  ```
837
1019
 
@@ -840,20 +1022,31 @@ simulateConsentSubmitted({ analytics: 'granted' })
840
1022
  ## Minimal Recipes
841
1023
 
842
1024
  ```ts
843
- // CCPA opt-out (no banner shown)
844
- new ConsentiSetup({ core: { regulation: 'ccpa' } })
1025
+ // Auto geo-route — no backend, client-side timezone + language heuristic
1026
+ new ConsentiSetup({})
1027
+
1028
+ // CCPA opt-out for all visitors
1029
+ new ConsentiSetup({ compliance: { type: 'opt-out' } })
845
1030
 
846
1031
  // Cross-subdomain consent
847
- new ConsentiSetup({ core: { regulation: 'gdpr', storage: 'cookie', cookieDomains: '.example.com' } })
1032
+ new ConsentiSetup({ core: { storage: 'cookie', cookieDomains: '.example.com' } })
848
1033
 
849
1034
  // Authenticated user — cross-device sync via API
850
- new ConsentiSetup({ core: { regulation: 'gdpr', userId: '{{ server_user_id }}' }, api: { enabled: true } })
1035
+ new ConsentiSetup({
1036
+ core: { userId: '{{ server_user_id }}' },
1037
+ api: { enabled: true },
1038
+ })
851
1039
 
852
1040
  // GPC strict mode + GTM
853
1041
  new ConsentiSetup({
854
- core: { regulation: 'gdpr', autoHonorGPC: 'strict' },
1042
+ core: { autoHonorGPC: 'strict' },
855
1043
  utils: { gtm: { containerId: 'GTM-XXXXXX', adsDataRedaction: true } },
856
1044
  })
1045
+
1046
+ // Verbose debug logging during development
1047
+ new ConsentiSetup({
1048
+ core: { console: ['error', 'warning', 'info', 'log'] },
1049
+ })
857
1050
  ```
858
1051
 
859
1052
  ---
@@ -864,17 +1057,32 @@ All types are exported from the main entry:
864
1057
 
865
1058
  ```ts
866
1059
  import type {
867
- ConsentiConfig, // top-level config object
868
- CoreConfig, // core section
869
- ApiConfig, // api section
870
- UtilsConfig, // utils section
871
- GtmConfig, // utils.gtm section
872
- ThemeConfig, // core.theme section
873
- ConsentValue, // 'granted' | 'denied' | 'objected'
874
- ConsentiProfile, // local profile class
1060
+ ConsentiConfig, // top-level config object
1061
+ CoreConfig, // core section
1062
+ ApiConfig, // api section
1063
+ ComplianceWidgetConfig, // compliance section
1064
+ AgeGateWidgetConfig, // compliance.ageGate section
1065
+ WidgetCountryResolverFn, // custom geo resolver function type
1066
+ UtilsConfig, // utils section
1067
+ GtmConfig, // utils.gtm section
1068
+ ThemeConfig, // core.theme section
1069
+ ConsentValue, // 'granted' | 'denied' | 'objected'
1070
+ ConsentiProfile, // local profile class
1071
+ NonEmptyArray, // [T, ...T[]] — used by usePrebuiltProfiles
875
1072
  } from '@consenti/ui'
876
1073
  ```
877
1074
 
1075
+ ### Breaking changes from v0.0.x
1076
+
1077
+ The following `CoreConfig` fields were **removed** in v0.1.x:
1078
+
1079
+ | Removed field | Replacement |
1080
+ |----------------------|-------------|
1081
+ | `core.profileId` | Use `compliance.type` with a compliance group or local profile key |
1082
+ | `core.regulation` | Use `compliance.type` — compliance group is derived server-side or from pre-built profiles |
1083
+ | `core.signCookies` | Implicit: set `core.cookieSigningKey` to enable signing; no separate flag needed |
1084
+ | `core.privacyPolicyUrl` | Add a link button directly in the profile banner/modal `buttons` array with `action: 'link'` |
1085
+
878
1086
  ---
879
1087
 
880
1088
  ## Contributing