@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
package/dist/zest.js ADDED
@@ -0,0 +1,3109 @@
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
+ * Built-in translations for Zest
847
+ * Language is auto-detected from <html lang=""> or navigator.language
848
+ */
849
+
850
+ const translations = {
851
+ en: {
852
+ labels: {
853
+ banner: {
854
+ title: 'We value your privacy',
855
+ 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.',
856
+ acceptAll: 'Accept All',
857
+ rejectAll: 'Reject All',
858
+ settings: 'Settings'
859
+ },
860
+ modal: {
861
+ title: 'Privacy Settings',
862
+ description: 'Manage your cookie preferences. You can enable or disable different types of cookies below.',
863
+ save: 'Save Preferences',
864
+ acceptAll: 'Accept All',
865
+ rejectAll: 'Reject All'
866
+ },
867
+ widget: {
868
+ label: 'Cookie Settings'
869
+ }
870
+ },
871
+ categories: {
872
+ essential: {
873
+ label: 'Essential',
874
+ description: 'Required for the website to function properly. Cannot be disabled.'
875
+ },
876
+ functional: {
877
+ label: 'Functional',
878
+ description: 'Enable personalized features like language preferences and themes.'
879
+ },
880
+ analytics: {
881
+ label: 'Analytics',
882
+ description: 'Help us understand how visitors interact with our website.'
883
+ },
884
+ marketing: {
885
+ label: 'Marketing',
886
+ description: 'Used to deliver relevant advertisements and track campaign performance.'
887
+ }
888
+ }
889
+ },
890
+
891
+ de: {
892
+ labels: {
893
+ banner: {
894
+ title: 'Wir respektieren Ihre Privatsphäre',
895
+ 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.',
896
+ acceptAll: 'Alle akzeptieren',
897
+ rejectAll: 'Alle ablehnen',
898
+ settings: 'Einstellungen'
899
+ },
900
+ modal: {
901
+ title: 'Datenschutzeinstellungen',
902
+ description: 'Verwalten Sie Ihre Cookie-Einstellungen. Sie können verschiedene Arten von Cookies unten aktivieren oder deaktivieren.',
903
+ save: 'Einstellungen speichern',
904
+ acceptAll: 'Alle akzeptieren',
905
+ rejectAll: 'Alle ablehnen'
906
+ },
907
+ widget: {
908
+ label: 'Cookie-Einstellungen'
909
+ }
910
+ },
911
+ categories: {
912
+ essential: {
913
+ label: 'Notwendig',
914
+ description: 'Erforderlich für die ordnungsgemäße Funktion der Website. Können nicht deaktiviert werden.'
915
+ },
916
+ functional: {
917
+ label: 'Funktional',
918
+ description: 'Ermöglichen personalisierte Funktionen wie Spracheinstellungen und Designs.'
919
+ },
920
+ analytics: {
921
+ label: 'Analytisch',
922
+ description: 'Helfen uns zu verstehen, wie Besucher mit unserer Website interagieren.'
923
+ },
924
+ marketing: {
925
+ label: 'Marketing',
926
+ description: 'Werden verwendet, um relevante Werbung anzuzeigen und die Kampagnenleistung zu messen.'
927
+ }
928
+ }
929
+ },
930
+
931
+ es: {
932
+ labels: {
933
+ banner: {
934
+ title: 'Valoramos tu privacidad',
935
+ description: 'Utilizamos cookies para mejorar tu experiencia de navegación, ofrecer contenido personalizado y analizar nuestro tráfico. Al hacer clic en "Aceptar todo", consientes el uso de cookies.',
936
+ acceptAll: 'Aceptar todo',
937
+ rejectAll: 'Rechazar todo',
938
+ settings: 'Configuración'
939
+ },
940
+ modal: {
941
+ title: 'Configuración de privacidad',
942
+ description: 'Gestiona tus preferencias de cookies. Puedes activar o desactivar diferentes tipos de cookies a continuación.',
943
+ save: 'Guardar preferencias',
944
+ acceptAll: 'Aceptar todo',
945
+ rejectAll: 'Rechazar todo'
946
+ },
947
+ widget: {
948
+ label: 'Configuración de cookies'
949
+ }
950
+ },
951
+ categories: {
952
+ essential: {
953
+ label: 'Esenciales',
954
+ description: 'Necesarias para el funcionamiento del sitio web. No se pueden desactivar.'
955
+ },
956
+ functional: {
957
+ label: 'Funcionales',
958
+ description: 'Permiten funciones personalizadas como preferencias de idioma y temas.'
959
+ },
960
+ analytics: {
961
+ label: 'Analíticas',
962
+ description: 'Nos ayudan a entender cómo los visitantes interactúan con nuestro sitio web.'
963
+ },
964
+ marketing: {
965
+ label: 'Marketing',
966
+ description: 'Se utilizan para mostrar anuncios relevantes y medir el rendimiento de las campañas.'
967
+ }
968
+ }
969
+ },
970
+
971
+ fr: {
972
+ labels: {
973
+ banner: {
974
+ title: 'Nous respectons votre vie privée',
975
+ description: 'Nous utilisons des cookies pour améliorer votre expérience de navigation, proposer du contenu personnalisé et analyser notre trafic. En cliquant sur « Tout accepter », vous consentez à l\'utilisation de cookies.',
976
+ acceptAll: 'Tout accepter',
977
+ rejectAll: 'Tout refuser',
978
+ settings: 'Paramètres'
979
+ },
980
+ modal: {
981
+ title: 'Paramètres de confidentialité',
982
+ description: 'Gérez vos préférences en matière de cookies. Vous pouvez activer ou désactiver différents types de cookies ci-dessous.',
983
+ save: 'Enregistrer les préférences',
984
+ acceptAll: 'Tout accepter',
985
+ rejectAll: 'Tout refuser'
986
+ },
987
+ widget: {
988
+ label: 'Paramètres des cookies'
989
+ }
990
+ },
991
+ categories: {
992
+ essential: {
993
+ label: 'Essentiels',
994
+ description: 'Nécessaires au bon fonctionnement du site. Ne peuvent pas être désactivés.'
995
+ },
996
+ functional: {
997
+ label: 'Fonctionnels',
998
+ description: 'Permettent des fonctionnalités personnalisées comme les préférences de langue et de thème.'
999
+ },
1000
+ analytics: {
1001
+ label: 'Analytiques',
1002
+ description: 'Nous aident à comprendre comment les visiteurs interagissent avec notre site.'
1003
+ },
1004
+ marketing: {
1005
+ label: 'Marketing',
1006
+ description: 'Utilisés pour afficher des publicités pertinentes et mesurer les performances des campagnes.'
1007
+ }
1008
+ }
1009
+ },
1010
+
1011
+ it: {
1012
+ labels: {
1013
+ banner: {
1014
+ title: 'Rispettiamo la tua privacy',
1015
+ description: 'Utilizziamo i cookie per migliorare la tua esperienza di navigazione, fornire contenuti personalizzati e analizzare il nostro traffico. Cliccando su "Accetta tutto", acconsenti all\'uso dei cookie.',
1016
+ acceptAll: 'Accetta tutto',
1017
+ rejectAll: 'Rifiuta tutto',
1018
+ settings: 'Impostazioni'
1019
+ },
1020
+ modal: {
1021
+ title: 'Impostazioni privacy',
1022
+ description: 'Gestisci le tue preferenze sui cookie. Puoi attivare o disattivare diversi tipi di cookie qui sotto.',
1023
+ save: 'Salva preferenze',
1024
+ acceptAll: 'Accetta tutto',
1025
+ rejectAll: 'Rifiuta tutto'
1026
+ },
1027
+ widget: {
1028
+ label: 'Impostazioni cookie'
1029
+ }
1030
+ },
1031
+ categories: {
1032
+ essential: {
1033
+ label: 'Essenziali',
1034
+ description: 'Necessari per il corretto funzionamento del sito. Non possono essere disattivati.'
1035
+ },
1036
+ functional: {
1037
+ label: 'Funzionali',
1038
+ description: 'Abilitano funzionalità personalizzate come preferenze di lingua e tema.'
1039
+ },
1040
+ analytics: {
1041
+ label: 'Analitici',
1042
+ description: 'Ci aiutano a capire come i visitatori interagiscono con il nostro sito.'
1043
+ },
1044
+ marketing: {
1045
+ label: 'Marketing',
1046
+ description: 'Utilizzati per mostrare annunci pertinenti e misurare le prestazioni delle campagne.'
1047
+ }
1048
+ }
1049
+ },
1050
+
1051
+ pt: {
1052
+ labels: {
1053
+ banner: {
1054
+ title: 'Valorizamos sua privacidade',
1055
+ description: 'Usamos cookies para melhorar sua experiência de navegação, fornecer conteúdo personalizado e analisar nosso tráfego. Ao clicar em "Aceitar tudo", você consente com o uso de cookies.',
1056
+ acceptAll: 'Aceitar tudo',
1057
+ rejectAll: 'Rejeitar tudo',
1058
+ settings: 'Configurações'
1059
+ },
1060
+ modal: {
1061
+ title: 'Configurações de privacidade',
1062
+ description: 'Gerencie suas preferências de cookies. Você pode ativar ou desativar diferentes tipos de cookies abaixo.',
1063
+ save: 'Salvar preferências',
1064
+ acceptAll: 'Aceitar tudo',
1065
+ rejectAll: 'Rejeitar tudo'
1066
+ },
1067
+ widget: {
1068
+ label: 'Configurações de cookies'
1069
+ }
1070
+ },
1071
+ categories: {
1072
+ essential: {
1073
+ label: 'Essenciais',
1074
+ description: 'Necessários para o funcionamento do site. Não podem ser desativados.'
1075
+ },
1076
+ functional: {
1077
+ label: 'Funcionais',
1078
+ description: 'Permitem recursos personalizados como preferências de idioma e tema.'
1079
+ },
1080
+ analytics: {
1081
+ label: 'Analíticos',
1082
+ description: 'Nos ajudam a entender como os visitantes interagem com nosso site.'
1083
+ },
1084
+ marketing: {
1085
+ label: 'Marketing',
1086
+ description: 'Usados para exibir anúncios relevantes e medir o desempenho de campanhas.'
1087
+ }
1088
+ }
1089
+ },
1090
+
1091
+ nl: {
1092
+ labels: {
1093
+ banner: {
1094
+ title: 'Wij respecteren uw privacy',
1095
+ description: 'Wij gebruiken cookies om uw browse-ervaring te verbeteren, gepersonaliseerde inhoud aan te bieden en ons verkeer te analyseren. Door op "Alles accepteren" te klikken, stemt u in met het gebruik van cookies.',
1096
+ acceptAll: 'Alles accepteren',
1097
+ rejectAll: 'Alles weigeren',
1098
+ settings: 'Instellingen'
1099
+ },
1100
+ modal: {
1101
+ title: 'Privacy-instellingen',
1102
+ description: 'Beheer uw cookievoorkeuren. U kunt hieronder verschillende soorten cookies in- of uitschakelen.',
1103
+ save: 'Voorkeuren opslaan',
1104
+ acceptAll: 'Alles accepteren',
1105
+ rejectAll: 'Alles weigeren'
1106
+ },
1107
+ widget: {
1108
+ label: 'Cookie-instellingen'
1109
+ }
1110
+ },
1111
+ categories: {
1112
+ essential: {
1113
+ label: 'Essentieel',
1114
+ description: 'Noodzakelijk voor de goede werking van de website. Kunnen niet worden uitgeschakeld.'
1115
+ },
1116
+ functional: {
1117
+ label: 'Functioneel',
1118
+ description: 'Maken gepersonaliseerde functies mogelijk zoals taal- en themavoorkeuren.'
1119
+ },
1120
+ analytics: {
1121
+ label: 'Analytisch',
1122
+ description: 'Helpen ons te begrijpen hoe bezoekers onze website gebruiken.'
1123
+ },
1124
+ marketing: {
1125
+ label: 'Marketing',
1126
+ description: 'Worden gebruikt om relevante advertenties te tonen en campagneprestaties te meten.'
1127
+ }
1128
+ }
1129
+ },
1130
+
1131
+ pl: {
1132
+ labels: {
1133
+ banner: {
1134
+ title: 'Szanujemy Twoją prywatność',
1135
+ description: 'Używamy plików cookie, aby poprawić Twoje wrażenia z przeglądania, dostarczać spersonalizowane treści i analizować nasz ruch. Klikając „Zaakceptuj wszystko", wyrażasz zgodę na używanie plików cookie.',
1136
+ acceptAll: 'Zaakceptuj wszystko',
1137
+ rejectAll: 'Odrzuć wszystko',
1138
+ settings: 'Ustawienia'
1139
+ },
1140
+ modal: {
1141
+ title: 'Ustawienia prywatności',
1142
+ description: 'Zarządzaj swoimi preferencjami dotyczącymi plików cookie. Możesz włączyć lub wyłączyć różne typy plików cookie poniżej.',
1143
+ save: 'Zapisz preferencje',
1144
+ acceptAll: 'Zaakceptuj wszystko',
1145
+ rejectAll: 'Odrzuć wszystko'
1146
+ },
1147
+ widget: {
1148
+ label: 'Ustawienia plików cookie'
1149
+ }
1150
+ },
1151
+ categories: {
1152
+ essential: {
1153
+ label: 'Niezbędne',
1154
+ description: 'Wymagane do prawidłowego działania strony. Nie można ich wyłączyć.'
1155
+ },
1156
+ functional: {
1157
+ label: 'Funkcjonalne',
1158
+ description: 'Umożliwiają spersonalizowane funkcje, takie jak preferencje językowe i motywy.'
1159
+ },
1160
+ analytics: {
1161
+ label: 'Analityczne',
1162
+ description: 'Pomagają nam zrozumieć, jak odwiedzający korzystają z naszej strony.'
1163
+ },
1164
+ marketing: {
1165
+ label: 'Marketingowe',
1166
+ description: 'Służą do wyświetlania odpowiednich reklam i mierzenia skuteczności kampanii.'
1167
+ }
1168
+ }
1169
+ },
1170
+
1171
+ uk: {
1172
+ labels: {
1173
+ banner: {
1174
+ title: 'Ми цінуємо вашу конфіденційність',
1175
+ description: 'Ми використовуємо файли cookie для покращення вашого досвіду перегляду, надання персоналізованого контенту та аналізу нашого трафіку. Натискаючи «Прийняти все», ви погоджуєтесь на використання файлів cookie.',
1176
+ acceptAll: 'Прийняти все',
1177
+ rejectAll: 'Відхилити все',
1178
+ settings: 'Налаштування'
1179
+ },
1180
+ modal: {
1181
+ title: 'Налаштування конфіденційності',
1182
+ description: 'Керуйте своїми налаштуваннями файлів cookie. Ви можете ввімкнути або вимкнути різні типи файлів cookie нижче.',
1183
+ save: 'Зберегти налаштування',
1184
+ acceptAll: 'Прийняти все',
1185
+ rejectAll: 'Відхилити все'
1186
+ },
1187
+ widget: {
1188
+ label: 'Налаштування cookie'
1189
+ }
1190
+ },
1191
+ categories: {
1192
+ essential: {
1193
+ label: 'Необхідні',
1194
+ description: 'Потрібні для правильної роботи сайту. Не можуть бути вимкнені.'
1195
+ },
1196
+ functional: {
1197
+ label: 'Функціональні',
1198
+ description: 'Дозволяють персоналізовані функції, такі як мовні налаштування та теми.'
1199
+ },
1200
+ analytics: {
1201
+ label: 'Аналітичні',
1202
+ description: 'Допомагають нам зрозуміти, як відвідувачі взаємодіють з нашим сайтом.'
1203
+ },
1204
+ marketing: {
1205
+ label: 'Маркетингові',
1206
+ description: 'Використовуються для показу релевантної реклами та вимірювання ефективності кампаній.'
1207
+ }
1208
+ }
1209
+ },
1210
+
1211
+ ru: {
1212
+ labels: {
1213
+ banner: {
1214
+ title: 'Мы ценим вашу конфиденциальность',
1215
+ description: 'Мы используем файлы cookie для улучшения вашего опыта просмотра, предоставления персонализированного контента и анализа нашего трафика. Нажимая «Принять все», вы соглашаетесь на использование файлов cookie.',
1216
+ acceptAll: 'Принять все',
1217
+ rejectAll: 'Отклонить все',
1218
+ settings: 'Настройки'
1219
+ },
1220
+ modal: {
1221
+ title: 'Настройки конфиденциальности',
1222
+ description: 'Управляйте своими настройками файлов cookie. Вы можете включить или отключить различные типы файлов cookie ниже.',
1223
+ save: 'Сохранить настройки',
1224
+ acceptAll: 'Принять все',
1225
+ rejectAll: 'Отклонить все'
1226
+ },
1227
+ widget: {
1228
+ label: 'Настройки cookie'
1229
+ }
1230
+ },
1231
+ categories: {
1232
+ essential: {
1233
+ label: 'Необходимые',
1234
+ description: 'Требуются для правильной работы сайта. Не могут быть отключены.'
1235
+ },
1236
+ functional: {
1237
+ label: 'Функциональные',
1238
+ description: 'Позволяют использовать персонализированные функции, такие как языковые настройки и темы.'
1239
+ },
1240
+ analytics: {
1241
+ label: 'Аналитические',
1242
+ description: 'Помогают нам понять, как посетители взаимодействуют с нашим сайтом.'
1243
+ },
1244
+ marketing: {
1245
+ label: 'Маркетинговые',
1246
+ description: 'Используются для показа релевантной рекламы и измерения эффективности кампаний.'
1247
+ }
1248
+ }
1249
+ },
1250
+
1251
+ ja: {
1252
+ labels: {
1253
+ banner: {
1254
+ title: 'プライバシーを尊重します',
1255
+ description: '当サイトでは、ブラウジング体験の向上、パーソナライズされたコンテンツの提供、トラフィックの分析のためにCookieを使用しています。「すべて同意」をクリックすると、Cookieの使用に同意したことになります。',
1256
+ acceptAll: 'すべて同意',
1257
+ rejectAll: 'すべて拒否',
1258
+ settings: '設定'
1259
+ },
1260
+ modal: {
1261
+ title: 'プライバシー設定',
1262
+ description: 'Cookieの設定を管理できます。以下で各種Cookieを有効または無効にできます。',
1263
+ save: '設定を保存',
1264
+ acceptAll: 'すべて同意',
1265
+ rejectAll: 'すべて拒否'
1266
+ },
1267
+ widget: {
1268
+ label: 'Cookie設定'
1269
+ }
1270
+ },
1271
+ categories: {
1272
+ essential: {
1273
+ label: '必須',
1274
+ description: 'サイトの正常な動作に必要です。無効にすることはできません。'
1275
+ },
1276
+ functional: {
1277
+ label: '機能性',
1278
+ description: '言語設定やテーマなどのパーソナライズ機能を有効にします。'
1279
+ },
1280
+ analytics: {
1281
+ label: '分析',
1282
+ description: '訪問者がサイトをどのように利用しているかを理解するのに役立ちます。'
1283
+ },
1284
+ marketing: {
1285
+ label: 'マーケティング',
1286
+ description: '関連性の高い広告を表示し、キャンペーンの効果を測定するために使用されます。'
1287
+ }
1288
+ }
1289
+ },
1290
+
1291
+ zh: {
1292
+ labels: {
1293
+ banner: {
1294
+ title: '我们重视您的隐私',
1295
+ description: '我们使用Cookie来改善您的浏览体验、提供个性化内容并分析我们的流量。点击"全部接受"即表示您同意我们使用Cookie。',
1296
+ acceptAll: '全部接受',
1297
+ rejectAll: '全部拒绝',
1298
+ settings: '设置'
1299
+ },
1300
+ modal: {
1301
+ title: '隐私设置',
1302
+ description: '管理您的Cookie偏好设置。您可以在下方启用或禁用不同类型的Cookie。',
1303
+ save: '保存设置',
1304
+ acceptAll: '全部接受',
1305
+ rejectAll: '全部拒绝'
1306
+ },
1307
+ widget: {
1308
+ label: 'Cookie设置'
1309
+ }
1310
+ },
1311
+ categories: {
1312
+ essential: {
1313
+ label: '必要',
1314
+ description: '网站正常运行所必需的。无法禁用。'
1315
+ },
1316
+ functional: {
1317
+ label: '功能性',
1318
+ description: '启用个性化功能,如语言偏好和主题设置。'
1319
+ },
1320
+ analytics: {
1321
+ label: '分析',
1322
+ description: '帮助我们了解访问者如何与网站互动。'
1323
+ },
1324
+ marketing: {
1325
+ label: '营销',
1326
+ description: '用于展示相关广告并衡量营销活动效果。'
1327
+ }
1328
+ }
1329
+ }
1330
+ };
1331
+
1332
+ /**
1333
+ * Detect language from various sources
1334
+ * Priority: config.lang > <html lang=""> > navigator.language > 'en'
1335
+ */
1336
+ function detectLanguage(configLang) {
1337
+ // 1. Explicit config
1338
+ if (configLang && configLang !== 'auto') {
1339
+ return normalizeLanguage(configLang);
1340
+ }
1341
+
1342
+ // 2. HTML lang attribute
1343
+ const htmlLang = document.documentElement.lang;
1344
+ if (htmlLang) {
1345
+ const normalized = normalizeLanguage(htmlLang);
1346
+ if (translations[normalized]) {
1347
+ return normalized;
1348
+ }
1349
+ }
1350
+
1351
+ // 3. Browser language
1352
+ if (typeof navigator !== 'undefined' && navigator.language) {
1353
+ const normalized = normalizeLanguage(navigator.language);
1354
+ if (translations[normalized]) {
1355
+ return normalized;
1356
+ }
1357
+ }
1358
+
1359
+ // 4. Default to English
1360
+ return 'en';
1361
+ }
1362
+
1363
+ /**
1364
+ * Normalize language code (e.g., 'en-US' -> 'en', 'es_MX' -> 'es')
1365
+ */
1366
+ function normalizeLanguage(lang) {
1367
+ if (!lang) return 'en';
1368
+
1369
+ // ISO 639-1 language codes are always 2 characters
1370
+ const base = lang.slice(0, 2).toLowerCase();
1371
+
1372
+ // Check if we support it
1373
+ if (translations[base]) {
1374
+ return base;
1375
+ }
1376
+
1377
+ return 'en';
1378
+ }
1379
+
1380
+ /**
1381
+ * Get translation for a language
1382
+ */
1383
+ function getTranslation(lang) {
1384
+ return translations[lang] || translations.en;
1385
+ }
1386
+
1387
+ /**
1388
+ * Default configuration values
1389
+ */
1390
+
1391
+
1392
+ const DEFAULTS = {
1393
+ // Language: 'auto' | 'en' | 'de' | 'es' | 'fr' | 'it' | 'pt' | 'nl' | 'pl' | 'uk' | 'ru' | 'ja' | 'zh'
1394
+ lang: 'auto',
1395
+
1396
+ // UI positioning
1397
+ position: 'bottom', // 'bottom' | 'bottom-left' | 'bottom-right' | 'top'
1398
+
1399
+ // Theming
1400
+ theme: 'auto', // 'light' | 'dark' | 'auto'
1401
+ accentColor: '#0071e3',
1402
+
1403
+ // Categories
1404
+ categories: DEFAULT_CATEGORIES,
1405
+
1406
+ // UI Labels
1407
+ labels: {
1408
+ banner: {
1409
+ title: 'We value your privacy',
1410
+ 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.',
1411
+ acceptAll: 'Accept All',
1412
+ rejectAll: 'Reject All',
1413
+ settings: 'Settings'
1414
+ },
1415
+ modal: {
1416
+ title: 'Privacy Settings',
1417
+ description: 'Manage your cookie preferences. You can enable or disable different types of cookies below.',
1418
+ save: 'Save Preferences',
1419
+ acceptAll: 'Accept All',
1420
+ rejectAll: 'Reject All'
1421
+ },
1422
+ widget: {
1423
+ label: 'Cookie Settings'
1424
+ }
1425
+ },
1426
+
1427
+ // Behavior
1428
+ autoInit: true,
1429
+ showWidget: true,
1430
+ expiration: 365,
1431
+
1432
+ // Do Not Track / Global Privacy Control
1433
+ // respectDNT: true = respect DNT/GPC signals
1434
+ // dntBehavior: 'reject' | 'preselect' | 'ignore'
1435
+ // - 'reject': auto-reject non-essential, don't show banner
1436
+ // - 'preselect': show banner with non-essential unchecked (same as normal)
1437
+ // - 'ignore': ignore DNT completely
1438
+ respectDNT: true,
1439
+ dntBehavior: 'reject',
1440
+
1441
+ // Custom styles to inject into Shadow DOM
1442
+ customStyles: '',
1443
+
1444
+ // Blocking mode: 'manual' | 'safe' | 'strict' | 'doomsday'
1445
+ mode: 'safe',
1446
+
1447
+ // Custom domains to block (in addition to mode-based blocking)
1448
+ blockedDomains: [], // days
1449
+
1450
+ // Links
1451
+ policyUrl: null,
1452
+ imprintUrl: null,
1453
+
1454
+ // Callbacks
1455
+ callbacks: {
1456
+ onAccept: null,
1457
+ onReject: null,
1458
+ onChange: null,
1459
+ onReady: null
1460
+ }
1461
+ };
1462
+
1463
+ /**
1464
+ * Merge user config with defaults (deep merge)
1465
+ */
1466
+ function mergeConfig(userConfig) {
1467
+ const config = { ...DEFAULTS };
1468
+
1469
+ if (!userConfig) {
1470
+ userConfig = {};
1471
+ }
1472
+
1473
+ // Simple properties
1474
+ const simpleKeys = ['lang', 'position', 'theme', 'accentColor', 'autoInit', 'showWidget', 'expiration', 'policyUrl', 'imprintUrl', 'customStyles', 'mode', 'blockedDomains', 'respectDNT', 'dntBehavior'];
1475
+ for (const key of simpleKeys) {
1476
+ if (userConfig[key] !== undefined) {
1477
+ config[key] = userConfig[key];
1478
+ }
1479
+ }
1480
+
1481
+ // Detect language and get translations
1482
+ const detectedLang = detectLanguage(config.lang);
1483
+ config.lang = detectedLang;
1484
+ const translation = getTranslation(detectedLang);
1485
+
1486
+ // Deep merge labels (translation < user config)
1487
+ const translationLabels = translation.labels || {};
1488
+ const userLabels = userConfig.labels || {};
1489
+ config.labels = {
1490
+ banner: {
1491
+ ...DEFAULTS.labels.banner,
1492
+ ...translationLabels.banner,
1493
+ ...userLabels.banner
1494
+ },
1495
+ modal: {
1496
+ ...DEFAULTS.labels.modal,
1497
+ ...translationLabels.modal,
1498
+ ...userLabels.modal
1499
+ },
1500
+ widget: {
1501
+ ...DEFAULTS.labels.widget,
1502
+ ...translationLabels.widget,
1503
+ ...userLabels.widget
1504
+ }
1505
+ };
1506
+
1507
+ // Deep merge categories (translation < user config)
1508
+ const translationCategories = translation.categories || {};
1509
+ const userCategories = userConfig.categories || {};
1510
+ config.categories = { ...DEFAULTS.categories };
1511
+ for (const key of Object.keys(DEFAULTS.categories)) {
1512
+ config.categories[key] = {
1513
+ ...DEFAULTS.categories[key],
1514
+ ...translationCategories[key],
1515
+ ...userCategories[key]
1516
+ };
1517
+ }
1518
+
1519
+ // Merge callbacks
1520
+ if (userConfig.callbacks) {
1521
+ config.callbacks = { ...DEFAULTS.callbacks, ...userConfig.callbacks };
1522
+ }
1523
+
1524
+ // Patterns (for pattern matcher)
1525
+ if (userConfig.patterns) {
1526
+ config.patterns = userConfig.patterns;
1527
+ }
1528
+
1529
+ return config;
1530
+ }
1531
+
1532
+ /**
1533
+ * Configuration Parser - Reads config from various sources
1534
+ */
1535
+
1536
+
1537
+ /**
1538
+ * Parse data attributes from script tag
1539
+ */
1540
+ function parseDataAttributes() {
1541
+ // Find the Zest script tag
1542
+ const script = document.currentScript ||
1543
+ document.querySelector('script[data-zest]') ||
1544
+ document.querySelector('script[src*="zest"]');
1545
+
1546
+ if (!script) {
1547
+ return {};
1548
+ }
1549
+
1550
+ const config = {};
1551
+
1552
+ // Position
1553
+ const position = script.getAttribute('data-position');
1554
+ if (position) config.position = position;
1555
+
1556
+ // Theme
1557
+ const theme = script.getAttribute('data-theme');
1558
+ if (theme) config.theme = theme;
1559
+
1560
+ // Accent color
1561
+ const accent = script.getAttribute('data-accent') || script.getAttribute('data-accent-color');
1562
+ if (accent) config.accentColor = accent;
1563
+
1564
+ // Policy URL
1565
+ const policyUrl = script.getAttribute('data-policy-url') || script.getAttribute('data-privacy-url');
1566
+ if (policyUrl) config.policyUrl = policyUrl;
1567
+
1568
+ // Imprint URL
1569
+ const imprintUrl = script.getAttribute('data-imprint-url');
1570
+ if (imprintUrl) config.imprintUrl = imprintUrl;
1571
+
1572
+ // Show widget
1573
+ const showWidget = script.getAttribute('data-show-widget');
1574
+ if (showWidget !== null) config.showWidget = showWidget !== 'false';
1575
+
1576
+ // Auto init
1577
+ const autoInit = script.getAttribute('data-auto-init');
1578
+ if (autoInit !== null) config.autoInit = autoInit !== 'false';
1579
+
1580
+ // Expiration
1581
+ const expiration = script.getAttribute('data-expiration');
1582
+ if (expiration) config.expiration = parseInt(expiration, 10);
1583
+
1584
+ return config;
1585
+ }
1586
+
1587
+ /**
1588
+ * Parse window.ZestConfig object
1589
+ */
1590
+ function parseWindowConfig() {
1591
+ if (typeof window !== 'undefined' && window.ZestConfig) {
1592
+ return window.ZestConfig;
1593
+ }
1594
+ return {};
1595
+ }
1596
+
1597
+ /**
1598
+ * Get final merged configuration
1599
+ * Priority: data attributes > window.ZestConfig > defaults
1600
+ */
1601
+ function getConfig() {
1602
+ const windowConfig = parseWindowConfig();
1603
+ const dataConfig = parseDataAttributes();
1604
+
1605
+ // Merge: defaults < windowConfig < dataConfig
1606
+ return mergeConfig({
1607
+ ...windowConfig,
1608
+ ...dataConfig
1609
+ });
1610
+ }
1611
+
1612
+ /**
1613
+ * Update configuration at runtime
1614
+ */
1615
+ let currentConfig = null;
1616
+
1617
+ function setConfig(config) {
1618
+ currentConfig = mergeConfig(config);
1619
+ return currentConfig;
1620
+ }
1621
+
1622
+ function getCurrentConfig() {
1623
+ if (!currentConfig) {
1624
+ currentConfig = getConfig();
1625
+ }
1626
+ return currentConfig;
1627
+ }
1628
+
1629
+ /**
1630
+ * Consent Store - Manages consent state persistence
1631
+ */
1632
+
1633
+
1634
+ const COOKIE_NAME = 'zest_consent';
1635
+ const CONSENT_VERSION = '1.0';
1636
+
1637
+ // Current consent state
1638
+ let consent = null;
1639
+
1640
+ /**
1641
+ * Get the original cookie setter (bypasses interception)
1642
+ */
1643
+ function setRawCookie(value) {
1644
+ const descriptor = getOriginalCookieDescriptor();
1645
+ if (descriptor?.set) {
1646
+ descriptor.set.call(document, value);
1647
+ } else {
1648
+ // Fallback if interceptor not initialized yet
1649
+ document.cookie = value;
1650
+ }
1651
+ }
1652
+
1653
+ /**
1654
+ * Get the original cookie getter
1655
+ */
1656
+ function getRawCookie() {
1657
+ const descriptor = getOriginalCookieDescriptor();
1658
+ if (descriptor?.get) {
1659
+ return descriptor.get.call(document);
1660
+ }
1661
+ return document.cookie;
1662
+ }
1663
+
1664
+ /**
1665
+ * Load consent from cookie
1666
+ */
1667
+ function loadConsent() {
1668
+ try {
1669
+ const cookies = getRawCookie();
1670
+ const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
1671
+
1672
+ if (match) {
1673
+ const data = JSON.parse(decodeURIComponent(match[1]));
1674
+ consent = data.categories || getDefaultConsent();
1675
+ return { ...consent };
1676
+ }
1677
+ } catch (e) {
1678
+ // Invalid or missing cookie
1679
+ }
1680
+
1681
+ consent = getDefaultConsent();
1682
+ return { ...consent };
1683
+ }
1684
+
1685
+ /**
1686
+ * Save consent to cookie
1687
+ */
1688
+ function saveConsent(expirationDays = 365) {
1689
+ if (!consent) {
1690
+ consent = getDefaultConsent();
1691
+ }
1692
+
1693
+ const data = {
1694
+ version: CONSENT_VERSION,
1695
+ timestamp: Date.now(),
1696
+ categories: consent
1697
+ };
1698
+
1699
+ const expires = new Date(Date.now() + expirationDays * 24 * 60 * 60 * 1000).toUTCString();
1700
+ const cookieValue = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(data))}; expires=${expires}; path=/; SameSite=Lax`;
1701
+
1702
+ setRawCookie(cookieValue);
1703
+ }
1704
+
1705
+ /**
1706
+ * Get current consent state
1707
+ */
1708
+ function getConsent() {
1709
+ if (!consent) {
1710
+ consent = loadConsent();
1711
+ }
1712
+ return { ...consent };
1713
+ }
1714
+
1715
+ /**
1716
+ * Update consent state
1717
+ */
1718
+ function updateConsent(newConsent, expirationDays = 365) {
1719
+ const previous = consent ? { ...consent } : getDefaultConsent();
1720
+
1721
+ consent = {
1722
+ essential: true, // Always true
1723
+ functional: !!newConsent.functional,
1724
+ analytics: !!newConsent.analytics,
1725
+ marketing: !!newConsent.marketing
1726
+ };
1727
+
1728
+ saveConsent(expirationDays);
1729
+
1730
+ return { current: { ...consent }, previous };
1731
+ }
1732
+
1733
+ /**
1734
+ * Check if specific category is allowed
1735
+ */
1736
+ function hasConsent(category) {
1737
+ if (!consent) {
1738
+ consent = loadConsent();
1739
+ }
1740
+ return consent[category] === true;
1741
+ }
1742
+
1743
+ /**
1744
+ * Accept all categories
1745
+ */
1746
+ function acceptAll(expirationDays = 365) {
1747
+ return updateConsent({
1748
+ functional: true,
1749
+ analytics: true,
1750
+ marketing: true
1751
+ }, expirationDays);
1752
+ }
1753
+
1754
+ /**
1755
+ * Reject all (except essential)
1756
+ */
1757
+ function rejectAll(expirationDays = 365) {
1758
+ return updateConsent({
1759
+ functional: false,
1760
+ analytics: false,
1761
+ marketing: false
1762
+ }, expirationDays);
1763
+ }
1764
+
1765
+ /**
1766
+ * Reset consent (clear cookie)
1767
+ */
1768
+ function resetConsent() {
1769
+ setRawCookie(`${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`);
1770
+ consent = null;
1771
+ }
1772
+
1773
+ /**
1774
+ * Check if consent has been given (any decision made)
1775
+ */
1776
+ function hasConsentDecision() {
1777
+ try {
1778
+ const cookies = getRawCookie();
1779
+ return cookies.includes(COOKIE_NAME);
1780
+ } catch (e) {
1781
+ return false;
1782
+ }
1783
+ }
1784
+
1785
+ /**
1786
+ * Get consent proof for compliance
1787
+ */
1788
+ function getConsentProof() {
1789
+ try {
1790
+ const cookies = getRawCookie();
1791
+ const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
1792
+
1793
+ if (match) {
1794
+ return JSON.parse(decodeURIComponent(match[1]));
1795
+ }
1796
+ } catch (e) {
1797
+ // Invalid cookie
1798
+ }
1799
+
1800
+ return null;
1801
+ }
1802
+
1803
+ /**
1804
+ * Events - Custom event dispatching for consent changes
1805
+ */
1806
+
1807
+ // Event names
1808
+ const EVENTS = {
1809
+ READY: 'zest:ready',
1810
+ CONSENT: 'zest:consent',
1811
+ REJECT: 'zest:reject',
1812
+ CHANGE: 'zest:change',
1813
+ SHOW: 'zest:show',
1814
+ HIDE: 'zest:hide'
1815
+ };
1816
+
1817
+ /**
1818
+ * Dispatch a custom event
1819
+ */
1820
+ function emit(eventName, detail = {}) {
1821
+ const event = new CustomEvent(eventName, {
1822
+ detail,
1823
+ bubbles: true,
1824
+ cancelable: true
1825
+ });
1826
+
1827
+ document.dispatchEvent(event);
1828
+ return event;
1829
+ }
1830
+
1831
+ /**
1832
+ * Emit ready event
1833
+ */
1834
+ function emitReady(consent) {
1835
+ return emit(EVENTS.READY, { consent });
1836
+ }
1837
+
1838
+ /**
1839
+ * Emit consent event (user accepted)
1840
+ */
1841
+ function emitConsent(consent, previous) {
1842
+ return emit(EVENTS.CONSENT, { consent, previous });
1843
+ }
1844
+
1845
+ /**
1846
+ * Emit reject event (user rejected all)
1847
+ */
1848
+ function emitReject(consent) {
1849
+ return emit(EVENTS.REJECT, { consent });
1850
+ }
1851
+
1852
+ /**
1853
+ * Emit change event (any consent change)
1854
+ */
1855
+ function emitChange(consent, previous) {
1856
+ return emit(EVENTS.CHANGE, { consent, previous });
1857
+ }
1858
+
1859
+ /**
1860
+ * Emit show event (banner/modal shown)
1861
+ */
1862
+ function emitShow(type = 'banner') {
1863
+ return emit(EVENTS.SHOW, { type });
1864
+ }
1865
+
1866
+ /**
1867
+ * Emit hide event (banner/modal hidden)
1868
+ */
1869
+ function emitHide(type = 'banner') {
1870
+ return emit(EVENTS.HIDE, { type });
1871
+ }
1872
+
1873
+ /**
1874
+ * Styles - Shadow DOM encapsulated CSS with theming
1875
+ */
1876
+
1877
+ /**
1878
+ * Generate CSS with custom properties
1879
+ */
1880
+ function generateStyles(config) {
1881
+ const accentColor = config.accentColor || '#4F46E5';
1882
+
1883
+ return `
1884
+ :host {
1885
+ --zest-accent: ${accentColor};
1886
+ --zest-accent-hover: ${adjustColor(accentColor, -15)};
1887
+ --zest-bg: #ffffff;
1888
+ --zest-bg-secondary: #f3f4f6;
1889
+ --zest-text: #1f2937;
1890
+ --zest-text-secondary: #6b7280;
1891
+ --zest-border: #e5e7eb;
1892
+ --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
1893
+ --zest-radius: 12px;
1894
+ --zest-radius-sm: 8px;
1895
+ --zest-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1896
+
1897
+ font-family: var(--zest-font);
1898
+ font-size: 14px;
1899
+ line-height: 1.5;
1900
+ color: var(--zest-text);
1901
+ box-sizing: border-box;
1902
+ }
1903
+
1904
+ :host([data-theme="dark"]) {
1905
+ --zest-bg: #1f2937;
1906
+ --zest-bg-secondary: #374151;
1907
+ --zest-text: #f9fafb;
1908
+ --zest-text-secondary: #9ca3af;
1909
+ --zest-border: #4b5563;
1910
+ --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
1911
+ }
1912
+
1913
+ @media (prefers-color-scheme: dark) {
1914
+ :host([data-theme="auto"]) {
1915
+ --zest-bg: #1f2937;
1916
+ --zest-bg-secondary: #374151;
1917
+ --zest-text: #f9fafb;
1918
+ --zest-text-secondary: #9ca3af;
1919
+ --zest-border: #4b5563;
1920
+ --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
1921
+ }
1922
+ }
1923
+
1924
+ *, *::before, *::after {
1925
+ box-sizing: border-box;
1926
+ }
1927
+
1928
+ /* Banner */
1929
+ .zest-banner {
1930
+ position: fixed;
1931
+ z-index: 999999;
1932
+ max-width: 480px;
1933
+ padding: 20px;
1934
+ background: var(--zest-bg);
1935
+ border-radius: var(--zest-radius);
1936
+ box-shadow: var(--zest-shadow);
1937
+ animation: zest-slide-in 0.3s ease-out;
1938
+ }
1939
+
1940
+ .zest-banner--bottom {
1941
+ bottom: 20px;
1942
+ left: 50%;
1943
+ transform: translateX(-50%);
1944
+ }
1945
+
1946
+ .zest-banner--bottom-left {
1947
+ bottom: 20px;
1948
+ left: 20px;
1949
+ }
1950
+
1951
+ .zest-banner--bottom-right {
1952
+ bottom: 20px;
1953
+ right: 20px;
1954
+ }
1955
+
1956
+ .zest-banner--top {
1957
+ top: 20px;
1958
+ left: 50%;
1959
+ transform: translateX(-50%);
1960
+ }
1961
+
1962
+ @keyframes zest-slide-in {
1963
+ from {
1964
+ opacity: 0;
1965
+ transform: translateX(-50%) translateY(20px);
1966
+ }
1967
+ to {
1968
+ opacity: 1;
1969
+ transform: translateX(-50%) translateY(0);
1970
+ }
1971
+ }
1972
+
1973
+ .zest-banner--bottom-left {
1974
+ animation-name: zest-slide-in-left;
1975
+ }
1976
+
1977
+ @keyframes zest-slide-in-left {
1978
+ from {
1979
+ opacity: 0;
1980
+ transform: translateY(20px);
1981
+ }
1982
+ to {
1983
+ opacity: 1;
1984
+ transform: translateY(0);
1985
+ }
1986
+ }
1987
+
1988
+ .zest-banner--bottom-right {
1989
+ animation-name: zest-slide-in-right;
1990
+ }
1991
+
1992
+ @keyframes zest-slide-in-right {
1993
+ from {
1994
+ opacity: 0;
1995
+ transform: translateY(20px);
1996
+ }
1997
+ to {
1998
+ opacity: 1;
1999
+ transform: translateY(0);
2000
+ }
2001
+ }
2002
+
2003
+ @media (prefers-reduced-motion: reduce) {
2004
+ .zest-banner,
2005
+ .zest-modal {
2006
+ animation: none;
2007
+ }
2008
+ }
2009
+
2010
+ .zest-banner__title {
2011
+ margin: 0 0 8px 0;
2012
+ font-size: 16px;
2013
+ font-weight: 600;
2014
+ color: var(--zest-text);
2015
+ }
2016
+
2017
+ .zest-banner__description {
2018
+ margin: 0 0 16px 0;
2019
+ font-size: 14px;
2020
+ color: var(--zest-text-secondary);
2021
+ }
2022
+
2023
+ .zest-banner__buttons {
2024
+ display: flex;
2025
+ flex-wrap: wrap;
2026
+ gap: 8px;
2027
+ }
2028
+
2029
+ /* Buttons */
2030
+ .zest-btn {
2031
+ display: inline-flex;
2032
+ align-items: center;
2033
+ justify-content: center;
2034
+ padding: 10px 16px;
2035
+ font-size: 14px;
2036
+ font-weight: 500;
2037
+ font-family: inherit;
2038
+ border: none;
2039
+ border-radius: var(--zest-radius-sm);
2040
+ cursor: pointer;
2041
+ transition: background-color 0.15s ease, transform 0.1s ease;
2042
+ }
2043
+
2044
+ .zest-btn:hover {
2045
+ transform: translateY(-1px);
2046
+ }
2047
+
2048
+ .zest-btn:active {
2049
+ transform: translateY(0);
2050
+ }
2051
+
2052
+ .zest-btn:focus-visible {
2053
+ outline: 2px solid var(--zest-accent);
2054
+ outline-offset: 2px;
2055
+ }
2056
+
2057
+ .zest-btn--primary {
2058
+ background: var(--zest-accent);
2059
+ color: #ffffff;
2060
+ }
2061
+
2062
+ .zest-btn--primary:hover {
2063
+ background: var(--zest-accent-hover);
2064
+ }
2065
+
2066
+ .zest-btn--secondary {
2067
+ background: var(--zest-bg-secondary);
2068
+ color: var(--zest-text);
2069
+ }
2070
+
2071
+ .zest-btn--secondary:hover {
2072
+ background: var(--zest-border);
2073
+ }
2074
+
2075
+ .zest-btn--ghost {
2076
+ background: transparent;
2077
+ color: var(--zest-text-secondary);
2078
+ }
2079
+
2080
+ .zest-btn--ghost:hover {
2081
+ background: var(--zest-bg-secondary);
2082
+ color: var(--zest-text);
2083
+ }
2084
+
2085
+ /* Modal */
2086
+ .zest-modal-overlay {
2087
+ position: fixed;
2088
+ inset: 0;
2089
+ z-index: 999998;
2090
+ display: flex;
2091
+ align-items: center;
2092
+ justify-content: center;
2093
+ padding: 20px;
2094
+ background: rgba(0, 0, 0, 0.5);
2095
+ animation: zest-fade-in 0.2s ease-out;
2096
+ }
2097
+
2098
+ @keyframes zest-fade-in {
2099
+ from { opacity: 0; }
2100
+ to { opacity: 1; }
2101
+ }
2102
+
2103
+ .zest-modal {
2104
+ width: 100%;
2105
+ max-width: 500px;
2106
+ max-height: 90vh;
2107
+ overflow-y: auto;
2108
+ background: var(--zest-bg);
2109
+ border-radius: var(--zest-radius);
2110
+ box-shadow: var(--zest-shadow);
2111
+ animation: zest-modal-in 0.3s ease-out;
2112
+ }
2113
+
2114
+ @keyframes zest-modal-in {
2115
+ from {
2116
+ opacity: 0;
2117
+ transform: scale(0.95);
2118
+ }
2119
+ to {
2120
+ opacity: 1;
2121
+ transform: scale(1);
2122
+ }
2123
+ }
2124
+
2125
+ .zest-modal__header {
2126
+ padding: 20px 20px 0;
2127
+ }
2128
+
2129
+ .zest-modal__title {
2130
+ margin: 0 0 8px 0;
2131
+ font-size: 18px;
2132
+ font-weight: 600;
2133
+ color: var(--zest-text);
2134
+ }
2135
+
2136
+ .zest-modal__description {
2137
+ margin: 0;
2138
+ font-size: 14px;
2139
+ color: var(--zest-text-secondary);
2140
+ }
2141
+
2142
+ .zest-modal__body {
2143
+ padding: 20px;
2144
+ }
2145
+
2146
+ .zest-modal__footer {
2147
+ display: flex;
2148
+ flex-wrap: wrap;
2149
+ gap: 8px;
2150
+ padding: 0 20px 20px;
2151
+ }
2152
+
2153
+ /* Categories */
2154
+ .zest-category {
2155
+ padding: 16px;
2156
+ margin-bottom: 12px;
2157
+ background: var(--zest-bg-secondary);
2158
+ border-radius: var(--zest-radius-sm);
2159
+ }
2160
+
2161
+ .zest-category:last-child {
2162
+ margin-bottom: 0;
2163
+ }
2164
+
2165
+ .zest-category__header {
2166
+ display: flex;
2167
+ align-items: center;
2168
+ justify-content: space-between;
2169
+ gap: 12px;
2170
+ }
2171
+
2172
+ .zest-category__info {
2173
+ flex: 1;
2174
+ }
2175
+
2176
+ .zest-category__label {
2177
+ display: block;
2178
+ font-size: 14px;
2179
+ font-weight: 600;
2180
+ color: var(--zest-text);
2181
+ }
2182
+
2183
+ .zest-category__description {
2184
+ margin: 4px 0 0;
2185
+ font-size: 13px;
2186
+ color: var(--zest-text-secondary);
2187
+ }
2188
+
2189
+ /* Toggle Switch */
2190
+ .zest-toggle {
2191
+ position: relative;
2192
+ width: 44px;
2193
+ height: 24px;
2194
+ flex-shrink: 0;
2195
+ }
2196
+
2197
+ .zest-toggle__input {
2198
+ position: absolute;
2199
+ opacity: 0;
2200
+ width: 100%;
2201
+ height: 100%;
2202
+ cursor: pointer;
2203
+ margin: 0;
2204
+ }
2205
+
2206
+ .zest-toggle__input:disabled {
2207
+ cursor: not-allowed;
2208
+ }
2209
+
2210
+ .zest-toggle__slider {
2211
+ position: absolute;
2212
+ inset: 0;
2213
+ background: var(--zest-border);
2214
+ border-radius: 12px;
2215
+ transition: background-color 0.2s ease;
2216
+ pointer-events: none;
2217
+ }
2218
+
2219
+ .zest-toggle__slider::before {
2220
+ content: '';
2221
+ position: absolute;
2222
+ top: 2px;
2223
+ left: 2px;
2224
+ width: 20px;
2225
+ height: 20px;
2226
+ background: #ffffff;
2227
+ border-radius: 50%;
2228
+ transition: transform 0.2s ease;
2229
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
2230
+ }
2231
+
2232
+ .zest-toggle__input:checked + .zest-toggle__slider {
2233
+ background: var(--zest-accent);
2234
+ }
2235
+
2236
+ .zest-toggle__input:checked + .zest-toggle__slider::before {
2237
+ transform: translateX(20px);
2238
+ }
2239
+
2240
+ .zest-toggle__input:focus-visible + .zest-toggle__slider {
2241
+ outline: 2px solid var(--zest-accent);
2242
+ outline-offset: 2px;
2243
+ }
2244
+
2245
+ .zest-toggle__input:disabled + .zest-toggle__slider {
2246
+ opacity: 0.6;
2247
+ }
2248
+
2249
+ /* Widget */
2250
+ .zest-widget {
2251
+ position: fixed;
2252
+ z-index: 999997;
2253
+ bottom: 20px;
2254
+ left: 20px;
2255
+ }
2256
+
2257
+ .zest-widget__btn {
2258
+ display: flex;
2259
+ align-items: center;
2260
+ justify-content: center;
2261
+ width: 48px;
2262
+ height: 48px;
2263
+ padding: 0;
2264
+ background: var(--zest-bg);
2265
+ border: 1px solid var(--zest-border);
2266
+ border-radius: 50%;
2267
+ box-shadow: var(--zest-shadow);
2268
+ cursor: pointer;
2269
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
2270
+ }
2271
+
2272
+ .zest-widget__btn:hover {
2273
+ transform: scale(1.05);
2274
+ box-shadow: 0 12px 28px -5px rgba(0, 0, 0, 0.15);
2275
+ }
2276
+
2277
+ .zest-widget__btn:focus-visible {
2278
+ outline: 2px solid var(--zest-accent);
2279
+ outline-offset: 2px;
2280
+ }
2281
+
2282
+ .zest-widget__icon {
2283
+ width: 24px;
2284
+ height: 24px;
2285
+ fill: var(--zest-text);
2286
+ }
2287
+
2288
+ /* Link */
2289
+ .zest-link {
2290
+ color: var(--zest-accent);
2291
+ text-decoration: none;
2292
+ }
2293
+
2294
+ .zest-link:hover {
2295
+ text-decoration: underline;
2296
+ }
2297
+
2298
+ /* Mobile */
2299
+ @media (max-width: 480px) {
2300
+ .zest-banner {
2301
+ left: 10px;
2302
+ right: 10px;
2303
+ max-width: none;
2304
+ transform: none;
2305
+ }
2306
+
2307
+ .zest-banner--bottom,
2308
+ .zest-banner--bottom-left,
2309
+ .zest-banner--bottom-right {
2310
+ bottom: 10px;
2311
+ }
2312
+
2313
+ .zest-banner--top {
2314
+ top: 10px;
2315
+ transform: none;
2316
+ }
2317
+
2318
+ @keyframes zest-slide-in {
2319
+ from {
2320
+ opacity: 0;
2321
+ transform: translateY(20px);
2322
+ }
2323
+ to {
2324
+ opacity: 1;
2325
+ transform: translateY(0);
2326
+ }
2327
+ }
2328
+
2329
+ .zest-banner__buttons {
2330
+ flex-direction: column;
2331
+ }
2332
+
2333
+ .zest-btn {
2334
+ width: 100%;
2335
+ }
2336
+
2337
+ .zest-modal-overlay {
2338
+ padding: 10px;
2339
+ }
2340
+
2341
+ .zest-widget {
2342
+ bottom: 10px;
2343
+ left: 10px;
2344
+ }
2345
+ }
2346
+
2347
+ /* Hidden utility */
2348
+ .zest-hidden {
2349
+ display: none !important;
2350
+ }
2351
+ ${config.customStyles || ''}
2352
+ `;
2353
+ }
2354
+
2355
+ /**
2356
+ * Adjust color brightness
2357
+ */
2358
+ function adjustColor(hex, percent) {
2359
+ const num = parseInt(hex.replace('#', ''), 16);
2360
+ const amt = Math.round(2.55 * percent);
2361
+ const R = Math.min(255, Math.max(0, (num >> 16) + amt));
2362
+ const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt));
2363
+ const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt));
2364
+ return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
2365
+ }
2366
+
2367
+ /**
2368
+ * Cookie icon SVG
2369
+ */
2370
+ 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>`;
2371
+
2372
+ /**
2373
+ * Banner - Main consent banner component
2374
+ */
2375
+
2376
+
2377
+ let bannerElement = null;
2378
+ let shadowRoot$2 = null;
2379
+
2380
+ /**
2381
+ * Create the banner HTML
2382
+ */
2383
+ function createBannerHTML(config) {
2384
+ const labels = config.labels.banner;
2385
+ const position = config.position || 'bottom';
2386
+
2387
+ return `
2388
+ <div class="zest-banner zest-banner--${position}" role="dialog" aria-modal="false" aria-label="${labels.title}">
2389
+ <h2 class="zest-banner__title">${labels.title}</h2>
2390
+ <p class="zest-banner__description">${labels.description}</p>
2391
+ <div class="zest-banner__buttons">
2392
+ <button type="button" class="zest-btn zest-btn--primary" data-action="accept-all">
2393
+ ${labels.acceptAll}
2394
+ </button>
2395
+ <button type="button" class="zest-btn zest-btn--secondary" data-action="reject-all">
2396
+ ${labels.rejectAll}
2397
+ </button>
2398
+ <button type="button" class="zest-btn zest-btn--ghost" data-action="settings">
2399
+ ${labels.settings}
2400
+ </button>
2401
+ </div>
2402
+ </div>
2403
+ `;
2404
+ }
2405
+
2406
+ /**
2407
+ * Create and mount the banner
2408
+ */
2409
+ function createBanner(callbacks = {}) {
2410
+ if (bannerElement) {
2411
+ return bannerElement;
2412
+ }
2413
+
2414
+ const config = getCurrentConfig();
2415
+
2416
+ // Create host element
2417
+ bannerElement = document.createElement('zest-banner');
2418
+ bannerElement.setAttribute('data-theme', config.theme || 'light');
2419
+
2420
+ // Create shadow root
2421
+ shadowRoot$2 = bannerElement.attachShadow({ mode: 'open' });
2422
+
2423
+ // Add styles
2424
+ const styleEl = document.createElement('style');
2425
+ styleEl.textContent = generateStyles(config);
2426
+ shadowRoot$2.appendChild(styleEl);
2427
+
2428
+ // Add banner HTML
2429
+ const container = document.createElement('div');
2430
+ container.innerHTML = createBannerHTML(config);
2431
+ shadowRoot$2.appendChild(container.firstElementChild);
2432
+
2433
+ // Add event listeners
2434
+ const banner = shadowRoot$2.querySelector('.zest-banner');
2435
+
2436
+ banner.addEventListener('click', (e) => {
2437
+ const action = e.target.dataset.action;
2438
+ if (!action) return;
2439
+
2440
+ switch (action) {
2441
+ case 'accept-all':
2442
+ callbacks.onAcceptAll?.();
2443
+ break;
2444
+ case 'reject-all':
2445
+ callbacks.onRejectAll?.();
2446
+ break;
2447
+ case 'settings':
2448
+ callbacks.onSettings?.();
2449
+ break;
2450
+ }
2451
+ });
2452
+
2453
+ // Keyboard handling
2454
+ banner.addEventListener('keydown', (e) => {
2455
+ if (e.key === 'Escape') {
2456
+ callbacks.onSettings?.();
2457
+ }
2458
+ });
2459
+
2460
+ // Mount to document
2461
+ document.body.appendChild(bannerElement);
2462
+
2463
+ // Focus first button for accessibility
2464
+ requestAnimationFrame(() => {
2465
+ const firstButton = shadowRoot$2.querySelector('button');
2466
+ firstButton?.focus();
2467
+ });
2468
+
2469
+ return bannerElement;
2470
+ }
2471
+
2472
+ /**
2473
+ * Show the banner
2474
+ */
2475
+ function showBanner(callbacks = {}) {
2476
+ if (!bannerElement) {
2477
+ createBanner(callbacks);
2478
+ } else {
2479
+ bannerElement.classList.remove('zest-hidden');
2480
+ }
2481
+ }
2482
+
2483
+ /**
2484
+ * Hide the banner
2485
+ */
2486
+ function hideBanner() {
2487
+ if (bannerElement) {
2488
+ bannerElement.remove();
2489
+ bannerElement = null;
2490
+ shadowRoot$2 = null;
2491
+ }
2492
+ }
2493
+
2494
+ /**
2495
+ * Modal - Settings modal component for category toggles
2496
+ */
2497
+
2498
+
2499
+ let modalElement = null;
2500
+ let shadowRoot$1 = null;
2501
+ let currentSelections = {};
2502
+
2503
+ /**
2504
+ * Create category toggle HTML
2505
+ */
2506
+ function createCategoryHTML(category, isChecked, isRequired) {
2507
+ const disabled = isRequired ? 'disabled' : '';
2508
+ const checked = isChecked ? 'checked' : '';
2509
+
2510
+ return `
2511
+ <div class="zest-category">
2512
+ <div class="zest-category__header">
2513
+ <div class="zest-category__info">
2514
+ <span class="zest-category__label">${category.label}</span>
2515
+ <p class="zest-category__description">${category.description}</p>
2516
+ </div>
2517
+ <label class="zest-toggle">
2518
+ <input
2519
+ type="checkbox"
2520
+ class="zest-toggle__input"
2521
+ data-category="${category.id}"
2522
+ ${checked}
2523
+ ${disabled}
2524
+ aria-label="${category.label}"
2525
+ >
2526
+ <span class="zest-toggle__slider"></span>
2527
+ </label>
2528
+ </div>
2529
+ </div>
2530
+ `;
2531
+ }
2532
+
2533
+ /**
2534
+ * Create the modal HTML
2535
+ */
2536
+ function createModalHTML(config, consent) {
2537
+ const labels = config.labels.modal;
2538
+ const categories = config.categories || DEFAULT_CATEGORIES;
2539
+
2540
+ const categoriesHTML = Object.values(categories)
2541
+ .map(cat => createCategoryHTML(
2542
+ cat,
2543
+ consent[cat.id] ?? cat.default,
2544
+ cat.required
2545
+ ))
2546
+ .join('');
2547
+
2548
+ const policyLink = config.policyUrl
2549
+ ? `<a href="${config.policyUrl}" class="zest-link" target="_blank" rel="noopener">Privacy Policy</a>`
2550
+ : '';
2551
+
2552
+ return `
2553
+ <div class="zest-modal-overlay" role="dialog" aria-modal="true" aria-label="${labels.title}">
2554
+ <div class="zest-modal">
2555
+ <div class="zest-modal__header">
2556
+ <h2 class="zest-modal__title">${labels.title}</h2>
2557
+ <p class="zest-modal__description">${labels.description} ${policyLink}</p>
2558
+ </div>
2559
+ <div class="zest-modal__body">
2560
+ ${categoriesHTML}
2561
+ </div>
2562
+ <div class="zest-modal__footer">
2563
+ <button type="button" class="zest-btn zest-btn--primary" data-action="save">
2564
+ ${labels.save}
2565
+ </button>
2566
+ <button type="button" class="zest-btn zest-btn--secondary" data-action="accept-all">
2567
+ ${labels.acceptAll}
2568
+ </button>
2569
+ <button type="button" class="zest-btn zest-btn--ghost" data-action="reject-all">
2570
+ ${labels.rejectAll}
2571
+ </button>
2572
+ </div>
2573
+ </div>
2574
+ </div>
2575
+ `;
2576
+ }
2577
+
2578
+ /**
2579
+ * Get current selections from toggles
2580
+ */
2581
+ function getSelections() {
2582
+ if (!shadowRoot$1) return currentSelections;
2583
+
2584
+ const toggles = shadowRoot$1.querySelectorAll('.zest-toggle__input');
2585
+ const selections = { essential: true };
2586
+
2587
+ toggles.forEach(toggle => {
2588
+ const category = toggle.dataset.category;
2589
+ if (category && category !== 'essential') {
2590
+ selections[category] = toggle.checked;
2591
+ }
2592
+ });
2593
+
2594
+ return selections;
2595
+ }
2596
+
2597
+ /**
2598
+ * Create and show the modal
2599
+ */
2600
+ function showModal(consent = {}, callbacks = {}) {
2601
+ if (modalElement) {
2602
+ return modalElement;
2603
+ }
2604
+
2605
+ const config = getCurrentConfig();
2606
+ currentSelections = { ...consent };
2607
+
2608
+ // Create host element
2609
+ modalElement = document.createElement('zest-modal');
2610
+ modalElement.setAttribute('data-theme', config.theme || 'light');
2611
+
2612
+ // Create shadow root
2613
+ shadowRoot$1 = modalElement.attachShadow({ mode: 'open' });
2614
+
2615
+ // Add styles
2616
+ const styleEl = document.createElement('style');
2617
+ styleEl.textContent = generateStyles(config);
2618
+ shadowRoot$1.appendChild(styleEl);
2619
+
2620
+ // Add modal HTML
2621
+ const container = document.createElement('div');
2622
+ container.innerHTML = createModalHTML(config, consent);
2623
+ shadowRoot$1.appendChild(container.firstElementChild);
2624
+
2625
+ // Add event listeners
2626
+ const modal = shadowRoot$1.querySelector('.zest-modal-overlay');
2627
+
2628
+ // Button clicks
2629
+ modal.addEventListener('click', (e) => {
2630
+ const action = e.target.dataset.action;
2631
+ if (!action) {
2632
+ // Click on overlay background to close
2633
+ if (e.target === modal) {
2634
+ callbacks.onClose?.();
2635
+ }
2636
+ return;
2637
+ }
2638
+
2639
+ switch (action) {
2640
+ case 'save':
2641
+ callbacks.onSave?.(getSelections());
2642
+ break;
2643
+ case 'accept-all':
2644
+ callbacks.onAcceptAll?.();
2645
+ break;
2646
+ case 'reject-all':
2647
+ callbacks.onRejectAll?.();
2648
+ break;
2649
+ }
2650
+ });
2651
+
2652
+ // Keyboard handling
2653
+ modal.addEventListener('keydown', (e) => {
2654
+ if (e.key === 'Escape') {
2655
+ callbacks.onClose?.();
2656
+ }
2657
+ });
2658
+
2659
+ // Track toggle changes
2660
+ shadowRoot$1.querySelectorAll('.zest-toggle__input').forEach(toggle => {
2661
+ toggle.addEventListener('change', () => {
2662
+ currentSelections = getSelections();
2663
+ });
2664
+ });
2665
+
2666
+ // Mount to document
2667
+ document.body.appendChild(modalElement);
2668
+
2669
+ // Trap focus
2670
+ requestAnimationFrame(() => {
2671
+ const firstButton = shadowRoot$1.querySelector('button');
2672
+ firstButton?.focus();
2673
+ });
2674
+
2675
+ return modalElement;
2676
+ }
2677
+
2678
+ /**
2679
+ * Hide the modal
2680
+ */
2681
+ function hideModal() {
2682
+ if (modalElement) {
2683
+ modalElement.remove();
2684
+ modalElement = null;
2685
+ shadowRoot$1 = null;
2686
+ }
2687
+ }
2688
+
2689
+ /**
2690
+ * Widget - Minimal floating button to reopen settings
2691
+ */
2692
+
2693
+
2694
+ let widgetElement = null;
2695
+ let shadowRoot = null;
2696
+
2697
+ /**
2698
+ * Create the widget HTML
2699
+ */
2700
+ function createWidgetHTML(config) {
2701
+ const labels = config.labels.widget;
2702
+
2703
+ return `
2704
+ <div class="zest-widget">
2705
+ <button type="button" class="zest-widget__btn" aria-label="${labels.label}" title="${labels.label}">
2706
+ <span class="zest-widget__icon">${COOKIE_ICON}</span>
2707
+ </button>
2708
+ </div>
2709
+ `;
2710
+ }
2711
+
2712
+ /**
2713
+ * Create and mount the widget
2714
+ */
2715
+ function createWidget(callbacks = {}) {
2716
+ if (widgetElement) {
2717
+ return widgetElement;
2718
+ }
2719
+
2720
+ const config = getCurrentConfig();
2721
+
2722
+ // Create host element
2723
+ widgetElement = document.createElement('zest-widget');
2724
+ widgetElement.setAttribute('data-theme', config.theme || 'light');
2725
+
2726
+ // Create shadow root
2727
+ shadowRoot = widgetElement.attachShadow({ mode: 'open' });
2728
+
2729
+ // Add styles
2730
+ const styleEl = document.createElement('style');
2731
+ styleEl.textContent = generateStyles(config);
2732
+ shadowRoot.appendChild(styleEl);
2733
+
2734
+ // Add widget HTML
2735
+ const container = document.createElement('div');
2736
+ container.innerHTML = createWidgetHTML(config);
2737
+ shadowRoot.appendChild(container.firstElementChild);
2738
+
2739
+ // Add event listener
2740
+ const button = shadowRoot.querySelector('.zest-widget__btn');
2741
+ button.addEventListener('click', () => {
2742
+ callbacks.onClick?.();
2743
+ });
2744
+
2745
+ // Mount to document
2746
+ document.body.appendChild(widgetElement);
2747
+
2748
+ return widgetElement;
2749
+ }
2750
+
2751
+ /**
2752
+ * Show the widget
2753
+ */
2754
+ function showWidget(callbacks = {}) {
2755
+ if (!widgetElement) {
2756
+ createWidget(callbacks);
2757
+ } else {
2758
+ widgetElement.style.display = '';
2759
+ }
2760
+ }
2761
+
2762
+ /**
2763
+ * Hide the widget
2764
+ */
2765
+ function hideWidget() {
2766
+ if (widgetElement) {
2767
+ widgetElement.style.display = 'none';
2768
+ }
2769
+ }
2770
+
2771
+ /**
2772
+ * Remove the widget completely
2773
+ */
2774
+ function removeWidget() {
2775
+ if (widgetElement) {
2776
+ widgetElement.remove();
2777
+ widgetElement = null;
2778
+ shadowRoot = null;
2779
+ }
2780
+ }
2781
+
2782
+ /**
2783
+ * Zest - Lightweight Cookie Consent Toolkit
2784
+ * Main entry point
2785
+ */
2786
+
2787
+
2788
+ // State
2789
+ let initialized = false;
2790
+ let config = null;
2791
+
2792
+ /**
2793
+ * Consent checker function shared across interceptors
2794
+ */
2795
+ function checkConsent(category) {
2796
+ return hasConsent(category);
2797
+ }
2798
+
2799
+ /**
2800
+ * Replay all queued items for newly allowed categories
2801
+ */
2802
+ function replayAll(allowedCategories) {
2803
+ replayCookies(allowedCategories);
2804
+ replayStorage(allowedCategories);
2805
+ replayScripts(allowedCategories);
2806
+ }
2807
+
2808
+ /**
2809
+ * Handle accept all
2810
+ */
2811
+ function handleAcceptAll() {
2812
+ const result = acceptAll(config.expiration);
2813
+ const categories = getCategoryIds();
2814
+
2815
+ hideBanner();
2816
+ hideModal();
2817
+
2818
+ replayAll(categories);
2819
+
2820
+ if (config.showWidget) {
2821
+ showWidget({ onClick: handleShowSettings });
2822
+ }
2823
+
2824
+ emitConsent(result.current, result.previous);
2825
+ emitChange(result.current, result.previous);
2826
+ config.callbacks?.onAccept?.(result.current);
2827
+ config.callbacks?.onChange?.(result.current);
2828
+ }
2829
+
2830
+ /**
2831
+ * Handle reject all
2832
+ */
2833
+ function handleRejectAll() {
2834
+ const result = rejectAll(config.expiration);
2835
+
2836
+ hideBanner();
2837
+ hideModal();
2838
+
2839
+ if (config.showWidget) {
2840
+ showWidget({ onClick: handleShowSettings });
2841
+ }
2842
+
2843
+ emitReject(result.current);
2844
+ emitChange(result.current, result.previous);
2845
+ config.callbacks?.onReject?.();
2846
+ config.callbacks?.onChange?.(result.current);
2847
+ }
2848
+
2849
+ /**
2850
+ * Handle save preferences from modal
2851
+ */
2852
+ function handleSavePreferences(selections) {
2853
+ const result = updateConsent(selections, config.expiration);
2854
+
2855
+ // Find newly allowed categories
2856
+ const newlyAllowed = Object.keys(result.current).filter(
2857
+ cat => result.current[cat] && !result.previous[cat]
2858
+ );
2859
+
2860
+ if (newlyAllowed.length > 0) {
2861
+ replayAll(newlyAllowed);
2862
+ }
2863
+
2864
+ hideModal();
2865
+
2866
+ if (config.showWidget) {
2867
+ showWidget({ onClick: handleShowSettings });
2868
+ }
2869
+
2870
+ // Determine if this was acceptance or rejection based on selections
2871
+ const hasNonEssential = Object.entries(selections)
2872
+ .some(([cat, val]) => cat !== 'essential' && val);
2873
+
2874
+ if (hasNonEssential) {
2875
+ emitConsent(result.current, result.previous);
2876
+ } else {
2877
+ emitReject(result.current);
2878
+ }
2879
+
2880
+ emitChange(result.current, result.previous);
2881
+ config.callbacks?.onChange?.(result.current);
2882
+ }
2883
+
2884
+ /**
2885
+ * Handle show settings
2886
+ */
2887
+ function handleShowSettings() {
2888
+ hideBanner();
2889
+ hideWidget();
2890
+
2891
+ showModal(getConsent(), {
2892
+ onSave: handleSavePreferences,
2893
+ onAcceptAll: handleAcceptAll,
2894
+ onRejectAll: handleRejectAll,
2895
+ onClose: handleCloseModal
2896
+ });
2897
+
2898
+ emitShow('modal');
2899
+ }
2900
+
2901
+ /**
2902
+ * Handle close modal
2903
+ */
2904
+ function handleCloseModal() {
2905
+ hideModal();
2906
+ emitHide('modal');
2907
+
2908
+ // Show widget if consent was already given
2909
+ if (hasConsentDecision() && config.showWidget) {
2910
+ showWidget({ onClick: handleShowSettings });
2911
+ } else {
2912
+ // Show banner again if no decision made
2913
+ showBanner({
2914
+ onAcceptAll: handleAcceptAll,
2915
+ onRejectAll: handleRejectAll,
2916
+ onSettings: handleShowSettings
2917
+ });
2918
+ }
2919
+ }
2920
+
2921
+ /**
2922
+ * Initialize Zest
2923
+ */
2924
+ function init(userConfig = {}) {
2925
+ if (initialized) {
2926
+ console.warn('[Zest] Already initialized');
2927
+ return Zest;
2928
+ }
2929
+
2930
+ // Merge config
2931
+ config = setConfig(userConfig);
2932
+
2933
+ // Set patterns if provided
2934
+ if (config.patterns) {
2935
+ setPatterns(config.patterns);
2936
+ }
2937
+
2938
+ // Set up consent checkers
2939
+ setConsentChecker$2(checkConsent);
2940
+ setConsentChecker$1(checkConsent);
2941
+ setConsentChecker(checkConsent);
2942
+
2943
+ // Start interception
2944
+ interceptCookies();
2945
+ interceptStorage();
2946
+ startScriptBlocking(config.mode, config.blockedDomains);
2947
+
2948
+ // Load saved consent
2949
+ const consent = loadConsent();
2950
+
2951
+ initialized = true;
2952
+
2953
+ // Check Do Not Track / Global Privacy Control
2954
+ const dntEnabled = isDoNotTrackEnabled();
2955
+ let dntApplied = false;
2956
+
2957
+ if (dntEnabled && config.respectDNT && config.dntBehavior !== 'ignore') {
2958
+ if (config.dntBehavior === 'reject' && !hasConsentDecision()) {
2959
+ // Auto-reject non-essential cookies silently
2960
+ const result = rejectAll(config.expiration);
2961
+ dntApplied = true;
2962
+
2963
+ // Emit events
2964
+ emitReject(result.current);
2965
+ emitChange(result.current, result.previous);
2966
+ config.callbacks?.onReject?.();
2967
+ config.callbacks?.onChange?.(result.current);
2968
+ }
2969
+ // 'preselect' behavior is handled by default (banner shows with defaults off)
2970
+ }
2971
+
2972
+ // Emit ready event
2973
+ emitReady(consent);
2974
+ config.callbacks?.onReady?.(consent);
2975
+
2976
+ // Show UI based on consent state
2977
+ if (!hasConsentDecision() && !dntApplied) {
2978
+ // No consent decision yet - show banner
2979
+ showBanner({
2980
+ onAcceptAll: handleAcceptAll,
2981
+ onRejectAll: handleRejectAll,
2982
+ onSettings: handleShowSettings
2983
+ });
2984
+ emitShow('banner');
2985
+ } else {
2986
+ // Consent already given (or DNT auto-rejected) - show widget for reopening settings
2987
+ if (config.showWidget) {
2988
+ showWidget({ onClick: handleShowSettings });
2989
+ }
2990
+ }
2991
+
2992
+ return Zest;
2993
+ }
2994
+
2995
+ /**
2996
+ * Public API
2997
+ */
2998
+ const Zest = {
2999
+ // Initialization
3000
+ init,
3001
+
3002
+ // Banner control
3003
+ show() {
3004
+ if (!initialized) {
3005
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
3006
+ return;
3007
+ }
3008
+ hideModal();
3009
+ hideWidget();
3010
+ showBanner({
3011
+ onAcceptAll: handleAcceptAll,
3012
+ onRejectAll: handleRejectAll,
3013
+ onSettings: handleShowSettings
3014
+ });
3015
+ emitShow('banner');
3016
+ },
3017
+
3018
+ hide() {
3019
+ hideBanner();
3020
+ emitHide('banner');
3021
+ },
3022
+
3023
+ // Settings modal
3024
+ showSettings() {
3025
+ if (!initialized) {
3026
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
3027
+ return;
3028
+ }
3029
+ handleShowSettings();
3030
+ },
3031
+
3032
+ hideSettings() {
3033
+ hideModal();
3034
+ emitHide('modal');
3035
+ },
3036
+
3037
+ // Consent management
3038
+ getConsent,
3039
+ hasConsent,
3040
+ hasConsentDecision,
3041
+ getConsentProof,
3042
+
3043
+ // DNT detection
3044
+ isDoNotTrackEnabled,
3045
+ getDNTDetails,
3046
+
3047
+ // Accept/Reject programmatically
3048
+ acceptAll() {
3049
+ if (!initialized) {
3050
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
3051
+ return;
3052
+ }
3053
+ handleAcceptAll();
3054
+ },
3055
+
3056
+ rejectAll() {
3057
+ if (!initialized) {
3058
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
3059
+ return;
3060
+ }
3061
+ handleRejectAll();
3062
+ },
3063
+
3064
+ // Reset and show banner again
3065
+ reset() {
3066
+ resetConsent();
3067
+ hideModal();
3068
+ removeWidget();
3069
+
3070
+ if (initialized) {
3071
+ showBanner({
3072
+ onAcceptAll: handleAcceptAll,
3073
+ onRejectAll: handleRejectAll,
3074
+ onSettings: handleShowSettings
3075
+ });
3076
+ emitShow('banner');
3077
+ }
3078
+ },
3079
+
3080
+ // Config
3081
+ getConfig: getCurrentConfig,
3082
+
3083
+ // Events
3084
+ EVENTS
3085
+ };
3086
+
3087
+ // Auto-init if config present
3088
+ if (typeof window !== 'undefined') {
3089
+ // Make Zest available globally
3090
+ window.Zest = Zest;
3091
+
3092
+ const autoInit = () => {
3093
+ const cfg = getConfig();
3094
+ if (cfg.autoInit !== false) {
3095
+ init(window.ZestConfig);
3096
+ }
3097
+ };
3098
+
3099
+ if (document.readyState === 'loading') {
3100
+ document.addEventListener('DOMContentLoaded', autoInit);
3101
+ } else {
3102
+ autoInit();
3103
+ }
3104
+ }
3105
+
3106
+ return Zest;
3107
+
3108
+ })();
3109
+ //# sourceMappingURL=zest.js.map