@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.
- package/README.md +216 -70
- package/dist/zest.de.js +776 -286
- package/dist/zest.de.js.map +1 -1
- package/dist/zest.de.min.js +1 -1
- package/dist/zest.en.js +776 -286
- package/dist/zest.en.js.map +1 -1
- package/dist/zest.en.min.js +1 -1
- package/dist/zest.es.js +776 -286
- package/dist/zest.es.js.map +1 -1
- package/dist/zest.es.min.js +1 -1
- package/dist/zest.esm.js +776 -286
- package/dist/zest.esm.js.map +1 -1
- package/dist/zest.esm.min.js +1 -1
- package/dist/zest.fr.js +776 -286
- package/dist/zest.fr.js.map +1 -1
- package/dist/zest.fr.min.js +1 -1
- package/dist/zest.headless.esm.js +2299 -0
- package/dist/zest.headless.esm.js.map +1 -0
- package/dist/zest.headless.esm.min.js +1 -0
- package/dist/zest.it.js +776 -286
- package/dist/zest.it.js.map +1 -1
- package/dist/zest.it.min.js +1 -1
- package/dist/zest.ja.js +776 -286
- package/dist/zest.ja.js.map +1 -1
- package/dist/zest.ja.min.js +1 -1
- package/dist/zest.js +776 -286
- package/dist/zest.js.map +1 -1
- package/dist/zest.min.js +1 -1
- package/dist/zest.nl.js +776 -286
- package/dist/zest.nl.js.map +1 -1
- package/dist/zest.nl.min.js +1 -1
- package/dist/zest.pl.js +776 -286
- package/dist/zest.pl.js.map +1 -1
- package/dist/zest.pl.min.js +1 -1
- package/dist/zest.pt.js +776 -286
- package/dist/zest.pt.js.map +1 -1
- package/dist/zest.pt.min.js +1 -1
- package/dist/zest.ru.js +776 -286
- package/dist/zest.ru.js.map +1 -1
- package/dist/zest.ru.min.js +1 -1
- package/dist/zest.uk.js +776 -286
- package/dist/zest.uk.js.map +1 -1
- package/dist/zest.uk.min.js +1 -1
- package/dist/zest.zh.js +776 -286
- package/dist/zest.zh.js.map +1 -1
- package/dist/zest.zh.min.js +1 -1
- package/package.json +17 -4
- package/src/api/public-api.js +97 -0
- package/src/config/defaults.js +150 -0
- package/src/config/parser.js +104 -0
- package/src/core/categories.js +52 -0
- package/src/core/cookie-interceptor.js +131 -0
- package/src/core/dnt.js +56 -0
- package/src/core/known-trackers.js +195 -0
- package/src/core/pattern-matcher.js +111 -0
- package/src/core/script-blocker.js +314 -0
- package/src/core/security.js +204 -0
- package/src/core/storage-interceptor.js +173 -0
- package/src/core-lifecycle.js +192 -0
- package/src/headless.js +133 -0
- package/src/i18n/lang-en.js +54 -0
- package/src/i18n/single/lang-de.js +55 -0
- package/src/i18n/single/lang-en.js +55 -0
- package/src/i18n/single/lang-es.js +55 -0
- package/src/i18n/single/lang-fr.js +55 -0
- package/src/i18n/single/lang-it.js +55 -0
- package/src/i18n/single/lang-ja.js +55 -0
- package/src/i18n/single/lang-nl.js +55 -0
- package/src/i18n/single/lang-pl.js +55 -0
- package/src/i18n/single/lang-pt.js +55 -0
- package/src/i18n/single/lang-ru.js +55 -0
- package/src/i18n/single/lang-uk.js +55 -0
- package/src/i18n/single/lang-zh.js +55 -0
- package/src/i18n/translations.js +546 -0
- package/src/index.js +266 -0
- package/src/integrations/consent-signals.js +71 -0
- package/src/storage/consent-store.js +201 -0
- package/src/storage/events.js +84 -0
- package/src/ui/banner.js +134 -0
- package/src/ui/modal.js +215 -0
- package/src/ui/styles.js +519 -0
- package/src/ui/widget.js +105 -0
package/src/ui/banner.js
ADDED
|
@@ -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
|
+
}
|
package/src/ui/modal.js
ADDED
|
@@ -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
|
+
}
|