@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,519 @@
1
+ /**
2
+ * Styles - Shadow DOM encapsulated CSS with theming
3
+ */
4
+
5
+ import { safeColor, sanitizeCustomStyles } from '../core/security.js';
6
+
7
+ const DEFAULT_ACCENT = '#4F46E5';
8
+
9
+ /**
10
+ * Generate CSS with custom properties
11
+ */
12
+ export function generateStyles(config) {
13
+ // Only accept colors that pass strict validation — an unvalidated
14
+ // value is a CSS-injection vector (e.g. `red; } * { display:none; /*`).
15
+ const accentColor = safeColor(config.accentColor) || DEFAULT_ACCENT;
16
+ const customCss = sanitizeCustomStyles(config.customStyles);
17
+
18
+ return `
19
+ :host {
20
+ --zest-accent: ${accentColor};
21
+ --zest-accent-hover: ${adjustColor(accentColor, -15)};
22
+ --zest-bg: #ffffff;
23
+ --zest-bg-secondary: #f3f4f6;
24
+ --zest-text: #1f2937;
25
+ --zest-text-secondary: #6b7280;
26
+ --zest-border: #e5e7eb;
27
+ --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
28
+ --zest-radius: 12px;
29
+ --zest-radius-sm: 8px;
30
+ --zest-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
31
+
32
+ font-family: var(--zest-font);
33
+ font-size: 14px;
34
+ line-height: 1.5;
35
+ color: var(--zest-text);
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ :host([data-theme="dark"]) {
40
+ --zest-bg: #1f2937;
41
+ --zest-bg-secondary: #374151;
42
+ --zest-text: #f9fafb;
43
+ --zest-text-secondary: #9ca3af;
44
+ --zest-border: #4b5563;
45
+ --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
46
+ }
47
+
48
+ @media (prefers-color-scheme: dark) {
49
+ :host([data-theme="auto"]) {
50
+ --zest-bg: #1f2937;
51
+ --zest-bg-secondary: #374151;
52
+ --zest-text: #f9fafb;
53
+ --zest-text-secondary: #9ca3af;
54
+ --zest-border: #4b5563;
55
+ --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
56
+ }
57
+ }
58
+
59
+ *, *::before, *::after {
60
+ box-sizing: border-box;
61
+ }
62
+
63
+ /* Banner */
64
+ .zest-banner {
65
+ position: fixed;
66
+ z-index: 999999;
67
+ max-width: 480px;
68
+ padding: 20px;
69
+ background: var(--zest-bg);
70
+ border-radius: var(--zest-radius);
71
+ box-shadow: var(--zest-shadow);
72
+ animation: zest-slide-in 0.3s ease-out;
73
+ }
74
+
75
+ .zest-banner--bottom {
76
+ bottom: 20px;
77
+ left: 50%;
78
+ transform: translateX(-50%);
79
+ }
80
+
81
+ .zest-banner--bottom-left {
82
+ bottom: 20px;
83
+ left: 20px;
84
+ }
85
+
86
+ .zest-banner--bottom-right {
87
+ bottom: 20px;
88
+ right: 20px;
89
+ }
90
+
91
+ .zest-banner--top {
92
+ top: 20px;
93
+ left: 50%;
94
+ transform: translateX(-50%);
95
+ }
96
+
97
+ @keyframes zest-slide-in {
98
+ from {
99
+ opacity: 0;
100
+ transform: translateX(-50%) translateY(20px);
101
+ }
102
+ to {
103
+ opacity: 1;
104
+ transform: translateX(-50%) translateY(0);
105
+ }
106
+ }
107
+
108
+ .zest-banner--bottom-left {
109
+ animation-name: zest-slide-in-left;
110
+ }
111
+
112
+ @keyframes zest-slide-in-left {
113
+ from {
114
+ opacity: 0;
115
+ transform: translateY(20px);
116
+ }
117
+ to {
118
+ opacity: 1;
119
+ transform: translateY(0);
120
+ }
121
+ }
122
+
123
+ .zest-banner--bottom-right {
124
+ animation-name: zest-slide-in-right;
125
+ }
126
+
127
+ @keyframes zest-slide-in-right {
128
+ from {
129
+ opacity: 0;
130
+ transform: translateY(20px);
131
+ }
132
+ to {
133
+ opacity: 1;
134
+ transform: translateY(0);
135
+ }
136
+ }
137
+
138
+ @media (prefers-reduced-motion: reduce) {
139
+ .zest-banner,
140
+ .zest-modal {
141
+ animation: none;
142
+ }
143
+ }
144
+
145
+ .zest-banner__title {
146
+ margin: 0 0 8px 0;
147
+ font-size: 16px;
148
+ font-weight: 600;
149
+ color: var(--zest-text);
150
+ }
151
+
152
+ .zest-banner__description {
153
+ margin: 0 0 16px 0;
154
+ font-size: 14px;
155
+ color: var(--zest-text-secondary);
156
+ }
157
+
158
+ .zest-banner__buttons {
159
+ display: flex;
160
+ flex-wrap: wrap;
161
+ gap: 8px;
162
+ }
163
+
164
+ /* Buttons */
165
+ .zest-btn {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ padding: 10px 16px;
170
+ font-size: 14px;
171
+ font-weight: 500;
172
+ font-family: inherit;
173
+ border: none;
174
+ border-radius: var(--zest-radius-sm);
175
+ cursor: pointer;
176
+ transition: background-color 0.15s ease, transform 0.1s ease;
177
+ }
178
+
179
+ .zest-btn:hover {
180
+ transform: translateY(-1px);
181
+ }
182
+
183
+ .zest-btn:active {
184
+ transform: translateY(0);
185
+ }
186
+
187
+ .zest-btn:focus-visible {
188
+ outline: 2px solid var(--zest-accent);
189
+ outline-offset: 2px;
190
+ }
191
+
192
+ .zest-btn--primary {
193
+ background: var(--zest-accent);
194
+ color: #ffffff;
195
+ }
196
+
197
+ .zest-btn--primary:hover {
198
+ background: var(--zest-accent-hover);
199
+ }
200
+
201
+ .zest-btn--secondary {
202
+ background: var(--zest-bg-secondary);
203
+ color: var(--zest-text);
204
+ }
205
+
206
+ .zest-btn--secondary:hover {
207
+ background: var(--zest-border);
208
+ }
209
+
210
+ .zest-btn--ghost {
211
+ background: transparent;
212
+ color: var(--zest-text-secondary);
213
+ }
214
+
215
+ .zest-btn--ghost:hover {
216
+ background: var(--zest-bg-secondary);
217
+ color: var(--zest-text);
218
+ }
219
+
220
+ /* Modal */
221
+ .zest-modal-overlay {
222
+ position: fixed;
223
+ inset: 0;
224
+ z-index: 999998;
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ padding: 20px;
229
+ background: rgba(0, 0, 0, 0.5);
230
+ animation: zest-fade-in 0.2s ease-out;
231
+ }
232
+
233
+ @keyframes zest-fade-in {
234
+ from { opacity: 0; }
235
+ to { opacity: 1; }
236
+ }
237
+
238
+ .zest-modal {
239
+ width: 100%;
240
+ max-width: 500px;
241
+ max-height: 90vh;
242
+ overflow-y: auto;
243
+ background: var(--zest-bg);
244
+ border-radius: var(--zest-radius);
245
+ box-shadow: var(--zest-shadow);
246
+ animation: zest-modal-in 0.3s ease-out;
247
+ }
248
+
249
+ @keyframes zest-modal-in {
250
+ from {
251
+ opacity: 0;
252
+ transform: scale(0.95);
253
+ }
254
+ to {
255
+ opacity: 1;
256
+ transform: scale(1);
257
+ }
258
+ }
259
+
260
+ .zest-modal__header {
261
+ padding: 20px 20px 0;
262
+ }
263
+
264
+ .zest-modal__title {
265
+ margin: 0 0 8px 0;
266
+ font-size: 18px;
267
+ font-weight: 600;
268
+ color: var(--zest-text);
269
+ }
270
+
271
+ .zest-modal__description {
272
+ margin: 0;
273
+ font-size: 14px;
274
+ color: var(--zest-text-secondary);
275
+ }
276
+
277
+ .zest-modal__body {
278
+ padding: 20px;
279
+ }
280
+
281
+ .zest-modal__footer {
282
+ display: flex;
283
+ flex-wrap: wrap;
284
+ gap: 8px;
285
+ padding: 0 20px 20px;
286
+ }
287
+
288
+ /* Categories */
289
+ .zest-category {
290
+ padding: 16px;
291
+ margin-bottom: 12px;
292
+ background: var(--zest-bg-secondary);
293
+ border-radius: var(--zest-radius-sm);
294
+ }
295
+
296
+ .zest-category:last-child {
297
+ margin-bottom: 0;
298
+ }
299
+
300
+ .zest-category__header {
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: space-between;
304
+ gap: 12px;
305
+ }
306
+
307
+ .zest-category__info {
308
+ flex: 1;
309
+ }
310
+
311
+ .zest-category__label {
312
+ display: block;
313
+ font-size: 14px;
314
+ font-weight: 600;
315
+ color: var(--zest-text);
316
+ }
317
+
318
+ .zest-category__description {
319
+ margin: 4px 0 0;
320
+ font-size: 13px;
321
+ color: var(--zest-text-secondary);
322
+ }
323
+
324
+ /* Toggle Switch */
325
+ .zest-toggle {
326
+ position: relative;
327
+ width: 44px;
328
+ height: 24px;
329
+ flex-shrink: 0;
330
+ }
331
+
332
+ .zest-toggle__input {
333
+ position: absolute;
334
+ opacity: 0;
335
+ width: 100%;
336
+ height: 100%;
337
+ cursor: pointer;
338
+ margin: 0;
339
+ }
340
+
341
+ .zest-toggle__input:disabled {
342
+ cursor: not-allowed;
343
+ }
344
+
345
+ .zest-toggle__slider {
346
+ position: absolute;
347
+ inset: 0;
348
+ background: var(--zest-border);
349
+ border-radius: 12px;
350
+ transition: background-color 0.2s ease;
351
+ pointer-events: none;
352
+ }
353
+
354
+ .zest-toggle__slider::before {
355
+ content: '';
356
+ position: absolute;
357
+ top: 2px;
358
+ left: 2px;
359
+ width: 20px;
360
+ height: 20px;
361
+ background: #ffffff;
362
+ border-radius: 50%;
363
+ transition: transform 0.2s ease;
364
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
365
+ }
366
+
367
+ .zest-toggle__input:checked + .zest-toggle__slider {
368
+ background: var(--zest-accent);
369
+ }
370
+
371
+ .zest-toggle__input:checked + .zest-toggle__slider::before {
372
+ transform: translateX(20px);
373
+ }
374
+
375
+ .zest-toggle__input:focus-visible + .zest-toggle__slider {
376
+ outline: 2px solid var(--zest-accent);
377
+ outline-offset: 2px;
378
+ }
379
+
380
+ .zest-toggle__input:disabled + .zest-toggle__slider {
381
+ opacity: 0.6;
382
+ }
383
+
384
+ /* Widget */
385
+ .zest-widget {
386
+ position: fixed;
387
+ z-index: 999997;
388
+ bottom: 20px;
389
+ left: 20px;
390
+ }
391
+
392
+ .zest-widget__btn {
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: center;
396
+ width: 48px;
397
+ height: 48px;
398
+ padding: 0;
399
+ background: var(--zest-bg);
400
+ border: 1px solid var(--zest-border);
401
+ border-radius: 50%;
402
+ box-shadow: var(--zest-shadow);
403
+ cursor: pointer;
404
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
405
+ }
406
+
407
+ .zest-widget__btn:hover {
408
+ transform: scale(1.05);
409
+ box-shadow: 0 12px 28px -5px rgba(0, 0, 0, 0.15);
410
+ }
411
+
412
+ .zest-widget__btn:focus-visible {
413
+ outline: 2px solid var(--zest-accent);
414
+ outline-offset: 2px;
415
+ }
416
+
417
+ .zest-widget__icon {
418
+ width: 24px;
419
+ height: 24px;
420
+ fill: var(--zest-text);
421
+ }
422
+
423
+ /* Link */
424
+ .zest-link {
425
+ color: var(--zest-accent);
426
+ text-decoration: none;
427
+ }
428
+
429
+ .zest-link:hover {
430
+ text-decoration: underline;
431
+ }
432
+
433
+ /* Mobile */
434
+ @media (max-width: 480px) {
435
+ .zest-banner {
436
+ left: 10px;
437
+ right: 10px;
438
+ max-width: none;
439
+ transform: none;
440
+ }
441
+
442
+ .zest-banner--bottom,
443
+ .zest-banner--bottom-left,
444
+ .zest-banner--bottom-right {
445
+ bottom: 10px;
446
+ }
447
+
448
+ .zest-banner--top {
449
+ top: 10px;
450
+ transform: none;
451
+ }
452
+
453
+ @keyframes zest-slide-in {
454
+ from {
455
+ opacity: 0;
456
+ transform: translateY(20px);
457
+ }
458
+ to {
459
+ opacity: 1;
460
+ transform: translateY(0);
461
+ }
462
+ }
463
+
464
+ .zest-banner__buttons {
465
+ flex-direction: column;
466
+ }
467
+
468
+ .zest-btn {
469
+ width: 100%;
470
+ }
471
+
472
+ .zest-modal-overlay {
473
+ padding: 10px;
474
+ }
475
+
476
+ .zest-widget {
477
+ bottom: 10px;
478
+ left: 10px;
479
+ }
480
+ }
481
+
482
+ /* Hidden utility */
483
+ .zest-hidden {
484
+ display: none !important;
485
+ }
486
+ ${customCss}
487
+ `;
488
+ }
489
+
490
+ /**
491
+ * Adjust color brightness. Falls back to the default accent if the input
492
+ * cannot be parsed as a hex color (non-hex inputs pass safeColor but
493
+ * can't be brightness-shifted mathematically).
494
+ */
495
+ function adjustColor(hex, percent) {
496
+ if (typeof hex !== 'string' || !/^#[0-9a-fA-F]{3,8}$/.test(hex.trim())) {
497
+ hex = DEFAULT_ACCENT;
498
+ }
499
+ let clean = hex.trim().replace('#', '');
500
+ // Expand 3-digit form to 6
501
+ if (clean.length === 3) {
502
+ clean = clean.split('').map(c => c + c).join('');
503
+ }
504
+ // Strip alpha if present
505
+ if (clean.length === 8) clean = clean.slice(0, 6);
506
+ if (clean.length !== 6) clean = DEFAULT_ACCENT.slice(1);
507
+
508
+ const num = parseInt(clean, 16);
509
+ const amt = Math.round(2.55 * percent);
510
+ const R = Math.min(255, Math.max(0, (num >> 16) + amt));
511
+ const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt));
512
+ const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt));
513
+ return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
514
+ }
515
+
516
+ /**
517
+ * Cookie icon SVG
518
+ */
519
+ export 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>`;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Widget - Minimal floating button to reopen settings
3
+ */
4
+
5
+ import { generateStyles, COOKIE_ICON } from './styles.js';
6
+ import { getCurrentConfig } from '../config/parser.js';
7
+ import { escapeHTML } from '../core/security.js';
8
+
9
+ let widgetElement = null;
10
+ let shadowRoot = null;
11
+
12
+ /**
13
+ * Create the widget HTML
14
+ */
15
+ function createWidgetHTML(config) {
16
+ const labels = config.labels.widget;
17
+ const safeLabel = escapeHTML(labels.label);
18
+
19
+ return `
20
+ <div class="zest-widget">
21
+ <button type="button" class="zest-widget__btn" aria-label="${safeLabel}" title="${safeLabel}">
22
+ <span class="zest-widget__icon">${COOKIE_ICON}</span>
23
+ </button>
24
+ </div>
25
+ `;
26
+ }
27
+
28
+ /**
29
+ * Create and mount the widget
30
+ */
31
+ export function createWidget(callbacks = {}) {
32
+ if (widgetElement) {
33
+ return widgetElement;
34
+ }
35
+
36
+ const config = getCurrentConfig();
37
+
38
+ // Create host element
39
+ widgetElement = document.createElement('zest-widget');
40
+ widgetElement.setAttribute('data-theme', config.theme || 'light');
41
+
42
+ // Create shadow root
43
+ shadowRoot = widgetElement.attachShadow({ mode: 'open' });
44
+
45
+ // Add styles
46
+ const styleEl = document.createElement('style');
47
+ styleEl.textContent = generateStyles(config);
48
+ shadowRoot.appendChild(styleEl);
49
+
50
+ // Add widget HTML
51
+ const container = document.createElement('div');
52
+ container.innerHTML = createWidgetHTML(config);
53
+ shadowRoot.appendChild(container.firstElementChild);
54
+
55
+ // Add event listener
56
+ const button = shadowRoot.querySelector('.zest-widget__btn');
57
+ button.addEventListener('click', () => {
58
+ callbacks.onClick?.();
59
+ });
60
+
61
+ // Mount to document
62
+ document.body.appendChild(widgetElement);
63
+
64
+ return widgetElement;
65
+ }
66
+
67
+ /**
68
+ * Show the widget
69
+ */
70
+ export function showWidget(callbacks = {}) {
71
+ if (!widgetElement) {
72
+ createWidget(callbacks);
73
+ } else {
74
+ widgetElement.style.display = '';
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Hide the widget
80
+ */
81
+ export function hideWidget() {
82
+ if (widgetElement) {
83
+ widgetElement.style.display = 'none';
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Remove the widget completely
89
+ */
90
+ export function removeWidget() {
91
+ if (widgetElement) {
92
+ widgetElement.remove();
93
+ widgetElement = null;
94
+ shadowRoot = null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check if widget is visible
100
+ */
101
+ export function isWidgetVisible() {
102
+ return widgetElement !== null &&
103
+ document.body.contains(widgetElement) &&
104
+ widgetElement.style.display !== 'none';
105
+ }