@prosdevlab/experience-sdk-plugins 0.1.3 → 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 +6 -6
- package/CHANGELOG.md +26 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +107 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/banner/banner.ts +149 -51
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosdevlab/experience-sdk-plugins@0.1.
|
|
2
|
+
> @prosdevlab/experience-sdk-plugins@0.1.4 build /home/runner/work/experience-sdk/experience-sdk/packages/plugins
|
|
3
3
|
> tsup
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
[34mCLI[39m Target: es2024
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
13
|
-
[32mESM[39m [1mdist/index.js.map [22m[
|
|
14
|
-
[32mESM[39m ⚡️ Build success in
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m19.53 KB[39m
|
|
13
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m48.09 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 140ms
|
|
15
15
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
17
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m5.
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 857ms
|
|
17
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m5.46 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @prosdevlab/experience-sdk-plugins
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- df2c286: feat(banner): add pushDown option to push content down instead of overlay
|
|
8
|
+
|
|
9
|
+
Add optional `pushDown` config to banner plugin that allows top banners to smoothly push page content down (add margin-top) instead of overlaying it.
|
|
10
|
+
|
|
11
|
+
**Usage:**
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
init({
|
|
15
|
+
banner: {
|
|
16
|
+
position: "top",
|
|
17
|
+
pushDown: "header", // CSS selector of element to push down
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Benefits:**
|
|
23
|
+
|
|
24
|
+
- Opt-in feature (default behavior unchanged)
|
|
25
|
+
- Smooth transition with CSS animations
|
|
26
|
+
- Improves UX for sticky navigation
|
|
27
|
+
- Automatically removes margin when banner is dismissed
|
|
28
|
+
|
|
3
29
|
## 0.1.3
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -117,6 +117,7 @@ interface BannerPluginConfig {
|
|
|
117
117
|
position?: 'top' | 'bottom';
|
|
118
118
|
dismissable?: boolean;
|
|
119
119
|
zIndex?: number;
|
|
120
|
+
pushDown?: string;
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
interface BannerPlugin {
|
|
@@ -134,7 +135,23 @@ interface BannerPlugin {
|
|
|
134
135
|
* import { createInstance } from '@prosdevlab/experience-sdk';
|
|
135
136
|
* import { bannerPlugin } from '@prosdevlab/experience-sdk-plugins';
|
|
136
137
|
*
|
|
137
|
-
*
|
|
138
|
+
* // Basic usage (banner overlays at top)
|
|
139
|
+
* const sdk = createInstance({
|
|
140
|
+
* banner: {
|
|
141
|
+
* position: 'top',
|
|
142
|
+
* dismissable: true
|
|
143
|
+
* }
|
|
144
|
+
* });
|
|
145
|
+
* sdk.use(bannerPlugin);
|
|
146
|
+
*
|
|
147
|
+
* // With pushDown (pushes navigation down instead of overlaying)
|
|
148
|
+
* const sdk = createInstance({
|
|
149
|
+
* banner: {
|
|
150
|
+
* position: 'top',
|
|
151
|
+
* dismissable: true,
|
|
152
|
+
* pushDown: 'header' // CSS selector of element to push down
|
|
153
|
+
* }
|
|
154
|
+
* });
|
|
138
155
|
* sdk.use(bannerPlugin);
|
|
139
156
|
* ```
|
|
140
157
|
*/
|
package/dist/index.js
CHANGED
|
@@ -115,16 +115,12 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
115
115
|
left: 0;
|
|
116
116
|
right: 0;
|
|
117
117
|
width: 100%;
|
|
118
|
-
padding: 16px 20px;
|
|
119
118
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
120
119
|
font-size: 14px;
|
|
121
120
|
line-height: 1.5;
|
|
122
|
-
display: flex;
|
|
123
|
-
align-items: center;
|
|
124
|
-
justify-content: space-between;
|
|
125
121
|
box-sizing: border-box;
|
|
126
122
|
z-index: 10000;
|
|
127
|
-
background: #
|
|
123
|
+
background: #ffffff;
|
|
128
124
|
color: #111827;
|
|
129
125
|
border-bottom: 1px solid #e5e7eb;
|
|
130
126
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
@@ -144,33 +140,38 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
144
140
|
.xp-banner__container {
|
|
145
141
|
display: flex;
|
|
146
142
|
align-items: center;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
gap: 16px;
|
|
144
|
+
max-width: 1280px;
|
|
145
|
+
margin: 0 auto;
|
|
146
|
+
padding: 14px 24px;
|
|
150
147
|
}
|
|
151
148
|
|
|
152
149
|
.xp-banner__content {
|
|
153
150
|
flex: 1;
|
|
154
151
|
min-width: 0;
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
gap: 4px;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
.xp-banner__title {
|
|
158
158
|
font-weight: 600;
|
|
159
|
-
margin
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
margin: 0;
|
|
160
|
+
font-size: 15px;
|
|
161
|
+
line-height: 1.4;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
.xp-banner__message {
|
|
165
165
|
margin: 0;
|
|
166
166
|
font-size: 14px;
|
|
167
|
+
line-height: 1.5;
|
|
168
|
+
color: #6b7280;
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
.xp-banner__buttons {
|
|
170
172
|
display: flex;
|
|
171
173
|
align-items: center;
|
|
172
|
-
gap:
|
|
173
|
-
flex-wrap: wrap;
|
|
174
|
+
gap: 8px;
|
|
174
175
|
flex-shrink: 0;
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -183,6 +184,10 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
183
184
|
cursor: pointer;
|
|
184
185
|
transition: all 0.2s;
|
|
185
186
|
text-decoration: none;
|
|
187
|
+
display: inline-flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
white-space: nowrap;
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
.xp-banner__button--primary {
|
|
@@ -195,71 +200,93 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
.xp-banner__button--secondary {
|
|
198
|
-
background: #
|
|
203
|
+
background: #f3f4f6;
|
|
199
204
|
color: #374151;
|
|
200
|
-
border: 1px solid #
|
|
205
|
+
border: 1px solid #e5e7eb;
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
.xp-banner__button--secondary:hover {
|
|
204
|
-
background: #
|
|
209
|
+
background: #e5e7eb;
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
.xp-banner__button--link {
|
|
208
213
|
background: transparent;
|
|
209
214
|
color: #2563eb;
|
|
210
|
-
padding:
|
|
215
|
+
padding: 6px 12px;
|
|
211
216
|
font-weight: 400;
|
|
212
|
-
text-decoration: underline;
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
.xp-banner__button--link:hover {
|
|
216
|
-
background:
|
|
220
|
+
background: #f3f4f6;
|
|
221
|
+
text-decoration: underline;
|
|
217
222
|
}
|
|
218
223
|
|
|
219
224
|
.xp-banner__close {
|
|
220
225
|
background: transparent;
|
|
221
226
|
border: none;
|
|
222
|
-
color: #
|
|
223
|
-
font-size:
|
|
227
|
+
color: #9ca3af;
|
|
228
|
+
font-size: 20px;
|
|
224
229
|
line-height: 1;
|
|
225
230
|
cursor: pointer;
|
|
226
|
-
padding:
|
|
231
|
+
padding: 4px;
|
|
227
232
|
margin: 0;
|
|
228
|
-
|
|
229
|
-
transition: opacity 0.2s;
|
|
233
|
+
transition: color 0.2s;
|
|
230
234
|
flex-shrink: 0;
|
|
235
|
+
width: 28px;
|
|
236
|
+
height: 28px;
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
justify-content: center;
|
|
240
|
+
border-radius: 4px;
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
.xp-banner__close:hover {
|
|
234
|
-
|
|
244
|
+
color: #111827;
|
|
245
|
+
background: #f3f4f6;
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
@media (max-width: 640px) {
|
|
238
249
|
.xp-banner__container {
|
|
239
|
-
flex-
|
|
240
|
-
|
|
250
|
+
flex-wrap: wrap;
|
|
251
|
+
padding: 14px 16px;
|
|
252
|
+
position: relative;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.xp-banner__content {
|
|
256
|
+
flex: 1 1 100%;
|
|
257
|
+
padding-right: 32px;
|
|
241
258
|
}
|
|
242
259
|
|
|
243
260
|
.xp-banner__buttons {
|
|
261
|
+
flex: 1 1 auto;
|
|
244
262
|
width: 100%;
|
|
245
|
-
flex-direction: column;
|
|
246
263
|
}
|
|
247
264
|
|
|
248
265
|
.xp-banner__button {
|
|
249
|
-
|
|
266
|
+
flex: 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.xp-banner__close {
|
|
270
|
+
position: absolute;
|
|
271
|
+
top: 12px;
|
|
272
|
+
right: 12px;
|
|
250
273
|
}
|
|
251
274
|
}
|
|
252
275
|
|
|
253
276
|
/* Dark mode support */
|
|
254
277
|
@media (prefers-color-scheme: dark) {
|
|
255
278
|
.xp-banner {
|
|
256
|
-
background: #
|
|
257
|
-
color: #
|
|
258
|
-
border-bottom-color: #
|
|
279
|
+
background: #111827;
|
|
280
|
+
color: #f9fafb;
|
|
281
|
+
border-bottom-color: #1f2937;
|
|
259
282
|
}
|
|
260
283
|
|
|
261
284
|
.xp-banner--bottom {
|
|
262
|
-
border-top-color: #
|
|
285
|
+
border-top-color: #1f2937;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.xp-banner__message {
|
|
289
|
+
color: #9ca3af;
|
|
263
290
|
}
|
|
264
291
|
|
|
265
292
|
.xp-banner__button--primary {
|
|
@@ -271,21 +298,30 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
271
298
|
}
|
|
272
299
|
|
|
273
300
|
.xp-banner__button--secondary {
|
|
274
|
-
background: #
|
|
275
|
-
color: #
|
|
276
|
-
border-color: #
|
|
301
|
+
background: #1f2937;
|
|
302
|
+
color: #f9fafb;
|
|
303
|
+
border-color: #374151;
|
|
277
304
|
}
|
|
278
305
|
|
|
279
306
|
.xp-banner__button--secondary:hover {
|
|
280
|
-
background: #
|
|
307
|
+
background: #374151;
|
|
281
308
|
}
|
|
282
309
|
|
|
283
310
|
.xp-banner__button--link {
|
|
284
|
-
color: #
|
|
311
|
+
color: #60a5fa;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.xp-banner__button--link:hover {
|
|
315
|
+
background: #1f2937;
|
|
285
316
|
}
|
|
286
317
|
|
|
287
318
|
.xp-banner__close {
|
|
288
|
-
color: #
|
|
319
|
+
color: #6b7280;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.xp-banner__close:hover {
|
|
323
|
+
color: #f9fafb;
|
|
324
|
+
background: #1f2937;
|
|
289
325
|
}
|
|
290
326
|
}
|
|
291
327
|
`;
|
|
@@ -326,14 +362,6 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
326
362
|
message.innerHTML = sanitizeHTML(content.message);
|
|
327
363
|
contentDiv.appendChild(message);
|
|
328
364
|
container.appendChild(contentDiv);
|
|
329
|
-
banner.appendChild(contentDiv);
|
|
330
|
-
const buttonContainer = document.createElement("div");
|
|
331
|
-
buttonContainer.style.cssText = `
|
|
332
|
-
display: flex;
|
|
333
|
-
align-items: center;
|
|
334
|
-
gap: 12px;
|
|
335
|
-
flex-wrap: wrap;
|
|
336
|
-
`;
|
|
337
365
|
const buttonsDiv = document.createElement("div");
|
|
338
366
|
buttonsDiv.className = "xp-banner__buttons";
|
|
339
367
|
function createButton(buttonConfig) {
|
|
@@ -387,6 +415,31 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
387
415
|
container.appendChild(buttonsDiv);
|
|
388
416
|
return banner;
|
|
389
417
|
}
|
|
418
|
+
function applyPushDown(banner, position) {
|
|
419
|
+
const pushDownSelector = config.get("banner.pushDown");
|
|
420
|
+
if (!pushDownSelector || position !== "top") {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const targetElement = document.querySelector(pushDownSelector);
|
|
424
|
+
if (!targetElement || !(targetElement instanceof HTMLElement)) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const height = banner.offsetHeight;
|
|
428
|
+
targetElement.style.transition = "margin-top 0.3s ease";
|
|
429
|
+
targetElement.style.marginTop = `${height}px`;
|
|
430
|
+
}
|
|
431
|
+
function removePushDown() {
|
|
432
|
+
const pushDownSelector = config.get("banner.pushDown");
|
|
433
|
+
if (!pushDownSelector) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const targetElement = document.querySelector(pushDownSelector);
|
|
437
|
+
if (!targetElement || !(targetElement instanceof HTMLElement)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
targetElement.style.transition = "margin-top 0.3s ease";
|
|
441
|
+
targetElement.style.marginTop = "0";
|
|
442
|
+
}
|
|
390
443
|
function show(experience) {
|
|
391
444
|
if (activeBanners.has(experience.id)) {
|
|
392
445
|
return;
|
|
@@ -397,6 +450,9 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
397
450
|
const banner = createBannerElement(experience);
|
|
398
451
|
document.body.appendChild(banner);
|
|
399
452
|
activeBanners.set(experience.id, banner);
|
|
453
|
+
const content = experience.content;
|
|
454
|
+
const position = content.position ?? config.get("banner.position") ?? "top";
|
|
455
|
+
applyPushDown(banner, position);
|
|
400
456
|
instance.emit("experiences:shown", {
|
|
401
457
|
experienceId: experience.id,
|
|
402
458
|
type: "banner",
|
|
@@ -410,6 +466,9 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
410
466
|
banner.parentNode.removeChild(banner);
|
|
411
467
|
}
|
|
412
468
|
activeBanners.delete(experienceId);
|
|
469
|
+
if (activeBanners.size === 0) {
|
|
470
|
+
removePushDown();
|
|
471
|
+
}
|
|
413
472
|
} else {
|
|
414
473
|
for (const [id, banner] of activeBanners.entries()) {
|
|
415
474
|
if (banner?.parentNode) {
|
|
@@ -417,6 +476,7 @@ var bannerPlugin = (plugin, instance, config) => {
|
|
|
417
476
|
}
|
|
418
477
|
activeBanners.delete(id);
|
|
419
478
|
}
|
|
479
|
+
removePushDown();
|
|
420
480
|
}
|
|
421
481
|
}
|
|
422
482
|
function isShowing() {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/sanitize.ts","../src/banner/banner.ts","../src/debug/debug.ts","../src/frequency/frequency.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * HTML Sanitizer\n *\n * Lightweight HTML sanitizer for experience content (messages, titles).\n * Whitelist-based approach that only allows safe formatting tags.\n *\n * Security: Prevents XSS attacks by stripping dangerous tags and attributes.\n */\n\n/**\n * Allowed HTML tags for sanitization\n * Only safe formatting tags are permitted\n */\nconst ALLOWED_TAGS = ['strong', 'em', 'a', 'br', 'span', 'b', 'i', 'p'] as const;\n\n/**\n * Allowed attributes per tag\n */\nconst ALLOWED_ATTRIBUTES: Record<string, string[]> = {\n a: ['href', 'class', 'style', 'title'],\n span: ['class', 'style'],\n p: ['class', 'style'],\n // Other tags have no attributes allowed\n};\n\n/**\n * Sanitize HTML string by removing dangerous tags and attributes\n *\n * @param html - HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n *\n * @example\n * ```typescript\n * sanitizeHTML('<strong>Hello</strong><script>alert(\"xss\")</script>');\n * // Returns: '<strong>Hello</strong>'\n * ```\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') {\n return '';\n }\n\n // Create a temporary DOM element to parse HTML\n const temp = document.createElement('div');\n temp.innerHTML = html;\n\n /**\n * Recursively sanitize a DOM node\n */\n function sanitizeNode(node: Node): string {\n // Text nodes - escape HTML entities\n if (node.nodeType === Node.TEXT_NODE) {\n return escapeHTML(node.textContent || '');\n }\n\n // Element nodes\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element;\n const tagName = element.tagName.toLowerCase();\n\n // Handle tags with whitespace (malformed HTML like \"< script >\")\n // Browser normalizes these, but if we see a tag that's not in our list,\n // it might be a dangerous tag that was normalized\n if (!tagName || tagName.includes(' ')) {\n return '';\n }\n\n // If tag is not allowed, return empty string\n if (!ALLOWED_TAGS.includes(tagName as any)) {\n return '';\n }\n\n // Get allowed attributes for this tag\n const allowedAttrs = ALLOWED_ATTRIBUTES[tagName] || [];\n\n // Build attribute string\n const attrs: string[] = [];\n for (const attr of allowedAttrs) {\n const value = element.getAttribute(attr);\n if (value !== null) {\n // Sanitize attribute values\n if (attr === 'href') {\n // Only allow safe URLs (http, https, mailto, tel, relative)\n const sanitizedHref = sanitizeURL(value);\n if (sanitizedHref) {\n attrs.push(`href=\"${escapeAttribute(sanitizedHref)}\"`);\n }\n } else {\n // For all other attributes (title, class, style), escape HTML entities\n attrs.push(`${attr}=\"${escapeAttribute(value)}\"`);\n }\n }\n }\n\n const attrString = attrs.length > 0 ? ' ' + attrs.join(' ') : '';\n\n // Process child nodes\n let innerHTML = '';\n for (const child of Array.from(element.childNodes)) {\n innerHTML += sanitizeNode(child);\n }\n\n // Self-closing tags\n if (tagName === 'br') {\n return `<br${attrString} />`;\n }\n\n return `<${tagName}${attrString}>${innerHTML}</${tagName}>`;\n }\n\n return '';\n }\n\n // Sanitize all nodes\n let sanitized = '';\n for (const child of Array.from(temp.childNodes)) {\n sanitized += sanitizeNode(child);\n }\n\n return sanitized;\n}\n\n/**\n * Escape HTML entities to prevent XSS in text content\n */\nfunction escapeHTML(text: string): string {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Escape HTML entities for use in attribute values\n */\nfunction escapeAttribute(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Sanitize URL to prevent javascript: and data: XSS attacks\n *\n * @param url - URL to sanitize\n * @returns Sanitized URL or empty string if unsafe\n */\nfunction sanitizeURL(url: string): string {\n if (!url || typeof url !== 'string') {\n return '';\n }\n\n // Decode URL-encoded characters to check for encoded attacks\n let decoded: string;\n try {\n decoded = decodeURIComponent(url);\n } catch {\n // If decoding fails, use original\n decoded = url;\n }\n\n const trimmed = decoded.trim().toLowerCase();\n\n // Block javascript: and data: protocols (check both original and decoded)\n if (\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n url.toLowerCase().trim().startsWith('javascript:') ||\n url.toLowerCase().trim().startsWith('data:')\n ) {\n return '';\n }\n\n // Allow http, https, mailto, tel, and relative URLs\n if (\n trimmed.startsWith('http://') ||\n trimmed.startsWith('https://') ||\n trimmed.startsWith('mailto:') ||\n trimmed.startsWith('tel:') ||\n trimmed.startsWith('/') ||\n trimmed.startsWith('#') ||\n trimmed.startsWith('?')\n ) {\n return url; // Return original (case preserved)\n }\n\n // Allow relative paths without protocol\n if (!trimmed.includes(':')) {\n return url;\n }\n\n // Block everything else\n return '';\n}\n","/**\n * Banner Plugin\n *\n * Renders banner experiences at the top or bottom of the page.\n * Auto-shows banners when experiences are evaluated.\n */\n\nimport type { PluginFunction } from '@lytics/sdk-kit';\nimport type { BannerContent, Decision, Experience } from '../types';\nimport { sanitizeHTML } from '../utils/sanitize';\n\nexport interface BannerPluginConfig {\n banner?: {\n position?: 'top' | 'bottom';\n dismissable?: boolean;\n zIndex?: number;\n };\n}\n\nexport interface BannerPlugin {\n show(experience: Experience): void;\n remove(): void;\n isShowing(): boolean;\n}\n\n/**\n * Banner Plugin\n *\n * Automatically renders banner experiences when they are evaluated.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@prosdevlab/experience-sdk';\n * import { bannerPlugin } from '@prosdevlab/experience-sdk-plugins';\n *\n * const sdk = createInstance({ banner: { position: 'top', dismissable: true } });\n * sdk.use(bannerPlugin);\n * ```\n */\nexport const bannerPlugin: PluginFunction = (plugin, instance, config) => {\n plugin.ns('banner');\n\n // Set defaults\n plugin.defaults({\n banner: {\n position: 'top',\n dismissable: true,\n zIndex: 10000,\n },\n });\n\n // Track multiple active banners by experience ID\n const activeBanners = new Map<string, HTMLElement>();\n\n /**\n * Inject default banner styles if not already present\n */\n function injectDefaultStyles(): void {\n const styleId = 'xp-banner-styles';\n if (document.getElementById(styleId)) {\n return; // Already injected\n }\n\n const style = document.createElement('style');\n style.id = styleId;\n style.textContent = `\n .xp-banner {\n position: fixed;\n left: 0;\n right: 0;\n width: 100%;\n padding: 16px 20px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n display: flex;\n align-items: center;\n justify-content: space-between;\n box-sizing: border-box;\n z-index: 10000;\n background: #f9fafb;\n color: #111827;\n border-bottom: 1px solid #e5e7eb;\n box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);\n }\n \n .xp-banner--top {\n top: 0;\n }\n \n .xp-banner--bottom {\n bottom: 0;\n border-bottom: none;\n border-top: 1px solid #e5e7eb;\n box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0.05);\n }\n \n .xp-banner__container {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 20px;\n width: 100%;\n }\n \n .xp-banner__content {\n flex: 1;\n min-width: 0;\n }\n \n .xp-banner__title {\n font-weight: 600;\n margin-bottom: 4px;\n margin-top: 0;\n font-size: 14px;\n }\n \n .xp-banner__message {\n margin: 0;\n font-size: 14px;\n }\n \n .xp-banner__buttons {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n flex-shrink: 0;\n }\n \n .xp-banner__button {\n padding: 8px 16px;\n border: none;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n text-decoration: none;\n }\n \n .xp-banner__button--primary {\n background: #2563eb;\n color: #ffffff;\n }\n \n .xp-banner__button--primary:hover {\n background: #1d4ed8;\n }\n \n .xp-banner__button--secondary {\n background: #ffffff;\n color: #374151;\n border: 1px solid #d1d5db;\n }\n \n .xp-banner__button--secondary:hover {\n background: #f9fafb;\n }\n \n .xp-banner__button--link {\n background: transparent;\n color: #2563eb;\n padding: 4px 8px;\n font-weight: 400;\n text-decoration: underline;\n }\n \n .xp-banner__button--link:hover {\n background: rgba(0, 0, 0, 0.05);\n }\n \n .xp-banner__close {\n background: transparent;\n border: none;\n color: #6b7280;\n font-size: 24px;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n margin: 0;\n opacity: 0.7;\n transition: opacity 0.2s;\n flex-shrink: 0;\n }\n \n .xp-banner__close:hover {\n opacity: 1;\n }\n \n @media (max-width: 640px) {\n .xp-banner__container {\n flex-direction: column;\n align-items: stretch;\n }\n \n .xp-banner__buttons {\n width: 100%;\n flex-direction: column;\n }\n \n .xp-banner__button {\n width: 100%;\n }\n }\n \n /* Dark mode support */\n @media (prefers-color-scheme: dark) {\n .xp-banner {\n background: #1f2937;\n color: #f3f4f6;\n border-bottom-color: #374151;\n }\n \n .xp-banner--bottom {\n border-top-color: #374151;\n }\n \n .xp-banner__button--primary {\n background: #3b82f6;\n }\n \n .xp-banner__button--primary:hover {\n background: #2563eb;\n }\n \n .xp-banner__button--secondary {\n background: #374151;\n color: #f3f4f6;\n border-color: #4b5563;\n }\n \n .xp-banner__button--secondary:hover {\n background: #4b5563;\n }\n \n .xp-banner__button--link {\n color: #93c5fd;\n }\n \n .xp-banner__close {\n color: #9ca3af;\n }\n }\n `;\n document.head.appendChild(style);\n }\n\n /**\n * Create banner DOM element\n */\n function createBannerElement(experience: Experience): HTMLElement {\n const content = experience.content as BannerContent;\n // Allow per-experience position override, fall back to global config\n const position = content.position ?? config.get('banner.position') ?? 'top';\n const dismissable = content.dismissable ?? config.get('banner.dismissable') ?? true;\n const zIndex = config.get('banner.zIndex') ?? 10000;\n\n // Inject default styles if needed\n injectDefaultStyles();\n\n // Create banner container\n const banner = document.createElement('div');\n banner.setAttribute('data-experience-id', experience.id);\n\n // Build className: base classes + position + user's custom class\n const baseClasses = ['xp-banner', `xp-banner--${position}`];\n if (content.className) {\n baseClasses.push(content.className);\n }\n banner.className = baseClasses.join(' ');\n\n // Apply user's custom styles\n if (content.style) {\n Object.assign(banner.style, content.style);\n }\n\n // Override z-index if configured\n if (zIndex !== 10000) {\n banner.style.zIndex = String(zIndex);\n }\n\n // Create container\n const container = document.createElement('div');\n container.className = 'xp-banner__container';\n banner.appendChild(container);\n\n // Create content container\n const contentDiv = document.createElement('div');\n contentDiv.className = 'xp-banner__content';\n\n // Add title if present\n if (content.title) {\n const title = document.createElement('h3');\n title.className = 'xp-banner__title';\n // Sanitize HTML to prevent XSS attacks\n title.innerHTML = sanitizeHTML(content.title);\n contentDiv.appendChild(title);\n }\n\n // Add message\n const message = document.createElement('p');\n message.className = 'xp-banner__message';\n // Sanitize HTML to prevent XSS attacks\n message.innerHTML = sanitizeHTML(content.message);\n contentDiv.appendChild(message);\n\n container.appendChild(contentDiv);\n\n banner.appendChild(contentDiv);\n\n // Create button container for actions and/or dismiss\n const buttonContainer = document.createElement('div');\n buttonContainer.style.cssText = `\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n `;\n\n // Create buttons container\n const buttonsDiv = document.createElement('div');\n buttonsDiv.className = 'xp-banner__buttons';\n\n // Helper function to create button with variant styling\n function createButton(buttonConfig: {\n text: string;\n action?: string;\n url?: string;\n variant?: 'primary' | 'secondary' | 'link';\n metadata?: Record<string, unknown>;\n className?: string;\n style?: Record<string, string>;\n }): HTMLButtonElement {\n const button = document.createElement('button');\n button.textContent = buttonConfig.text;\n\n const variant = buttonConfig.variant || 'primary';\n\n // Build className: base class + variant + user's custom class\n const buttonClasses = ['xp-banner__button', `xp-banner__button--${variant}`];\n if (buttonConfig.className) {\n buttonClasses.push(buttonConfig.className);\n }\n button.className = buttonClasses.join(' ');\n\n // Apply user's custom styles\n if (buttonConfig.style) {\n Object.assign(button.style, buttonConfig.style);\n }\n\n button.addEventListener('click', () => {\n // Emit action event\n instance.emit('experiences:action', {\n experienceId: experience.id,\n type: 'banner',\n action: buttonConfig.action,\n url: buttonConfig.url,\n metadata: buttonConfig.metadata,\n variant: variant,\n timestamp: Date.now(),\n });\n\n // Navigate if URL provided\n if (buttonConfig.url) {\n window.location.href = buttonConfig.url;\n }\n });\n\n return button;\n }\n\n // Add buttons from buttons array\n if (content.buttons && content.buttons.length > 0) {\n content.buttons.forEach((buttonConfig) => {\n const button = createButton(buttonConfig);\n buttonsDiv.appendChild(button);\n });\n }\n\n // Add dismiss button if dismissable\n if (dismissable) {\n const closeButton = document.createElement('button');\n closeButton.className = 'xp-banner__close';\n closeButton.innerHTML = '×';\n closeButton.setAttribute('aria-label', 'Close banner');\n\n closeButton.addEventListener('click', () => {\n remove(experience.id);\n instance.emit('experiences:dismissed', {\n experienceId: experience.id,\n type: 'banner',\n });\n });\n\n buttonsDiv.appendChild(closeButton);\n }\n\n container.appendChild(buttonsDiv);\n\n return banner;\n }\n\n /**\n * Show a banner experience\n */\n function show(experience: Experience): void {\n // If banner already showing for this experience, skip\n if (activeBanners.has(experience.id)) {\n return;\n }\n\n // Only show if we're in a browser environment\n if (typeof document === 'undefined') {\n return;\n }\n\n const banner = createBannerElement(experience);\n document.body.appendChild(banner);\n activeBanners.set(experience.id, banner);\n\n instance.emit('experiences:shown', {\n experienceId: experience.id,\n type: 'banner',\n timestamp: Date.now(),\n });\n }\n\n /**\n * Remove a banner by experience ID (or all if no ID provided)\n */\n function remove(experienceId?: string): void {\n if (experienceId) {\n // Remove specific banner\n const banner = activeBanners.get(experienceId);\n if (banner?.parentNode) {\n banner.parentNode.removeChild(banner);\n }\n activeBanners.delete(experienceId);\n } else {\n // Remove all banners\n for (const [id, banner] of activeBanners.entries()) {\n if (banner?.parentNode) {\n banner.parentNode.removeChild(banner);\n }\n activeBanners.delete(id);\n }\n }\n }\n\n /**\n * Check if any banner is currently showing\n */\n function isShowing(): boolean {\n return activeBanners.size > 0;\n }\n\n // Expose banner API\n plugin.expose({\n banner: {\n show,\n remove,\n isShowing,\n },\n });\n\n // Auto-show banner on experiences:evaluated event\n instance.on('experiences:evaluated', (payload: unknown) => {\n // Handle both single decision and array of decisions\n // evaluate() emits: { decision, experience }\n // evaluateAll() emits: [{ decision, experience }, ...]\n const items = Array.isArray(payload) ? payload : [payload];\n\n for (const item of items) {\n // Item is { decision, experience }\n const typedItem = item as { decision?: Decision; experience?: Experience };\n const decision = typedItem.decision;\n const experience = typedItem.experience;\n\n // Only handle banner-type experiences\n if (experience?.type === 'banner') {\n if (decision?.show) {\n show(experience);\n } else if (experience.id && activeBanners.has(experience.id)) {\n // Hide specific banner if decision says don't show\n remove(experience.id);\n }\n }\n }\n });\n\n // Cleanup on destroy\n instance.on('sdk:destroy', () => {\n remove();\n });\n};\n","/**\n * Debug Plugin\n *\n * Emits structured debug events to window and optionally logs to console.\n * Useful for debugging and Chrome extension integration.\n */\n\nimport type { PluginFunction } from '@lytics/sdk-kit';\n\nexport interface DebugPluginConfig {\n debug?: {\n enabled?: boolean;\n console?: boolean;\n window?: boolean;\n };\n}\n\nexport interface DebugPlugin {\n log(message: string, data?: unknown): void;\n isEnabled(): boolean;\n}\n\n/**\n * Debug Plugin\n *\n * Listens to all SDK events and emits them as window events for debugging.\n * Also optionally logs to console.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@prosdevlab/experience-sdk';\n * import { debugPlugin } from '@prosdevlab/experience-sdk-plugins';\n *\n * const sdk = createInstance({ debug: { enabled: true, console: true } });\n * sdk.use(debugPlugin);\n * ```\n */\nexport const debugPlugin: PluginFunction = (plugin, instance, config) => {\n plugin.ns('debug');\n\n // Set defaults\n plugin.defaults({\n debug: {\n enabled: false,\n console: false,\n window: true,\n },\n });\n\n // Helper to check if debug is enabled\n const isEnabled = (): boolean => config.get('debug.enabled') ?? false;\n const shouldLogConsole = (): boolean => config.get('debug.console') ?? false;\n const shouldEmitWindow = (): boolean => config.get('debug.window') ?? true;\n\n // Log function\n const log = (message: string, data?: unknown): void => {\n if (!isEnabled()) return;\n\n const timestamp = new Date().toISOString();\n const logData = {\n timestamp,\n message,\n data,\n };\n\n // Console logging\n if (shouldLogConsole()) {\n console.log(`[experiences] ${message}`, data || '');\n }\n\n // Window event emission\n if (shouldEmitWindow() && typeof window !== 'undefined') {\n const event = new CustomEvent('experience-sdk:debug', {\n detail: logData,\n });\n window.dispatchEvent(event);\n }\n };\n\n // Expose debug API\n plugin.expose({\n debug: {\n log,\n isEnabled,\n },\n });\n\n // If debug is enabled, listen to all events\n if (isEnabled()) {\n // Listen to experiences:* events\n instance.on('experiences:ready', () => {\n if (!isEnabled()) return;\n log('SDK initialized and ready');\n });\n\n instance.on('experiences:registered', (payload) => {\n if (!isEnabled()) return;\n log('Experience registered', payload);\n });\n\n instance.on('experiences:evaluated', (payload) => {\n if (!isEnabled()) return;\n log('Experience evaluated', payload);\n });\n }\n};\n","/**\n * Frequency Capping Plugin\n *\n * Tracks experience impressions and enforces frequency caps.\n * Uses sdk-kit's storage plugin for persistence.\n */\n\nimport type { PluginFunction, SDK } from '@lytics/sdk-kit';\nimport { type StoragePlugin, storagePlugin } from '@lytics/sdk-kit-plugins';\nimport type { Decision, TraceStep } from '../types';\n\nexport interface FrequencyPluginConfig {\n frequency?: {\n enabled?: boolean;\n namespace?: string;\n };\n}\n\nexport interface FrequencyPlugin {\n getImpressionCount(experienceId: string): number;\n hasReachedCap(experienceId: string, max: number, per: 'session' | 'day' | 'week'): boolean;\n recordImpression(experienceId: string): void;\n}\n\ninterface ImpressionData {\n count: number;\n lastImpression: number;\n impressions: number[];\n per?: 'session' | 'day' | 'week'; // Track which storage type this uses\n}\n\n/**\n * Frequency Capping Plugin\n *\n * Automatically tracks impressions and enforces frequency caps.\n * Requires storage plugin for persistence.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@prosdevlab/experience-sdk';\n * import { frequencyPlugin } from '@prosdevlab/experience-sdk-plugins';\n *\n * const sdk = createInstance({ frequency: { enabled: true } });\n * sdk.use(frequencyPlugin);\n * ```\n */\nexport const frequencyPlugin: PluginFunction = (plugin, instance, config) => {\n plugin.ns('frequency');\n\n // Set defaults\n plugin.defaults({\n frequency: {\n enabled: true,\n namespace: 'experiences:frequency',\n },\n });\n\n // Track experience frequency configs\n const experienceFrequencyMap = new Map<string, 'session' | 'day' | 'week'>();\n\n // Auto-load storage plugin if not already loaded\n if (!(instance as SDK & { storage?: StoragePlugin }).storage) {\n instance.use(storagePlugin);\n }\n\n const isEnabled = (): boolean => config.get('frequency.enabled') ?? true;\n const getNamespace = (): string => config.get('frequency.namespace') ?? 'experiences:frequency';\n\n // Helper to get the right storage backend based on frequency type\n const getStorageBackend = (per: 'session' | 'day' | 'week'): Storage => {\n return per === 'session' ? sessionStorage : localStorage;\n };\n\n // Helper to get storage key\n const getStorageKey = (experienceId: string): string => {\n return `${getNamespace()}:${experienceId}`;\n };\n\n // Helper to get impression data\n const getImpressionData = (\n experienceId: string,\n per: 'session' | 'day' | 'week'\n ): ImpressionData => {\n const storage = getStorageBackend(per);\n const key = getStorageKey(experienceId);\n const raw = storage.getItem(key);\n\n if (!raw) {\n return {\n count: 0,\n lastImpression: 0,\n impressions: [],\n per,\n };\n }\n\n try {\n return JSON.parse(raw) as ImpressionData;\n } catch {\n return {\n count: 0,\n lastImpression: 0,\n impressions: [],\n per,\n };\n }\n };\n\n // Helper to save impression data\n const saveImpressionData = (experienceId: string, data: ImpressionData): void => {\n const per = data.per || 'session'; // Default to session if not specified\n const storage = getStorageBackend(per);\n const key = getStorageKey(experienceId);\n storage.setItem(key, JSON.stringify(data));\n };\n\n // Get time window in milliseconds\n const getTimeWindow = (per: 'session' | 'day' | 'week'): number => {\n switch (per) {\n case 'session':\n return Number.POSITIVE_INFINITY; // Session storage handles this\n case 'day':\n return 24 * 60 * 60 * 1000; // 24 hours\n case 'week':\n return 7 * 24 * 60 * 60 * 1000; // 7 days\n }\n };\n\n /**\n * Get impression count for an experience\n */\n const getImpressionCount = (\n experienceId: string,\n per: 'session' | 'day' | 'week' = 'session'\n ): number => {\n if (!isEnabled()) return 0;\n const data = getImpressionData(experienceId, per);\n return data.count;\n };\n\n /**\n * Check if an experience has reached its frequency cap\n */\n const hasReachedCap = (\n experienceId: string,\n max: number,\n per: 'session' | 'day' | 'week'\n ): boolean => {\n if (!isEnabled()) return false;\n\n const data = getImpressionData(experienceId, per);\n const timeWindow = getTimeWindow(per);\n const now = Date.now();\n\n // For session caps, just check total count\n if (per === 'session') {\n return data.count >= max;\n }\n\n // For time-based caps, count impressions within the window\n const recentImpressions = data.impressions.filter((timestamp) => now - timestamp < timeWindow);\n\n return recentImpressions.length >= max;\n };\n\n /**\n * Record an impression for an experience\n */\n const recordImpression = (\n experienceId: string,\n per: 'session' | 'day' | 'week' = 'session'\n ): void => {\n if (!isEnabled()) return;\n\n const data = getImpressionData(experienceId, per);\n const now = Date.now();\n\n // Update count and add timestamp\n data.count += 1;\n data.lastImpression = now;\n data.impressions.push(now);\n data.per = per; // Store the frequency type\n\n // Keep only recent impressions (last 7 days)\n const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;\n data.impressions = data.impressions.filter((ts) => ts > sevenDaysAgo);\n\n // Save updated data\n saveImpressionData(experienceId, data);\n\n // Emit event\n instance.emit('experiences:impression-recorded', {\n experienceId,\n count: data.count,\n timestamp: now,\n });\n };\n\n // Expose frequency API\n plugin.expose({\n frequency: {\n getImpressionCount,\n hasReachedCap,\n recordImpression,\n // Internal method to register experience frequency config\n _registerExperience: (experienceId: string, per: 'session' | 'day' | 'week') => {\n experienceFrequencyMap.set(experienceId, per);\n },\n },\n });\n\n // Listen to evaluation events and record impressions\n if (isEnabled()) {\n instance.on('experiences:evaluated', (payload: unknown) => {\n // Handle both single decision and array of decisions\n // evaluate() emits: { decision, experience }\n // evaluateAll() emits: [{ decision, experience }, ...]\n const items = Array.isArray(payload) ? payload : [payload];\n\n for (const item of items) {\n // Item is { decision, experience }\n const decision = (item as { decision?: Decision }).decision;\n\n // Only record if experience was shown\n if (decision?.show && decision.experienceId) {\n // Try to get the 'per' value from our map, fall back to checking the input in trace\n let per: 'session' | 'day' | 'week' =\n experienceFrequencyMap.get(decision.experienceId) || 'session';\n\n // If not in map, try to infer from the decision trace\n if (!experienceFrequencyMap.has(decision.experienceId)) {\n const freqStep = decision.trace.find(\n (t: TraceStep) => t.step === 'check-frequency-cap'\n );\n if (freqStep?.input && typeof freqStep.input === 'object' && 'per' in freqStep.input) {\n per = (freqStep.input as { per: 'session' | 'day' | 'week' }).per;\n // Cache it for next time\n experienceFrequencyMap.set(decision.experienceId, per);\n }\n }\n\n recordImpression(decision.experienceId, per);\n }\n }\n });\n }\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/sanitize.ts","../src/banner/banner.ts","../src/debug/debug.ts","../src/frequency/frequency.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * HTML Sanitizer\n *\n * Lightweight HTML sanitizer for experience content (messages, titles).\n * Whitelist-based approach that only allows safe formatting tags.\n *\n * Security: Prevents XSS attacks by stripping dangerous tags and attributes.\n */\n\n/**\n * Allowed HTML tags for sanitization\n * Only safe formatting tags are permitted\n */\nconst ALLOWED_TAGS = ['strong', 'em', 'a', 'br', 'span', 'b', 'i', 'p'] as const;\n\n/**\n * Allowed attributes per tag\n */\nconst ALLOWED_ATTRIBUTES: Record<string, string[]> = {\n a: ['href', 'class', 'style', 'title'],\n span: ['class', 'style'],\n p: ['class', 'style'],\n // Other tags have no attributes allowed\n};\n\n/**\n * Sanitize HTML string by removing dangerous tags and attributes\n *\n * @param html - HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n *\n * @example\n * ```typescript\n * sanitizeHTML('<strong>Hello</strong><script>alert(\"xss\")</script>');\n * // Returns: '<strong>Hello</strong>'\n * ```\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') {\n return '';\n }\n\n // Create a temporary DOM element to parse HTML\n const temp = document.createElement('div');\n temp.innerHTML = html;\n\n /**\n * Recursively sanitize a DOM node\n */\n function sanitizeNode(node: Node): string {\n // Text nodes - escape HTML entities\n if (node.nodeType === Node.TEXT_NODE) {\n return escapeHTML(node.textContent || '');\n }\n\n // Element nodes\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element;\n const tagName = element.tagName.toLowerCase();\n\n // Handle tags with whitespace (malformed HTML like \"< script >\")\n // Browser normalizes these, but if we see a tag that's not in our list,\n // it might be a dangerous tag that was normalized\n if (!tagName || tagName.includes(' ')) {\n return '';\n }\n\n // If tag is not allowed, return empty string\n if (!ALLOWED_TAGS.includes(tagName as any)) {\n return '';\n }\n\n // Get allowed attributes for this tag\n const allowedAttrs = ALLOWED_ATTRIBUTES[tagName] || [];\n\n // Build attribute string\n const attrs: string[] = [];\n for (const attr of allowedAttrs) {\n const value = element.getAttribute(attr);\n if (value !== null) {\n // Sanitize attribute values\n if (attr === 'href') {\n // Only allow safe URLs (http, https, mailto, tel, relative)\n const sanitizedHref = sanitizeURL(value);\n if (sanitizedHref) {\n attrs.push(`href=\"${escapeAttribute(sanitizedHref)}\"`);\n }\n } else {\n // For all other attributes (title, class, style), escape HTML entities\n attrs.push(`${attr}=\"${escapeAttribute(value)}\"`);\n }\n }\n }\n\n const attrString = attrs.length > 0 ? ' ' + attrs.join(' ') : '';\n\n // Process child nodes\n let innerHTML = '';\n for (const child of Array.from(element.childNodes)) {\n innerHTML += sanitizeNode(child);\n }\n\n // Self-closing tags\n if (tagName === 'br') {\n return `<br${attrString} />`;\n }\n\n return `<${tagName}${attrString}>${innerHTML}</${tagName}>`;\n }\n\n return '';\n }\n\n // Sanitize all nodes\n let sanitized = '';\n for (const child of Array.from(temp.childNodes)) {\n sanitized += sanitizeNode(child);\n }\n\n return sanitized;\n}\n\n/**\n * Escape HTML entities to prevent XSS in text content\n */\nfunction escapeHTML(text: string): string {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Escape HTML entities for use in attribute values\n */\nfunction escapeAttribute(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Sanitize URL to prevent javascript: and data: XSS attacks\n *\n * @param url - URL to sanitize\n * @returns Sanitized URL or empty string if unsafe\n */\nfunction sanitizeURL(url: string): string {\n if (!url || typeof url !== 'string') {\n return '';\n }\n\n // Decode URL-encoded characters to check for encoded attacks\n let decoded: string;\n try {\n decoded = decodeURIComponent(url);\n } catch {\n // If decoding fails, use original\n decoded = url;\n }\n\n const trimmed = decoded.trim().toLowerCase();\n\n // Block javascript: and data: protocols (check both original and decoded)\n if (\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n url.toLowerCase().trim().startsWith('javascript:') ||\n url.toLowerCase().trim().startsWith('data:')\n ) {\n return '';\n }\n\n // Allow http, https, mailto, tel, and relative URLs\n if (\n trimmed.startsWith('http://') ||\n trimmed.startsWith('https://') ||\n trimmed.startsWith('mailto:') ||\n trimmed.startsWith('tel:') ||\n trimmed.startsWith('/') ||\n trimmed.startsWith('#') ||\n trimmed.startsWith('?')\n ) {\n return url; // Return original (case preserved)\n }\n\n // Allow relative paths without protocol\n if (!trimmed.includes(':')) {\n return url;\n }\n\n // Block everything else\n return '';\n}\n","/**\n * Banner Plugin\n *\n * Renders banner experiences at the top or bottom of the page.\n * Auto-shows banners when experiences are evaluated.\n */\n\nimport type { PluginFunction } from '@lytics/sdk-kit';\nimport type { BannerContent, Decision, Experience } from '../types';\nimport { sanitizeHTML } from '../utils/sanitize';\n\nexport interface BannerPluginConfig {\n banner?: {\n position?: 'top' | 'bottom';\n dismissable?: boolean;\n zIndex?: number;\n pushDown?: string; // CSS selector of element to push down (add margin-top)\n };\n}\n\nexport interface BannerPlugin {\n show(experience: Experience): void;\n remove(): void;\n isShowing(): boolean;\n}\n\n/**\n * Banner Plugin\n *\n * Automatically renders banner experiences when they are evaluated.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@prosdevlab/experience-sdk';\n * import { bannerPlugin } from '@prosdevlab/experience-sdk-plugins';\n *\n * // Basic usage (banner overlays at top)\n * const sdk = createInstance({\n * banner: {\n * position: 'top',\n * dismissable: true\n * }\n * });\n * sdk.use(bannerPlugin);\n *\n * // With pushDown (pushes navigation down instead of overlaying)\n * const sdk = createInstance({\n * banner: {\n * position: 'top',\n * dismissable: true,\n * pushDown: 'header' // CSS selector of element to push down\n * }\n * });\n * sdk.use(bannerPlugin);\n * ```\n */\nexport const bannerPlugin: PluginFunction = (plugin, instance, config) => {\n plugin.ns('banner');\n\n // Set defaults\n plugin.defaults({\n banner: {\n position: 'top',\n dismissable: true,\n zIndex: 10000,\n },\n });\n\n // Track multiple active banners by experience ID\n const activeBanners = new Map<string, HTMLElement>();\n\n /**\n * Inject default banner styles if not already present\n */\n function injectDefaultStyles(): void {\n const styleId = 'xp-banner-styles';\n if (document.getElementById(styleId)) {\n return; // Already injected\n }\n\n const style = document.createElement('style');\n style.id = styleId;\n style.textContent = `\n .xp-banner {\n position: fixed;\n left: 0;\n right: 0;\n width: 100%;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n box-sizing: border-box;\n z-index: 10000;\n background: #ffffff;\n color: #111827;\n border-bottom: 1px solid #e5e7eb;\n box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);\n }\n \n .xp-banner--top {\n top: 0;\n }\n \n .xp-banner--bottom {\n bottom: 0;\n border-bottom: none;\n border-top: 1px solid #e5e7eb;\n box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0.05);\n }\n \n .xp-banner__container {\n display: flex;\n align-items: center;\n gap: 16px;\n max-width: 1280px;\n margin: 0 auto;\n padding: 14px 24px;\n }\n \n .xp-banner__content {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n \n .xp-banner__title {\n font-weight: 600;\n margin: 0;\n font-size: 15px;\n line-height: 1.4;\n }\n \n .xp-banner__message {\n margin: 0;\n font-size: 14px;\n line-height: 1.5;\n color: #6b7280;\n }\n \n .xp-banner__buttons {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n }\n \n .xp-banner__button {\n padding: 8px 16px;\n border: none;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n text-decoration: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n white-space: nowrap;\n }\n \n .xp-banner__button--primary {\n background: #2563eb;\n color: #ffffff;\n }\n \n .xp-banner__button--primary:hover {\n background: #1d4ed8;\n }\n \n .xp-banner__button--secondary {\n background: #f3f4f6;\n color: #374151;\n border: 1px solid #e5e7eb;\n }\n \n .xp-banner__button--secondary:hover {\n background: #e5e7eb;\n }\n \n .xp-banner__button--link {\n background: transparent;\n color: #2563eb;\n padding: 6px 12px;\n font-weight: 400;\n }\n \n .xp-banner__button--link:hover {\n background: #f3f4f6;\n text-decoration: underline;\n }\n \n .xp-banner__close {\n background: transparent;\n border: none;\n color: #9ca3af;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n padding: 4px;\n margin: 0;\n transition: color 0.2s;\n flex-shrink: 0;\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n }\n \n .xp-banner__close:hover {\n color: #111827;\n background: #f3f4f6;\n }\n \n @media (max-width: 640px) {\n .xp-banner__container {\n flex-wrap: wrap;\n padding: 14px 16px;\n position: relative;\n }\n \n .xp-banner__content {\n flex: 1 1 100%;\n padding-right: 32px;\n }\n \n .xp-banner__buttons {\n flex: 1 1 auto;\n width: 100%;\n }\n \n .xp-banner__button {\n flex: 1;\n }\n \n .xp-banner__close {\n position: absolute;\n top: 12px;\n right: 12px;\n }\n }\n \n /* Dark mode support */\n @media (prefers-color-scheme: dark) {\n .xp-banner {\n background: #111827;\n color: #f9fafb;\n border-bottom-color: #1f2937;\n }\n \n .xp-banner--bottom {\n border-top-color: #1f2937;\n }\n \n .xp-banner__message {\n color: #9ca3af;\n }\n \n .xp-banner__button--primary {\n background: #3b82f6;\n }\n \n .xp-banner__button--primary:hover {\n background: #2563eb;\n }\n \n .xp-banner__button--secondary {\n background: #1f2937;\n color: #f9fafb;\n border-color: #374151;\n }\n \n .xp-banner__button--secondary:hover {\n background: #374151;\n }\n \n .xp-banner__button--link {\n color: #60a5fa;\n }\n \n .xp-banner__button--link:hover {\n background: #1f2937;\n }\n \n .xp-banner__close {\n color: #6b7280;\n }\n \n .xp-banner__close:hover {\n color: #f9fafb;\n background: #1f2937;\n }\n }\n `;\n document.head.appendChild(style);\n }\n\n /**\n * Create banner DOM element\n */\n function createBannerElement(experience: Experience): HTMLElement {\n const content = experience.content as BannerContent;\n // Allow per-experience position override, fall back to global config\n const position = content.position ?? config.get('banner.position') ?? 'top';\n const dismissable = content.dismissable ?? config.get('banner.dismissable') ?? true;\n const zIndex = config.get('banner.zIndex') ?? 10000;\n\n // Inject default styles if needed\n injectDefaultStyles();\n\n // Create banner container\n const banner = document.createElement('div');\n banner.setAttribute('data-experience-id', experience.id);\n\n // Build className: base classes + position + user's custom class\n const baseClasses = ['xp-banner', `xp-banner--${position}`];\n if (content.className) {\n baseClasses.push(content.className);\n }\n banner.className = baseClasses.join(' ');\n\n // Apply user's custom styles\n if (content.style) {\n Object.assign(banner.style, content.style);\n }\n\n // Override z-index if configured\n if (zIndex !== 10000) {\n banner.style.zIndex = String(zIndex);\n }\n\n // Create container\n const container = document.createElement('div');\n container.className = 'xp-banner__container';\n banner.appendChild(container);\n\n // Create content container\n const contentDiv = document.createElement('div');\n contentDiv.className = 'xp-banner__content';\n\n // Add title if present\n if (content.title) {\n const title = document.createElement('h3');\n title.className = 'xp-banner__title';\n // Sanitize HTML to prevent XSS attacks\n title.innerHTML = sanitizeHTML(content.title);\n contentDiv.appendChild(title);\n }\n\n // Add message\n const message = document.createElement('p');\n message.className = 'xp-banner__message';\n // Sanitize HTML to prevent XSS attacks\n message.innerHTML = sanitizeHTML(content.message);\n contentDiv.appendChild(message);\n\n container.appendChild(contentDiv);\n\n // Create buttons container\n const buttonsDiv = document.createElement('div');\n buttonsDiv.className = 'xp-banner__buttons';\n\n // Helper function to create button with variant styling\n function createButton(buttonConfig: {\n text: string;\n action?: string;\n url?: string;\n variant?: 'primary' | 'secondary' | 'link';\n metadata?: Record<string, unknown>;\n className?: string;\n style?: Record<string, string>;\n }): HTMLButtonElement {\n const button = document.createElement('button');\n button.textContent = buttonConfig.text;\n\n const variant = buttonConfig.variant || 'primary';\n\n // Build className: base class + variant + user's custom class\n const buttonClasses = ['xp-banner__button', `xp-banner__button--${variant}`];\n if (buttonConfig.className) {\n buttonClasses.push(buttonConfig.className);\n }\n button.className = buttonClasses.join(' ');\n\n // Apply user's custom styles\n if (buttonConfig.style) {\n Object.assign(button.style, buttonConfig.style);\n }\n\n button.addEventListener('click', () => {\n // Emit action event\n instance.emit('experiences:action', {\n experienceId: experience.id,\n type: 'banner',\n action: buttonConfig.action,\n url: buttonConfig.url,\n metadata: buttonConfig.metadata,\n variant: variant,\n timestamp: Date.now(),\n });\n\n // Navigate if URL provided\n if (buttonConfig.url) {\n window.location.href = buttonConfig.url;\n }\n });\n\n return button;\n }\n\n // Add buttons from buttons array\n if (content.buttons && content.buttons.length > 0) {\n content.buttons.forEach((buttonConfig) => {\n const button = createButton(buttonConfig);\n buttonsDiv.appendChild(button);\n });\n }\n\n // Add dismiss button if dismissable\n if (dismissable) {\n const closeButton = document.createElement('button');\n closeButton.className = 'xp-banner__close';\n closeButton.innerHTML = '×';\n closeButton.setAttribute('aria-label', 'Close banner');\n\n closeButton.addEventListener('click', () => {\n remove(experience.id);\n instance.emit('experiences:dismissed', {\n experienceId: experience.id,\n type: 'banner',\n });\n });\n\n buttonsDiv.appendChild(closeButton);\n }\n\n container.appendChild(buttonsDiv);\n\n return banner;\n }\n\n /**\n * Apply pushDown margin to target element\n */\n function applyPushDown(banner: HTMLElement, position: 'top' | 'bottom'): void {\n const pushDownSelector = config.get('banner.pushDown');\n\n if (!pushDownSelector || position !== 'top') {\n return; // Only push down for top banners\n }\n\n const targetElement = document.querySelector(pushDownSelector);\n if (!targetElement || !(targetElement instanceof HTMLElement)) {\n return;\n }\n\n // Get banner height\n const height = banner.offsetHeight;\n\n // Apply margin-top with transition\n targetElement.style.transition = 'margin-top 0.3s ease';\n targetElement.style.marginTop = `${height}px`;\n }\n\n /**\n * Remove pushDown margin from target element\n */\n function removePushDown(): void {\n const pushDownSelector = config.get('banner.pushDown');\n\n if (!pushDownSelector) {\n return;\n }\n\n const targetElement = document.querySelector(pushDownSelector);\n if (!targetElement || !(targetElement instanceof HTMLElement)) {\n return;\n }\n\n // Remove margin-top with transition\n targetElement.style.transition = 'margin-top 0.3s ease';\n targetElement.style.marginTop = '0';\n }\n\n /**\n * Show a banner experience\n */\n function show(experience: Experience): void {\n // If banner already showing for this experience, skip\n if (activeBanners.has(experience.id)) {\n return;\n }\n\n // Only show if we're in a browser environment\n if (typeof document === 'undefined') {\n return;\n }\n\n const banner = createBannerElement(experience);\n document.body.appendChild(banner);\n activeBanners.set(experience.id, banner);\n\n // Apply pushDown to target element if configured\n const content = experience.content as BannerContent;\n const position = content.position ?? config.get('banner.position') ?? 'top';\n applyPushDown(banner, position);\n\n instance.emit('experiences:shown', {\n experienceId: experience.id,\n type: 'banner',\n timestamp: Date.now(),\n });\n }\n\n /**\n * Remove a banner by experience ID (or all if no ID provided)\n */\n function remove(experienceId?: string): void {\n if (experienceId) {\n // Remove specific banner\n const banner = activeBanners.get(experienceId);\n if (banner?.parentNode) {\n banner.parentNode.removeChild(banner);\n }\n activeBanners.delete(experienceId);\n\n // Remove pushDown if no more banners\n if (activeBanners.size === 0) {\n removePushDown();\n }\n } else {\n // Remove all banners\n for (const [id, banner] of activeBanners.entries()) {\n if (banner?.parentNode) {\n banner.parentNode.removeChild(banner);\n }\n activeBanners.delete(id);\n }\n\n // Remove pushDown\n removePushDown();\n }\n }\n\n /**\n * Check if any banner is currently showing\n */\n function isShowing(): boolean {\n return activeBanners.size > 0;\n }\n\n // Expose banner API\n plugin.expose({\n banner: {\n show,\n remove,\n isShowing,\n },\n });\n\n // Auto-show banner on experiences:evaluated event\n instance.on('experiences:evaluated', (payload: unknown) => {\n // Handle both single decision and array of decisions\n // evaluate() emits: { decision, experience }\n // evaluateAll() emits: [{ decision, experience }, ...]\n const items = Array.isArray(payload) ? payload : [payload];\n\n for (const item of items) {\n // Item is { decision, experience }\n const typedItem = item as { decision?: Decision; experience?: Experience };\n const decision = typedItem.decision;\n const experience = typedItem.experience;\n\n // Only handle banner-type experiences\n if (experience?.type === 'banner') {\n if (decision?.show) {\n show(experience);\n } else if (experience.id && activeBanners.has(experience.id)) {\n // Hide specific banner if decision says don't show\n remove(experience.id);\n }\n }\n }\n });\n\n // Cleanup on destroy\n instance.on('sdk:destroy', () => {\n remove();\n });\n};\n","/**\n * Debug Plugin\n *\n * Emits structured debug events to window and optionally logs to console.\n * Useful for debugging and Chrome extension integration.\n */\n\nimport type { PluginFunction } from '@lytics/sdk-kit';\n\nexport interface DebugPluginConfig {\n debug?: {\n enabled?: boolean;\n console?: boolean;\n window?: boolean;\n };\n}\n\nexport interface DebugPlugin {\n log(message: string, data?: unknown): void;\n isEnabled(): boolean;\n}\n\n/**\n * Debug Plugin\n *\n * Listens to all SDK events and emits them as window events for debugging.\n * Also optionally logs to console.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@prosdevlab/experience-sdk';\n * import { debugPlugin } from '@prosdevlab/experience-sdk-plugins';\n *\n * const sdk = createInstance({ debug: { enabled: true, console: true } });\n * sdk.use(debugPlugin);\n * ```\n */\nexport const debugPlugin: PluginFunction = (plugin, instance, config) => {\n plugin.ns('debug');\n\n // Set defaults\n plugin.defaults({\n debug: {\n enabled: false,\n console: false,\n window: true,\n },\n });\n\n // Helper to check if debug is enabled\n const isEnabled = (): boolean => config.get('debug.enabled') ?? false;\n const shouldLogConsole = (): boolean => config.get('debug.console') ?? false;\n const shouldEmitWindow = (): boolean => config.get('debug.window') ?? true;\n\n // Log function\n const log = (message: string, data?: unknown): void => {\n if (!isEnabled()) return;\n\n const timestamp = new Date().toISOString();\n const logData = {\n timestamp,\n message,\n data,\n };\n\n // Console logging\n if (shouldLogConsole()) {\n console.log(`[experiences] ${message}`, data || '');\n }\n\n // Window event emission\n if (shouldEmitWindow() && typeof window !== 'undefined') {\n const event = new CustomEvent('experience-sdk:debug', {\n detail: logData,\n });\n window.dispatchEvent(event);\n }\n };\n\n // Expose debug API\n plugin.expose({\n debug: {\n log,\n isEnabled,\n },\n });\n\n // If debug is enabled, listen to all events\n if (isEnabled()) {\n // Listen to experiences:* events\n instance.on('experiences:ready', () => {\n if (!isEnabled()) return;\n log('SDK initialized and ready');\n });\n\n instance.on('experiences:registered', (payload) => {\n if (!isEnabled()) return;\n log('Experience registered', payload);\n });\n\n instance.on('experiences:evaluated', (payload) => {\n if (!isEnabled()) return;\n log('Experience evaluated', payload);\n });\n }\n};\n","/**\n * Frequency Capping Plugin\n *\n * Tracks experience impressions and enforces frequency caps.\n * Uses sdk-kit's storage plugin for persistence.\n */\n\nimport type { PluginFunction, SDK } from '@lytics/sdk-kit';\nimport { type StoragePlugin, storagePlugin } from '@lytics/sdk-kit-plugins';\nimport type { Decision, TraceStep } from '../types';\n\nexport interface FrequencyPluginConfig {\n frequency?: {\n enabled?: boolean;\n namespace?: string;\n };\n}\n\nexport interface FrequencyPlugin {\n getImpressionCount(experienceId: string): number;\n hasReachedCap(experienceId: string, max: number, per: 'session' | 'day' | 'week'): boolean;\n recordImpression(experienceId: string): void;\n}\n\ninterface ImpressionData {\n count: number;\n lastImpression: number;\n impressions: number[];\n per?: 'session' | 'day' | 'week'; // Track which storage type this uses\n}\n\n/**\n * Frequency Capping Plugin\n *\n * Automatically tracks impressions and enforces frequency caps.\n * Requires storage plugin for persistence.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@prosdevlab/experience-sdk';\n * import { frequencyPlugin } from '@prosdevlab/experience-sdk-plugins';\n *\n * const sdk = createInstance({ frequency: { enabled: true } });\n * sdk.use(frequencyPlugin);\n * ```\n */\nexport const frequencyPlugin: PluginFunction = (plugin, instance, config) => {\n plugin.ns('frequency');\n\n // Set defaults\n plugin.defaults({\n frequency: {\n enabled: true,\n namespace: 'experiences:frequency',\n },\n });\n\n // Track experience frequency configs\n const experienceFrequencyMap = new Map<string, 'session' | 'day' | 'week'>();\n\n // Auto-load storage plugin if not already loaded\n if (!(instance as SDK & { storage?: StoragePlugin }).storage) {\n instance.use(storagePlugin);\n }\n\n const isEnabled = (): boolean => config.get('frequency.enabled') ?? true;\n const getNamespace = (): string => config.get('frequency.namespace') ?? 'experiences:frequency';\n\n // Helper to get the right storage backend based on frequency type\n const getStorageBackend = (per: 'session' | 'day' | 'week'): Storage => {\n return per === 'session' ? sessionStorage : localStorage;\n };\n\n // Helper to get storage key\n const getStorageKey = (experienceId: string): string => {\n return `${getNamespace()}:${experienceId}`;\n };\n\n // Helper to get impression data\n const getImpressionData = (\n experienceId: string,\n per: 'session' | 'day' | 'week'\n ): ImpressionData => {\n const storage = getStorageBackend(per);\n const key = getStorageKey(experienceId);\n const raw = storage.getItem(key);\n\n if (!raw) {\n return {\n count: 0,\n lastImpression: 0,\n impressions: [],\n per,\n };\n }\n\n try {\n return JSON.parse(raw) as ImpressionData;\n } catch {\n return {\n count: 0,\n lastImpression: 0,\n impressions: [],\n per,\n };\n }\n };\n\n // Helper to save impression data\n const saveImpressionData = (experienceId: string, data: ImpressionData): void => {\n const per = data.per || 'session'; // Default to session if not specified\n const storage = getStorageBackend(per);\n const key = getStorageKey(experienceId);\n storage.setItem(key, JSON.stringify(data));\n };\n\n // Get time window in milliseconds\n const getTimeWindow = (per: 'session' | 'day' | 'week'): number => {\n switch (per) {\n case 'session':\n return Number.POSITIVE_INFINITY; // Session storage handles this\n case 'day':\n return 24 * 60 * 60 * 1000; // 24 hours\n case 'week':\n return 7 * 24 * 60 * 60 * 1000; // 7 days\n }\n };\n\n /**\n * Get impression count for an experience\n */\n const getImpressionCount = (\n experienceId: string,\n per: 'session' | 'day' | 'week' = 'session'\n ): number => {\n if (!isEnabled()) return 0;\n const data = getImpressionData(experienceId, per);\n return data.count;\n };\n\n /**\n * Check if an experience has reached its frequency cap\n */\n const hasReachedCap = (\n experienceId: string,\n max: number,\n per: 'session' | 'day' | 'week'\n ): boolean => {\n if (!isEnabled()) return false;\n\n const data = getImpressionData(experienceId, per);\n const timeWindow = getTimeWindow(per);\n const now = Date.now();\n\n // For session caps, just check total count\n if (per === 'session') {\n return data.count >= max;\n }\n\n // For time-based caps, count impressions within the window\n const recentImpressions = data.impressions.filter((timestamp) => now - timestamp < timeWindow);\n\n return recentImpressions.length >= max;\n };\n\n /**\n * Record an impression for an experience\n */\n const recordImpression = (\n experienceId: string,\n per: 'session' | 'day' | 'week' = 'session'\n ): void => {\n if (!isEnabled()) return;\n\n const data = getImpressionData(experienceId, per);\n const now = Date.now();\n\n // Update count and add timestamp\n data.count += 1;\n data.lastImpression = now;\n data.impressions.push(now);\n data.per = per; // Store the frequency type\n\n // Keep only recent impressions (last 7 days)\n const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;\n data.impressions = data.impressions.filter((ts) => ts > sevenDaysAgo);\n\n // Save updated data\n saveImpressionData(experienceId, data);\n\n // Emit event\n instance.emit('experiences:impression-recorded', {\n experienceId,\n count: data.count,\n timestamp: now,\n });\n };\n\n // Expose frequency API\n plugin.expose({\n frequency: {\n getImpressionCount,\n hasReachedCap,\n recordImpression,\n // Internal method to register experience frequency config\n _registerExperience: (experienceId: string, per: 'session' | 'day' | 'week') => {\n experienceFrequencyMap.set(experienceId, per);\n },\n },\n });\n\n // Listen to evaluation events and record impressions\n if (isEnabled()) {\n instance.on('experiences:evaluated', (payload: unknown) => {\n // Handle both single decision and array of decisions\n // evaluate() emits: { decision, experience }\n // evaluateAll() emits: [{ decision, experience }, ...]\n const items = Array.isArray(payload) ? payload : [payload];\n\n for (const item of items) {\n // Item is { decision, experience }\n const decision = (item as { decision?: Decision }).decision;\n\n // Only record if experience was shown\n if (decision?.show && decision.experienceId) {\n // Try to get the 'per' value from our map, fall back to checking the input in trace\n let per: 'session' | 'day' | 'week' =\n experienceFrequencyMap.get(decision.experienceId) || 'session';\n\n // If not in map, try to infer from the decision trace\n if (!experienceFrequencyMap.has(decision.experienceId)) {\n const freqStep = decision.trace.find(\n (t: TraceStep) => t.step === 'check-frequency-cap'\n );\n if (freqStep?.input && typeof freqStep.input === 'object' && 'per' in freqStep.input) {\n per = (freqStep.input as { per: 'session' | 'day' | 'week' }).per;\n // Cache it for next time\n experienceFrequencyMap.set(decision.experienceId, per);\n }\n }\n\n recordImpression(decision.experienceId, per);\n }\n }\n });\n }\n};\n"]}
|
package/package.json
CHANGED
package/src/banner/banner.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface BannerPluginConfig {
|
|
|
14
14
|
position?: 'top' | 'bottom';
|
|
15
15
|
dismissable?: boolean;
|
|
16
16
|
zIndex?: number;
|
|
17
|
+
pushDown?: string; // CSS selector of element to push down (add margin-top)
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -33,7 +34,23 @@ export interface BannerPlugin {
|
|
|
33
34
|
* import { createInstance } from '@prosdevlab/experience-sdk';
|
|
34
35
|
* import { bannerPlugin } from '@prosdevlab/experience-sdk-plugins';
|
|
35
36
|
*
|
|
36
|
-
*
|
|
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
|
+
* });
|
|
37
54
|
* sdk.use(bannerPlugin);
|
|
38
55
|
* ```
|
|
39
56
|
*/
|
|
@@ -69,16 +86,12 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
69
86
|
left: 0;
|
|
70
87
|
right: 0;
|
|
71
88
|
width: 100%;
|
|
72
|
-
padding: 16px 20px;
|
|
73
89
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
74
90
|
font-size: 14px;
|
|
75
91
|
line-height: 1.5;
|
|
76
|
-
display: flex;
|
|
77
|
-
align-items: center;
|
|
78
|
-
justify-content: space-between;
|
|
79
92
|
box-sizing: border-box;
|
|
80
93
|
z-index: 10000;
|
|
81
|
-
background: #
|
|
94
|
+
background: #ffffff;
|
|
82
95
|
color: #111827;
|
|
83
96
|
border-bottom: 1px solid #e5e7eb;
|
|
84
97
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
|
@@ -98,33 +111,38 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
98
111
|
.xp-banner__container {
|
|
99
112
|
display: flex;
|
|
100
113
|
align-items: center;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
gap: 16px;
|
|
115
|
+
max-width: 1280px;
|
|
116
|
+
margin: 0 auto;
|
|
117
|
+
padding: 14px 24px;
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
.xp-banner__content {
|
|
107
121
|
flex: 1;
|
|
108
122
|
min-width: 0;
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: 4px;
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
.xp-banner__title {
|
|
112
129
|
font-weight: 600;
|
|
113
|
-
margin
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
margin: 0;
|
|
131
|
+
font-size: 15px;
|
|
132
|
+
line-height: 1.4;
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
.xp-banner__message {
|
|
119
136
|
margin: 0;
|
|
120
137
|
font-size: 14px;
|
|
138
|
+
line-height: 1.5;
|
|
139
|
+
color: #6b7280;
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
.xp-banner__buttons {
|
|
124
143
|
display: flex;
|
|
125
144
|
align-items: center;
|
|
126
|
-
gap:
|
|
127
|
-
flex-wrap: wrap;
|
|
145
|
+
gap: 8px;
|
|
128
146
|
flex-shrink: 0;
|
|
129
147
|
}
|
|
130
148
|
|
|
@@ -137,6 +155,10 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
137
155
|
cursor: pointer;
|
|
138
156
|
transition: all 0.2s;
|
|
139
157
|
text-decoration: none;
|
|
158
|
+
display: inline-flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
white-space: nowrap;
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
.xp-banner__button--primary {
|
|
@@ -149,71 +171,93 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
.xp-banner__button--secondary {
|
|
152
|
-
background: #
|
|
174
|
+
background: #f3f4f6;
|
|
153
175
|
color: #374151;
|
|
154
|
-
border: 1px solid #
|
|
176
|
+
border: 1px solid #e5e7eb;
|
|
155
177
|
}
|
|
156
178
|
|
|
157
179
|
.xp-banner__button--secondary:hover {
|
|
158
|
-
background: #
|
|
180
|
+
background: #e5e7eb;
|
|
159
181
|
}
|
|
160
182
|
|
|
161
183
|
.xp-banner__button--link {
|
|
162
184
|
background: transparent;
|
|
163
185
|
color: #2563eb;
|
|
164
|
-
padding:
|
|
186
|
+
padding: 6px 12px;
|
|
165
187
|
font-weight: 400;
|
|
166
|
-
text-decoration: underline;
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
.xp-banner__button--link:hover {
|
|
170
|
-
background:
|
|
191
|
+
background: #f3f4f6;
|
|
192
|
+
text-decoration: underline;
|
|
171
193
|
}
|
|
172
194
|
|
|
173
195
|
.xp-banner__close {
|
|
174
196
|
background: transparent;
|
|
175
197
|
border: none;
|
|
176
|
-
color: #
|
|
177
|
-
font-size:
|
|
198
|
+
color: #9ca3af;
|
|
199
|
+
font-size: 20px;
|
|
178
200
|
line-height: 1;
|
|
179
201
|
cursor: pointer;
|
|
180
|
-
padding:
|
|
202
|
+
padding: 4px;
|
|
181
203
|
margin: 0;
|
|
182
|
-
|
|
183
|
-
transition: opacity 0.2s;
|
|
204
|
+
transition: color 0.2s;
|
|
184
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;
|
|
185
212
|
}
|
|
186
213
|
|
|
187
214
|
.xp-banner__close:hover {
|
|
188
|
-
|
|
215
|
+
color: #111827;
|
|
216
|
+
background: #f3f4f6;
|
|
189
217
|
}
|
|
190
218
|
|
|
191
219
|
@media (max-width: 640px) {
|
|
192
220
|
.xp-banner__container {
|
|
193
|
-
flex-
|
|
194
|
-
|
|
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;
|
|
195
229
|
}
|
|
196
230
|
|
|
197
231
|
.xp-banner__buttons {
|
|
232
|
+
flex: 1 1 auto;
|
|
198
233
|
width: 100%;
|
|
199
|
-
flex-direction: column;
|
|
200
234
|
}
|
|
201
235
|
|
|
202
236
|
.xp-banner__button {
|
|
203
|
-
|
|
237
|
+
flex: 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.xp-banner__close {
|
|
241
|
+
position: absolute;
|
|
242
|
+
top: 12px;
|
|
243
|
+
right: 12px;
|
|
204
244
|
}
|
|
205
245
|
}
|
|
206
246
|
|
|
207
247
|
/* Dark mode support */
|
|
208
248
|
@media (prefers-color-scheme: dark) {
|
|
209
249
|
.xp-banner {
|
|
210
|
-
background: #
|
|
211
|
-
color: #
|
|
212
|
-
border-bottom-color: #
|
|
250
|
+
background: #111827;
|
|
251
|
+
color: #f9fafb;
|
|
252
|
+
border-bottom-color: #1f2937;
|
|
213
253
|
}
|
|
214
254
|
|
|
215
255
|
.xp-banner--bottom {
|
|
216
|
-
border-top-color: #
|
|
256
|
+
border-top-color: #1f2937;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.xp-banner__message {
|
|
260
|
+
color: #9ca3af;
|
|
217
261
|
}
|
|
218
262
|
|
|
219
263
|
.xp-banner__button--primary {
|
|
@@ -225,21 +269,30 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
225
269
|
}
|
|
226
270
|
|
|
227
271
|
.xp-banner__button--secondary {
|
|
228
|
-
background: #
|
|
229
|
-
color: #
|
|
230
|
-
border-color: #
|
|
272
|
+
background: #1f2937;
|
|
273
|
+
color: #f9fafb;
|
|
274
|
+
border-color: #374151;
|
|
231
275
|
}
|
|
232
276
|
|
|
233
277
|
.xp-banner__button--secondary:hover {
|
|
234
|
-
background: #
|
|
278
|
+
background: #374151;
|
|
235
279
|
}
|
|
236
280
|
|
|
237
281
|
.xp-banner__button--link {
|
|
238
|
-
color: #
|
|
282
|
+
color: #60a5fa;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.xp-banner__button--link:hover {
|
|
286
|
+
background: #1f2937;
|
|
239
287
|
}
|
|
240
288
|
|
|
241
289
|
.xp-banner__close {
|
|
242
|
-
color: #
|
|
290
|
+
color: #6b7280;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.xp-banner__close:hover {
|
|
294
|
+
color: #f9fafb;
|
|
295
|
+
background: #1f2937;
|
|
243
296
|
}
|
|
244
297
|
}
|
|
245
298
|
`;
|
|
@@ -307,17 +360,6 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
307
360
|
|
|
308
361
|
container.appendChild(contentDiv);
|
|
309
362
|
|
|
310
|
-
banner.appendChild(contentDiv);
|
|
311
|
-
|
|
312
|
-
// Create button container for actions and/or dismiss
|
|
313
|
-
const buttonContainer = document.createElement('div');
|
|
314
|
-
buttonContainer.style.cssText = `
|
|
315
|
-
display: flex;
|
|
316
|
-
align-items: center;
|
|
317
|
-
gap: 12px;
|
|
318
|
-
flex-wrap: wrap;
|
|
319
|
-
`;
|
|
320
|
-
|
|
321
363
|
// Create buttons container
|
|
322
364
|
const buttonsDiv = document.createElement('div');
|
|
323
365
|
buttonsDiv.className = 'xp-banner__buttons';
|
|
@@ -401,6 +443,49 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
401
443
|
return banner;
|
|
402
444
|
}
|
|
403
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
|
+
|
|
404
489
|
/**
|
|
405
490
|
* Show a banner experience
|
|
406
491
|
*/
|
|
@@ -419,6 +504,11 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
419
504
|
document.body.appendChild(banner);
|
|
420
505
|
activeBanners.set(experience.id, banner);
|
|
421
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
|
+
|
|
422
512
|
instance.emit('experiences:shown', {
|
|
423
513
|
experienceId: experience.id,
|
|
424
514
|
type: 'banner',
|
|
@@ -437,6 +527,11 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
437
527
|
banner.parentNode.removeChild(banner);
|
|
438
528
|
}
|
|
439
529
|
activeBanners.delete(experienceId);
|
|
530
|
+
|
|
531
|
+
// Remove pushDown if no more banners
|
|
532
|
+
if (activeBanners.size === 0) {
|
|
533
|
+
removePushDown();
|
|
534
|
+
}
|
|
440
535
|
} else {
|
|
441
536
|
// Remove all banners
|
|
442
537
|
for (const [id, banner] of activeBanners.entries()) {
|
|
@@ -445,6 +540,9 @@ export const bannerPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
445
540
|
}
|
|
446
541
|
activeBanners.delete(id);
|
|
447
542
|
}
|
|
543
|
+
|
|
544
|
+
// Remove pushDown
|
|
545
|
+
removePushDown();
|
|
448
546
|
}
|
|
449
547
|
}
|
|
450
548
|
|