@freshjuice/zest 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/dist/zest.de.js +2621 -0
  4. package/dist/zest.de.js.map +1 -0
  5. package/dist/zest.de.min.js +1 -0
  6. package/dist/zest.en.js +2621 -0
  7. package/dist/zest.en.js.map +1 -0
  8. package/dist/zest.en.min.js +1 -0
  9. package/dist/zest.es.js +2621 -0
  10. package/dist/zest.es.js.map +1 -0
  11. package/dist/zest.es.min.js +1 -0
  12. package/dist/zest.esm.js +3104 -0
  13. package/dist/zest.esm.js.map +1 -0
  14. package/dist/zest.esm.min.js +1 -0
  15. package/dist/zest.fr.js +2621 -0
  16. package/dist/zest.fr.js.map +1 -0
  17. package/dist/zest.fr.min.js +1 -0
  18. package/dist/zest.it.js +2621 -0
  19. package/dist/zest.it.js.map +1 -0
  20. package/dist/zest.it.min.js +1 -0
  21. package/dist/zest.ja.js +2621 -0
  22. package/dist/zest.ja.js.map +1 -0
  23. package/dist/zest.ja.min.js +1 -0
  24. package/dist/zest.js +3109 -0
  25. package/dist/zest.js.map +1 -0
  26. package/dist/zest.min.js +1 -0
  27. package/dist/zest.nl.js +2621 -0
  28. package/dist/zest.nl.js.map +1 -0
  29. package/dist/zest.nl.min.js +1 -0
  30. package/dist/zest.pl.js +2621 -0
  31. package/dist/zest.pl.js.map +1 -0
  32. package/dist/zest.pl.min.js +1 -0
  33. package/dist/zest.pt.js +2621 -0
  34. package/dist/zest.pt.js.map +1 -0
  35. package/dist/zest.pt.min.js +1 -0
  36. package/dist/zest.ru.js +2621 -0
  37. package/dist/zest.ru.js.map +1 -0
  38. package/dist/zest.ru.min.js +1 -0
  39. package/dist/zest.uk.js +2621 -0
  40. package/dist/zest.uk.js.map +1 -0
  41. package/dist/zest.uk.min.js +1 -0
  42. package/dist/zest.zh.js +2621 -0
  43. package/dist/zest.zh.js.map +1 -0
  44. package/dist/zest.zh.min.js +1 -0
  45. package/locales/de.json +40 -0
  46. package/locales/en.json +40 -0
  47. package/locales/es.json +40 -0
  48. package/locales/fr.json +40 -0
  49. package/locales/it.json +40 -0
  50. package/locales/ja.json +40 -0
  51. package/locales/nl.json +40 -0
  52. package/locales/pl.json +40 -0
  53. package/locales/pt.json +40 -0
  54. package/locales/ru.json +40 -0
  55. package/locales/uk.json +40 -0
  56. package/locales/zh.json +40 -0
  57. package/package.json +63 -0
  58. package/zest.config.schema.json +256 -0
@@ -0,0 +1,2621 @@
1
+ var Zest = (function () {
2
+ 'use strict';
3
+
4
+ /**
5
+ * Pattern Matcher - Categorizes cookies and storage keys by pattern
6
+ */
7
+
8
+ /**
9
+ * Default patterns for each category
10
+ */
11
+ const DEFAULT_PATTERNS = {
12
+ essential: [
13
+ /^zest_/,
14
+ /^csrf/i,
15
+ /^xsrf/i,
16
+ /^session/i,
17
+ /^__host-/i,
18
+ /^__secure-/i
19
+ ],
20
+ functional: [
21
+ /^lang/i,
22
+ /^locale/i,
23
+ /^theme/i,
24
+ /^preferences/i,
25
+ /^ui_/i
26
+ ],
27
+ analytics: [
28
+ /^_ga/,
29
+ /^_gid/,
30
+ /^_gat/,
31
+ /^_utm/,
32
+ /^__utm/,
33
+ /^plausible/i,
34
+ /^_pk_/,
35
+ /^matomo/i,
36
+ /^_hj/,
37
+ /^ajs_/
38
+ ],
39
+ marketing: [
40
+ /^_fbp/,
41
+ /^_fbc/,
42
+ /^_gcl/,
43
+ /^_ttp/,
44
+ /^ads/i,
45
+ /^doubleclick/i,
46
+ /^__gads/,
47
+ /^__gpi/,
48
+ /^_pin_/,
49
+ /^li_/
50
+ ]
51
+ };
52
+
53
+ let patterns = { ...DEFAULT_PATTERNS };
54
+
55
+ /**
56
+ * Set custom patterns
57
+ */
58
+ function setPatterns(customPatterns) {
59
+ patterns = { ...DEFAULT_PATTERNS };
60
+ for (const [category, regexList] of Object.entries(customPatterns)) {
61
+ if (Array.isArray(regexList)) {
62
+ patterns[category] = regexList.map(p =>
63
+ p instanceof RegExp ? p : new RegExp(p)
64
+ );
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Determine category for a cookie/storage key name
71
+ * @param {string} name - Cookie or storage key name
72
+ * @returns {string} Category ID (defaults to 'marketing' for unknown)
73
+ */
74
+ function getCategoryForName(name) {
75
+ for (const [category, regexList] of Object.entries(patterns)) {
76
+ if (regexList.some(regex => regex.test(name))) {
77
+ return category;
78
+ }
79
+ }
80
+ // Unknown items default to marketing (strictest)
81
+ return 'marketing';
82
+ }
83
+
84
+ /**
85
+ * Parse cookie string to extract name
86
+ * @param {string} cookieString - Full cookie string (e.g., "name=value; path=/")
87
+ * @returns {string|null} Cookie name or null
88
+ */
89
+ function parseCookieName(cookieString) {
90
+ const match = cookieString.match(/^([^=]+)/);
91
+ return match ? match[1].trim() : null;
92
+ }
93
+
94
+ /**
95
+ * Cookie Interceptor - Intercepts document.cookie operations
96
+ */
97
+
98
+
99
+ // Store original descriptor
100
+ let originalCookieDescriptor = null;
101
+
102
+ // Queue for blocked cookies
103
+ const cookieQueue = [];
104
+
105
+ // Reference to consent checker function
106
+ let checkConsent$3 = () => false;
107
+
108
+ /**
109
+ * Set the consent checker function
110
+ */
111
+ function setConsentChecker$2(fn) {
112
+ checkConsent$3 = fn;
113
+ }
114
+
115
+ /**
116
+ * Get the original cookie descriptor
117
+ */
118
+ function getOriginalCookieDescriptor() {
119
+ return originalCookieDescriptor;
120
+ }
121
+
122
+ /**
123
+ * Replay queued cookies for allowed categories
124
+ */
125
+ function replayCookies(allowedCategories) {
126
+ const remaining = [];
127
+
128
+ for (const item of cookieQueue) {
129
+ if (allowedCategories.includes(item.category)) {
130
+ // Set the cookie using original setter
131
+ if (originalCookieDescriptor?.set) {
132
+ originalCookieDescriptor.set.call(document, item.value);
133
+ }
134
+ } else {
135
+ remaining.push(item);
136
+ }
137
+ }
138
+
139
+ cookieQueue.length = 0;
140
+ cookieQueue.push(...remaining);
141
+ }
142
+
143
+ /**
144
+ * Start intercepting cookies
145
+ */
146
+ function interceptCookies() {
147
+ // Store original
148
+ originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
149
+
150
+ if (!originalCookieDescriptor) {
151
+ console.warn('[Zest] Could not get cookie descriptor');
152
+ return false;
153
+ }
154
+
155
+ Object.defineProperty(document, 'cookie', {
156
+ get() {
157
+ // Always allow reading
158
+ return originalCookieDescriptor.get.call(document);
159
+ },
160
+ set(value) {
161
+ const name = parseCookieName(value);
162
+ if (!name) {
163
+ return;
164
+ }
165
+
166
+ const category = getCategoryForName(name);
167
+
168
+ if (checkConsent$3(category)) {
169
+ // Consent given - set cookie
170
+ originalCookieDescriptor.set.call(document, value);
171
+ } else {
172
+ // No consent - queue for later
173
+ cookieQueue.push({
174
+ value,
175
+ name,
176
+ category,
177
+ timestamp: Date.now()
178
+ });
179
+ }
180
+ },
181
+ configurable: true
182
+ });
183
+
184
+ return true;
185
+ }
186
+
187
+ /**
188
+ * Storage Interceptor - Intercepts localStorage and sessionStorage operations
189
+ */
190
+
191
+
192
+ // Store originals
193
+ let originalLocalStorage = null;
194
+ let originalSessionStorage = null;
195
+
196
+ // Queues for blocked operations
197
+ const localStorageQueue = [];
198
+ const sessionStorageQueue = [];
199
+
200
+ // Reference to consent checker function
201
+ let checkConsent$2 = () => false;
202
+
203
+ /**
204
+ * Set the consent checker function
205
+ */
206
+ function setConsentChecker$1(fn) {
207
+ checkConsent$2 = fn;
208
+ }
209
+
210
+ /**
211
+ * Create a proxy for storage API
212
+ */
213
+ function createStorageProxy(storage, queue, storageName) {
214
+ return new Proxy(storage, {
215
+ get(target, prop) {
216
+ if (prop === 'setItem') {
217
+ return (key, value) => {
218
+ const category = getCategoryForName(key);
219
+
220
+ if (checkConsent$2(category)) {
221
+ target.setItem(key, value);
222
+ } else {
223
+ queue.push({
224
+ key,
225
+ value,
226
+ category,
227
+ timestamp: Date.now()
228
+ });
229
+ }
230
+ };
231
+ }
232
+
233
+ // Allow all other operations
234
+ const val = target[prop];
235
+ return typeof val === 'function' ? val.bind(target) : val;
236
+ }
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Replay queued storage operations for allowed categories
242
+ */
243
+ function replayStorage(allowedCategories) {
244
+ // Replay localStorage
245
+ const remainingLocal = [];
246
+ for (const item of localStorageQueue) {
247
+ if (allowedCategories.includes(item.category)) {
248
+ originalLocalStorage?.setItem(item.key, item.value);
249
+ } else {
250
+ remainingLocal.push(item);
251
+ }
252
+ }
253
+ localStorageQueue.length = 0;
254
+ localStorageQueue.push(...remainingLocal);
255
+
256
+ // Replay sessionStorage
257
+ const remainingSession = [];
258
+ for (const item of sessionStorageQueue) {
259
+ if (allowedCategories.includes(item.category)) {
260
+ originalSessionStorage?.setItem(item.key, item.value);
261
+ } else {
262
+ remainingSession.push(item);
263
+ }
264
+ }
265
+ sessionStorageQueue.length = 0;
266
+ sessionStorageQueue.push(...remainingSession);
267
+ }
268
+
269
+ /**
270
+ * Start intercepting storage APIs
271
+ */
272
+ function interceptStorage() {
273
+ try {
274
+ originalLocalStorage = window.localStorage;
275
+ originalSessionStorage = window.sessionStorage;
276
+
277
+ Object.defineProperty(window, 'localStorage', {
278
+ value: createStorageProxy(originalLocalStorage, localStorageQueue, 'localStorage'),
279
+ configurable: true,
280
+ writable: false
281
+ });
282
+
283
+ Object.defineProperty(window, 'sessionStorage', {
284
+ value: createStorageProxy(originalSessionStorage, sessionStorageQueue, 'sessionStorage'),
285
+ configurable: true,
286
+ writable: false
287
+ });
288
+
289
+ return true;
290
+ } catch (e) {
291
+ console.warn('[Zest] Could not intercept storage APIs:', e);
292
+ return false;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Known Trackers - Lists of known tracking script domains by category
298
+ */
299
+
300
+ /**
301
+ * Safe mode - Major, well-known trackers only
302
+ */
303
+ const SAFE_TRACKERS = {
304
+ analytics: [
305
+ 'google-analytics.com',
306
+ 'www.google-analytics.com',
307
+ 'analytics.google.com',
308
+ 'googletagmanager.com',
309
+ 'www.googletagmanager.com',
310
+ 'plausible.io',
311
+ 'cloudflareinsights.com',
312
+ 'static.cloudflareinsights.com'
313
+ ],
314
+ marketing: [
315
+ 'connect.facebook.net',
316
+ 'www.facebook.com/tr',
317
+ 'ads.google.com',
318
+ 'www.googleadservices.com',
319
+ 'googleads.g.doubleclick.net',
320
+ 'pagead2.googlesyndication.com'
321
+ ]
322
+ };
323
+
324
+ /**
325
+ * Strict mode - Extended list including less common trackers
326
+ */
327
+ const STRICT_TRACKERS = {
328
+ analytics: [
329
+ ...SAFE_TRACKERS.analytics,
330
+ 'analytics.tiktok.com',
331
+ 'matomo.', // partial match
332
+ 'hotjar.com',
333
+ 'static.hotjar.com',
334
+ 'script.hotjar.com',
335
+ 'clarity.ms',
336
+ 'www.clarity.ms',
337
+ 'heapanalytics.com',
338
+ 'cdn.heapanalytics.com',
339
+ 'mixpanel.com',
340
+ 'cdn.mxpnl.com',
341
+ 'segment.com',
342
+ 'cdn.segment.com',
343
+ 'api.segment.io',
344
+ 'fullstory.com',
345
+ 'rs.fullstory.com',
346
+ 'amplitude.com',
347
+ 'cdn.amplitude.com',
348
+ 'mouseflow.com',
349
+ 'cdn.mouseflow.com',
350
+ 'luckyorange.com',
351
+ 'cdn.luckyorange.net',
352
+ 'crazyegg.com',
353
+ 'script.crazyegg.com'
354
+ ],
355
+ marketing: [
356
+ ...SAFE_TRACKERS.marketing,
357
+ 'snap.licdn.com',
358
+ 'px.ads.linkedin.com',
359
+ 'ads.linkedin.com',
360
+ 'analytics.twitter.com',
361
+ 'static.ads-twitter.com',
362
+ 't.co',
363
+ 'analytics.tiktok.com',
364
+ 'ads.tiktok.com',
365
+ 'sc-static.net', // Snapchat
366
+ 'tr.snapchat.com',
367
+ 'ct.pinterest.com',
368
+ 'pintrk.com',
369
+ 's.pinimg.com',
370
+ 'widgets.pinterest.com',
371
+ 'bat.bing.com',
372
+ 'ads.yahoo.com',
373
+ 'sp.analytics.yahoo.com',
374
+ 'amazon-adsystem.com',
375
+ 'z-na.amazon-adsystem.com',
376
+ 'criteo.com',
377
+ 'static.criteo.net',
378
+ 'dis.criteo.com',
379
+ 'taboola.com',
380
+ 'cdn.taboola.com',
381
+ 'trc.taboola.com',
382
+ 'outbrain.com',
383
+ 'widgets.outbrain.com',
384
+ 'adroll.com',
385
+ 's.adroll.com'
386
+ ],
387
+ functional: [
388
+ 'cdn.onesignal.com',
389
+ 'onesignal.com',
390
+ 'pusher.com',
391
+ 'js.pusher.com',
392
+ 'intercom.io',
393
+ 'widget.intercom.io',
394
+ 'js.intercomcdn.com',
395
+ 'crisp.chat',
396
+ 'client.crisp.chat',
397
+ 'cdn.livechatinc.com',
398
+ 'livechatinc.com',
399
+ 'tawk.to',
400
+ 'embed.tawk.to',
401
+ 'zendesk.com',
402
+ 'static.zdassets.com'
403
+ ]
404
+ };
405
+
406
+ /**
407
+ * Check if a URL matches any tracker in the list
408
+ */
409
+ function matchesTrackerList(url, trackerList) {
410
+ try {
411
+ const urlObj = new URL(url);
412
+ const hostname = urlObj.hostname.toLowerCase();
413
+ const fullUrl = url.toLowerCase();
414
+
415
+ for (const domain of trackerList) {
416
+ // Support partial matches (e.g., "matomo." matches "analytics.matomo.cloud")
417
+ if (domain.endsWith('.')) {
418
+ if (hostname.includes(domain.slice(0, -1))) {
419
+ return true;
420
+ }
421
+ } else if (hostname === domain || hostname.endsWith('.' + domain)) {
422
+ return true;
423
+ } else if (fullUrl.includes(domain)) {
424
+ return true;
425
+ }
426
+ }
427
+ } catch (e) {
428
+ // Invalid URL
429
+ }
430
+ return false;
431
+ }
432
+
433
+ /**
434
+ * Get category for a script URL based on tracker lists
435
+ */
436
+ function getCategoryForScript(url, mode = 'safe') {
437
+ const trackers = mode === 'strict' ? STRICT_TRACKERS : SAFE_TRACKERS;
438
+
439
+ for (const [category, domains] of Object.entries(trackers)) {
440
+ if (matchesTrackerList(url, domains)) {
441
+ return category;
442
+ }
443
+ }
444
+
445
+ return null;
446
+ }
447
+
448
+ /**
449
+ * Check if URL is third-party (different domain)
450
+ */
451
+ function isThirdParty(url) {
452
+ try {
453
+ const scriptHost = new URL(url).hostname;
454
+ const pageHost = window.location.hostname;
455
+
456
+ // Remove www. for comparison
457
+ const normalizeHost = (h) => h.replace(/^www\./, '');
458
+
459
+ return normalizeHost(scriptHost) !== normalizeHost(pageHost);
460
+ } catch (e) {
461
+ return false;
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Script Blocker - Blocks and manages consent-gated scripts
467
+ *
468
+ * Modes:
469
+ * - manual: Only blocks scripts with data-consent-category attribute
470
+ * - safe: Manual + known major trackers (Google, Facebook, etc.)
471
+ * - strict: Safe + extended tracker list (Hotjar, Mixpanel, etc.)
472
+ * - doomsday: Block ALL third-party scripts
473
+ */
474
+
475
+
476
+ // Queue for blocked scripts
477
+ const scriptQueue = [];
478
+
479
+ // MutationObserver instance
480
+ let observer = null;
481
+
482
+ // Current blocking mode
483
+ let blockingMode = 'safe';
484
+
485
+ // Custom blocked domains (user-defined)
486
+ let customBlockedDomains = [];
487
+
488
+ // Reference to consent checker function
489
+ let checkConsent$1 = () => false;
490
+
491
+ /**
492
+ * Set the consent checker function
493
+ */
494
+ function setConsentChecker(fn) {
495
+ checkConsent$1 = fn;
496
+ }
497
+
498
+ /**
499
+ * Check if script URL matches custom blocked domains
500
+ */
501
+ function matchesCustomDomains(url) {
502
+ if (!url || customBlockedDomains.length === 0) return null;
503
+
504
+ try {
505
+ const hostname = new URL(url).hostname.toLowerCase();
506
+
507
+ for (const entry of customBlockedDomains) {
508
+ const domain = typeof entry === 'string' ? entry : entry.domain;
509
+ const category = typeof entry === 'string' ? 'marketing' : (entry.category || 'marketing');
510
+
511
+ if (hostname === domain || hostname.endsWith('.' + domain)) {
512
+ return category;
513
+ }
514
+ }
515
+ } catch (e) {
516
+ // Invalid URL
517
+ }
518
+
519
+ return null;
520
+ }
521
+
522
+ /**
523
+ * Determine if a script should be blocked and get its category
524
+ */
525
+ function getScriptBlockCategory(script) {
526
+ // 1. Check for explicit data-consent-category attribute (always respected)
527
+ const explicitCategory = script.getAttribute('data-consent-category');
528
+ if (explicitCategory) {
529
+ return explicitCategory;
530
+ }
531
+
532
+ // 2. Skip if script has data-zest-allow attribute
533
+ if (script.hasAttribute('data-zest-allow')) {
534
+ return null;
535
+ }
536
+
537
+ const src = script.src;
538
+
539
+ // No src = inline script, only block if explicitly tagged
540
+ if (!src) {
541
+ return null;
542
+ }
543
+
544
+ // 3. Check custom blocked domains
545
+ const customCategory = matchesCustomDomains(src);
546
+ if (customCategory) {
547
+ return customCategory;
548
+ }
549
+
550
+ // 4. Mode-based blocking
551
+ switch (blockingMode) {
552
+ case 'manual':
553
+ // Only explicit tags, already checked above
554
+ return null;
555
+
556
+ case 'safe':
557
+ case 'strict':
558
+ // Check against known tracker lists
559
+ return getCategoryForScript(src, blockingMode);
560
+
561
+ case 'doomsday':
562
+ // Block all third-party scripts
563
+ if (isThirdParty(src)) {
564
+ // Try to categorize, default to marketing
565
+ return getCategoryForScript(src, 'strict') || 'marketing';
566
+ }
567
+ return null;
568
+
569
+ default:
570
+ return null;
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Block a script element
576
+ */
577
+ function blockScript(script) {
578
+ // Skip already processed scripts
579
+ if (script.hasAttribute('data-zest-processed')) {
580
+ return false;
581
+ }
582
+
583
+ const category = getScriptBlockCategory(script);
584
+
585
+ if (!category) {
586
+ script.setAttribute('data-zest-processed', 'allowed');
587
+ return false;
588
+ }
589
+
590
+ if (checkConsent$1(category)) {
591
+ // Consent already given - allow script
592
+ script.setAttribute('data-zest-processed', 'allowed');
593
+ return false;
594
+ }
595
+
596
+ // Store script info for later execution
597
+ const scriptInfo = {
598
+ category,
599
+ src: script.src,
600
+ inline: script.textContent,
601
+ type: script.type,
602
+ async: script.async,
603
+ defer: script.defer,
604
+ timestamp: Date.now()
605
+ };
606
+
607
+ // Mark as processed
608
+ script.setAttribute('data-zest-processed', 'blocked');
609
+ script.setAttribute('data-consent-category', category);
610
+
611
+ // Disable the script
612
+ script.type = 'text/plain';
613
+
614
+ // If it has a src, also remove it to prevent loading
615
+ if (script.src) {
616
+ script.setAttribute('data-blocked-src', script.src);
617
+ script.removeAttribute('src');
618
+ }
619
+
620
+ scriptQueue.push(scriptInfo);
621
+ return true;
622
+ }
623
+
624
+ /**
625
+ * Execute a queued script
626
+ */
627
+ function executeScript(scriptInfo) {
628
+ const script = document.createElement('script');
629
+
630
+ if (scriptInfo.src) {
631
+ script.src = scriptInfo.src;
632
+ } else if (scriptInfo.inline) {
633
+ script.textContent = scriptInfo.inline;
634
+ }
635
+
636
+ if (scriptInfo.async) script.async = true;
637
+ if (scriptInfo.defer) script.defer = true;
638
+
639
+ script.setAttribute('data-zest-processed', 'executed');
640
+ script.setAttribute('data-consent-executed', 'true');
641
+
642
+ document.head.appendChild(script);
643
+ }
644
+
645
+ /**
646
+ * Replay queued scripts for allowed categories
647
+ */
648
+ function replayScripts(allowedCategories) {
649
+ const remaining = [];
650
+
651
+ for (const scriptInfo of scriptQueue) {
652
+ if (allowedCategories.includes(scriptInfo.category)) {
653
+ executeScript(scriptInfo);
654
+ } else {
655
+ remaining.push(scriptInfo);
656
+ }
657
+ }
658
+
659
+ scriptQueue.length = 0;
660
+ scriptQueue.push(...remaining);
661
+
662
+ // Also re-enable any blocked scripts in the DOM
663
+ const blockedScripts = document.querySelectorAll('script[data-zest-processed="blocked"]');
664
+ blockedScripts.forEach(script => {
665
+ const category = script.getAttribute('data-consent-category');
666
+ if (allowedCategories.includes(category)) {
667
+ // Clone and replace to execute
668
+ const newScript = document.createElement('script');
669
+
670
+ const blockedSrc = script.getAttribute('data-blocked-src');
671
+ if (blockedSrc) {
672
+ newScript.src = blockedSrc;
673
+ } else {
674
+ newScript.textContent = script.textContent;
675
+ }
676
+
677
+ if (script.async) newScript.async = true;
678
+ if (script.defer) newScript.defer = true;
679
+
680
+ newScript.setAttribute('data-zest-processed', 'executed');
681
+ newScript.setAttribute('data-consent-executed', 'true');
682
+ script.parentNode?.replaceChild(newScript, script);
683
+ }
684
+ });
685
+ }
686
+
687
+ /**
688
+ * Process existing scripts in the DOM
689
+ */
690
+ function processExistingScripts() {
691
+ const scripts = document.querySelectorAll('script:not([data-zest-processed])');
692
+ scripts.forEach(blockScript);
693
+ }
694
+
695
+ /**
696
+ * Handle mutations (new scripts added to DOM)
697
+ */
698
+ function handleMutations(mutations) {
699
+ for (const mutation of mutations) {
700
+ for (const node of mutation.addedNodes) {
701
+ if (node.nodeName === 'SCRIPT' && !node.hasAttribute('data-zest-processed')) {
702
+ blockScript(node);
703
+ }
704
+
705
+ // Check child scripts
706
+ if (node.querySelectorAll) {
707
+ const scripts = node.querySelectorAll('script:not([data-zest-processed])');
708
+ scripts.forEach(blockScript);
709
+ }
710
+ }
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Start observing for new scripts
716
+ */
717
+ function startScriptBlocking(mode = 'safe', customDomains = []) {
718
+ blockingMode = mode;
719
+ customBlockedDomains = customDomains;
720
+
721
+ // Process existing scripts
722
+ processExistingScripts();
723
+
724
+ // Watch for new scripts
725
+ observer = new MutationObserver(handleMutations);
726
+
727
+ observer.observe(document.documentElement, {
728
+ childList: true,
729
+ subtree: true
730
+ });
731
+
732
+ return true;
733
+ }
734
+
735
+ /**
736
+ * Default consent categories
737
+ */
738
+ const DEFAULT_CATEGORIES = {
739
+ essential: {
740
+ id: 'essential',
741
+ label: 'Essential',
742
+ description: 'Required for the website to function properly. Cannot be disabled.',
743
+ required: true,
744
+ default: true
745
+ },
746
+ functional: {
747
+ id: 'functional',
748
+ label: 'Functional',
749
+ description: 'Enable personalized features like language preferences and themes.',
750
+ required: false,
751
+ default: false
752
+ },
753
+ analytics: {
754
+ id: 'analytics',
755
+ label: 'Analytics',
756
+ description: 'Help us understand how visitors interact with our website.',
757
+ required: false,
758
+ default: false
759
+ },
760
+ marketing: {
761
+ id: 'marketing',
762
+ label: 'Marketing',
763
+ description: 'Used to deliver relevant advertisements and track campaign performance.',
764
+ required: false,
765
+ default: false
766
+ }
767
+ };
768
+
769
+ /**
770
+ * Default consent state
771
+ */
772
+ function getDefaultConsent() {
773
+ return {
774
+ essential: true,
775
+ functional: false,
776
+ analytics: false,
777
+ marketing: false
778
+ };
779
+ }
780
+
781
+ /**
782
+ * Get all category IDs
783
+ */
784
+ function getCategoryIds() {
785
+ return Object.keys(DEFAULT_CATEGORIES);
786
+ }
787
+
788
+ /**
789
+ * Do Not Track (DNT) Detection
790
+ *
791
+ * Detects browser DNT/GPC signals for privacy compliance
792
+ */
793
+
794
+ /**
795
+ * Check if Do Not Track is enabled
796
+ * Checks both DNT header and Global Privacy Control (GPC)
797
+ */
798
+ function isDoNotTrackEnabled() {
799
+ if (typeof navigator === 'undefined') {
800
+ return false;
801
+ }
802
+
803
+ // Check DNT (Do Not Track)
804
+ // Values: "1" = enabled, "0" = disabled, null/undefined = not set
805
+ const dnt = navigator.doNotTrack ||
806
+ window.doNotTrack ||
807
+ navigator.msDoNotTrack;
808
+
809
+ if (dnt === '1' || dnt === 'yes' || dnt === true) {
810
+ return true;
811
+ }
812
+
813
+ // Check GPC (Global Privacy Control) - newer standard
814
+ // https://globalprivacycontrol.org/
815
+ if (navigator.globalPrivacyControl === true) {
816
+ return true;
817
+ }
818
+
819
+ return false;
820
+ }
821
+
822
+ /**
823
+ * Get DNT signal details for logging/debugging
824
+ */
825
+ function getDNTDetails() {
826
+ if (typeof navigator === 'undefined') {
827
+ return { enabled: false, source: null };
828
+ }
829
+
830
+ const dnt = navigator.doNotTrack ||
831
+ window.doNotTrack ||
832
+ navigator.msDoNotTrack;
833
+
834
+ if (dnt === '1' || dnt === 'yes' || dnt === true) {
835
+ return { enabled: true, source: 'dnt' };
836
+ }
837
+
838
+ if (navigator.globalPrivacyControl === true) {
839
+ return { enabled: true, source: 'gpc' };
840
+ }
841
+
842
+ return { enabled: false, source: null };
843
+ }
844
+
845
+ /**
846
+ * DE only translation - auto-generated
847
+ * Do not edit manually, run: npm run build
848
+ */
849
+ const translations = {
850
+ de: {
851
+ "labels": {
852
+ "banner": {
853
+ "title": "Wir respektieren Ihre Privatsphäre",
854
+ "description": "Wir verwenden Cookies, um Ihr Surferlebnis zu verbessern, personalisierte Inhalte bereitzustellen und unseren Datenverkehr zu analysieren. Mit einem Klick auf „Alle akzeptieren\" stimmen Sie der Verwendung von Cookies zu.",
855
+ "acceptAll": "Alle akzeptieren",
856
+ "rejectAll": "Alle ablehnen",
857
+ "settings": "Einstellungen"
858
+ },
859
+ "modal": {
860
+ "title": "Datenschutzeinstellungen",
861
+ "description": "Verwalten Sie Ihre Cookie-Einstellungen. Sie können verschiedene Arten von Cookies unten aktivieren oder deaktivieren.",
862
+ "save": "Einstellungen speichern",
863
+ "acceptAll": "Alle akzeptieren",
864
+ "rejectAll": "Alle ablehnen"
865
+ },
866
+ "widget": {
867
+ "label": "Cookie-Einstellungen"
868
+ }
869
+ },
870
+ "categories": {
871
+ "essential": {
872
+ "label": "Notwendig",
873
+ "description": "Erforderlich für die ordnungsgemäße Funktion der Website. Können nicht deaktiviert werden."
874
+ },
875
+ "functional": {
876
+ "label": "Funktional",
877
+ "description": "Ermöglichen personalisierte Funktionen wie Spracheinstellungen und Designs."
878
+ },
879
+ "analytics": {
880
+ "label": "Analytisch",
881
+ "description": "Helfen uns zu verstehen, wie Besucher mit unserer Website interagieren."
882
+ },
883
+ "marketing": {
884
+ "label": "Marketing",
885
+ "description": "Werden verwendet, um relevante Werbung anzuzeigen und die Kampagnenleistung zu messen."
886
+ }
887
+ }
888
+ }
889
+ };
890
+
891
+ function detectLanguage() {
892
+ return 'de';
893
+ }
894
+
895
+ function getTranslation() {
896
+ return translations.de;
897
+ }
898
+
899
+ /**
900
+ * Default configuration values
901
+ */
902
+
903
+
904
+ const DEFAULTS = {
905
+ // Language: 'auto' | 'en' | 'de' | 'es' | 'fr' | 'it' | 'pt' | 'nl' | 'pl' | 'uk' | 'ru' | 'ja' | 'zh'
906
+ lang: 'auto',
907
+
908
+ // UI positioning
909
+ position: 'bottom', // 'bottom' | 'bottom-left' | 'bottom-right' | 'top'
910
+
911
+ // Theming
912
+ theme: 'auto', // 'light' | 'dark' | 'auto'
913
+ accentColor: '#0071e3',
914
+
915
+ // Categories
916
+ categories: DEFAULT_CATEGORIES,
917
+
918
+ // UI Labels
919
+ labels: {
920
+ banner: {
921
+ title: 'We value your privacy',
922
+ description: 'We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies.',
923
+ acceptAll: 'Accept All',
924
+ rejectAll: 'Reject All',
925
+ settings: 'Settings'
926
+ },
927
+ modal: {
928
+ title: 'Privacy Settings',
929
+ description: 'Manage your cookie preferences. You can enable or disable different types of cookies below.',
930
+ save: 'Save Preferences',
931
+ acceptAll: 'Accept All',
932
+ rejectAll: 'Reject All'
933
+ },
934
+ widget: {
935
+ label: 'Cookie Settings'
936
+ }
937
+ },
938
+
939
+ // Behavior
940
+ autoInit: true,
941
+ showWidget: true,
942
+ expiration: 365,
943
+
944
+ // Do Not Track / Global Privacy Control
945
+ // respectDNT: true = respect DNT/GPC signals
946
+ // dntBehavior: 'reject' | 'preselect' | 'ignore'
947
+ // - 'reject': auto-reject non-essential, don't show banner
948
+ // - 'preselect': show banner with non-essential unchecked (same as normal)
949
+ // - 'ignore': ignore DNT completely
950
+ respectDNT: true,
951
+ dntBehavior: 'reject',
952
+
953
+ // Custom styles to inject into Shadow DOM
954
+ customStyles: '',
955
+
956
+ // Blocking mode: 'manual' | 'safe' | 'strict' | 'doomsday'
957
+ mode: 'safe',
958
+
959
+ // Custom domains to block (in addition to mode-based blocking)
960
+ blockedDomains: [], // days
961
+
962
+ // Links
963
+ policyUrl: null,
964
+ imprintUrl: null,
965
+
966
+ // Callbacks
967
+ callbacks: {
968
+ onAccept: null,
969
+ onReject: null,
970
+ onChange: null,
971
+ onReady: null
972
+ }
973
+ };
974
+
975
+ /**
976
+ * Merge user config with defaults (deep merge)
977
+ */
978
+ function mergeConfig(userConfig) {
979
+ const config = { ...DEFAULTS };
980
+
981
+ if (!userConfig) {
982
+ userConfig = {};
983
+ }
984
+
985
+ // Simple properties
986
+ const simpleKeys = ['lang', 'position', 'theme', 'accentColor', 'autoInit', 'showWidget', 'expiration', 'policyUrl', 'imprintUrl', 'customStyles', 'mode', 'blockedDomains', 'respectDNT', 'dntBehavior'];
987
+ for (const key of simpleKeys) {
988
+ if (userConfig[key] !== undefined) {
989
+ config[key] = userConfig[key];
990
+ }
991
+ }
992
+
993
+ // Detect language and get translations
994
+ const detectedLang = detectLanguage(config.lang);
995
+ config.lang = detectedLang;
996
+ const translation = getTranslation();
997
+
998
+ // Deep merge labels (translation < user config)
999
+ const translationLabels = translation.labels || {};
1000
+ const userLabels = userConfig.labels || {};
1001
+ config.labels = {
1002
+ banner: {
1003
+ ...DEFAULTS.labels.banner,
1004
+ ...translationLabels.banner,
1005
+ ...userLabels.banner
1006
+ },
1007
+ modal: {
1008
+ ...DEFAULTS.labels.modal,
1009
+ ...translationLabels.modal,
1010
+ ...userLabels.modal
1011
+ },
1012
+ widget: {
1013
+ ...DEFAULTS.labels.widget,
1014
+ ...translationLabels.widget,
1015
+ ...userLabels.widget
1016
+ }
1017
+ };
1018
+
1019
+ // Deep merge categories (translation < user config)
1020
+ const translationCategories = translation.categories || {};
1021
+ const userCategories = userConfig.categories || {};
1022
+ config.categories = { ...DEFAULTS.categories };
1023
+ for (const key of Object.keys(DEFAULTS.categories)) {
1024
+ config.categories[key] = {
1025
+ ...DEFAULTS.categories[key],
1026
+ ...translationCategories[key],
1027
+ ...userCategories[key]
1028
+ };
1029
+ }
1030
+
1031
+ // Merge callbacks
1032
+ if (userConfig.callbacks) {
1033
+ config.callbacks = { ...DEFAULTS.callbacks, ...userConfig.callbacks };
1034
+ }
1035
+
1036
+ // Patterns (for pattern matcher)
1037
+ if (userConfig.patterns) {
1038
+ config.patterns = userConfig.patterns;
1039
+ }
1040
+
1041
+ return config;
1042
+ }
1043
+
1044
+ /**
1045
+ * Configuration Parser - Reads config from various sources
1046
+ */
1047
+
1048
+
1049
+ /**
1050
+ * Parse data attributes from script tag
1051
+ */
1052
+ function parseDataAttributes() {
1053
+ // Find the Zest script tag
1054
+ const script = document.currentScript ||
1055
+ document.querySelector('script[data-zest]') ||
1056
+ document.querySelector('script[src*="zest"]');
1057
+
1058
+ if (!script) {
1059
+ return {};
1060
+ }
1061
+
1062
+ const config = {};
1063
+
1064
+ // Position
1065
+ const position = script.getAttribute('data-position');
1066
+ if (position) config.position = position;
1067
+
1068
+ // Theme
1069
+ const theme = script.getAttribute('data-theme');
1070
+ if (theme) config.theme = theme;
1071
+
1072
+ // Accent color
1073
+ const accent = script.getAttribute('data-accent') || script.getAttribute('data-accent-color');
1074
+ if (accent) config.accentColor = accent;
1075
+
1076
+ // Policy URL
1077
+ const policyUrl = script.getAttribute('data-policy-url') || script.getAttribute('data-privacy-url');
1078
+ if (policyUrl) config.policyUrl = policyUrl;
1079
+
1080
+ // Imprint URL
1081
+ const imprintUrl = script.getAttribute('data-imprint-url');
1082
+ if (imprintUrl) config.imprintUrl = imprintUrl;
1083
+
1084
+ // Show widget
1085
+ const showWidget = script.getAttribute('data-show-widget');
1086
+ if (showWidget !== null) config.showWidget = showWidget !== 'false';
1087
+
1088
+ // Auto init
1089
+ const autoInit = script.getAttribute('data-auto-init');
1090
+ if (autoInit !== null) config.autoInit = autoInit !== 'false';
1091
+
1092
+ // Expiration
1093
+ const expiration = script.getAttribute('data-expiration');
1094
+ if (expiration) config.expiration = parseInt(expiration, 10);
1095
+
1096
+ return config;
1097
+ }
1098
+
1099
+ /**
1100
+ * Parse window.ZestConfig object
1101
+ */
1102
+ function parseWindowConfig() {
1103
+ if (typeof window !== 'undefined' && window.ZestConfig) {
1104
+ return window.ZestConfig;
1105
+ }
1106
+ return {};
1107
+ }
1108
+
1109
+ /**
1110
+ * Get final merged configuration
1111
+ * Priority: data attributes > window.ZestConfig > defaults
1112
+ */
1113
+ function getConfig() {
1114
+ const windowConfig = parseWindowConfig();
1115
+ const dataConfig = parseDataAttributes();
1116
+
1117
+ // Merge: defaults < windowConfig < dataConfig
1118
+ return mergeConfig({
1119
+ ...windowConfig,
1120
+ ...dataConfig
1121
+ });
1122
+ }
1123
+
1124
+ /**
1125
+ * Update configuration at runtime
1126
+ */
1127
+ let currentConfig = null;
1128
+
1129
+ function setConfig(config) {
1130
+ currentConfig = mergeConfig(config);
1131
+ return currentConfig;
1132
+ }
1133
+
1134
+ function getCurrentConfig() {
1135
+ if (!currentConfig) {
1136
+ currentConfig = getConfig();
1137
+ }
1138
+ return currentConfig;
1139
+ }
1140
+
1141
+ /**
1142
+ * Consent Store - Manages consent state persistence
1143
+ */
1144
+
1145
+
1146
+ const COOKIE_NAME = 'zest_consent';
1147
+ const CONSENT_VERSION = '1.0';
1148
+
1149
+ // Current consent state
1150
+ let consent = null;
1151
+
1152
+ /**
1153
+ * Get the original cookie setter (bypasses interception)
1154
+ */
1155
+ function setRawCookie(value) {
1156
+ const descriptor = getOriginalCookieDescriptor();
1157
+ if (descriptor?.set) {
1158
+ descriptor.set.call(document, value);
1159
+ } else {
1160
+ // Fallback if interceptor not initialized yet
1161
+ document.cookie = value;
1162
+ }
1163
+ }
1164
+
1165
+ /**
1166
+ * Get the original cookie getter
1167
+ */
1168
+ function getRawCookie() {
1169
+ const descriptor = getOriginalCookieDescriptor();
1170
+ if (descriptor?.get) {
1171
+ return descriptor.get.call(document);
1172
+ }
1173
+ return document.cookie;
1174
+ }
1175
+
1176
+ /**
1177
+ * Load consent from cookie
1178
+ */
1179
+ function loadConsent() {
1180
+ try {
1181
+ const cookies = getRawCookie();
1182
+ const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
1183
+
1184
+ if (match) {
1185
+ const data = JSON.parse(decodeURIComponent(match[1]));
1186
+ consent = data.categories || getDefaultConsent();
1187
+ return { ...consent };
1188
+ }
1189
+ } catch (e) {
1190
+ // Invalid or missing cookie
1191
+ }
1192
+
1193
+ consent = getDefaultConsent();
1194
+ return { ...consent };
1195
+ }
1196
+
1197
+ /**
1198
+ * Save consent to cookie
1199
+ */
1200
+ function saveConsent(expirationDays = 365) {
1201
+ if (!consent) {
1202
+ consent = getDefaultConsent();
1203
+ }
1204
+
1205
+ const data = {
1206
+ version: CONSENT_VERSION,
1207
+ timestamp: Date.now(),
1208
+ categories: consent
1209
+ };
1210
+
1211
+ const expires = new Date(Date.now() + expirationDays * 24 * 60 * 60 * 1000).toUTCString();
1212
+ const cookieValue = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(data))}; expires=${expires}; path=/; SameSite=Lax`;
1213
+
1214
+ setRawCookie(cookieValue);
1215
+ }
1216
+
1217
+ /**
1218
+ * Get current consent state
1219
+ */
1220
+ function getConsent() {
1221
+ if (!consent) {
1222
+ consent = loadConsent();
1223
+ }
1224
+ return { ...consent };
1225
+ }
1226
+
1227
+ /**
1228
+ * Update consent state
1229
+ */
1230
+ function updateConsent(newConsent, expirationDays = 365) {
1231
+ const previous = consent ? { ...consent } : getDefaultConsent();
1232
+
1233
+ consent = {
1234
+ essential: true, // Always true
1235
+ functional: !!newConsent.functional,
1236
+ analytics: !!newConsent.analytics,
1237
+ marketing: !!newConsent.marketing
1238
+ };
1239
+
1240
+ saveConsent(expirationDays);
1241
+
1242
+ return { current: { ...consent }, previous };
1243
+ }
1244
+
1245
+ /**
1246
+ * Check if specific category is allowed
1247
+ */
1248
+ function hasConsent(category) {
1249
+ if (!consent) {
1250
+ consent = loadConsent();
1251
+ }
1252
+ return consent[category] === true;
1253
+ }
1254
+
1255
+ /**
1256
+ * Accept all categories
1257
+ */
1258
+ function acceptAll(expirationDays = 365) {
1259
+ return updateConsent({
1260
+ functional: true,
1261
+ analytics: true,
1262
+ marketing: true
1263
+ }, expirationDays);
1264
+ }
1265
+
1266
+ /**
1267
+ * Reject all (except essential)
1268
+ */
1269
+ function rejectAll(expirationDays = 365) {
1270
+ return updateConsent({
1271
+ functional: false,
1272
+ analytics: false,
1273
+ marketing: false
1274
+ }, expirationDays);
1275
+ }
1276
+
1277
+ /**
1278
+ * Reset consent (clear cookie)
1279
+ */
1280
+ function resetConsent() {
1281
+ setRawCookie(`${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`);
1282
+ consent = null;
1283
+ }
1284
+
1285
+ /**
1286
+ * Check if consent has been given (any decision made)
1287
+ */
1288
+ function hasConsentDecision() {
1289
+ try {
1290
+ const cookies = getRawCookie();
1291
+ return cookies.includes(COOKIE_NAME);
1292
+ } catch (e) {
1293
+ return false;
1294
+ }
1295
+ }
1296
+
1297
+ /**
1298
+ * Get consent proof for compliance
1299
+ */
1300
+ function getConsentProof() {
1301
+ try {
1302
+ const cookies = getRawCookie();
1303
+ const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
1304
+
1305
+ if (match) {
1306
+ return JSON.parse(decodeURIComponent(match[1]));
1307
+ }
1308
+ } catch (e) {
1309
+ // Invalid cookie
1310
+ }
1311
+
1312
+ return null;
1313
+ }
1314
+
1315
+ /**
1316
+ * Events - Custom event dispatching for consent changes
1317
+ */
1318
+
1319
+ // Event names
1320
+ const EVENTS = {
1321
+ READY: 'zest:ready',
1322
+ CONSENT: 'zest:consent',
1323
+ REJECT: 'zest:reject',
1324
+ CHANGE: 'zest:change',
1325
+ SHOW: 'zest:show',
1326
+ HIDE: 'zest:hide'
1327
+ };
1328
+
1329
+ /**
1330
+ * Dispatch a custom event
1331
+ */
1332
+ function emit(eventName, detail = {}) {
1333
+ const event = new CustomEvent(eventName, {
1334
+ detail,
1335
+ bubbles: true,
1336
+ cancelable: true
1337
+ });
1338
+
1339
+ document.dispatchEvent(event);
1340
+ return event;
1341
+ }
1342
+
1343
+ /**
1344
+ * Emit ready event
1345
+ */
1346
+ function emitReady(consent) {
1347
+ return emit(EVENTS.READY, { consent });
1348
+ }
1349
+
1350
+ /**
1351
+ * Emit consent event (user accepted)
1352
+ */
1353
+ function emitConsent(consent, previous) {
1354
+ return emit(EVENTS.CONSENT, { consent, previous });
1355
+ }
1356
+
1357
+ /**
1358
+ * Emit reject event (user rejected all)
1359
+ */
1360
+ function emitReject(consent) {
1361
+ return emit(EVENTS.REJECT, { consent });
1362
+ }
1363
+
1364
+ /**
1365
+ * Emit change event (any consent change)
1366
+ */
1367
+ function emitChange(consent, previous) {
1368
+ return emit(EVENTS.CHANGE, { consent, previous });
1369
+ }
1370
+
1371
+ /**
1372
+ * Emit show event (banner/modal shown)
1373
+ */
1374
+ function emitShow(type = 'banner') {
1375
+ return emit(EVENTS.SHOW, { type });
1376
+ }
1377
+
1378
+ /**
1379
+ * Emit hide event (banner/modal hidden)
1380
+ */
1381
+ function emitHide(type = 'banner') {
1382
+ return emit(EVENTS.HIDE, { type });
1383
+ }
1384
+
1385
+ /**
1386
+ * Styles - Shadow DOM encapsulated CSS with theming
1387
+ */
1388
+
1389
+ /**
1390
+ * Generate CSS with custom properties
1391
+ */
1392
+ function generateStyles(config) {
1393
+ const accentColor = config.accentColor || '#4F46E5';
1394
+
1395
+ return `
1396
+ :host {
1397
+ --zest-accent: ${accentColor};
1398
+ --zest-accent-hover: ${adjustColor(accentColor, -15)};
1399
+ --zest-bg: #ffffff;
1400
+ --zest-bg-secondary: #f3f4f6;
1401
+ --zest-text: #1f2937;
1402
+ --zest-text-secondary: #6b7280;
1403
+ --zest-border: #e5e7eb;
1404
+ --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
1405
+ --zest-radius: 12px;
1406
+ --zest-radius-sm: 8px;
1407
+ --zest-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1408
+
1409
+ font-family: var(--zest-font);
1410
+ font-size: 14px;
1411
+ line-height: 1.5;
1412
+ color: var(--zest-text);
1413
+ box-sizing: border-box;
1414
+ }
1415
+
1416
+ :host([data-theme="dark"]) {
1417
+ --zest-bg: #1f2937;
1418
+ --zest-bg-secondary: #374151;
1419
+ --zest-text: #f9fafb;
1420
+ --zest-text-secondary: #9ca3af;
1421
+ --zest-border: #4b5563;
1422
+ --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
1423
+ }
1424
+
1425
+ @media (prefers-color-scheme: dark) {
1426
+ :host([data-theme="auto"]) {
1427
+ --zest-bg: #1f2937;
1428
+ --zest-bg-secondary: #374151;
1429
+ --zest-text: #f9fafb;
1430
+ --zest-text-secondary: #9ca3af;
1431
+ --zest-border: #4b5563;
1432
+ --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
1433
+ }
1434
+ }
1435
+
1436
+ *, *::before, *::after {
1437
+ box-sizing: border-box;
1438
+ }
1439
+
1440
+ /* Banner */
1441
+ .zest-banner {
1442
+ position: fixed;
1443
+ z-index: 999999;
1444
+ max-width: 480px;
1445
+ padding: 20px;
1446
+ background: var(--zest-bg);
1447
+ border-radius: var(--zest-radius);
1448
+ box-shadow: var(--zest-shadow);
1449
+ animation: zest-slide-in 0.3s ease-out;
1450
+ }
1451
+
1452
+ .zest-banner--bottom {
1453
+ bottom: 20px;
1454
+ left: 50%;
1455
+ transform: translateX(-50%);
1456
+ }
1457
+
1458
+ .zest-banner--bottom-left {
1459
+ bottom: 20px;
1460
+ left: 20px;
1461
+ }
1462
+
1463
+ .zest-banner--bottom-right {
1464
+ bottom: 20px;
1465
+ right: 20px;
1466
+ }
1467
+
1468
+ .zest-banner--top {
1469
+ top: 20px;
1470
+ left: 50%;
1471
+ transform: translateX(-50%);
1472
+ }
1473
+
1474
+ @keyframes zest-slide-in {
1475
+ from {
1476
+ opacity: 0;
1477
+ transform: translateX(-50%) translateY(20px);
1478
+ }
1479
+ to {
1480
+ opacity: 1;
1481
+ transform: translateX(-50%) translateY(0);
1482
+ }
1483
+ }
1484
+
1485
+ .zest-banner--bottom-left {
1486
+ animation-name: zest-slide-in-left;
1487
+ }
1488
+
1489
+ @keyframes zest-slide-in-left {
1490
+ from {
1491
+ opacity: 0;
1492
+ transform: translateY(20px);
1493
+ }
1494
+ to {
1495
+ opacity: 1;
1496
+ transform: translateY(0);
1497
+ }
1498
+ }
1499
+
1500
+ .zest-banner--bottom-right {
1501
+ animation-name: zest-slide-in-right;
1502
+ }
1503
+
1504
+ @keyframes zest-slide-in-right {
1505
+ from {
1506
+ opacity: 0;
1507
+ transform: translateY(20px);
1508
+ }
1509
+ to {
1510
+ opacity: 1;
1511
+ transform: translateY(0);
1512
+ }
1513
+ }
1514
+
1515
+ @media (prefers-reduced-motion: reduce) {
1516
+ .zest-banner,
1517
+ .zest-modal {
1518
+ animation: none;
1519
+ }
1520
+ }
1521
+
1522
+ .zest-banner__title {
1523
+ margin: 0 0 8px 0;
1524
+ font-size: 16px;
1525
+ font-weight: 600;
1526
+ color: var(--zest-text);
1527
+ }
1528
+
1529
+ .zest-banner__description {
1530
+ margin: 0 0 16px 0;
1531
+ font-size: 14px;
1532
+ color: var(--zest-text-secondary);
1533
+ }
1534
+
1535
+ .zest-banner__buttons {
1536
+ display: flex;
1537
+ flex-wrap: wrap;
1538
+ gap: 8px;
1539
+ }
1540
+
1541
+ /* Buttons */
1542
+ .zest-btn {
1543
+ display: inline-flex;
1544
+ align-items: center;
1545
+ justify-content: center;
1546
+ padding: 10px 16px;
1547
+ font-size: 14px;
1548
+ font-weight: 500;
1549
+ font-family: inherit;
1550
+ border: none;
1551
+ border-radius: var(--zest-radius-sm);
1552
+ cursor: pointer;
1553
+ transition: background-color 0.15s ease, transform 0.1s ease;
1554
+ }
1555
+
1556
+ .zest-btn:hover {
1557
+ transform: translateY(-1px);
1558
+ }
1559
+
1560
+ .zest-btn:active {
1561
+ transform: translateY(0);
1562
+ }
1563
+
1564
+ .zest-btn:focus-visible {
1565
+ outline: 2px solid var(--zest-accent);
1566
+ outline-offset: 2px;
1567
+ }
1568
+
1569
+ .zest-btn--primary {
1570
+ background: var(--zest-accent);
1571
+ color: #ffffff;
1572
+ }
1573
+
1574
+ .zest-btn--primary:hover {
1575
+ background: var(--zest-accent-hover);
1576
+ }
1577
+
1578
+ .zest-btn--secondary {
1579
+ background: var(--zest-bg-secondary);
1580
+ color: var(--zest-text);
1581
+ }
1582
+
1583
+ .zest-btn--secondary:hover {
1584
+ background: var(--zest-border);
1585
+ }
1586
+
1587
+ .zest-btn--ghost {
1588
+ background: transparent;
1589
+ color: var(--zest-text-secondary);
1590
+ }
1591
+
1592
+ .zest-btn--ghost:hover {
1593
+ background: var(--zest-bg-secondary);
1594
+ color: var(--zest-text);
1595
+ }
1596
+
1597
+ /* Modal */
1598
+ .zest-modal-overlay {
1599
+ position: fixed;
1600
+ inset: 0;
1601
+ z-index: 999998;
1602
+ display: flex;
1603
+ align-items: center;
1604
+ justify-content: center;
1605
+ padding: 20px;
1606
+ background: rgba(0, 0, 0, 0.5);
1607
+ animation: zest-fade-in 0.2s ease-out;
1608
+ }
1609
+
1610
+ @keyframes zest-fade-in {
1611
+ from { opacity: 0; }
1612
+ to { opacity: 1; }
1613
+ }
1614
+
1615
+ .zest-modal {
1616
+ width: 100%;
1617
+ max-width: 500px;
1618
+ max-height: 90vh;
1619
+ overflow-y: auto;
1620
+ background: var(--zest-bg);
1621
+ border-radius: var(--zest-radius);
1622
+ box-shadow: var(--zest-shadow);
1623
+ animation: zest-modal-in 0.3s ease-out;
1624
+ }
1625
+
1626
+ @keyframes zest-modal-in {
1627
+ from {
1628
+ opacity: 0;
1629
+ transform: scale(0.95);
1630
+ }
1631
+ to {
1632
+ opacity: 1;
1633
+ transform: scale(1);
1634
+ }
1635
+ }
1636
+
1637
+ .zest-modal__header {
1638
+ padding: 20px 20px 0;
1639
+ }
1640
+
1641
+ .zest-modal__title {
1642
+ margin: 0 0 8px 0;
1643
+ font-size: 18px;
1644
+ font-weight: 600;
1645
+ color: var(--zest-text);
1646
+ }
1647
+
1648
+ .zest-modal__description {
1649
+ margin: 0;
1650
+ font-size: 14px;
1651
+ color: var(--zest-text-secondary);
1652
+ }
1653
+
1654
+ .zest-modal__body {
1655
+ padding: 20px;
1656
+ }
1657
+
1658
+ .zest-modal__footer {
1659
+ display: flex;
1660
+ flex-wrap: wrap;
1661
+ gap: 8px;
1662
+ padding: 0 20px 20px;
1663
+ }
1664
+
1665
+ /* Categories */
1666
+ .zest-category {
1667
+ padding: 16px;
1668
+ margin-bottom: 12px;
1669
+ background: var(--zest-bg-secondary);
1670
+ border-radius: var(--zest-radius-sm);
1671
+ }
1672
+
1673
+ .zest-category:last-child {
1674
+ margin-bottom: 0;
1675
+ }
1676
+
1677
+ .zest-category__header {
1678
+ display: flex;
1679
+ align-items: center;
1680
+ justify-content: space-between;
1681
+ gap: 12px;
1682
+ }
1683
+
1684
+ .zest-category__info {
1685
+ flex: 1;
1686
+ }
1687
+
1688
+ .zest-category__label {
1689
+ display: block;
1690
+ font-size: 14px;
1691
+ font-weight: 600;
1692
+ color: var(--zest-text);
1693
+ }
1694
+
1695
+ .zest-category__description {
1696
+ margin: 4px 0 0;
1697
+ font-size: 13px;
1698
+ color: var(--zest-text-secondary);
1699
+ }
1700
+
1701
+ /* Toggle Switch */
1702
+ .zest-toggle {
1703
+ position: relative;
1704
+ width: 44px;
1705
+ height: 24px;
1706
+ flex-shrink: 0;
1707
+ }
1708
+
1709
+ .zest-toggle__input {
1710
+ position: absolute;
1711
+ opacity: 0;
1712
+ width: 100%;
1713
+ height: 100%;
1714
+ cursor: pointer;
1715
+ margin: 0;
1716
+ }
1717
+
1718
+ .zest-toggle__input:disabled {
1719
+ cursor: not-allowed;
1720
+ }
1721
+
1722
+ .zest-toggle__slider {
1723
+ position: absolute;
1724
+ inset: 0;
1725
+ background: var(--zest-border);
1726
+ border-radius: 12px;
1727
+ transition: background-color 0.2s ease;
1728
+ pointer-events: none;
1729
+ }
1730
+
1731
+ .zest-toggle__slider::before {
1732
+ content: '';
1733
+ position: absolute;
1734
+ top: 2px;
1735
+ left: 2px;
1736
+ width: 20px;
1737
+ height: 20px;
1738
+ background: #ffffff;
1739
+ border-radius: 50%;
1740
+ transition: transform 0.2s ease;
1741
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
1742
+ }
1743
+
1744
+ .zest-toggle__input:checked + .zest-toggle__slider {
1745
+ background: var(--zest-accent);
1746
+ }
1747
+
1748
+ .zest-toggle__input:checked + .zest-toggle__slider::before {
1749
+ transform: translateX(20px);
1750
+ }
1751
+
1752
+ .zest-toggle__input:focus-visible + .zest-toggle__slider {
1753
+ outline: 2px solid var(--zest-accent);
1754
+ outline-offset: 2px;
1755
+ }
1756
+
1757
+ .zest-toggle__input:disabled + .zest-toggle__slider {
1758
+ opacity: 0.6;
1759
+ }
1760
+
1761
+ /* Widget */
1762
+ .zest-widget {
1763
+ position: fixed;
1764
+ z-index: 999997;
1765
+ bottom: 20px;
1766
+ left: 20px;
1767
+ }
1768
+
1769
+ .zest-widget__btn {
1770
+ display: flex;
1771
+ align-items: center;
1772
+ justify-content: center;
1773
+ width: 48px;
1774
+ height: 48px;
1775
+ padding: 0;
1776
+ background: var(--zest-bg);
1777
+ border: 1px solid var(--zest-border);
1778
+ border-radius: 50%;
1779
+ box-shadow: var(--zest-shadow);
1780
+ cursor: pointer;
1781
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
1782
+ }
1783
+
1784
+ .zest-widget__btn:hover {
1785
+ transform: scale(1.05);
1786
+ box-shadow: 0 12px 28px -5px rgba(0, 0, 0, 0.15);
1787
+ }
1788
+
1789
+ .zest-widget__btn:focus-visible {
1790
+ outline: 2px solid var(--zest-accent);
1791
+ outline-offset: 2px;
1792
+ }
1793
+
1794
+ .zest-widget__icon {
1795
+ width: 24px;
1796
+ height: 24px;
1797
+ fill: var(--zest-text);
1798
+ }
1799
+
1800
+ /* Link */
1801
+ .zest-link {
1802
+ color: var(--zest-accent);
1803
+ text-decoration: none;
1804
+ }
1805
+
1806
+ .zest-link:hover {
1807
+ text-decoration: underline;
1808
+ }
1809
+
1810
+ /* Mobile */
1811
+ @media (max-width: 480px) {
1812
+ .zest-banner {
1813
+ left: 10px;
1814
+ right: 10px;
1815
+ max-width: none;
1816
+ transform: none;
1817
+ }
1818
+
1819
+ .zest-banner--bottom,
1820
+ .zest-banner--bottom-left,
1821
+ .zest-banner--bottom-right {
1822
+ bottom: 10px;
1823
+ }
1824
+
1825
+ .zest-banner--top {
1826
+ top: 10px;
1827
+ transform: none;
1828
+ }
1829
+
1830
+ @keyframes zest-slide-in {
1831
+ from {
1832
+ opacity: 0;
1833
+ transform: translateY(20px);
1834
+ }
1835
+ to {
1836
+ opacity: 1;
1837
+ transform: translateY(0);
1838
+ }
1839
+ }
1840
+
1841
+ .zest-banner__buttons {
1842
+ flex-direction: column;
1843
+ }
1844
+
1845
+ .zest-btn {
1846
+ width: 100%;
1847
+ }
1848
+
1849
+ .zest-modal-overlay {
1850
+ padding: 10px;
1851
+ }
1852
+
1853
+ .zest-widget {
1854
+ bottom: 10px;
1855
+ left: 10px;
1856
+ }
1857
+ }
1858
+
1859
+ /* Hidden utility */
1860
+ .zest-hidden {
1861
+ display: none !important;
1862
+ }
1863
+ ${config.customStyles || ''}
1864
+ `;
1865
+ }
1866
+
1867
+ /**
1868
+ * Adjust color brightness
1869
+ */
1870
+ function adjustColor(hex, percent) {
1871
+ const num = parseInt(hex.replace('#', ''), 16);
1872
+ const amt = Math.round(2.55 * percent);
1873
+ const R = Math.min(255, Math.max(0, (num >> 16) + amt));
1874
+ const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt));
1875
+ const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt));
1876
+ return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
1877
+ }
1878
+
1879
+ /**
1880
+ * Cookie icon SVG
1881
+ */
1882
+ const COOKIE_ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10c0-.728-.078-1.437-.225-2.12a1 1 0 0 0-1.482-.63 3 3 0 0 1-4.086-3.72 1 1 0 0 0-.793-1.263A10.05 10.05 0 0 0 12 2zm0 2c.178 0 .354.006.528.017a5 5 0 0 0 5.955 5.955c.011.174.017.35.017.528 0 4.418-3.582 8-8 8s-8-3.582-8-8 3.582-8 8-8zm-4 6a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm5 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm-2 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/></svg>`;
1883
+
1884
+ /**
1885
+ * Banner - Main consent banner component
1886
+ */
1887
+
1888
+
1889
+ let bannerElement = null;
1890
+ let shadowRoot$2 = null;
1891
+
1892
+ /**
1893
+ * Create the banner HTML
1894
+ */
1895
+ function createBannerHTML(config) {
1896
+ const labels = config.labels.banner;
1897
+ const position = config.position || 'bottom';
1898
+
1899
+ return `
1900
+ <div class="zest-banner zest-banner--${position}" role="dialog" aria-modal="false" aria-label="${labels.title}">
1901
+ <h2 class="zest-banner__title">${labels.title}</h2>
1902
+ <p class="zest-banner__description">${labels.description}</p>
1903
+ <div class="zest-banner__buttons">
1904
+ <button type="button" class="zest-btn zest-btn--primary" data-action="accept-all">
1905
+ ${labels.acceptAll}
1906
+ </button>
1907
+ <button type="button" class="zest-btn zest-btn--secondary" data-action="reject-all">
1908
+ ${labels.rejectAll}
1909
+ </button>
1910
+ <button type="button" class="zest-btn zest-btn--ghost" data-action="settings">
1911
+ ${labels.settings}
1912
+ </button>
1913
+ </div>
1914
+ </div>
1915
+ `;
1916
+ }
1917
+
1918
+ /**
1919
+ * Create and mount the banner
1920
+ */
1921
+ function createBanner(callbacks = {}) {
1922
+ if (bannerElement) {
1923
+ return bannerElement;
1924
+ }
1925
+
1926
+ const config = getCurrentConfig();
1927
+
1928
+ // Create host element
1929
+ bannerElement = document.createElement('zest-banner');
1930
+ bannerElement.setAttribute('data-theme', config.theme || 'light');
1931
+
1932
+ // Create shadow root
1933
+ shadowRoot$2 = bannerElement.attachShadow({ mode: 'open' });
1934
+
1935
+ // Add styles
1936
+ const styleEl = document.createElement('style');
1937
+ styleEl.textContent = generateStyles(config);
1938
+ shadowRoot$2.appendChild(styleEl);
1939
+
1940
+ // Add banner HTML
1941
+ const container = document.createElement('div');
1942
+ container.innerHTML = createBannerHTML(config);
1943
+ shadowRoot$2.appendChild(container.firstElementChild);
1944
+
1945
+ // Add event listeners
1946
+ const banner = shadowRoot$2.querySelector('.zest-banner');
1947
+
1948
+ banner.addEventListener('click', (e) => {
1949
+ const action = e.target.dataset.action;
1950
+ if (!action) return;
1951
+
1952
+ switch (action) {
1953
+ case 'accept-all':
1954
+ callbacks.onAcceptAll?.();
1955
+ break;
1956
+ case 'reject-all':
1957
+ callbacks.onRejectAll?.();
1958
+ break;
1959
+ case 'settings':
1960
+ callbacks.onSettings?.();
1961
+ break;
1962
+ }
1963
+ });
1964
+
1965
+ // Keyboard handling
1966
+ banner.addEventListener('keydown', (e) => {
1967
+ if (e.key === 'Escape') {
1968
+ callbacks.onSettings?.();
1969
+ }
1970
+ });
1971
+
1972
+ // Mount to document
1973
+ document.body.appendChild(bannerElement);
1974
+
1975
+ // Focus first button for accessibility
1976
+ requestAnimationFrame(() => {
1977
+ const firstButton = shadowRoot$2.querySelector('button');
1978
+ firstButton?.focus();
1979
+ });
1980
+
1981
+ return bannerElement;
1982
+ }
1983
+
1984
+ /**
1985
+ * Show the banner
1986
+ */
1987
+ function showBanner(callbacks = {}) {
1988
+ if (!bannerElement) {
1989
+ createBanner(callbacks);
1990
+ } else {
1991
+ bannerElement.classList.remove('zest-hidden');
1992
+ }
1993
+ }
1994
+
1995
+ /**
1996
+ * Hide the banner
1997
+ */
1998
+ function hideBanner() {
1999
+ if (bannerElement) {
2000
+ bannerElement.remove();
2001
+ bannerElement = null;
2002
+ shadowRoot$2 = null;
2003
+ }
2004
+ }
2005
+
2006
+ /**
2007
+ * Modal - Settings modal component for category toggles
2008
+ */
2009
+
2010
+
2011
+ let modalElement = null;
2012
+ let shadowRoot$1 = null;
2013
+ let currentSelections = {};
2014
+
2015
+ /**
2016
+ * Create category toggle HTML
2017
+ */
2018
+ function createCategoryHTML(category, isChecked, isRequired) {
2019
+ const disabled = isRequired ? 'disabled' : '';
2020
+ const checked = isChecked ? 'checked' : '';
2021
+
2022
+ return `
2023
+ <div class="zest-category">
2024
+ <div class="zest-category__header">
2025
+ <div class="zest-category__info">
2026
+ <span class="zest-category__label">${category.label}</span>
2027
+ <p class="zest-category__description">${category.description}</p>
2028
+ </div>
2029
+ <label class="zest-toggle">
2030
+ <input
2031
+ type="checkbox"
2032
+ class="zest-toggle__input"
2033
+ data-category="${category.id}"
2034
+ ${checked}
2035
+ ${disabled}
2036
+ aria-label="${category.label}"
2037
+ >
2038
+ <span class="zest-toggle__slider"></span>
2039
+ </label>
2040
+ </div>
2041
+ </div>
2042
+ `;
2043
+ }
2044
+
2045
+ /**
2046
+ * Create the modal HTML
2047
+ */
2048
+ function createModalHTML(config, consent) {
2049
+ const labels = config.labels.modal;
2050
+ const categories = config.categories || DEFAULT_CATEGORIES;
2051
+
2052
+ const categoriesHTML = Object.values(categories)
2053
+ .map(cat => createCategoryHTML(
2054
+ cat,
2055
+ consent[cat.id] ?? cat.default,
2056
+ cat.required
2057
+ ))
2058
+ .join('');
2059
+
2060
+ const policyLink = config.policyUrl
2061
+ ? `<a href="${config.policyUrl}" class="zest-link" target="_blank" rel="noopener">Privacy Policy</a>`
2062
+ : '';
2063
+
2064
+ return `
2065
+ <div class="zest-modal-overlay" role="dialog" aria-modal="true" aria-label="${labels.title}">
2066
+ <div class="zest-modal">
2067
+ <div class="zest-modal__header">
2068
+ <h2 class="zest-modal__title">${labels.title}</h2>
2069
+ <p class="zest-modal__description">${labels.description} ${policyLink}</p>
2070
+ </div>
2071
+ <div class="zest-modal__body">
2072
+ ${categoriesHTML}
2073
+ </div>
2074
+ <div class="zest-modal__footer">
2075
+ <button type="button" class="zest-btn zest-btn--primary" data-action="save">
2076
+ ${labels.save}
2077
+ </button>
2078
+ <button type="button" class="zest-btn zest-btn--secondary" data-action="accept-all">
2079
+ ${labels.acceptAll}
2080
+ </button>
2081
+ <button type="button" class="zest-btn zest-btn--ghost" data-action="reject-all">
2082
+ ${labels.rejectAll}
2083
+ </button>
2084
+ </div>
2085
+ </div>
2086
+ </div>
2087
+ `;
2088
+ }
2089
+
2090
+ /**
2091
+ * Get current selections from toggles
2092
+ */
2093
+ function getSelections() {
2094
+ if (!shadowRoot$1) return currentSelections;
2095
+
2096
+ const toggles = shadowRoot$1.querySelectorAll('.zest-toggle__input');
2097
+ const selections = { essential: true };
2098
+
2099
+ toggles.forEach(toggle => {
2100
+ const category = toggle.dataset.category;
2101
+ if (category && category !== 'essential') {
2102
+ selections[category] = toggle.checked;
2103
+ }
2104
+ });
2105
+
2106
+ return selections;
2107
+ }
2108
+
2109
+ /**
2110
+ * Create and show the modal
2111
+ */
2112
+ function showModal(consent = {}, callbacks = {}) {
2113
+ if (modalElement) {
2114
+ return modalElement;
2115
+ }
2116
+
2117
+ const config = getCurrentConfig();
2118
+ currentSelections = { ...consent };
2119
+
2120
+ // Create host element
2121
+ modalElement = document.createElement('zest-modal');
2122
+ modalElement.setAttribute('data-theme', config.theme || 'light');
2123
+
2124
+ // Create shadow root
2125
+ shadowRoot$1 = modalElement.attachShadow({ mode: 'open' });
2126
+
2127
+ // Add styles
2128
+ const styleEl = document.createElement('style');
2129
+ styleEl.textContent = generateStyles(config);
2130
+ shadowRoot$1.appendChild(styleEl);
2131
+
2132
+ // Add modal HTML
2133
+ const container = document.createElement('div');
2134
+ container.innerHTML = createModalHTML(config, consent);
2135
+ shadowRoot$1.appendChild(container.firstElementChild);
2136
+
2137
+ // Add event listeners
2138
+ const modal = shadowRoot$1.querySelector('.zest-modal-overlay');
2139
+
2140
+ // Button clicks
2141
+ modal.addEventListener('click', (e) => {
2142
+ const action = e.target.dataset.action;
2143
+ if (!action) {
2144
+ // Click on overlay background to close
2145
+ if (e.target === modal) {
2146
+ callbacks.onClose?.();
2147
+ }
2148
+ return;
2149
+ }
2150
+
2151
+ switch (action) {
2152
+ case 'save':
2153
+ callbacks.onSave?.(getSelections());
2154
+ break;
2155
+ case 'accept-all':
2156
+ callbacks.onAcceptAll?.();
2157
+ break;
2158
+ case 'reject-all':
2159
+ callbacks.onRejectAll?.();
2160
+ break;
2161
+ }
2162
+ });
2163
+
2164
+ // Keyboard handling
2165
+ modal.addEventListener('keydown', (e) => {
2166
+ if (e.key === 'Escape') {
2167
+ callbacks.onClose?.();
2168
+ }
2169
+ });
2170
+
2171
+ // Track toggle changes
2172
+ shadowRoot$1.querySelectorAll('.zest-toggle__input').forEach(toggle => {
2173
+ toggle.addEventListener('change', () => {
2174
+ currentSelections = getSelections();
2175
+ });
2176
+ });
2177
+
2178
+ // Mount to document
2179
+ document.body.appendChild(modalElement);
2180
+
2181
+ // Trap focus
2182
+ requestAnimationFrame(() => {
2183
+ const firstButton = shadowRoot$1.querySelector('button');
2184
+ firstButton?.focus();
2185
+ });
2186
+
2187
+ return modalElement;
2188
+ }
2189
+
2190
+ /**
2191
+ * Hide the modal
2192
+ */
2193
+ function hideModal() {
2194
+ if (modalElement) {
2195
+ modalElement.remove();
2196
+ modalElement = null;
2197
+ shadowRoot$1 = null;
2198
+ }
2199
+ }
2200
+
2201
+ /**
2202
+ * Widget - Minimal floating button to reopen settings
2203
+ */
2204
+
2205
+
2206
+ let widgetElement = null;
2207
+ let shadowRoot = null;
2208
+
2209
+ /**
2210
+ * Create the widget HTML
2211
+ */
2212
+ function createWidgetHTML(config) {
2213
+ const labels = config.labels.widget;
2214
+
2215
+ return `
2216
+ <div class="zest-widget">
2217
+ <button type="button" class="zest-widget__btn" aria-label="${labels.label}" title="${labels.label}">
2218
+ <span class="zest-widget__icon">${COOKIE_ICON}</span>
2219
+ </button>
2220
+ </div>
2221
+ `;
2222
+ }
2223
+
2224
+ /**
2225
+ * Create and mount the widget
2226
+ */
2227
+ function createWidget(callbacks = {}) {
2228
+ if (widgetElement) {
2229
+ return widgetElement;
2230
+ }
2231
+
2232
+ const config = getCurrentConfig();
2233
+
2234
+ // Create host element
2235
+ widgetElement = document.createElement('zest-widget');
2236
+ widgetElement.setAttribute('data-theme', config.theme || 'light');
2237
+
2238
+ // Create shadow root
2239
+ shadowRoot = widgetElement.attachShadow({ mode: 'open' });
2240
+
2241
+ // Add styles
2242
+ const styleEl = document.createElement('style');
2243
+ styleEl.textContent = generateStyles(config);
2244
+ shadowRoot.appendChild(styleEl);
2245
+
2246
+ // Add widget HTML
2247
+ const container = document.createElement('div');
2248
+ container.innerHTML = createWidgetHTML(config);
2249
+ shadowRoot.appendChild(container.firstElementChild);
2250
+
2251
+ // Add event listener
2252
+ const button = shadowRoot.querySelector('.zest-widget__btn');
2253
+ button.addEventListener('click', () => {
2254
+ callbacks.onClick?.();
2255
+ });
2256
+
2257
+ // Mount to document
2258
+ document.body.appendChild(widgetElement);
2259
+
2260
+ return widgetElement;
2261
+ }
2262
+
2263
+ /**
2264
+ * Show the widget
2265
+ */
2266
+ function showWidget(callbacks = {}) {
2267
+ if (!widgetElement) {
2268
+ createWidget(callbacks);
2269
+ } else {
2270
+ widgetElement.style.display = '';
2271
+ }
2272
+ }
2273
+
2274
+ /**
2275
+ * Hide the widget
2276
+ */
2277
+ function hideWidget() {
2278
+ if (widgetElement) {
2279
+ widgetElement.style.display = 'none';
2280
+ }
2281
+ }
2282
+
2283
+ /**
2284
+ * Remove the widget completely
2285
+ */
2286
+ function removeWidget() {
2287
+ if (widgetElement) {
2288
+ widgetElement.remove();
2289
+ widgetElement = null;
2290
+ shadowRoot = null;
2291
+ }
2292
+ }
2293
+
2294
+ /**
2295
+ * Zest - Lightweight Cookie Consent Toolkit
2296
+ * Main entry point
2297
+ */
2298
+
2299
+
2300
+ // State
2301
+ let initialized = false;
2302
+ let config = null;
2303
+
2304
+ /**
2305
+ * Consent checker function shared across interceptors
2306
+ */
2307
+ function checkConsent(category) {
2308
+ return hasConsent(category);
2309
+ }
2310
+
2311
+ /**
2312
+ * Replay all queued items for newly allowed categories
2313
+ */
2314
+ function replayAll(allowedCategories) {
2315
+ replayCookies(allowedCategories);
2316
+ replayStorage(allowedCategories);
2317
+ replayScripts(allowedCategories);
2318
+ }
2319
+
2320
+ /**
2321
+ * Handle accept all
2322
+ */
2323
+ function handleAcceptAll() {
2324
+ const result = acceptAll(config.expiration);
2325
+ const categories = getCategoryIds();
2326
+
2327
+ hideBanner();
2328
+ hideModal();
2329
+
2330
+ replayAll(categories);
2331
+
2332
+ if (config.showWidget) {
2333
+ showWidget({ onClick: handleShowSettings });
2334
+ }
2335
+
2336
+ emitConsent(result.current, result.previous);
2337
+ emitChange(result.current, result.previous);
2338
+ config.callbacks?.onAccept?.(result.current);
2339
+ config.callbacks?.onChange?.(result.current);
2340
+ }
2341
+
2342
+ /**
2343
+ * Handle reject all
2344
+ */
2345
+ function handleRejectAll() {
2346
+ const result = rejectAll(config.expiration);
2347
+
2348
+ hideBanner();
2349
+ hideModal();
2350
+
2351
+ if (config.showWidget) {
2352
+ showWidget({ onClick: handleShowSettings });
2353
+ }
2354
+
2355
+ emitReject(result.current);
2356
+ emitChange(result.current, result.previous);
2357
+ config.callbacks?.onReject?.();
2358
+ config.callbacks?.onChange?.(result.current);
2359
+ }
2360
+
2361
+ /**
2362
+ * Handle save preferences from modal
2363
+ */
2364
+ function handleSavePreferences(selections) {
2365
+ const result = updateConsent(selections, config.expiration);
2366
+
2367
+ // Find newly allowed categories
2368
+ const newlyAllowed = Object.keys(result.current).filter(
2369
+ cat => result.current[cat] && !result.previous[cat]
2370
+ );
2371
+
2372
+ if (newlyAllowed.length > 0) {
2373
+ replayAll(newlyAllowed);
2374
+ }
2375
+
2376
+ hideModal();
2377
+
2378
+ if (config.showWidget) {
2379
+ showWidget({ onClick: handleShowSettings });
2380
+ }
2381
+
2382
+ // Determine if this was acceptance or rejection based on selections
2383
+ const hasNonEssential = Object.entries(selections)
2384
+ .some(([cat, val]) => cat !== 'essential' && val);
2385
+
2386
+ if (hasNonEssential) {
2387
+ emitConsent(result.current, result.previous);
2388
+ } else {
2389
+ emitReject(result.current);
2390
+ }
2391
+
2392
+ emitChange(result.current, result.previous);
2393
+ config.callbacks?.onChange?.(result.current);
2394
+ }
2395
+
2396
+ /**
2397
+ * Handle show settings
2398
+ */
2399
+ function handleShowSettings() {
2400
+ hideBanner();
2401
+ hideWidget();
2402
+
2403
+ showModal(getConsent(), {
2404
+ onSave: handleSavePreferences,
2405
+ onAcceptAll: handleAcceptAll,
2406
+ onRejectAll: handleRejectAll,
2407
+ onClose: handleCloseModal
2408
+ });
2409
+
2410
+ emitShow('modal');
2411
+ }
2412
+
2413
+ /**
2414
+ * Handle close modal
2415
+ */
2416
+ function handleCloseModal() {
2417
+ hideModal();
2418
+ emitHide('modal');
2419
+
2420
+ // Show widget if consent was already given
2421
+ if (hasConsentDecision() && config.showWidget) {
2422
+ showWidget({ onClick: handleShowSettings });
2423
+ } else {
2424
+ // Show banner again if no decision made
2425
+ showBanner({
2426
+ onAcceptAll: handleAcceptAll,
2427
+ onRejectAll: handleRejectAll,
2428
+ onSettings: handleShowSettings
2429
+ });
2430
+ }
2431
+ }
2432
+
2433
+ /**
2434
+ * Initialize Zest
2435
+ */
2436
+ function init(userConfig = {}) {
2437
+ if (initialized) {
2438
+ console.warn('[Zest] Already initialized');
2439
+ return Zest;
2440
+ }
2441
+
2442
+ // Merge config
2443
+ config = setConfig(userConfig);
2444
+
2445
+ // Set patterns if provided
2446
+ if (config.patterns) {
2447
+ setPatterns(config.patterns);
2448
+ }
2449
+
2450
+ // Set up consent checkers
2451
+ setConsentChecker$2(checkConsent);
2452
+ setConsentChecker$1(checkConsent);
2453
+ setConsentChecker(checkConsent);
2454
+
2455
+ // Start interception
2456
+ interceptCookies();
2457
+ interceptStorage();
2458
+ startScriptBlocking(config.mode, config.blockedDomains);
2459
+
2460
+ // Load saved consent
2461
+ const consent = loadConsent();
2462
+
2463
+ initialized = true;
2464
+
2465
+ // Check Do Not Track / Global Privacy Control
2466
+ const dntEnabled = isDoNotTrackEnabled();
2467
+ let dntApplied = false;
2468
+
2469
+ if (dntEnabled && config.respectDNT && config.dntBehavior !== 'ignore') {
2470
+ if (config.dntBehavior === 'reject' && !hasConsentDecision()) {
2471
+ // Auto-reject non-essential cookies silently
2472
+ const result = rejectAll(config.expiration);
2473
+ dntApplied = true;
2474
+
2475
+ // Emit events
2476
+ emitReject(result.current);
2477
+ emitChange(result.current, result.previous);
2478
+ config.callbacks?.onReject?.();
2479
+ config.callbacks?.onChange?.(result.current);
2480
+ }
2481
+ // 'preselect' behavior is handled by default (banner shows with defaults off)
2482
+ }
2483
+
2484
+ // Emit ready event
2485
+ emitReady(consent);
2486
+ config.callbacks?.onReady?.(consent);
2487
+
2488
+ // Show UI based on consent state
2489
+ if (!hasConsentDecision() && !dntApplied) {
2490
+ // No consent decision yet - show banner
2491
+ showBanner({
2492
+ onAcceptAll: handleAcceptAll,
2493
+ onRejectAll: handleRejectAll,
2494
+ onSettings: handleShowSettings
2495
+ });
2496
+ emitShow('banner');
2497
+ } else {
2498
+ // Consent already given (or DNT auto-rejected) - show widget for reopening settings
2499
+ if (config.showWidget) {
2500
+ showWidget({ onClick: handleShowSettings });
2501
+ }
2502
+ }
2503
+
2504
+ return Zest;
2505
+ }
2506
+
2507
+ /**
2508
+ * Public API
2509
+ */
2510
+ const Zest = {
2511
+ // Initialization
2512
+ init,
2513
+
2514
+ // Banner control
2515
+ show() {
2516
+ if (!initialized) {
2517
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
2518
+ return;
2519
+ }
2520
+ hideModal();
2521
+ hideWidget();
2522
+ showBanner({
2523
+ onAcceptAll: handleAcceptAll,
2524
+ onRejectAll: handleRejectAll,
2525
+ onSettings: handleShowSettings
2526
+ });
2527
+ emitShow('banner');
2528
+ },
2529
+
2530
+ hide() {
2531
+ hideBanner();
2532
+ emitHide('banner');
2533
+ },
2534
+
2535
+ // Settings modal
2536
+ showSettings() {
2537
+ if (!initialized) {
2538
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
2539
+ return;
2540
+ }
2541
+ handleShowSettings();
2542
+ },
2543
+
2544
+ hideSettings() {
2545
+ hideModal();
2546
+ emitHide('modal');
2547
+ },
2548
+
2549
+ // Consent management
2550
+ getConsent,
2551
+ hasConsent,
2552
+ hasConsentDecision,
2553
+ getConsentProof,
2554
+
2555
+ // DNT detection
2556
+ isDoNotTrackEnabled,
2557
+ getDNTDetails,
2558
+
2559
+ // Accept/Reject programmatically
2560
+ acceptAll() {
2561
+ if (!initialized) {
2562
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
2563
+ return;
2564
+ }
2565
+ handleAcceptAll();
2566
+ },
2567
+
2568
+ rejectAll() {
2569
+ if (!initialized) {
2570
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
2571
+ return;
2572
+ }
2573
+ handleRejectAll();
2574
+ },
2575
+
2576
+ // Reset and show banner again
2577
+ reset() {
2578
+ resetConsent();
2579
+ hideModal();
2580
+ removeWidget();
2581
+
2582
+ if (initialized) {
2583
+ showBanner({
2584
+ onAcceptAll: handleAcceptAll,
2585
+ onRejectAll: handleRejectAll,
2586
+ onSettings: handleShowSettings
2587
+ });
2588
+ emitShow('banner');
2589
+ }
2590
+ },
2591
+
2592
+ // Config
2593
+ getConfig: getCurrentConfig,
2594
+
2595
+ // Events
2596
+ EVENTS
2597
+ };
2598
+
2599
+ // Auto-init if config present
2600
+ if (typeof window !== 'undefined') {
2601
+ // Make Zest available globally
2602
+ window.Zest = Zest;
2603
+
2604
+ const autoInit = () => {
2605
+ const cfg = getConfig();
2606
+ if (cfg.autoInit !== false) {
2607
+ init(window.ZestConfig);
2608
+ }
2609
+ };
2610
+
2611
+ if (document.readyState === 'loading') {
2612
+ document.addEventListener('DOMContentLoaded', autoInit);
2613
+ } else {
2614
+ autoInit();
2615
+ }
2616
+ }
2617
+
2618
+ return Zest;
2619
+
2620
+ })();
2621
+ //# sourceMappingURL=zest.de.js.map