@freshjuice/zest 0.1.0 → 1.0.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 (76) hide show
  1. package/README.md +46 -0
  2. package/dist/zest.de.js +104 -1
  3. package/dist/zest.de.js.map +1 -1
  4. package/dist/zest.de.min.js +1 -1
  5. package/dist/zest.en.js +104 -1
  6. package/dist/zest.en.js.map +1 -1
  7. package/dist/zest.en.min.js +1 -1
  8. package/dist/zest.es.js +104 -1
  9. package/dist/zest.es.js.map +1 -1
  10. package/dist/zest.es.min.js +1 -1
  11. package/dist/zest.esm.js +104 -1
  12. package/dist/zest.esm.js.map +1 -1
  13. package/dist/zest.esm.min.js +1 -1
  14. package/dist/zest.fr.js +104 -1
  15. package/dist/zest.fr.js.map +1 -1
  16. package/dist/zest.fr.min.js +1 -1
  17. package/dist/zest.it.js +104 -1
  18. package/dist/zest.it.js.map +1 -1
  19. package/dist/zest.it.min.js +1 -1
  20. package/dist/zest.ja.js +104 -1
  21. package/dist/zest.ja.js.map +1 -1
  22. package/dist/zest.ja.min.js +1 -1
  23. package/dist/zest.js +104 -1
  24. package/dist/zest.js.map +1 -1
  25. package/dist/zest.min.js +1 -1
  26. package/dist/zest.nl.js +104 -1
  27. package/dist/zest.nl.js.map +1 -1
  28. package/dist/zest.nl.min.js +1 -1
  29. package/dist/zest.pl.js +104 -1
  30. package/dist/zest.pl.js.map +1 -1
  31. package/dist/zest.pl.min.js +1 -1
  32. package/dist/zest.pt.js +104 -1
  33. package/dist/zest.pt.js.map +1 -1
  34. package/dist/zest.pt.min.js +1 -1
  35. package/dist/zest.ru.js +104 -1
  36. package/dist/zest.ru.js.map +1 -1
  37. package/dist/zest.ru.min.js +1 -1
  38. package/dist/zest.uk.js +104 -1
  39. package/dist/zest.uk.js.map +1 -1
  40. package/dist/zest.uk.min.js +1 -1
  41. package/dist/zest.zh.js +104 -1
  42. package/dist/zest.zh.js.map +1 -1
  43. package/dist/zest.zh.min.js +1 -1
  44. package/package.json +5 -4
  45. package/src/api/public-api.js +97 -0
  46. package/src/config/defaults.js +150 -0
  47. package/src/config/parser.js +104 -0
  48. package/src/core/categories.js +52 -0
  49. package/src/core/cookie-interceptor.js +116 -0
  50. package/src/core/dnt.js +56 -0
  51. package/src/core/known-trackers.js +168 -0
  52. package/src/core/pattern-matcher.js +96 -0
  53. package/src/core/script-blocker.js +308 -0
  54. package/src/core/storage-interceptor.js +169 -0
  55. package/src/i18n/lang-en.js +54 -0
  56. package/src/i18n/single/lang-de.js +55 -0
  57. package/src/i18n/single/lang-en.js +55 -0
  58. package/src/i18n/single/lang-es.js +55 -0
  59. package/src/i18n/single/lang-fr.js +55 -0
  60. package/src/i18n/single/lang-it.js +55 -0
  61. package/src/i18n/single/lang-ja.js +55 -0
  62. package/src/i18n/single/lang-nl.js +55 -0
  63. package/src/i18n/single/lang-pl.js +55 -0
  64. package/src/i18n/single/lang-pt.js +55 -0
  65. package/src/i18n/single/lang-ru.js +55 -0
  66. package/src/i18n/single/lang-uk.js +55 -0
  67. package/src/i18n/single/lang-zh.js +55 -0
  68. package/src/i18n/translations.js +546 -0
  69. package/src/index.js +377 -0
  70. package/src/integrations/consent-signals.js +71 -0
  71. package/src/storage/consent-store.js +177 -0
  72. package/src/storage/events.js +84 -0
  73. package/src/ui/banner.js +130 -0
  74. package/src/ui/modal.js +211 -0
  75. package/src/ui/styles.js +498 -0
  76. package/src/ui/widget.js +103 -0
package/src/index.js ADDED
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Zest - Lightweight Cookie Consent Toolkit
3
+ * Main entry point
4
+ */
5
+
6
+ // Core
7
+ import { interceptCookies, setConsentChecker as setCookieChecker, replayCookies, getOriginalCookieDescriptor } from './core/cookie-interceptor.js';
8
+ import { interceptStorage, setConsentChecker as setStorageChecker, replayStorage } from './core/storage-interceptor.js';
9
+ import { startScriptBlocking, setConsentChecker as setScriptChecker, replayScripts } from './core/script-blocker.js';
10
+ import { setPatterns } from './core/pattern-matcher.js';
11
+ import { getCategoryIds } from './core/categories.js';
12
+ import { isDoNotTrackEnabled, getDNTDetails } from './core/dnt.js';
13
+
14
+ // Integrations
15
+ import { applyConsentSignals } from './integrations/consent-signals.js';
16
+
17
+ // Config
18
+ import { getConfig, setConfig, getCurrentConfig } from './config/parser.js';
19
+
20
+ // Storage
21
+ import {
22
+ loadConsent,
23
+ getConsent,
24
+ hasConsent,
25
+ updateConsent,
26
+ acceptAll as storeAcceptAll,
27
+ rejectAll as storeRejectAll,
28
+ resetConsent,
29
+ hasConsentDecision,
30
+ getConsentProof
31
+ } from './storage/consent-store.js';
32
+ import { emitReady, emitConsent, emitReject, emitChange, emitShow, emitHide, EVENTS } from './storage/events.js';
33
+
34
+ // UI
35
+ import { showBanner, hideBanner, isBannerVisible } from './ui/banner.js';
36
+ import { showModal, hideModal, isModalVisible } from './ui/modal.js';
37
+ import { showWidget, hideWidget, removeWidget, isWidgetVisible } from './ui/widget.js';
38
+
39
+ // State
40
+ let initialized = false;
41
+ let config = null;
42
+
43
+ /**
44
+ * Consent checker function shared across interceptors
45
+ */
46
+ function checkConsent(category) {
47
+ return hasConsent(category);
48
+ }
49
+
50
+ /**
51
+ * Replay all queued items for newly allowed categories
52
+ */
53
+ function replayAll(allowedCategories) {
54
+ replayCookies(allowedCategories);
55
+ replayStorage(allowedCategories);
56
+ replayScripts(allowedCategories);
57
+ }
58
+
59
+ /**
60
+ * Handle accept all
61
+ */
62
+ function handleAcceptAll() {
63
+ const result = storeAcceptAll(config.expiration);
64
+ const categories = getCategoryIds();
65
+
66
+ applyConsentSignals(result.current, config, false);
67
+
68
+ hideBanner();
69
+ hideModal();
70
+
71
+ replayAll(categories);
72
+
73
+ if (config.showWidget) {
74
+ showWidget({ onClick: handleShowSettings });
75
+ }
76
+
77
+ emitConsent(result.current, result.previous);
78
+ emitChange(result.current, result.previous);
79
+ config.callbacks?.onAccept?.(result.current);
80
+ config.callbacks?.onChange?.(result.current);
81
+ }
82
+
83
+ /**
84
+ * Handle reject all
85
+ */
86
+ function handleRejectAll() {
87
+ const result = storeRejectAll(config.expiration);
88
+
89
+ applyConsentSignals(result.current, config, false);
90
+
91
+ hideBanner();
92
+ hideModal();
93
+
94
+ if (config.showWidget) {
95
+ showWidget({ onClick: handleShowSettings });
96
+ }
97
+
98
+ emitReject(result.current);
99
+ emitChange(result.current, result.previous);
100
+ config.callbacks?.onReject?.();
101
+ config.callbacks?.onChange?.(result.current);
102
+ }
103
+
104
+ /**
105
+ * Handle save preferences from modal
106
+ */
107
+ function handleSavePreferences(selections) {
108
+ const result = updateConsent(selections, config.expiration);
109
+
110
+ applyConsentSignals(result.current, config, false);
111
+
112
+ // Find newly allowed categories
113
+ const newlyAllowed = Object.keys(result.current).filter(
114
+ cat => result.current[cat] && !result.previous[cat]
115
+ );
116
+
117
+ if (newlyAllowed.length > 0) {
118
+ replayAll(newlyAllowed);
119
+ }
120
+
121
+ hideModal();
122
+
123
+ if (config.showWidget) {
124
+ showWidget({ onClick: handleShowSettings });
125
+ }
126
+
127
+ // Determine if this was acceptance or rejection based on selections
128
+ const hasNonEssential = Object.entries(selections)
129
+ .some(([cat, val]) => cat !== 'essential' && val);
130
+
131
+ if (hasNonEssential) {
132
+ emitConsent(result.current, result.previous);
133
+ } else {
134
+ emitReject(result.current);
135
+ }
136
+
137
+ emitChange(result.current, result.previous);
138
+ config.callbacks?.onChange?.(result.current);
139
+ }
140
+
141
+ /**
142
+ * Handle show settings
143
+ */
144
+ function handleShowSettings() {
145
+ hideBanner();
146
+ hideWidget();
147
+
148
+ showModal(getConsent(), {
149
+ onSave: handleSavePreferences,
150
+ onAcceptAll: handleAcceptAll,
151
+ onRejectAll: handleRejectAll,
152
+ onClose: handleCloseModal
153
+ });
154
+
155
+ emitShow('modal');
156
+ }
157
+
158
+ /**
159
+ * Handle close modal
160
+ */
161
+ function handleCloseModal() {
162
+ hideModal();
163
+ emitHide('modal');
164
+
165
+ // Show widget if consent was already given
166
+ if (hasConsentDecision() && config.showWidget) {
167
+ showWidget({ onClick: handleShowSettings });
168
+ } else {
169
+ // Show banner again if no decision made
170
+ showBanner({
171
+ onAcceptAll: handleAcceptAll,
172
+ onRejectAll: handleRejectAll,
173
+ onSettings: handleShowSettings
174
+ });
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Initialize Zest
180
+ */
181
+ function init(userConfig = {}) {
182
+ if (initialized) {
183
+ console.warn('[Zest] Already initialized');
184
+ return Zest;
185
+ }
186
+
187
+ // Merge config
188
+ config = setConfig(userConfig);
189
+
190
+ // Push default denied state to vendor consent mode APIs (must happen before scripts load)
191
+ applyConsentSignals(
192
+ { essential: true, functional: false, analytics: false, marketing: false },
193
+ config,
194
+ true
195
+ );
196
+
197
+ // Set patterns if provided
198
+ if (config.patterns) {
199
+ setPatterns(config.patterns);
200
+ }
201
+
202
+ // Set up consent checkers
203
+ setCookieChecker(checkConsent);
204
+ setStorageChecker(checkConsent);
205
+ setScriptChecker(checkConsent);
206
+
207
+ // Start interception
208
+ interceptCookies();
209
+ interceptStorage();
210
+ startScriptBlocking(config.mode, config.blockedDomains);
211
+
212
+ // Load saved consent
213
+ const consent = loadConsent();
214
+
215
+ initialized = true;
216
+
217
+ // Push update for returning visitors with saved consent
218
+ if (hasConsentDecision()) {
219
+ applyConsentSignals(consent, config, false);
220
+ }
221
+
222
+ // Check Do Not Track / Global Privacy Control
223
+ const dntEnabled = isDoNotTrackEnabled();
224
+ let dntApplied = false;
225
+
226
+ if (dntEnabled && config.respectDNT && config.dntBehavior !== 'ignore') {
227
+ if (config.dntBehavior === 'reject' && !hasConsentDecision()) {
228
+ // Auto-reject non-essential cookies silently
229
+ const result = storeRejectAll(config.expiration);
230
+ dntApplied = true;
231
+
232
+ applyConsentSignals(result.current, config, false);
233
+
234
+ // Emit events
235
+ emitReject(result.current);
236
+ emitChange(result.current, result.previous);
237
+ config.callbacks?.onReject?.();
238
+ config.callbacks?.onChange?.(result.current);
239
+ }
240
+ // 'preselect' behavior is handled by default (banner shows with defaults off)
241
+ }
242
+
243
+ // Emit ready event
244
+ emitReady(consent);
245
+ config.callbacks?.onReady?.(consent);
246
+
247
+ // Show UI based on consent state
248
+ if (!hasConsentDecision() && !dntApplied) {
249
+ // No consent decision yet - show banner
250
+ showBanner({
251
+ onAcceptAll: handleAcceptAll,
252
+ onRejectAll: handleRejectAll,
253
+ onSettings: handleShowSettings
254
+ });
255
+ emitShow('banner');
256
+ } else {
257
+ // Consent already given (or DNT auto-rejected) - show widget for reopening settings
258
+ if (config.showWidget) {
259
+ showWidget({ onClick: handleShowSettings });
260
+ }
261
+ }
262
+
263
+ return Zest;
264
+ }
265
+
266
+ /**
267
+ * Public API
268
+ */
269
+ const Zest = {
270
+ // Initialization
271
+ init,
272
+
273
+ // Banner control
274
+ show() {
275
+ if (!initialized) {
276
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
277
+ return;
278
+ }
279
+ hideModal();
280
+ hideWidget();
281
+ showBanner({
282
+ onAcceptAll: handleAcceptAll,
283
+ onRejectAll: handleRejectAll,
284
+ onSettings: handleShowSettings
285
+ });
286
+ emitShow('banner');
287
+ },
288
+
289
+ hide() {
290
+ hideBanner();
291
+ emitHide('banner');
292
+ },
293
+
294
+ // Settings modal
295
+ showSettings() {
296
+ if (!initialized) {
297
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
298
+ return;
299
+ }
300
+ handleShowSettings();
301
+ },
302
+
303
+ hideSettings() {
304
+ hideModal();
305
+ emitHide('modal');
306
+ },
307
+
308
+ // Consent management
309
+ getConsent,
310
+ hasConsent,
311
+ hasConsentDecision,
312
+ getConsentProof,
313
+
314
+ // DNT detection
315
+ isDoNotTrackEnabled,
316
+ getDNTDetails,
317
+
318
+ // Accept/Reject programmatically
319
+ acceptAll() {
320
+ if (!initialized) {
321
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
322
+ return;
323
+ }
324
+ handleAcceptAll();
325
+ },
326
+
327
+ rejectAll() {
328
+ if (!initialized) {
329
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
330
+ return;
331
+ }
332
+ handleRejectAll();
333
+ },
334
+
335
+ // Reset and show banner again
336
+ reset() {
337
+ resetConsent();
338
+ hideModal();
339
+ removeWidget();
340
+
341
+ if (initialized) {
342
+ showBanner({
343
+ onAcceptAll: handleAcceptAll,
344
+ onRejectAll: handleRejectAll,
345
+ onSettings: handleShowSettings
346
+ });
347
+ emitShow('banner');
348
+ }
349
+ },
350
+
351
+ // Config
352
+ getConfig: getCurrentConfig,
353
+
354
+ // Events
355
+ EVENTS
356
+ };
357
+
358
+ // Auto-init if config present
359
+ if (typeof window !== 'undefined') {
360
+ // Make Zest available globally
361
+ window.Zest = Zest;
362
+
363
+ const autoInit = () => {
364
+ const cfg = getConfig();
365
+ if (cfg.autoInit !== false) {
366
+ init(window.ZestConfig);
367
+ }
368
+ };
369
+
370
+ if (document.readyState === 'loading') {
371
+ document.addEventListener('DOMContentLoaded', autoInit);
372
+ } else {
373
+ autoInit();
374
+ }
375
+ }
376
+
377
+ export default Zest;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Consent Signals - Optional vendor consent mode integrations
3
+ *
4
+ * Pushes consent state to Google Consent Mode v2 and/or Microsoft UET
5
+ * Consent Mode when enabled via config.
6
+ */
7
+
8
+ /**
9
+ * Map Zest consent state to Google Consent Mode v2 signals
10
+ */
11
+ function toGoogleSignals(consent) {
12
+ const g = (val) => val ? 'granted' : 'denied';
13
+ return {
14
+ ad_storage: g(consent.marketing),
15
+ ad_user_data: g(consent.marketing),
16
+ ad_personalization: g(consent.marketing),
17
+ analytics_storage: g(consent.analytics),
18
+ functionality_storage: 'granted', // essential is always true
19
+ personalization_storage: g(consent.functional)
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Push consent signal to Google via gtag or dataLayer fallback.
25
+ * Uses a local function to preserve the `arguments` object shape
26
+ * that gtag/dataLayer expects (not an array).
27
+ */
28
+ function pushGoogle(type, signals) {
29
+ window.dataLayer = window.dataLayer || [];
30
+ if (typeof window.gtag === 'function') {
31
+ window.gtag('consent', type, signals);
32
+ } else {
33
+ function gtagFallback() { window.dataLayer.push(arguments); }
34
+ gtagFallback('consent', type, signals);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Map Zest consent state to Microsoft UET signal.
40
+ * Microsoft UET only exposes ad_storage.
41
+ */
42
+ function toMicrosoftSignals(consent) {
43
+ return { ad_storage: consent.marketing ? 'granted' : 'denied' };
44
+ }
45
+
46
+ /**
47
+ * Push consent signal to Microsoft UET
48
+ */
49
+ function pushMicrosoft(type, signals) {
50
+ window.uetq = window.uetq || [];
51
+ window.uetq.push('consent', type, signals);
52
+ }
53
+
54
+ /**
55
+ * Apply consent signals to enabled vendor integrations.
56
+ *
57
+ * @param {Object} consent Current Zest consent state
58
+ * @param {Object} config Merged Zest config
59
+ * @param {boolean} isDefault true on first call (pushes 'default'), false for updates
60
+ */
61
+ export function applyConsentSignals(consent, config, isDefault) {
62
+ const type = isDefault ? 'default' : 'update';
63
+
64
+ if (config.consentModeGoogle) {
65
+ pushGoogle(type, toGoogleSignals(consent));
66
+ }
67
+
68
+ if (config.consentModeMicrosoft) {
69
+ pushMicrosoft(type, toMicrosoftSignals(consent));
70
+ }
71
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Consent Store - Manages consent state persistence
3
+ */
4
+
5
+ import { getDefaultConsent } from '../core/categories.js';
6
+ import { getOriginalCookieDescriptor } from '../core/cookie-interceptor.js';
7
+
8
+ const COOKIE_NAME = 'zest_consent';
9
+ const CONSENT_VERSION = '1.0';
10
+
11
+ // Current consent state
12
+ let consent = null;
13
+
14
+ /**
15
+ * Get the original cookie setter (bypasses interception)
16
+ */
17
+ function setRawCookie(value) {
18
+ const descriptor = getOriginalCookieDescriptor();
19
+ if (descriptor?.set) {
20
+ descriptor.set.call(document, value);
21
+ } else {
22
+ // Fallback if interceptor not initialized yet
23
+ document.cookie = value;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Get the original cookie getter
29
+ */
30
+ function getRawCookie() {
31
+ const descriptor = getOriginalCookieDescriptor();
32
+ if (descriptor?.get) {
33
+ return descriptor.get.call(document);
34
+ }
35
+ return document.cookie;
36
+ }
37
+
38
+ /**
39
+ * Load consent from cookie
40
+ */
41
+ export function loadConsent() {
42
+ try {
43
+ const cookies = getRawCookie();
44
+ const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
45
+
46
+ if (match) {
47
+ const data = JSON.parse(decodeURIComponent(match[1]));
48
+ consent = data.categories || getDefaultConsent();
49
+ return { ...consent };
50
+ }
51
+ } catch (e) {
52
+ // Invalid or missing cookie
53
+ }
54
+
55
+ consent = getDefaultConsent();
56
+ return { ...consent };
57
+ }
58
+
59
+ /**
60
+ * Save consent to cookie
61
+ */
62
+ export function saveConsent(expirationDays = 365) {
63
+ if (!consent) {
64
+ consent = getDefaultConsent();
65
+ }
66
+
67
+ const data = {
68
+ version: CONSENT_VERSION,
69
+ timestamp: Date.now(),
70
+ categories: consent
71
+ };
72
+
73
+ const expires = new Date(Date.now() + expirationDays * 24 * 60 * 60 * 1000).toUTCString();
74
+ const cookieValue = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(data))}; expires=${expires}; path=/; SameSite=Lax`;
75
+
76
+ setRawCookie(cookieValue);
77
+ }
78
+
79
+ /**
80
+ * Get current consent state
81
+ */
82
+ export function getConsent() {
83
+ if (!consent) {
84
+ consent = loadConsent();
85
+ }
86
+ return { ...consent };
87
+ }
88
+
89
+ /**
90
+ * Update consent state
91
+ */
92
+ export function updateConsent(newConsent, expirationDays = 365) {
93
+ const previous = consent ? { ...consent } : getDefaultConsent();
94
+
95
+ consent = {
96
+ essential: true, // Always true
97
+ functional: !!newConsent.functional,
98
+ analytics: !!newConsent.analytics,
99
+ marketing: !!newConsent.marketing
100
+ };
101
+
102
+ saveConsent(expirationDays);
103
+
104
+ return { current: { ...consent }, previous };
105
+ }
106
+
107
+ /**
108
+ * Check if specific category is allowed
109
+ */
110
+ export function hasConsent(category) {
111
+ if (!consent) {
112
+ consent = loadConsent();
113
+ }
114
+ return consent[category] === true;
115
+ }
116
+
117
+ /**
118
+ * Accept all categories
119
+ */
120
+ export function acceptAll(expirationDays = 365) {
121
+ return updateConsent({
122
+ essential: true,
123
+ functional: true,
124
+ analytics: true,
125
+ marketing: true
126
+ }, expirationDays);
127
+ }
128
+
129
+ /**
130
+ * Reject all (except essential)
131
+ */
132
+ export function rejectAll(expirationDays = 365) {
133
+ return updateConsent({
134
+ essential: true,
135
+ functional: false,
136
+ analytics: false,
137
+ marketing: false
138
+ }, expirationDays);
139
+ }
140
+
141
+ /**
142
+ * Reset consent (clear cookie)
143
+ */
144
+ export function resetConsent() {
145
+ setRawCookie(`${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`);
146
+ consent = null;
147
+ }
148
+
149
+ /**
150
+ * Check if consent has been given (any decision made)
151
+ */
152
+ export function hasConsentDecision() {
153
+ try {
154
+ const cookies = getRawCookie();
155
+ return cookies.includes(COOKIE_NAME);
156
+ } catch (e) {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Get consent proof for compliance
163
+ */
164
+ export function getConsentProof() {
165
+ try {
166
+ const cookies = getRawCookie();
167
+ const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
168
+
169
+ if (match) {
170
+ return JSON.parse(decodeURIComponent(match[1]));
171
+ }
172
+ } catch (e) {
173
+ // Invalid cookie
174
+ }
175
+
176
+ return null;
177
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Events - Custom event dispatching for consent changes
3
+ */
4
+
5
+ // Event names
6
+ export const EVENTS = {
7
+ READY: 'zest:ready',
8
+ CONSENT: 'zest:consent',
9
+ REJECT: 'zest:reject',
10
+ CHANGE: 'zest:change',
11
+ SHOW: 'zest:show',
12
+ HIDE: 'zest:hide'
13
+ };
14
+
15
+ /**
16
+ * Dispatch a custom event
17
+ */
18
+ export function emit(eventName, detail = {}) {
19
+ const event = new CustomEvent(eventName, {
20
+ detail,
21
+ bubbles: true,
22
+ cancelable: true
23
+ });
24
+
25
+ document.dispatchEvent(event);
26
+ return event;
27
+ }
28
+
29
+ /**
30
+ * Emit ready event
31
+ */
32
+ export function emitReady(consent) {
33
+ return emit(EVENTS.READY, { consent });
34
+ }
35
+
36
+ /**
37
+ * Emit consent event (user accepted)
38
+ */
39
+ export function emitConsent(consent, previous) {
40
+ return emit(EVENTS.CONSENT, { consent, previous });
41
+ }
42
+
43
+ /**
44
+ * Emit reject event (user rejected all)
45
+ */
46
+ export function emitReject(consent) {
47
+ return emit(EVENTS.REJECT, { consent });
48
+ }
49
+
50
+ /**
51
+ * Emit change event (any consent change)
52
+ */
53
+ export function emitChange(consent, previous) {
54
+ return emit(EVENTS.CHANGE, { consent, previous });
55
+ }
56
+
57
+ /**
58
+ * Emit show event (banner/modal shown)
59
+ */
60
+ export function emitShow(type = 'banner') {
61
+ return emit(EVENTS.SHOW, { type });
62
+ }
63
+
64
+ /**
65
+ * Emit hide event (banner/modal hidden)
66
+ */
67
+ export function emitHide(type = 'banner') {
68
+ return emit(EVENTS.HIDE, { type });
69
+ }
70
+
71
+ /**
72
+ * Subscribe to an event
73
+ */
74
+ export function on(eventName, callback) {
75
+ document.addEventListener(eventName, callback);
76
+ return () => document.removeEventListener(eventName, callback);
77
+ }
78
+
79
+ /**
80
+ * Subscribe to an event once
81
+ */
82
+ export function once(eventName, callback) {
83
+ document.addEventListener(eventName, callback, { once: true });
84
+ }