@prosdevlab/experience-sdk-plugins 0.1.0 → 0.1.4
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/.turbo/turbo-build.log +17 -0
- package/CHANGELOG.md +63 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +694 -0
- package/dist/index.js.map +1 -0
- package/package.json +18 -11
- package/src/banner/banner.test.ts +370 -10
- package/src/banner/banner.ts +354 -129
- package/src/types.ts +22 -0
- package/src/utils/sanitize.test.ts +412 -0
- package/src/utils/sanitize.ts +196 -0
package/src/banner/banner.ts
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import type { PluginFunction } from '@lytics/sdk-kit';
|
|
9
9
|
import type { BannerContent, Decision, Experience } from '../types';
|
|
10
|
+
import { sanitizeHTML } from '../utils/sanitize';
|
|
10
11
|
|
|
11
12
|
export interface BannerPluginConfig {
|
|
12
13
|
banner?: {
|
|
13
14
|
position?: 'top' | 'bottom';
|
|
14
15
|
dismissable?: boolean;
|
|
15
16
|
zIndex?: number;
|
|
17
|
+
pushDown?: string; // CSS selector of element to push down (add margin-top)
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -32,7 +34,23 @@ export interface BannerPlugin {
|
|
|
32
34
|
* import { createInstance } from '@prosdevlab/experience-sdk';
|
|
33
35
|
* import { bannerPlugin } from '@prosdevlab/experience-sdk-plugins';
|
|
34
36
|
*
|
|
35
|
-
*
|
|
37
|
+
* // Basic usage (banner overlays at top)
|
|
38
|
+
* const sdk = createInstance({
|
|
39
|
+
* banner: {
|
|
40
|
+
* position: 'top',
|
|
41
|
+
* dismissable: true
|
|
42
|
+
* }
|
|
43
|
+
* });
|
|
44
|
+
* sdk.use(bannerPlugin);
|
|
45
|
+
*
|
|
46
|
+
* // With pushDown (pushes navigation down instead of overlaying)
|
|
47
|
+
* const sdk = createInstance({
|
|
48
|
+
* banner: {
|
|
49
|
+
* position: 'top',
|
|
50
|
+
* dismissable: true,
|
|
51
|
+
* pushDown: 'header' // CSS selector of element to push down
|
|
52
|
+
* }
|
|
53
|
+
* });
|
|
36
54
|
* sdk.use(bannerPlugin);
|
|
37
55
|
* ```
|
|
38
56
|
*/
|
|
@@ -51,6 +69,236 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
51
69
|
// Track multiple active banners by experience ID
|
|
52
70
|
const activeBanners = new Map<string, HTMLElement>();
|
|
53
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Inject default banner styles if not already present
|
|
74
|
+
*/
|
|
75
|
+
function injectDefaultStyles(): void {
|
|
76
|
+
const styleId = 'xp-banner-styles';
|
|
77
|
+
if (document.getElementById(styleId)) {
|
|
78
|
+
return; // Already injected
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const style = document.createElement('style');
|
|
82
|
+
style.id = styleId;
|
|
83
|
+
style.textContent = `
|
|
84
|
+
.xp-banner {
|
|
85
|
+
position: fixed;
|
|
86
|
+
left: 0;
|
|
87
|
+
right: 0;
|
|
88
|
+
width: 100%;
|
|
89
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
90
|
+
font-size: 14px;
|
|
91
|
+
line-height: 1.5;
|
|
92
|
+
box-sizing: border-box;
|
|
93
|
+
z-index: 10000;
|
|
94
|
+
background: #ffffff;
|
|
95
|
+
color: #111827;
|
|
96
|
+
border-bottom: 1px solid #e5e7eb;
|
|
97
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.xp-banner--top {
|
|
101
|
+
top: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.xp-banner--bottom {
|
|
105
|
+
bottom: 0;
|
|
106
|
+
border-bottom: none;
|
|
107
|
+
border-top: 1px solid #e5e7eb;
|
|
108
|
+
box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.xp-banner__container {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 16px;
|
|
115
|
+
max-width: 1280px;
|
|
116
|
+
margin: 0 auto;
|
|
117
|
+
padding: 14px 24px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.xp-banner__content {
|
|
121
|
+
flex: 1;
|
|
122
|
+
min-width: 0;
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: 4px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.xp-banner__title {
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
margin: 0;
|
|
131
|
+
font-size: 15px;
|
|
132
|
+
line-height: 1.4;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.xp-banner__message {
|
|
136
|
+
margin: 0;
|
|
137
|
+
font-size: 14px;
|
|
138
|
+
line-height: 1.5;
|
|
139
|
+
color: #6b7280;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.xp-banner__buttons {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 8px;
|
|
146
|
+
flex-shrink: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.xp-banner__button {
|
|
150
|
+
padding: 8px 16px;
|
|
151
|
+
border: none;
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
font-size: 14px;
|
|
154
|
+
font-weight: 500;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
transition: all 0.2s;
|
|
157
|
+
text-decoration: none;
|
|
158
|
+
display: inline-flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
white-space: nowrap;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.xp-banner__button--primary {
|
|
165
|
+
background: #2563eb;
|
|
166
|
+
color: #ffffff;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.xp-banner__button--primary:hover {
|
|
170
|
+
background: #1d4ed8;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.xp-banner__button--secondary {
|
|
174
|
+
background: #f3f4f6;
|
|
175
|
+
color: #374151;
|
|
176
|
+
border: 1px solid #e5e7eb;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.xp-banner__button--secondary:hover {
|
|
180
|
+
background: #e5e7eb;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.xp-banner__button--link {
|
|
184
|
+
background: transparent;
|
|
185
|
+
color: #2563eb;
|
|
186
|
+
padding: 6px 12px;
|
|
187
|
+
font-weight: 400;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.xp-banner__button--link:hover {
|
|
191
|
+
background: #f3f4f6;
|
|
192
|
+
text-decoration: underline;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.xp-banner__close {
|
|
196
|
+
background: transparent;
|
|
197
|
+
border: none;
|
|
198
|
+
color: #9ca3af;
|
|
199
|
+
font-size: 20px;
|
|
200
|
+
line-height: 1;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
padding: 4px;
|
|
203
|
+
margin: 0;
|
|
204
|
+
transition: color 0.2s;
|
|
205
|
+
flex-shrink: 0;
|
|
206
|
+
width: 28px;
|
|
207
|
+
height: 28px;
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
justify-content: center;
|
|
211
|
+
border-radius: 4px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.xp-banner__close:hover {
|
|
215
|
+
color: #111827;
|
|
216
|
+
background: #f3f4f6;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@media (max-width: 640px) {
|
|
220
|
+
.xp-banner__container {
|
|
221
|
+
flex-wrap: wrap;
|
|
222
|
+
padding: 14px 16px;
|
|
223
|
+
position: relative;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.xp-banner__content {
|
|
227
|
+
flex: 1 1 100%;
|
|
228
|
+
padding-right: 32px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.xp-banner__buttons {
|
|
232
|
+
flex: 1 1 auto;
|
|
233
|
+
width: 100%;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.xp-banner__button {
|
|
237
|
+
flex: 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.xp-banner__close {
|
|
241
|
+
position: absolute;
|
|
242
|
+
top: 12px;
|
|
243
|
+
right: 12px;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Dark mode support */
|
|
248
|
+
@media (prefers-color-scheme: dark) {
|
|
249
|
+
.xp-banner {
|
|
250
|
+
background: #111827;
|
|
251
|
+
color: #f9fafb;
|
|
252
|
+
border-bottom-color: #1f2937;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.xp-banner--bottom {
|
|
256
|
+
border-top-color: #1f2937;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.xp-banner__message {
|
|
260
|
+
color: #9ca3af;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.xp-banner__button--primary {
|
|
264
|
+
background: #3b82f6;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.xp-banner__button--primary:hover {
|
|
268
|
+
background: #2563eb;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.xp-banner__button--secondary {
|
|
272
|
+
background: #1f2937;
|
|
273
|
+
color: #f9fafb;
|
|
274
|
+
border-color: #374151;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.xp-banner__button--secondary:hover {
|
|
278
|
+
background: #374151;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.xp-banner__button--link {
|
|
282
|
+
color: #60a5fa;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.xp-banner__button--link:hover {
|
|
286
|
+
background: #1f2937;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.xp-banner__close {
|
|
290
|
+
color: #6b7280;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.xp-banner__close:hover {
|
|
294
|
+
color: #f9fafb;
|
|
295
|
+
background: #1f2937;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
document.head.appendChild(style);
|
|
300
|
+
}
|
|
301
|
+
|
|
54
302
|
/**
|
|
55
303
|
* Create banner DOM element
|
|
56
304
|
*/
|
|
@@ -61,89 +309,60 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
61
309
|
const dismissable = content.dismissable ?? config.get('banner.dismissable') ?? true;
|
|
62
310
|
const zIndex = config.get('banner.zIndex') ?? 10000;
|
|
63
311
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Theme-aware colors - professional subtle style
|
|
68
|
-
const bgColor = isDarkMode ? '#1f2937' : '#f9fafb';
|
|
69
|
-
const textColor = isDarkMode ? '#f3f4f6' : '#111827';
|
|
70
|
-
const borderColor = isDarkMode ? '#374151' : '#e5e7eb';
|
|
71
|
-
const shadowColor = isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.05)';
|
|
312
|
+
// Inject default styles if needed
|
|
313
|
+
injectDefaultStyles();
|
|
72
314
|
|
|
73
315
|
// Create banner container
|
|
74
316
|
const banner = document.createElement('div');
|
|
75
317
|
banner.setAttribute('data-experience-id', experience.id);
|
|
76
318
|
|
|
77
|
-
//
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
style.id = styleId;
|
|
82
|
-
style.textContent = `
|
|
83
|
-
@media (max-width: 640px) {
|
|
84
|
-
[data-experience-id="${experience.id}"] {
|
|
85
|
-
flex-direction: column !important;
|
|
86
|
-
align-items: flex-start !important;
|
|
87
|
-
}
|
|
88
|
-
[data-experience-id="${experience.id}"] > div:last-child {
|
|
89
|
-
width: 100%;
|
|
90
|
-
flex-direction: column !important;
|
|
91
|
-
}
|
|
92
|
-
[data-experience-id="${experience.id}"] button {
|
|
93
|
-
width: 100%;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
`;
|
|
97
|
-
document.head.appendChild(style);
|
|
319
|
+
// Build className: base classes + position + user's custom class
|
|
320
|
+
const baseClasses = ['xp-banner', `xp-banner--${position}`];
|
|
321
|
+
if (content.className) {
|
|
322
|
+
baseClasses.push(content.className);
|
|
98
323
|
}
|
|
324
|
+
banner.className = baseClasses.join(' ');
|
|
99
325
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
display: flex;
|
|
115
|
-
align-items: center;
|
|
116
|
-
justify-content: space-between;
|
|
117
|
-
box-sizing: border-box;
|
|
118
|
-
`;
|
|
326
|
+
// Apply user's custom styles
|
|
327
|
+
if (content.style) {
|
|
328
|
+
Object.assign(banner.style, content.style);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Override z-index if configured
|
|
332
|
+
if (zIndex !== 10000) {
|
|
333
|
+
banner.style.zIndex = String(zIndex);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Create container
|
|
337
|
+
const container = document.createElement('div');
|
|
338
|
+
container.className = 'xp-banner__container';
|
|
339
|
+
banner.appendChild(container);
|
|
119
340
|
|
|
120
341
|
// Create content container
|
|
121
342
|
const contentDiv = document.createElement('div');
|
|
122
|
-
contentDiv.
|
|
343
|
+
contentDiv.className = 'xp-banner__content';
|
|
123
344
|
|
|
124
345
|
// Add title if present
|
|
125
346
|
if (content.title) {
|
|
126
|
-
const title = document.createElement('
|
|
127
|
-
title.
|
|
128
|
-
|
|
347
|
+
const title = document.createElement('h3');
|
|
348
|
+
title.className = 'xp-banner__title';
|
|
349
|
+
// Sanitize HTML to prevent XSS attacks
|
|
350
|
+
title.innerHTML = sanitizeHTML(content.title);
|
|
129
351
|
contentDiv.appendChild(title);
|
|
130
352
|
}
|
|
131
353
|
|
|
132
354
|
// Add message
|
|
133
|
-
const message = document.createElement('
|
|
134
|
-
message.
|
|
355
|
+
const message = document.createElement('p');
|
|
356
|
+
message.className = 'xp-banner__message';
|
|
357
|
+
// Sanitize HTML to prevent XSS attacks
|
|
358
|
+
message.innerHTML = sanitizeHTML(content.message);
|
|
135
359
|
contentDiv.appendChild(message);
|
|
136
360
|
|
|
137
|
-
|
|
361
|
+
container.appendChild(contentDiv);
|
|
138
362
|
|
|
139
|
-
// Create
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
display: flex;
|
|
143
|
-
align-items: center;
|
|
144
|
-
gap: 12px;
|
|
145
|
-
flex-wrap: wrap;
|
|
146
|
-
`;
|
|
363
|
+
// Create buttons container
|
|
364
|
+
const buttonsDiv = document.createElement('div');
|
|
365
|
+
buttonsDiv.className = 'xp-banner__buttons';
|
|
147
366
|
|
|
148
367
|
// Helper function to create button with variant styling
|
|
149
368
|
function createButton(buttonConfig: {
|
|
@@ -152,53 +371,25 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
152
371
|
url?: string;
|
|
153
372
|
variant?: 'primary' | 'secondary' | 'link';
|
|
154
373
|
metadata?: Record<string, unknown>;
|
|
374
|
+
className?: string;
|
|
375
|
+
style?: Record<string, string>;
|
|
155
376
|
}): HTMLButtonElement {
|
|
156
377
|
const button = document.createElement('button');
|
|
157
378
|
button.textContent = buttonConfig.text;
|
|
158
379
|
|
|
159
380
|
const variant = buttonConfig.variant || 'primary';
|
|
160
381
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
bg = isDarkMode ? '#3b82f6' : '#2563eb';
|
|
166
|
-
hoverBg = isDarkMode ? '#2563eb' : '#1d4ed8';
|
|
167
|
-
textColor = '#ffffff';
|
|
168
|
-
border = 'none';
|
|
169
|
-
} else if (variant === 'secondary') {
|
|
170
|
-
bg = isDarkMode ? '#374151' : '#ffffff';
|
|
171
|
-
hoverBg = isDarkMode ? '#4b5563' : '#f9fafb';
|
|
172
|
-
textColor = isDarkMode ? '#f3f4f6' : '#374151';
|
|
173
|
-
border = isDarkMode ? '1px solid #4b5563' : '1px solid #d1d5db';
|
|
174
|
-
} else {
|
|
175
|
-
// 'link'
|
|
176
|
-
bg = 'transparent';
|
|
177
|
-
hoverBg = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)';
|
|
178
|
-
textColor = isDarkMode ? '#93c5fd' : '#2563eb';
|
|
179
|
-
border = 'none';
|
|
382
|
+
// Build className: base class + variant + user's custom class
|
|
383
|
+
const buttonClasses = ['xp-banner__button', `xp-banner__button--${variant}`];
|
|
384
|
+
if (buttonConfig.className) {
|
|
385
|
+
buttonClasses.push(buttonConfig.className);
|
|
180
386
|
}
|
|
387
|
+
button.className = buttonClasses.join(' ');
|
|
181
388
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
padding: ${variant === 'link' ? '4px 8px' : '8px 16px'};
|
|
187
|
-
font-size: 14px;
|
|
188
|
-
font-weight: ${variant === 'link' ? '400' : '500'};
|
|
189
|
-
border-radius: 6px;
|
|
190
|
-
cursor: pointer;
|
|
191
|
-
transition: all 0.2s;
|
|
192
|
-
text-decoration: ${variant === 'link' ? 'underline' : 'none'};
|
|
193
|
-
`;
|
|
194
|
-
|
|
195
|
-
button.addEventListener('mouseenter', () => {
|
|
196
|
-
button.style.background = hoverBg;
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
button.addEventListener('mouseleave', () => {
|
|
200
|
-
button.style.background = bg;
|
|
201
|
-
});
|
|
389
|
+
// Apply user's custom styles
|
|
390
|
+
if (buttonConfig.style) {
|
|
391
|
+
Object.assign(button.style, buttonConfig.style);
|
|
392
|
+
}
|
|
202
393
|
|
|
203
394
|
button.addEventListener('click', () => {
|
|
204
395
|
// Emit action event
|
|
@@ -225,39 +416,17 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
225
416
|
if (content.buttons && content.buttons.length > 0) {
|
|
226
417
|
content.buttons.forEach((buttonConfig) => {
|
|
227
418
|
const button = createButton(buttonConfig);
|
|
228
|
-
|
|
419
|
+
buttonsDiv.appendChild(button);
|
|
229
420
|
});
|
|
230
421
|
}
|
|
231
422
|
|
|
232
423
|
// Add dismiss button if dismissable
|
|
233
424
|
if (dismissable) {
|
|
234
425
|
const closeButton = document.createElement('button');
|
|
426
|
+
closeButton.className = 'xp-banner__close';
|
|
235
427
|
closeButton.innerHTML = '×';
|
|
236
428
|
closeButton.setAttribute('aria-label', 'Close banner');
|
|
237
429
|
|
|
238
|
-
const closeColor = isDarkMode ? '#9ca3af' : '#6b7280';
|
|
239
|
-
|
|
240
|
-
closeButton.style.cssText = `
|
|
241
|
-
background: transparent;
|
|
242
|
-
border: none;
|
|
243
|
-
color: ${closeColor};
|
|
244
|
-
font-size: 24px;
|
|
245
|
-
line-height: 1;
|
|
246
|
-
cursor: pointer;
|
|
247
|
-
padding: 0;
|
|
248
|
-
margin: 0;
|
|
249
|
-
opacity: 0.7;
|
|
250
|
-
transition: opacity 0.2s;
|
|
251
|
-
`;
|
|
252
|
-
|
|
253
|
-
closeButton.addEventListener('mouseenter', () => {
|
|
254
|
-
closeButton.style.opacity = '1';
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
closeButton.addEventListener('mouseleave', () => {
|
|
258
|
-
closeButton.style.opacity = '0.7';
|
|
259
|
-
});
|
|
260
|
-
|
|
261
430
|
closeButton.addEventListener('click', () => {
|
|
262
431
|
remove(experience.id);
|
|
263
432
|
instance.emit('experiences:dismissed', {
|
|
@@ -266,14 +435,57 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
266
435
|
});
|
|
267
436
|
});
|
|
268
437
|
|
|
269
|
-
|
|
438
|
+
buttonsDiv.appendChild(closeButton);
|
|
270
439
|
}
|
|
271
440
|
|
|
272
|
-
|
|
441
|
+
container.appendChild(buttonsDiv);
|
|
273
442
|
|
|
274
443
|
return banner;
|
|
275
444
|
}
|
|
276
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Apply pushDown margin to target element
|
|
448
|
+
*/
|
|
449
|
+
function applyPushDown(banner: HTMLElement, position: 'top' | 'bottom'): void {
|
|
450
|
+
const pushDownSelector = config.get('banner.pushDown');
|
|
451
|
+
|
|
452
|
+
if (!pushDownSelector || position !== 'top') {
|
|
453
|
+
return; // Only push down for top banners
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const targetElement = document.querySelector(pushDownSelector);
|
|
457
|
+
if (!targetElement || !(targetElement instanceof HTMLElement)) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Get banner height
|
|
462
|
+
const height = banner.offsetHeight;
|
|
463
|
+
|
|
464
|
+
// Apply margin-top with transition
|
|
465
|
+
targetElement.style.transition = 'margin-top 0.3s ease';
|
|
466
|
+
targetElement.style.marginTop = `${height}px`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Remove pushDown margin from target element
|
|
471
|
+
*/
|
|
472
|
+
function removePushDown(): void {
|
|
473
|
+
const pushDownSelector = config.get('banner.pushDown');
|
|
474
|
+
|
|
475
|
+
if (!pushDownSelector) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const targetElement = document.querySelector(pushDownSelector);
|
|
480
|
+
if (!targetElement || !(targetElement instanceof HTMLElement)) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Remove margin-top with transition
|
|
485
|
+
targetElement.style.transition = 'margin-top 0.3s ease';
|
|
486
|
+
targetElement.style.marginTop = '0';
|
|
487
|
+
}
|
|
488
|
+
|
|
277
489
|
/**
|
|
278
490
|
* Show a banner experience
|
|
279
491
|
*/
|
|
@@ -292,6 +504,11 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
292
504
|
document.body.appendChild(banner);
|
|
293
505
|
activeBanners.set(experience.id, banner);
|
|
294
506
|
|
|
507
|
+
// Apply pushDown to target element if configured
|
|
508
|
+
const content = experience.content as BannerContent;
|
|
509
|
+
const position = content.position ?? config.get('banner.position') ?? 'top';
|
|
510
|
+
applyPushDown(banner, position);
|
|
511
|
+
|
|
295
512
|
instance.emit('experiences:shown', {
|
|
296
513
|
experienceId: experience.id,
|
|
297
514
|
type: 'banner',
|
|
@@ -310,6 +527,11 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
310
527
|
banner.parentNode.removeChild(banner);
|
|
311
528
|
}
|
|
312
529
|
activeBanners.delete(experienceId);
|
|
530
|
+
|
|
531
|
+
// Remove pushDown if no more banners
|
|
532
|
+
if (activeBanners.size === 0) {
|
|
533
|
+
removePushDown();
|
|
534
|
+
}
|
|
313
535
|
} else {
|
|
314
536
|
// Remove all banners
|
|
315
537
|
for (const [id, banner] of activeBanners.entries()) {
|
|
@@ -318,6 +540,9 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
318
540
|
}
|
|
319
541
|
activeBanners.delete(id);
|
|
320
542
|
}
|
|
543
|
+
|
|
544
|
+
// Remove pushDown
|
|
545
|
+
removePushDown();
|
|
321
546
|
}
|
|
322
547
|
}
|
|
323
548
|
|
package/src/types.ts
CHANGED
|
@@ -20,9 +20,31 @@ export interface BannerContent {
|
|
|
20
20
|
url?: string;
|
|
21
21
|
variant?: 'primary' | 'secondary' | 'link';
|
|
22
22
|
metadata?: Record<string, any>;
|
|
23
|
+
className?: string;
|
|
24
|
+
style?: Record<string, string>;
|
|
23
25
|
}>;
|
|
24
26
|
dismissable?: boolean;
|
|
25
27
|
position?: 'top' | 'bottom';
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Modal content configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface ModalContent {
|
|
36
|
+
title: string;
|
|
37
|
+
message: string;
|
|
38
|
+
confirmText?: string;
|
|
39
|
+
cancelText?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tooltip content configuration
|
|
44
|
+
*/
|
|
45
|
+
export interface TooltipContent {
|
|
46
|
+
message: string;
|
|
47
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
26
48
|
}
|
|
27
49
|
|
|
28
50
|
/**
|