@freshjuice/zest 0.1.0 → 2.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 (82) hide show
  1. package/README.md +216 -70
  2. package/dist/zest.de.js +776 -286
  3. package/dist/zest.de.js.map +1 -1
  4. package/dist/zest.de.min.js +1 -1
  5. package/dist/zest.en.js +776 -286
  6. package/dist/zest.en.js.map +1 -1
  7. package/dist/zest.en.min.js +1 -1
  8. package/dist/zest.es.js +776 -286
  9. package/dist/zest.es.js.map +1 -1
  10. package/dist/zest.es.min.js +1 -1
  11. package/dist/zest.esm.js +776 -286
  12. package/dist/zest.esm.js.map +1 -1
  13. package/dist/zest.esm.min.js +1 -1
  14. package/dist/zest.fr.js +776 -286
  15. package/dist/zest.fr.js.map +1 -1
  16. package/dist/zest.fr.min.js +1 -1
  17. package/dist/zest.headless.esm.js +2299 -0
  18. package/dist/zest.headless.esm.js.map +1 -0
  19. package/dist/zest.headless.esm.min.js +1 -0
  20. package/dist/zest.it.js +776 -286
  21. package/dist/zest.it.js.map +1 -1
  22. package/dist/zest.it.min.js +1 -1
  23. package/dist/zest.ja.js +776 -286
  24. package/dist/zest.ja.js.map +1 -1
  25. package/dist/zest.ja.min.js +1 -1
  26. package/dist/zest.js +776 -286
  27. package/dist/zest.js.map +1 -1
  28. package/dist/zest.min.js +1 -1
  29. package/dist/zest.nl.js +776 -286
  30. package/dist/zest.nl.js.map +1 -1
  31. package/dist/zest.nl.min.js +1 -1
  32. package/dist/zest.pl.js +776 -286
  33. package/dist/zest.pl.js.map +1 -1
  34. package/dist/zest.pl.min.js +1 -1
  35. package/dist/zest.pt.js +776 -286
  36. package/dist/zest.pt.js.map +1 -1
  37. package/dist/zest.pt.min.js +1 -1
  38. package/dist/zest.ru.js +776 -286
  39. package/dist/zest.ru.js.map +1 -1
  40. package/dist/zest.ru.min.js +1 -1
  41. package/dist/zest.uk.js +776 -286
  42. package/dist/zest.uk.js.map +1 -1
  43. package/dist/zest.uk.min.js +1 -1
  44. package/dist/zest.zh.js +776 -286
  45. package/dist/zest.zh.js.map +1 -1
  46. package/dist/zest.zh.min.js +1 -1
  47. package/package.json +17 -4
  48. package/src/api/public-api.js +97 -0
  49. package/src/config/defaults.js +150 -0
  50. package/src/config/parser.js +104 -0
  51. package/src/core/categories.js +52 -0
  52. package/src/core/cookie-interceptor.js +131 -0
  53. package/src/core/dnt.js +56 -0
  54. package/src/core/known-trackers.js +195 -0
  55. package/src/core/pattern-matcher.js +111 -0
  56. package/src/core/script-blocker.js +314 -0
  57. package/src/core/security.js +204 -0
  58. package/src/core/storage-interceptor.js +173 -0
  59. package/src/core-lifecycle.js +192 -0
  60. package/src/headless.js +133 -0
  61. package/src/i18n/lang-en.js +54 -0
  62. package/src/i18n/single/lang-de.js +55 -0
  63. package/src/i18n/single/lang-en.js +55 -0
  64. package/src/i18n/single/lang-es.js +55 -0
  65. package/src/i18n/single/lang-fr.js +55 -0
  66. package/src/i18n/single/lang-it.js +55 -0
  67. package/src/i18n/single/lang-ja.js +55 -0
  68. package/src/i18n/single/lang-nl.js +55 -0
  69. package/src/i18n/single/lang-pl.js +55 -0
  70. package/src/i18n/single/lang-pt.js +55 -0
  71. package/src/i18n/single/lang-ru.js +55 -0
  72. package/src/i18n/single/lang-uk.js +55 -0
  73. package/src/i18n/single/lang-zh.js +55 -0
  74. package/src/i18n/translations.js +546 -0
  75. package/src/index.js +266 -0
  76. package/src/integrations/consent-signals.js +71 -0
  77. package/src/storage/consent-store.js +201 -0
  78. package/src/storage/events.js +84 -0
  79. package/src/ui/banner.js +134 -0
  80. package/src/ui/modal.js +215 -0
  81. package/src/ui/styles.js +519 -0
  82. package/src/ui/widget.js +105 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Banner - Main consent banner component
3
+ */
4
+
5
+ import { generateStyles } from './styles.js';
6
+ import { getCurrentConfig } from '../config/parser.js';
7
+ import { escapeHTML } from '../core/security.js';
8
+
9
+ let bannerElement = null;
10
+ let shadowRoot = null;
11
+
12
+ const SAFE_POSITIONS = new Set(['bottom', 'bottom-left', 'bottom-right', 'top']);
13
+
14
+ /**
15
+ * Create the banner HTML
16
+ */
17
+ function createBannerHTML(config) {
18
+ const labels = config.labels.banner;
19
+ const rawPosition = config.position || 'bottom';
20
+ const position = SAFE_POSITIONS.has(rawPosition) ? rawPosition : 'bottom';
21
+
22
+ return `
23
+ <div class="zest-banner zest-banner--${position}" role="dialog" aria-modal="false" aria-label="${escapeHTML(labels.title)}">
24
+ <h2 class="zest-banner__title">${escapeHTML(labels.title)}</h2>
25
+ <p class="zest-banner__description">${escapeHTML(labels.description)}</p>
26
+ <div class="zest-banner__buttons">
27
+ <button type="button" class="zest-btn zest-btn--primary" data-action="accept-all">
28
+ ${escapeHTML(labels.acceptAll)}
29
+ </button>
30
+ <button type="button" class="zest-btn zest-btn--secondary" data-action="reject-all">
31
+ ${escapeHTML(labels.rejectAll)}
32
+ </button>
33
+ <button type="button" class="zest-btn zest-btn--ghost" data-action="settings">
34
+ ${escapeHTML(labels.settings)}
35
+ </button>
36
+ </div>
37
+ </div>
38
+ `;
39
+ }
40
+
41
+ /**
42
+ * Create and mount the banner
43
+ */
44
+ export function createBanner(callbacks = {}) {
45
+ if (bannerElement) {
46
+ return bannerElement;
47
+ }
48
+
49
+ const config = getCurrentConfig();
50
+
51
+ // Create host element
52
+ bannerElement = document.createElement('zest-banner');
53
+ bannerElement.setAttribute('data-theme', config.theme || 'light');
54
+
55
+ // Create shadow root
56
+ shadowRoot = bannerElement.attachShadow({ mode: 'open' });
57
+
58
+ // Add styles
59
+ const styleEl = document.createElement('style');
60
+ styleEl.textContent = generateStyles(config);
61
+ shadowRoot.appendChild(styleEl);
62
+
63
+ // Add banner HTML
64
+ const container = document.createElement('div');
65
+ container.innerHTML = createBannerHTML(config);
66
+ shadowRoot.appendChild(container.firstElementChild);
67
+
68
+ // Add event listeners
69
+ const banner = shadowRoot.querySelector('.zest-banner');
70
+
71
+ banner.addEventListener('click', (e) => {
72
+ const action = e.target.dataset.action;
73
+ if (!action) return;
74
+
75
+ switch (action) {
76
+ case 'accept-all':
77
+ callbacks.onAcceptAll?.();
78
+ break;
79
+ case 'reject-all':
80
+ callbacks.onRejectAll?.();
81
+ break;
82
+ case 'settings':
83
+ callbacks.onSettings?.();
84
+ break;
85
+ }
86
+ });
87
+
88
+ // Keyboard handling
89
+ banner.addEventListener('keydown', (e) => {
90
+ if (e.key === 'Escape') {
91
+ callbacks.onSettings?.();
92
+ }
93
+ });
94
+
95
+ // Mount to document
96
+ document.body.appendChild(bannerElement);
97
+
98
+ // Focus first button for accessibility
99
+ requestAnimationFrame(() => {
100
+ const firstButton = shadowRoot.querySelector('button');
101
+ firstButton?.focus();
102
+ });
103
+
104
+ return bannerElement;
105
+ }
106
+
107
+ /**
108
+ * Show the banner
109
+ */
110
+ export function showBanner(callbacks = {}) {
111
+ if (!bannerElement) {
112
+ createBanner(callbacks);
113
+ } else {
114
+ bannerElement.classList.remove('zest-hidden');
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Hide the banner
120
+ */
121
+ export function hideBanner() {
122
+ if (bannerElement) {
123
+ bannerElement.remove();
124
+ bannerElement = null;
125
+ shadowRoot = null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Check if banner is visible
131
+ */
132
+ export function isBannerVisible() {
133
+ return bannerElement !== null && document.body.contains(bannerElement);
134
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Modal - Settings modal component for category toggles
3
+ */
4
+
5
+ import { generateStyles } from './styles.js';
6
+ import { getCurrentConfig } from '../config/parser.js';
7
+ import { DEFAULT_CATEGORIES } from '../core/categories.js';
8
+ import { escapeHTML, safeUrl } from '../core/security.js';
9
+
10
+ let modalElement = null;
11
+ let shadowRoot = null;
12
+ let currentSelections = {};
13
+
14
+ /**
15
+ * Create category toggle HTML
16
+ */
17
+ function createCategoryHTML(category, isChecked, isRequired) {
18
+ const disabled = isRequired ? 'disabled' : '';
19
+ const checked = isChecked ? 'checked' : '';
20
+ const safeId = escapeHTML(category.id);
21
+ const safeLabel = escapeHTML(category.label);
22
+
23
+ return `
24
+ <div class="zest-category">
25
+ <div class="zest-category__header">
26
+ <div class="zest-category__info">
27
+ <span class="zest-category__label">${safeLabel}</span>
28
+ <p class="zest-category__description">${escapeHTML(category.description)}</p>
29
+ </div>
30
+ <label class="zest-toggle">
31
+ <input
32
+ type="checkbox"
33
+ class="zest-toggle__input"
34
+ data-category="${safeId}"
35
+ ${checked}
36
+ ${disabled}
37
+ aria-label="${safeLabel}"
38
+ >
39
+ <span class="zest-toggle__slider"></span>
40
+ </label>
41
+ </div>
42
+ </div>
43
+ `;
44
+ }
45
+
46
+ /**
47
+ * Create the modal HTML
48
+ */
49
+ function createModalHTML(config, consent) {
50
+ const labels = config.labels.modal;
51
+ const categories = config.categories || DEFAULT_CATEGORIES;
52
+
53
+ const categoriesHTML = Object.values(categories)
54
+ .map(cat => createCategoryHTML(
55
+ cat,
56
+ consent[cat.id] ?? cat.default,
57
+ cat.required
58
+ ))
59
+ .join('');
60
+
61
+ const validatedPolicyUrl = config.policyUrl ? safeUrl(config.policyUrl) : null;
62
+ const policyLink = validatedPolicyUrl
63
+ ? `<a href="${escapeHTML(validatedPolicyUrl)}" class="zest-link" target="_blank" rel="noopener noreferrer">Privacy Policy</a>`
64
+ : '';
65
+
66
+ return `
67
+ <div class="zest-modal-overlay" role="dialog" aria-modal="true" aria-label="${escapeHTML(labels.title)}">
68
+ <div class="zest-modal">
69
+ <div class="zest-modal__header">
70
+ <h2 class="zest-modal__title">${escapeHTML(labels.title)}</h2>
71
+ <p class="zest-modal__description">${escapeHTML(labels.description)} ${policyLink}</p>
72
+ </div>
73
+ <div class="zest-modal__body">
74
+ ${categoriesHTML}
75
+ </div>
76
+ <div class="zest-modal__footer">
77
+ <button type="button" class="zest-btn zest-btn--primary" data-action="save">
78
+ ${escapeHTML(labels.save)}
79
+ </button>
80
+ <button type="button" class="zest-btn zest-btn--secondary" data-action="accept-all">
81
+ ${escapeHTML(labels.acceptAll)}
82
+ </button>
83
+ <button type="button" class="zest-btn zest-btn--ghost" data-action="reject-all">
84
+ ${escapeHTML(labels.rejectAll)}
85
+ </button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ `;
90
+ }
91
+
92
+ /**
93
+ * Get current selections from toggles
94
+ */
95
+ function getSelections() {
96
+ if (!shadowRoot) return currentSelections;
97
+
98
+ const toggles = shadowRoot.querySelectorAll('.zest-toggle__input');
99
+ const selections = { essential: true };
100
+
101
+ toggles.forEach(toggle => {
102
+ const category = toggle.dataset.category;
103
+ if (category && category !== 'essential') {
104
+ selections[category] = toggle.checked;
105
+ }
106
+ });
107
+
108
+ return selections;
109
+ }
110
+
111
+ /**
112
+ * Create and show the modal
113
+ */
114
+ export function showModal(consent = {}, callbacks = {}) {
115
+ if (modalElement) {
116
+ return modalElement;
117
+ }
118
+
119
+ const config = getCurrentConfig();
120
+ currentSelections = { ...consent };
121
+
122
+ // Create host element
123
+ modalElement = document.createElement('zest-modal');
124
+ modalElement.setAttribute('data-theme', config.theme || 'light');
125
+
126
+ // Create shadow root
127
+ shadowRoot = modalElement.attachShadow({ mode: 'open' });
128
+
129
+ // Add styles
130
+ const styleEl = document.createElement('style');
131
+ styleEl.textContent = generateStyles(config);
132
+ shadowRoot.appendChild(styleEl);
133
+
134
+ // Add modal HTML
135
+ const container = document.createElement('div');
136
+ container.innerHTML = createModalHTML(config, consent);
137
+ shadowRoot.appendChild(container.firstElementChild);
138
+
139
+ // Add event listeners
140
+ const modal = shadowRoot.querySelector('.zest-modal-overlay');
141
+
142
+ // Button clicks
143
+ modal.addEventListener('click', (e) => {
144
+ const action = e.target.dataset.action;
145
+ if (!action) {
146
+ // Click on overlay background to close
147
+ if (e.target === modal) {
148
+ callbacks.onClose?.();
149
+ }
150
+ return;
151
+ }
152
+
153
+ switch (action) {
154
+ case 'save':
155
+ callbacks.onSave?.(getSelections());
156
+ break;
157
+ case 'accept-all':
158
+ callbacks.onAcceptAll?.();
159
+ break;
160
+ case 'reject-all':
161
+ callbacks.onRejectAll?.();
162
+ break;
163
+ }
164
+ });
165
+
166
+ // Keyboard handling
167
+ modal.addEventListener('keydown', (e) => {
168
+ if (e.key === 'Escape') {
169
+ callbacks.onClose?.();
170
+ }
171
+ });
172
+
173
+ // Track toggle changes
174
+ shadowRoot.querySelectorAll('.zest-toggle__input').forEach(toggle => {
175
+ toggle.addEventListener('change', () => {
176
+ currentSelections = getSelections();
177
+ });
178
+ });
179
+
180
+ // Mount to document
181
+ document.body.appendChild(modalElement);
182
+
183
+ // Trap focus
184
+ requestAnimationFrame(() => {
185
+ const firstButton = shadowRoot.querySelector('button');
186
+ firstButton?.focus();
187
+ });
188
+
189
+ return modalElement;
190
+ }
191
+
192
+ /**
193
+ * Hide the modal
194
+ */
195
+ export function hideModal() {
196
+ if (modalElement) {
197
+ modalElement.remove();
198
+ modalElement = null;
199
+ shadowRoot = null;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Check if modal is visible
205
+ */
206
+ export function isModalVisible() {
207
+ return modalElement !== null && document.body.contains(modalElement);
208
+ }
209
+
210
+ /**
211
+ * Get current modal selections
212
+ */
213
+ export function getModalSelections() {
214
+ return getSelections();
215
+ }