@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @prosdevlab/experience-sdk-plugins@0.1.3 build /home/runner/work/experience-sdk/experience-sdk/packages/plugins
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
  CLI Building entry: src/index.ts
@@ -9,9 +9,9 @@
9
9
  CLI Target: es2024
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
- ESM dist/index.js 17.87 KB
13
- ESM dist/index.js.map 44.45 KB
14
- ESM ⚡️ Build success in 190ms
12
+ ESM dist/index.js 19.53 KB
13
+ ESM dist/index.js.map 48.09 KB
14
+ ESM ⚡️ Build success in 140ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 873ms
17
- DTS dist/index.d.ts 5.09 KB
16
+ DTS ⚡️ Build success in 857ms
17
+ DTS dist/index.d.ts 5.46 KB
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
- * const sdk = createInstance({ banner: { position: 'top', dismissable: true } });
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: #f9fafb;
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
- justify-content: space-between;
148
- gap: 20px;
149
- width: 100%;
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-bottom: 4px;
160
- margin-top: 0;
161
- font-size: 14px;
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: 12px;
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: #ffffff;
203
+ background: #f3f4f6;
199
204
  color: #374151;
200
- border: 1px solid #d1d5db;
205
+ border: 1px solid #e5e7eb;
201
206
  }
202
207
 
203
208
  .xp-banner__button--secondary:hover {
204
- background: #f9fafb;
209
+ background: #e5e7eb;
205
210
  }
206
211
 
207
212
  .xp-banner__button--link {
208
213
  background: transparent;
209
214
  color: #2563eb;
210
- padding: 4px 8px;
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: rgba(0, 0, 0, 0.05);
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: #6b7280;
223
- font-size: 24px;
227
+ color: #9ca3af;
228
+ font-size: 20px;
224
229
  line-height: 1;
225
230
  cursor: pointer;
226
- padding: 0;
231
+ padding: 4px;
227
232
  margin: 0;
228
- opacity: 0.7;
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
- opacity: 1;
244
+ color: #111827;
245
+ background: #f3f4f6;
235
246
  }
236
247
 
237
248
  @media (max-width: 640px) {
238
249
  .xp-banner__container {
239
- flex-direction: column;
240
- align-items: stretch;
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
- width: 100%;
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: #1f2937;
257
- color: #f3f4f6;
258
- border-bottom-color: #374151;
279
+ background: #111827;
280
+ color: #f9fafb;
281
+ border-bottom-color: #1f2937;
259
282
  }
260
283
 
261
284
  .xp-banner--bottom {
262
- border-top-color: #374151;
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: #374151;
275
- color: #f3f4f6;
276
- border-color: #4b5563;
301
+ background: #1f2937;
302
+ color: #f9fafb;
303
+ border-color: #374151;
277
304
  }
278
305
 
279
306
  .xp-banner__button--secondary:hover {
280
- background: #4b5563;
307
+ background: #374151;
281
308
  }
282
309
 
283
310
  .xp-banner__button--link {
284
- color: #93c5fd;
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: #9ca3af;
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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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 = '&times;';\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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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 = '&times;';\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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prosdevlab/experience-sdk-plugins",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Official plugins for Experience SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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
- * const sdk = createInstance({ banner: { position: 'top', dismissable: true } });
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: #f9fafb;
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
- justify-content: space-between;
102
- gap: 20px;
103
- width: 100%;
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-bottom: 4px;
114
- margin-top: 0;
115
- font-size: 14px;
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: 12px;
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: #ffffff;
174
+ background: #f3f4f6;
153
175
  color: #374151;
154
- border: 1px solid #d1d5db;
176
+ border: 1px solid #e5e7eb;
155
177
  }
156
178
 
157
179
  .xp-banner__button--secondary:hover {
158
- background: #f9fafb;
180
+ background: #e5e7eb;
159
181
  }
160
182
 
161
183
  .xp-banner__button--link {
162
184
  background: transparent;
163
185
  color: #2563eb;
164
- padding: 4px 8px;
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: rgba(0, 0, 0, 0.05);
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: #6b7280;
177
- font-size: 24px;
198
+ color: #9ca3af;
199
+ font-size: 20px;
178
200
  line-height: 1;
179
201
  cursor: pointer;
180
- padding: 0;
202
+ padding: 4px;
181
203
  margin: 0;
182
- opacity: 0.7;
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
- opacity: 1;
215
+ color: #111827;
216
+ background: #f3f4f6;
189
217
  }
190
218
 
191
219
  @media (max-width: 640px) {
192
220
  .xp-banner__container {
193
- flex-direction: column;
194
- align-items: stretch;
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
- width: 100%;
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: #1f2937;
211
- color: #f3f4f6;
212
- border-bottom-color: #374151;
250
+ background: #111827;
251
+ color: #f9fafb;
252
+ border-bottom-color: #1f2937;
213
253
  }
214
254
 
215
255
  .xp-banner--bottom {
216
- border-top-color: #374151;
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: #374151;
229
- color: #f3f4f6;
230
- border-color: #4b5563;
272
+ background: #1f2937;
273
+ color: #f9fafb;
274
+ border-color: #374151;
231
275
  }
232
276
 
233
277
  .xp-banner__button--secondary:hover {
234
- background: #4b5563;
278
+ background: #374151;
235
279
  }
236
280
 
237
281
  .xp-banner__button--link {
238
- color: #93c5fd;
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: #9ca3af;
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