@prosdevlab/experience-sdk-plugins 0.1.0 → 0.1.3
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 +37 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.js +634 -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 +246 -119
- 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,6 +7,7 @@
|
|
|
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?: {
|
|
@@ -51,6 +52,200 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
51
52
|
// Track multiple active banners by experience ID
|
|
52
53
|
const activeBanners = new Map<string, HTMLElement>();
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Inject default banner styles if not already present
|
|
57
|
+
*/
|
|
58
|
+
function injectDefaultStyles(): void {
|
|
59
|
+
const styleId = 'xp-banner-styles';
|
|
60
|
+
if (document.getElementById(styleId)) {
|
|
61
|
+
return; // Already injected
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const style = document.createElement('style');
|
|
65
|
+
style.id = styleId;
|
|
66
|
+
style.textContent = `
|
|
67
|
+
.xp-banner {
|
|
68
|
+
position: fixed;
|
|
69
|
+
left: 0;
|
|
70
|
+
right: 0;
|
|
71
|
+
width: 100%;
|
|
72
|
+
padding: 16px 20px;
|
|
73
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
74
|
+
font-size: 14px;
|
|
75
|
+
line-height: 1.5;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: space-between;
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
z-index: 10000;
|
|
81
|
+
background: #f9fafb;
|
|
82
|
+
color: #111827;
|
|
83
|
+
border-bottom: 1px solid #e5e7eb;
|
|
84
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.xp-banner--top {
|
|
88
|
+
top: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.xp-banner--bottom {
|
|
92
|
+
bottom: 0;
|
|
93
|
+
border-bottom: none;
|
|
94
|
+
border-top: 1px solid #e5e7eb;
|
|
95
|
+
box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.xp-banner__container {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
gap: 20px;
|
|
103
|
+
width: 100%;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.xp-banner__content {
|
|
107
|
+
flex: 1;
|
|
108
|
+
min-width: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.xp-banner__title {
|
|
112
|
+
font-weight: 600;
|
|
113
|
+
margin-bottom: 4px;
|
|
114
|
+
margin-top: 0;
|
|
115
|
+
font-size: 14px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.xp-banner__message {
|
|
119
|
+
margin: 0;
|
|
120
|
+
font-size: 14px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.xp-banner__buttons {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: 12px;
|
|
127
|
+
flex-wrap: wrap;
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.xp-banner__button {
|
|
132
|
+
padding: 8px 16px;
|
|
133
|
+
border: none;
|
|
134
|
+
border-radius: 6px;
|
|
135
|
+
font-size: 14px;
|
|
136
|
+
font-weight: 500;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
transition: all 0.2s;
|
|
139
|
+
text-decoration: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.xp-banner__button--primary {
|
|
143
|
+
background: #2563eb;
|
|
144
|
+
color: #ffffff;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.xp-banner__button--primary:hover {
|
|
148
|
+
background: #1d4ed8;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.xp-banner__button--secondary {
|
|
152
|
+
background: #ffffff;
|
|
153
|
+
color: #374151;
|
|
154
|
+
border: 1px solid #d1d5db;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.xp-banner__button--secondary:hover {
|
|
158
|
+
background: #f9fafb;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.xp-banner__button--link {
|
|
162
|
+
background: transparent;
|
|
163
|
+
color: #2563eb;
|
|
164
|
+
padding: 4px 8px;
|
|
165
|
+
font-weight: 400;
|
|
166
|
+
text-decoration: underline;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.xp-banner__button--link:hover {
|
|
170
|
+
background: rgba(0, 0, 0, 0.05);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.xp-banner__close {
|
|
174
|
+
background: transparent;
|
|
175
|
+
border: none;
|
|
176
|
+
color: #6b7280;
|
|
177
|
+
font-size: 24px;
|
|
178
|
+
line-height: 1;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
padding: 0;
|
|
181
|
+
margin: 0;
|
|
182
|
+
opacity: 0.7;
|
|
183
|
+
transition: opacity 0.2s;
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.xp-banner__close:hover {
|
|
188
|
+
opacity: 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@media (max-width: 640px) {
|
|
192
|
+
.xp-banner__container {
|
|
193
|
+
flex-direction: column;
|
|
194
|
+
align-items: stretch;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.xp-banner__buttons {
|
|
198
|
+
width: 100%;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.xp-banner__button {
|
|
203
|
+
width: 100%;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Dark mode support */
|
|
208
|
+
@media (prefers-color-scheme: dark) {
|
|
209
|
+
.xp-banner {
|
|
210
|
+
background: #1f2937;
|
|
211
|
+
color: #f3f4f6;
|
|
212
|
+
border-bottom-color: #374151;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.xp-banner--bottom {
|
|
216
|
+
border-top-color: #374151;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.xp-banner__button--primary {
|
|
220
|
+
background: #3b82f6;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.xp-banner__button--primary:hover {
|
|
224
|
+
background: #2563eb;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.xp-banner__button--secondary {
|
|
228
|
+
background: #374151;
|
|
229
|
+
color: #f3f4f6;
|
|
230
|
+
border-color: #4b5563;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.xp-banner__button--secondary:hover {
|
|
234
|
+
background: #4b5563;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.xp-banner__button--link {
|
|
238
|
+
color: #93c5fd;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.xp-banner__close {
|
|
242
|
+
color: #9ca3af;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
`;
|
|
246
|
+
document.head.appendChild(style);
|
|
247
|
+
}
|
|
248
|
+
|
|
54
249
|
/**
|
|
55
250
|
* Create banner DOM element
|
|
56
251
|
*/
|
|
@@ -61,79 +256,57 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
61
256
|
const dismissable = content.dismissable ?? config.get('banner.dismissable') ?? true;
|
|
62
257
|
const zIndex = config.get('banner.zIndex') ?? 10000;
|
|
63
258
|
|
|
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)';
|
|
259
|
+
// Inject default styles if needed
|
|
260
|
+
injectDefaultStyles();
|
|
72
261
|
|
|
73
262
|
// Create banner container
|
|
74
263
|
const banner = document.createElement('div');
|
|
75
264
|
banner.setAttribute('data-experience-id', experience.id);
|
|
76
265
|
|
|
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);
|
|
266
|
+
// Build className: base classes + position + user's custom class
|
|
267
|
+
const baseClasses = ['xp-banner', `xp-banner--${position}`];
|
|
268
|
+
if (content.className) {
|
|
269
|
+
baseClasses.push(content.className);
|
|
98
270
|
}
|
|
271
|
+
banner.className = baseClasses.join(' ');
|
|
99
272
|
|
|
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
|
-
`;
|
|
273
|
+
// Apply user's custom styles
|
|
274
|
+
if (content.style) {
|
|
275
|
+
Object.assign(banner.style, content.style);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Override z-index if configured
|
|
279
|
+
if (zIndex !== 10000) {
|
|
280
|
+
banner.style.zIndex = String(zIndex);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Create container
|
|
284
|
+
const container = document.createElement('div');
|
|
285
|
+
container.className = 'xp-banner__container';
|
|
286
|
+
banner.appendChild(container);
|
|
119
287
|
|
|
120
288
|
// Create content container
|
|
121
289
|
const contentDiv = document.createElement('div');
|
|
122
|
-
contentDiv.
|
|
290
|
+
contentDiv.className = 'xp-banner__content';
|
|
123
291
|
|
|
124
292
|
// Add title if present
|
|
125
293
|
if (content.title) {
|
|
126
|
-
const title = document.createElement('
|
|
127
|
-
title.
|
|
128
|
-
|
|
294
|
+
const title = document.createElement('h3');
|
|
295
|
+
title.className = 'xp-banner__title';
|
|
296
|
+
// Sanitize HTML to prevent XSS attacks
|
|
297
|
+
title.innerHTML = sanitizeHTML(content.title);
|
|
129
298
|
contentDiv.appendChild(title);
|
|
130
299
|
}
|
|
131
300
|
|
|
132
301
|
// Add message
|
|
133
|
-
const message = document.createElement('
|
|
134
|
-
message.
|
|
302
|
+
const message = document.createElement('p');
|
|
303
|
+
message.className = 'xp-banner__message';
|
|
304
|
+
// Sanitize HTML to prevent XSS attacks
|
|
305
|
+
message.innerHTML = sanitizeHTML(content.message);
|
|
135
306
|
contentDiv.appendChild(message);
|
|
136
307
|
|
|
308
|
+
container.appendChild(contentDiv);
|
|
309
|
+
|
|
137
310
|
banner.appendChild(contentDiv);
|
|
138
311
|
|
|
139
312
|
// Create button container for actions and/or dismiss
|
|
@@ -145,6 +318,10 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
145
318
|
flex-wrap: wrap;
|
|
146
319
|
`;
|
|
147
320
|
|
|
321
|
+
// Create buttons container
|
|
322
|
+
const buttonsDiv = document.createElement('div');
|
|
323
|
+
buttonsDiv.className = 'xp-banner__buttons';
|
|
324
|
+
|
|
148
325
|
// Helper function to create button with variant styling
|
|
149
326
|
function createButton(buttonConfig: {
|
|
150
327
|
text: string;
|
|
@@ -152,53 +329,25 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
152
329
|
url?: string;
|
|
153
330
|
variant?: 'primary' | 'secondary' | 'link';
|
|
154
331
|
metadata?: Record<string, unknown>;
|
|
332
|
+
className?: string;
|
|
333
|
+
style?: Record<string, string>;
|
|
155
334
|
}): HTMLButtonElement {
|
|
156
335
|
const button = document.createElement('button');
|
|
157
336
|
button.textContent = buttonConfig.text;
|
|
158
337
|
|
|
159
338
|
const variant = buttonConfig.variant || 'primary';
|
|
160
339
|
|
|
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';
|
|
340
|
+
// Build className: base class + variant + user's custom class
|
|
341
|
+
const buttonClasses = ['xp-banner__button', `xp-banner__button--${variant}`];
|
|
342
|
+
if (buttonConfig.className) {
|
|
343
|
+
buttonClasses.push(buttonConfig.className);
|
|
180
344
|
}
|
|
345
|
+
button.className = buttonClasses.join(' ');
|
|
181
346
|
|
|
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
|
-
});
|
|
347
|
+
// Apply user's custom styles
|
|
348
|
+
if (buttonConfig.style) {
|
|
349
|
+
Object.assign(button.style, buttonConfig.style);
|
|
350
|
+
}
|
|
202
351
|
|
|
203
352
|
button.addEventListener('click', () => {
|
|
204
353
|
// Emit action event
|
|
@@ -225,39 +374,17 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
225
374
|
if (content.buttons && content.buttons.length > 0) {
|
|
226
375
|
content.buttons.forEach((buttonConfig) => {
|
|
227
376
|
const button = createButton(buttonConfig);
|
|
228
|
-
|
|
377
|
+
buttonsDiv.appendChild(button);
|
|
229
378
|
});
|
|
230
379
|
}
|
|
231
380
|
|
|
232
381
|
// Add dismiss button if dismissable
|
|
233
382
|
if (dismissable) {
|
|
234
383
|
const closeButton = document.createElement('button');
|
|
384
|
+
closeButton.className = 'xp-banner__close';
|
|
235
385
|
closeButton.innerHTML = '×';
|
|
236
386
|
closeButton.setAttribute('aria-label', 'Close banner');
|
|
237
387
|
|
|
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
388
|
closeButton.addEventListener('click', () => {
|
|
262
389
|
remove(experience.id);
|
|
263
390
|
instance.emit('experiences:dismissed', {
|
|
@@ -266,10 +393,10 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
266
393
|
});
|
|
267
394
|
});
|
|
268
395
|
|
|
269
|
-
|
|
396
|
+
buttonsDiv.appendChild(closeButton);
|
|
270
397
|
}
|
|
271
398
|
|
|
272
|
-
|
|
399
|
+
container.appendChild(buttonsDiv);
|
|
273
400
|
|
|
274
401
|
return banner;
|
|
275
402
|
}
|
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
|
/**
|