@prosdevlab/experience-sdk-plugins 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +6 -6
  2. package/CHANGELOG.md +150 -0
  3. package/README.md +141 -79
  4. package/dist/index.d.ts +813 -35
  5. package/dist/index.js +1910 -66
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/banner/banner.ts +63 -62
  9. package/src/exit-intent/exit-intent.test.ts +423 -0
  10. package/src/exit-intent/exit-intent.ts +371 -0
  11. package/src/exit-intent/index.ts +6 -0
  12. package/src/exit-intent/types.ts +59 -0
  13. package/src/index.ts +7 -0
  14. package/src/inline/index.ts +3 -0
  15. package/src/inline/inline.test.ts +620 -0
  16. package/src/inline/inline.ts +269 -0
  17. package/src/inline/insertion.ts +66 -0
  18. package/src/inline/types.ts +52 -0
  19. package/src/integration.test.ts +421 -0
  20. package/src/modal/form-rendering.ts +262 -0
  21. package/src/modal/form-styles.ts +212 -0
  22. package/src/modal/form-validation.test.ts +413 -0
  23. package/src/modal/form-validation.ts +126 -0
  24. package/src/modal/index.ts +3 -0
  25. package/src/modal/modal-styles.ts +204 -0
  26. package/src/modal/modal.browser.test.ts +164 -0
  27. package/src/modal/modal.test.ts +1294 -0
  28. package/src/modal/modal.ts +685 -0
  29. package/src/modal/types.ts +114 -0
  30. package/src/page-visits/index.ts +6 -0
  31. package/src/page-visits/page-visits.test.ts +562 -0
  32. package/src/page-visits/page-visits.ts +314 -0
  33. package/src/page-visits/types.ts +119 -0
  34. package/src/scroll-depth/index.ts +6 -0
  35. package/src/scroll-depth/scroll-depth.test.ts +580 -0
  36. package/src/scroll-depth/scroll-depth.ts +398 -0
  37. package/src/scroll-depth/types.ts +122 -0
  38. package/src/time-delay/index.ts +6 -0
  39. package/src/time-delay/time-delay.test.ts +477 -0
  40. package/src/time-delay/time-delay.ts +296 -0
  41. package/src/time-delay/types.ts +89 -0
  42. package/src/types.ts +20 -36
  43. package/src/utils/sanitize.ts +5 -2
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { storagePlugin } from '@lytics/sdk-kit-plugins';
2
2
 
3
3
  // src/utils/sanitize.ts
4
- var ALLOWED_TAGS = ["strong", "em", "a", "br", "span", "b", "i", "p"];
4
+ var ALLOWED_TAGS = ["strong", "em", "a", "br", "span", "b", "i", "p", "div", "ul", "li"];
5
5
  var ALLOWED_ATTRIBUTES = {
6
6
  a: ["href", "class", "style", "title"],
7
7
  span: ["class", "style"],
8
- p: ["class", "style"]
8
+ p: ["class", "style"],
9
+ div: ["class", "style"],
10
+ ul: ["class", "style"],
11
+ li: ["class", "style"]
9
12
  // Other tags have no attributes allowed
10
13
  };
11
14
  function sanitizeHTML(html) {
@@ -42,7 +45,7 @@ function sanitizeHTML(html) {
42
45
  }
43
46
  }
44
47
  }
45
- const attrString = attrs.length > 0 ? " " + attrs.join(" ") : "";
48
+ const attrString = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
46
49
  let innerHTML = "";
47
50
  for (const child of Array.from(element.childNodes)) {
48
51
  innerHTML += sanitizeNode(child);
@@ -115,15 +118,15 @@ var bannerPlugin = (plugin, instance, config) => {
115
118
  left: 0;
116
119
  right: 0;
117
120
  width: 100%;
118
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
119
- font-size: 14px;
120
- line-height: 1.5;
121
+ font-family: var(--xp-banner-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
122
+ font-size: var(--xp-banner-font-size, 14px);
123
+ line-height: var(--xp-banner-line-height, 1.5);
121
124
  box-sizing: border-box;
122
- z-index: 10000;
123
- background: #ffffff;
124
- color: #111827;
125
- border-bottom: 1px solid #e5e7eb;
126
- box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
125
+ z-index: var(--xp-banner-z-index, 10000);
126
+ background: var(--xp-banner-bg, #ffffff);
127
+ color: var(--xp-banner-color, #111827);
128
+ border-bottom: var(--xp-banner-border-width, 1px) solid var(--xp-banner-border-color, #e5e7eb);
129
+ box-shadow: var(--xp-banner-shadow, 0 1px 3px 0 rgba(0, 0, 0, 0.05));
127
130
  }
128
131
 
129
132
  .xp-banner--top {
@@ -133,17 +136,17 @@ var bannerPlugin = (plugin, instance, config) => {
133
136
  .xp-banner--bottom {
134
137
  bottom: 0;
135
138
  border-bottom: none;
136
- border-top: 1px solid #e5e7eb;
137
- box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0.05);
139
+ border-top: var(--xp-banner-border-width, 1px) solid var(--xp-banner-border-color, #e5e7eb);
140
+ box-shadow: var(--xp-banner-shadow-bottom, 0 -1px 3px 0 rgba(0, 0, 0, 0.05));
138
141
  }
139
142
 
140
143
  .xp-banner__container {
141
144
  display: flex;
142
145
  align-items: center;
143
- gap: 16px;
144
- max-width: 1280px;
146
+ gap: var(--xp-banner-gap, 16px);
147
+ max-width: var(--xp-banner-max-width, 1280px);
145
148
  margin: 0 auto;
146
- padding: 14px 24px;
149
+ padding: var(--xp-banner-padding, 14px 24px);
147
150
  }
148
151
 
149
152
  .xp-banner__content {
@@ -151,36 +154,37 @@ var bannerPlugin = (plugin, instance, config) => {
151
154
  min-width: 0;
152
155
  display: flex;
153
156
  flex-direction: column;
154
- gap: 4px;
157
+ gap: var(--xp-banner-content-gap, 4px);
155
158
  }
156
159
 
157
160
  .xp-banner__title {
158
- font-weight: 600;
161
+ font-weight: var(--xp-banner-title-weight, 600);
159
162
  margin: 0;
160
- font-size: 15px;
161
- line-height: 1.4;
163
+ font-size: var(--xp-banner-title-size, 15px);
164
+ line-height: var(--xp-banner-title-line-height, 1.4);
165
+ color: var(--xp-banner-title-color, inherit);
162
166
  }
163
167
 
164
168
  .xp-banner__message {
165
169
  margin: 0;
166
- font-size: 14px;
167
- line-height: 1.5;
168
- color: #6b7280;
170
+ font-size: var(--xp-banner-message-size, 14px);
171
+ line-height: var(--xp-banner-message-line-height, 1.5);
172
+ color: var(--xp-banner-message-color, #6b7280);
169
173
  }
170
174
 
171
175
  .xp-banner__buttons {
172
176
  display: flex;
173
177
  align-items: center;
174
- gap: 8px;
178
+ gap: var(--xp-banner-buttons-gap, 8px);
175
179
  flex-shrink: 0;
176
180
  }
177
181
 
178
182
  .xp-banner__button {
179
- padding: 8px 16px;
183
+ padding: var(--xp-banner-button-padding, 8px 16px);
180
184
  border: none;
181
- border-radius: 6px;
182
- font-size: 14px;
183
- font-weight: 500;
185
+ border-radius: var(--xp-banner-button-radius, 6px);
186
+ font-size: var(--xp-banner-button-font-size, 14px);
187
+ font-weight: var(--xp-banner-button-font-weight, 500);
184
188
  cursor: pointer;
185
189
  transition: all 0.2s;
186
190
  text-decoration: none;
@@ -191,64 +195,64 @@ var bannerPlugin = (plugin, instance, config) => {
191
195
  }
192
196
 
193
197
  .xp-banner__button--primary {
194
- background: #2563eb;
195
- color: #ffffff;
198
+ background: var(--xp-banner-button-primary-bg, #2563eb);
199
+ color: var(--xp-banner-button-primary-color, #ffffff);
196
200
  }
197
201
 
198
202
  .xp-banner__button--primary:hover {
199
- background: #1d4ed8;
203
+ background: var(--xp-banner-button-primary-bg-hover, #1d4ed8);
200
204
  }
201
205
 
202
206
  .xp-banner__button--secondary {
203
- background: #f3f4f6;
204
- color: #374151;
205
- border: 1px solid #e5e7eb;
207
+ background: var(--xp-banner-button-secondary-bg, #f3f4f6);
208
+ color: var(--xp-banner-button-secondary-color, #374151);
209
+ border: var(--xp-banner-border-width, 1px) solid var(--xp-banner-button-secondary-border, #e5e7eb);
206
210
  }
207
211
 
208
212
  .xp-banner__button--secondary:hover {
209
- background: #e5e7eb;
213
+ background: var(--xp-banner-button-secondary-bg-hover, #e5e7eb);
210
214
  }
211
215
 
212
216
  .xp-banner__button--link {
213
217
  background: transparent;
214
- color: #2563eb;
215
- padding: 6px 12px;
216
- font-weight: 400;
218
+ color: var(--xp-banner-button-link-color, #2563eb);
219
+ padding: var(--xp-banner-button-link-padding, 6px 12px);
220
+ font-weight: var(--xp-banner-button-link-font-weight, 400);
217
221
  }
218
222
 
219
223
  .xp-banner__button--link:hover {
220
- background: #f3f4f6;
224
+ background: var(--xp-banner-button-link-bg-hover, #f3f4f6);
221
225
  text-decoration: underline;
222
226
  }
223
227
 
224
228
  .xp-banner__close {
225
229
  background: transparent;
226
230
  border: none;
227
- color: #9ca3af;
228
- font-size: 20px;
231
+ color: var(--xp-banner-close-color, #9ca3af);
232
+ font-size: var(--xp-banner-close-size, 20px);
229
233
  line-height: 1;
230
234
  cursor: pointer;
231
- padding: 4px;
235
+ padding: var(--xp-banner-close-padding, 4px);
232
236
  margin: 0;
233
237
  transition: color 0.2s;
234
238
  flex-shrink: 0;
235
- width: 28px;
236
- height: 28px;
239
+ width: var(--xp-banner-close-width, 28px);
240
+ height: var(--xp-banner-close-height, 28px);
237
241
  display: flex;
238
242
  align-items: center;
239
243
  justify-content: center;
240
- border-radius: 4px;
244
+ border-radius: var(--xp-banner-close-radius, 4px);
241
245
  }
242
246
 
243
247
  .xp-banner__close:hover {
244
- color: #111827;
245
- background: #f3f4f6;
248
+ color: var(--xp-banner-close-color-hover, #111827);
249
+ background: var(--xp-banner-close-bg-hover, #f3f4f6);
246
250
  }
247
251
 
248
252
  @media (max-width: 640px) {
249
253
  .xp-banner__container {
250
254
  flex-wrap: wrap;
251
- padding: 14px 16px;
255
+ padding: var(--xp-banner-padding-mobile, 14px 16px);
252
256
  position: relative;
253
257
  }
254
258
 
@@ -273,55 +277,55 @@ var bannerPlugin = (plugin, instance, config) => {
273
277
  }
274
278
  }
275
279
 
276
- /* Dark mode support */
280
+ /* Dark mode support - override CSS variables */
277
281
  @media (prefers-color-scheme: dark) {
278
282
  .xp-banner {
279
- background: #111827;
280
- color: #f9fafb;
281
- border-bottom-color: #1f2937;
283
+ background: var(--xp-banner-bg-dark, #111827);
284
+ color: var(--xp-banner-color-dark, #f9fafb);
285
+ border-bottom-color: var(--xp-banner-border-color-dark, #1f2937);
282
286
  }
283
287
 
284
288
  .xp-banner--bottom {
285
- border-top-color: #1f2937;
289
+ border-top-color: var(--xp-banner-border-color-dark, #1f2937);
286
290
  }
287
291
 
288
292
  .xp-banner__message {
289
- color: #9ca3af;
293
+ color: var(--xp-banner-message-color-dark, #9ca3af);
290
294
  }
291
295
 
292
296
  .xp-banner__button--primary {
293
- background: #3b82f6;
297
+ background: var(--xp-banner-button-primary-bg-dark, #3b82f6);
294
298
  }
295
299
 
296
300
  .xp-banner__button--primary:hover {
297
- background: #2563eb;
301
+ background: var(--xp-banner-button-primary-bg-hover-dark, #2563eb);
298
302
  }
299
303
 
300
304
  .xp-banner__button--secondary {
301
- background: #1f2937;
302
- color: #f9fafb;
303
- border-color: #374151;
305
+ background: var(--xp-banner-button-secondary-bg-dark, #1f2937);
306
+ color: var(--xp-banner-button-secondary-color-dark, #f9fafb);
307
+ border-color: var(--xp-banner-button-secondary-border-dark, #374151);
304
308
  }
305
309
 
306
310
  .xp-banner__button--secondary:hover {
307
- background: #374151;
311
+ background: var(--xp-banner-button-secondary-bg-hover-dark, #374151);
308
312
  }
309
313
 
310
314
  .xp-banner__button--link {
311
- color: #60a5fa;
315
+ color: var(--xp-banner-button-link-color-dark, #60a5fa);
312
316
  }
313
317
 
314
318
  .xp-banner__button--link:hover {
315
- background: #1f2937;
319
+ background: var(--xp-banner-button-link-bg-hover-dark, #1f2937);
316
320
  }
317
321
 
318
322
  .xp-banner__close {
319
- color: #6b7280;
323
+ color: var(--xp-banner-close-color-dark, #6b7280);
320
324
  }
321
325
 
322
326
  .xp-banner__close:hover {
323
- color: #f9fafb;
324
- background: #1f2937;
327
+ color: var(--xp-banner-close-color-hover-dark, #f9fafb);
328
+ background: var(--xp-banner-close-bg-hover-dark, #1f2937);
325
329
  }
326
330
  }
327
331
  `;
@@ -561,6 +565,175 @@ var debugPlugin = (plugin, instance, config) => {
561
565
  });
562
566
  }
563
567
  };
568
+
569
+ // src/exit-intent/exit-intent.ts
570
+ function isMobileDevice(userAgent) {
571
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
572
+ }
573
+ function hasMinTimeElapsed(pageLoadTime, minTime, currentTime) {
574
+ return currentTime - pageLoadTime >= minTime;
575
+ }
576
+ function addPositionToHistory(positions, newPosition, maxSize) {
577
+ const updated = [...positions, newPosition];
578
+ if (updated.length > maxSize) {
579
+ return updated.slice(1);
580
+ }
581
+ return updated;
582
+ }
583
+ function calculateVelocity(lastY, previousY) {
584
+ return Math.abs(lastY - previousY);
585
+ }
586
+ function shouldTriggerExitIntent(positions, sensitivity, relatedTarget) {
587
+ if (positions.length < 2) {
588
+ return { shouldTrigger: false, lastY: 0, previousY: 0, velocity: 0 };
589
+ }
590
+ if (relatedTarget && relatedTarget.nodeName !== "HTML") {
591
+ return { shouldTrigger: false, lastY: 0, previousY: 0, velocity: 0 };
592
+ }
593
+ const lastY = positions[positions.length - 1].y;
594
+ const previousY = positions[positions.length - 2].y;
595
+ const velocity = calculateVelocity(lastY, previousY);
596
+ const isMovingUp = lastY < previousY;
597
+ const isNearTop = lastY - velocity <= sensitivity;
598
+ return {
599
+ shouldTrigger: isMovingUp && isNearTop,
600
+ lastY,
601
+ previousY,
602
+ velocity
603
+ };
604
+ }
605
+ function createExitIntentEvent(lastY, previousY, velocity, pageLoadTime, timestamp) {
606
+ return {
607
+ timestamp,
608
+ lastY,
609
+ previousY,
610
+ velocity,
611
+ timeOnPage: timestamp - pageLoadTime
612
+ };
613
+ }
614
+ var exitIntentPlugin = (plugin, instance, config) => {
615
+ plugin.ns("experiences.exitIntent");
616
+ plugin.defaults({
617
+ exitIntent: {
618
+ sensitivity: 50,
619
+ minTimeOnPage: 2e3,
620
+ delay: 0,
621
+ positionHistorySize: 30,
622
+ disableOnMobile: true
623
+ }
624
+ });
625
+ const exitIntentConfig = config.get("exitIntent");
626
+ if (!exitIntentConfig) {
627
+ return;
628
+ }
629
+ let positions = [];
630
+ let triggered = false;
631
+ const pageLoadTime = Date.now();
632
+ let mouseMoveListener = null;
633
+ let mouseOutListener = null;
634
+ function shouldDisable() {
635
+ if (!exitIntentConfig?.disableOnMobile) {
636
+ return false;
637
+ }
638
+ return isMobileDevice(navigator.userAgent);
639
+ }
640
+ function trackPosition(e) {
641
+ const newPosition = { x: e.clientX, y: e.clientY };
642
+ const maxSize = exitIntentConfig?.positionHistorySize ?? 30;
643
+ positions = addPositionToHistory(positions, newPosition, maxSize);
644
+ }
645
+ function handleExitIntent(e) {
646
+ if (triggered) {
647
+ return;
648
+ }
649
+ const minTime = exitIntentConfig?.minTimeOnPage ?? 2e3;
650
+ if (!hasMinTimeElapsed(pageLoadTime, minTime, Date.now())) {
651
+ return;
652
+ }
653
+ const sensitivity = exitIntentConfig?.sensitivity ?? 50;
654
+ const relatedTarget = e.relatedTarget || e.toElement;
655
+ const result = shouldTriggerExitIntent(positions, sensitivity, relatedTarget);
656
+ if (result.shouldTrigger) {
657
+ triggered = true;
658
+ const eventPayload = createExitIntentEvent(
659
+ result.lastY,
660
+ result.previousY,
661
+ result.velocity,
662
+ pageLoadTime,
663
+ Date.now()
664
+ );
665
+ const delay = exitIntentConfig?.delay ?? 0;
666
+ if (delay > 0) {
667
+ setTimeout(() => {
668
+ instance.emit("trigger:exitIntent", eventPayload);
669
+ }, delay);
670
+ } else {
671
+ instance.emit("trigger:exitIntent", eventPayload);
672
+ }
673
+ try {
674
+ sessionStorage.setItem("xp:exitIntent:triggered", Date.now().toString());
675
+ } catch (_e) {
676
+ }
677
+ cleanup();
678
+ }
679
+ }
680
+ function cleanup() {
681
+ if (mouseMoveListener) {
682
+ document.removeEventListener("mousemove", mouseMoveListener);
683
+ mouseMoveListener = null;
684
+ }
685
+ if (mouseOutListener) {
686
+ document.removeEventListener("mouseout", mouseOutListener);
687
+ mouseOutListener = null;
688
+ }
689
+ }
690
+ function initialize() {
691
+ if (shouldDisable()) {
692
+ return;
693
+ }
694
+ try {
695
+ const storedTrigger = sessionStorage.getItem("xp:exitIntent:triggered");
696
+ if (storedTrigger) {
697
+ triggered = true;
698
+ return;
699
+ }
700
+ } catch (_e) {
701
+ }
702
+ mouseMoveListener = trackPosition;
703
+ mouseOutListener = handleExitIntent;
704
+ document.addEventListener("mousemove", mouseMoveListener);
705
+ document.addEventListener("mouseout", mouseOutListener);
706
+ }
707
+ plugin.expose({
708
+ exitIntent: {
709
+ /**
710
+ * Check if exit intent has been triggered
711
+ */
712
+ isTriggered: () => triggered,
713
+ /**
714
+ * Reset exit intent state (useful for testing)
715
+ */
716
+ reset: () => {
717
+ triggered = false;
718
+ positions = [];
719
+ try {
720
+ sessionStorage.removeItem("xp:exitIntent:triggered");
721
+ } catch (_e) {
722
+ }
723
+ cleanup();
724
+ initialize();
725
+ },
726
+ /**
727
+ * Get current position history
728
+ */
729
+ getPositions: () => [...positions]
730
+ }
731
+ });
732
+ initialize();
733
+ instance.on("sdk:destroy", () => {
734
+ cleanup();
735
+ });
736
+ };
564
737
  var frequencyPlugin = (plugin, instance, config) => {
565
738
  plugin.ns("frequency");
566
739
  plugin.defaults({
@@ -689,6 +862,1677 @@ var frequencyPlugin = (plugin, instance, config) => {
689
862
  }
690
863
  };
691
864
 
692
- export { bannerPlugin, debugPlugin, frequencyPlugin };
865
+ // src/inline/insertion.ts
866
+ function insertContent(selector, content, position, experienceId) {
867
+ const target = document.querySelector(selector);
868
+ if (!target) {
869
+ return null;
870
+ }
871
+ const wrapper = document.createElement("div");
872
+ wrapper.className = "xp-inline";
873
+ wrapper.setAttribute("data-xp-id", experienceId);
874
+ wrapper.innerHTML = content;
875
+ switch (position) {
876
+ case "replace":
877
+ target.innerHTML = "";
878
+ target.appendChild(wrapper);
879
+ break;
880
+ case "append":
881
+ target.appendChild(wrapper);
882
+ break;
883
+ case "prepend":
884
+ target.insertBefore(wrapper, target.firstChild);
885
+ break;
886
+ case "before":
887
+ target.parentElement?.insertBefore(wrapper, target);
888
+ break;
889
+ case "after":
890
+ target.parentElement?.insertBefore(wrapper, target.nextSibling);
891
+ break;
892
+ }
893
+ return wrapper;
894
+ }
895
+ function removeContent(experienceId) {
896
+ const element = document.querySelector(`[data-xp-id="${experienceId}"]`);
897
+ if (!element) {
898
+ return false;
899
+ }
900
+ element.remove();
901
+ return true;
902
+ }
903
+
904
+ // src/inline/inline.ts
905
+ var inlinePlugin = (plugin, instance, config) => {
906
+ plugin.ns("experiences.inline");
907
+ plugin.defaults({
908
+ inline: {
909
+ retry: false,
910
+ retryTimeout: 5e3
911
+ }
912
+ });
913
+ if (!instance.storage) {
914
+ instance.use(storagePlugin);
915
+ }
916
+ const sdkInstance = instance;
917
+ if (typeof document !== "undefined") {
918
+ const styleId = "xp-inline-styles";
919
+ if (!document.getElementById(styleId)) {
920
+ const style = document.createElement("style");
921
+ style.id = styleId;
922
+ style.textContent = getInlineStyles();
923
+ document.head.appendChild(style);
924
+ }
925
+ }
926
+ const activeInlines = /* @__PURE__ */ new Map();
927
+ const show = (experience) => {
928
+ const { id, content } = experience;
929
+ if (activeInlines.has(id)) {
930
+ return;
931
+ }
932
+ if (content.persist && content.dismissable && sdkInstance.storage) {
933
+ const dismissed = sdkInstance.storage.get(`xp-inline-dismissed-${id}`);
934
+ if (dismissed) {
935
+ instance.emit("experiences:inline:dismissed", {
936
+ experienceId: id,
937
+ reason: "previously-dismissed",
938
+ timestamp: Date.now()
939
+ });
940
+ return;
941
+ }
942
+ }
943
+ const element = insertContent(
944
+ content.selector,
945
+ sanitizeHTML(content.message),
946
+ content.position || "replace",
947
+ id
948
+ );
949
+ if (!element) {
950
+ instance.emit("experiences:inline:error", {
951
+ experienceId: id,
952
+ error: "selector-not-found",
953
+ selector: content.selector,
954
+ timestamp: Date.now()
955
+ });
956
+ const retryEnabled = config.get("inline.retry") ?? false;
957
+ const retryTimeout = config.get("inline.retryTimeout") ?? 5e3;
958
+ if (retryEnabled) {
959
+ setTimeout(() => {
960
+ show(experience);
961
+ }, retryTimeout);
962
+ }
963
+ return;
964
+ }
965
+ activeInlines.set(id, element);
966
+ if (content.className) {
967
+ element.classList.add(content.className);
968
+ }
969
+ if (content.style) {
970
+ Object.assign(element.style, content.style);
971
+ }
972
+ if (content.dismissable) {
973
+ const closeBtn = document.createElement("button");
974
+ closeBtn.className = "xp-inline__close";
975
+ closeBtn.setAttribute("aria-label", "Close");
976
+ closeBtn.textContent = "\xD7";
977
+ closeBtn.onclick = () => {
978
+ remove(id);
979
+ if (content.persist && sdkInstance.storage) {
980
+ sdkInstance.storage.set(`xp-inline-dismissed-${id}`, true);
981
+ }
982
+ instance.emit("experiences:dismissed", {
983
+ experienceId: id,
984
+ timestamp: Date.now()
985
+ });
986
+ };
987
+ element.prepend(closeBtn);
988
+ }
989
+ instance.emit("experiences:shown", {
990
+ experienceId: id,
991
+ type: "inline",
992
+ selector: content.selector,
993
+ position: content.position || "replace",
994
+ timestamp: Date.now()
995
+ });
996
+ };
997
+ const remove = (experienceId) => {
998
+ const element = activeInlines.get(experienceId);
999
+ if (!element) return;
1000
+ removeContent(experienceId);
1001
+ activeInlines.delete(experienceId);
1002
+ };
1003
+ const isShowing = (experienceId) => {
1004
+ if (experienceId) {
1005
+ return activeInlines.has(experienceId);
1006
+ }
1007
+ return activeInlines.size > 0;
1008
+ };
1009
+ plugin.expose({
1010
+ inline: {
1011
+ show,
1012
+ remove,
1013
+ isShowing
1014
+ }
1015
+ });
1016
+ instance.on("experiences:evaluated", (data) => {
1017
+ if (data.decision?.show && data.experience?.type === "inline") {
1018
+ show(data.experience);
1019
+ }
1020
+ });
1021
+ instance.on("sdk:destroy", () => {
1022
+ for (const id of Array.from(activeInlines.keys())) {
1023
+ remove(id);
1024
+ }
1025
+ });
1026
+ };
1027
+ function getInlineStyles() {
1028
+ return `
1029
+ :root {
1030
+ --xp-inline-close-size: 24px;
1031
+ --xp-inline-close-color: #666;
1032
+ --xp-inline-close-hover-color: #111;
1033
+ --xp-inline-close-bg: transparent;
1034
+ --xp-inline-close-hover-bg: rgba(0, 0, 0, 0.05);
1035
+ --xp-inline-close-border-radius: 4px;
1036
+ }
1037
+
1038
+ @media (prefers-color-scheme: dark) {
1039
+ :root {
1040
+ --xp-inline-close-color: #9ca3af;
1041
+ --xp-inline-close-hover-color: #f9fafb;
1042
+ --xp-inline-close-hover-bg: rgba(255, 255, 255, 0.1);
1043
+ }
1044
+ }
1045
+
1046
+ .xp-inline {
1047
+ position: relative;
1048
+ animation: xp-inline-enter 0.4s ease-out forwards;
1049
+ }
1050
+
1051
+ @keyframes xp-inline-enter {
1052
+ from {
1053
+ opacity: 0;
1054
+ transform: translateY(-8px);
1055
+ }
1056
+ to {
1057
+ opacity: 1;
1058
+ transform: translateY(0);
1059
+ }
1060
+ }
1061
+
1062
+ /* Respect user's motion preferences */
1063
+ @media (prefers-reduced-motion: reduce) {
1064
+ .xp-inline {
1065
+ animation: xp-inline-enter-reduced 0.2s ease-out forwards;
1066
+ }
1067
+
1068
+ @keyframes xp-inline-enter-reduced {
1069
+ from {
1070
+ opacity: 0;
1071
+ }
1072
+ to {
1073
+ opacity: 1;
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ .xp-inline__close {
1079
+ position: absolute;
1080
+ top: 8px;
1081
+ right: 8px;
1082
+ width: var(--xp-inline-close-size);
1083
+ height: var(--xp-inline-close-size);
1084
+ padding: 0;
1085
+ border: none;
1086
+ background: var(--xp-inline-close-bg);
1087
+ color: var(--xp-inline-close-color);
1088
+ font-size: 20px;
1089
+ line-height: 1;
1090
+ cursor: pointer;
1091
+ border-radius: var(--xp-inline-close-border-radius);
1092
+ transition: all 0.2s ease;
1093
+ display: flex;
1094
+ align-items: center;
1095
+ justify-content: center;
1096
+ z-index: 1;
1097
+ }
1098
+
1099
+ .xp-inline__close:hover {
1100
+ background: var(--xp-inline-close-hover-bg);
1101
+ color: var(--xp-inline-close-hover-color);
1102
+ }
1103
+ `;
1104
+ }
1105
+
1106
+ // src/modal/form-styles.ts
1107
+ function getFormStyles() {
1108
+ return `
1109
+ margin-top: var(--xp-form-spacing, 16px);
1110
+ display: flex;
1111
+ flex-direction: column;
1112
+ gap: var(--xp-form-gap, 16px);
1113
+ `.trim();
1114
+ }
1115
+ function getFieldStyles() {
1116
+ return `
1117
+ display: flex;
1118
+ flex-direction: column;
1119
+ gap: var(--xp-field-gap, 6px);
1120
+ `.trim();
1121
+ }
1122
+ function getLabelStyles() {
1123
+ return `
1124
+ font-size: var(--xp-label-font-size, 14px);
1125
+ font-weight: var(--xp-label-font-weight, 500);
1126
+ color: var(--xp-label-color, #374151);
1127
+ line-height: 1.5;
1128
+ `.trim();
1129
+ }
1130
+ function getRequiredStyles() {
1131
+ return `
1132
+ color: var(--xp-required-color, #ef4444);
1133
+ `.trim();
1134
+ }
1135
+ function getInputStyles() {
1136
+ return `
1137
+ padding: var(--xp-input-padding, 8px 12px);
1138
+ font-size: var(--xp-input-font-size, 14px);
1139
+ line-height: 1.5;
1140
+ color: var(--xp-input-color, #111827);
1141
+ background-color: var(--xp-input-bg, white);
1142
+ border: var(--xp-input-border-width, 1px) solid var(--xp-input-border-color, #d1d5db);
1143
+ border-radius: var(--xp-input-radius, 6px);
1144
+ transition: all 0.15s ease-in-out;
1145
+ outline: none;
1146
+ width: 100%;
1147
+ box-sizing: border-box;
1148
+ `.trim();
1149
+ }
1150
+ function getInputErrorStyles() {
1151
+ return `
1152
+ border-color: var(--xp-input-error-border, #ef4444);
1153
+ `.trim();
1154
+ }
1155
+ function getErrorMessageStyles() {
1156
+ return `
1157
+ font-size: var(--xp-error-font-size, 13px);
1158
+ color: var(--xp-error-color, #ef4444);
1159
+ line-height: 1.4;
1160
+ min-height: 18px;
1161
+ `.trim();
1162
+ }
1163
+ function getSubmitButtonStyles() {
1164
+ return `
1165
+ margin-top: var(--xp-submit-margin-top, 8px);
1166
+ padding: var(--xp-submit-padding, 10px 20px);
1167
+ font-size: var(--xp-submit-font-size, 14px);
1168
+ font-weight: var(--xp-submit-font-weight, 500);
1169
+ color: var(--xp-submit-color, white);
1170
+ background-color: var(--xp-submit-bg, #2563eb);
1171
+ border: none;
1172
+ border-radius: var(--xp-submit-radius, 6px);
1173
+ cursor: pointer;
1174
+ transition: all 0.2s;
1175
+ width: 100%;
1176
+ `.trim();
1177
+ }
1178
+ function getSubmitButtonHoverBg() {
1179
+ return "var(--xp-submit-bg-hover, #1d4ed8)";
1180
+ }
1181
+ function getFormStateStyles() {
1182
+ return `
1183
+ padding: var(--xp-state-padding, 16px);
1184
+ border-radius: var(--xp-state-radius, 8px);
1185
+ text-align: center;
1186
+ `.trim();
1187
+ }
1188
+ function getSuccessStateStyles() {
1189
+ return `
1190
+ background-color: var(--xp-success-bg, #f0fdf4);
1191
+ border: var(--xp-state-border-width, 1px) solid var(--xp-success-border, #86efac);
1192
+ `.trim();
1193
+ }
1194
+ function getErrorStateStyles() {
1195
+ return `
1196
+ background-color: var(--xp-error-bg, #fef2f2);
1197
+ border: var(--xp-state-border-width, 1px) solid var(--xp-error-border, #fca5a5);
1198
+ `.trim();
1199
+ }
1200
+ function getStateTitleStyles() {
1201
+ return `
1202
+ font-size: var(--xp-state-title-font-size, 16px);
1203
+ font-weight: var(--xp-state-title-font-weight, 600);
1204
+ margin: 0 0 var(--xp-state-title-margin-bottom, 8px) 0;
1205
+ color: var(--xp-state-title-color, #111827);
1206
+ `.trim();
1207
+ }
1208
+ function getStateMessageStyles() {
1209
+ return `
1210
+ font-size: var(--xp-state-message-font-size, 14px);
1211
+ line-height: 1.5;
1212
+ color: var(--xp-state-message-color, #374151);
1213
+ margin: 0;
1214
+ `.trim();
1215
+ }
1216
+ function getStateButtonsStyles() {
1217
+ return `
1218
+ margin-top: var(--xp-state-buttons-margin-top, 16px);
1219
+ display: flex;
1220
+ gap: var(--xp-state-buttons-gap, 8px);
1221
+ justify-content: center;
1222
+ flex-wrap: wrap;
1223
+ `.trim();
1224
+ }
1225
+
1226
+ // src/modal/form-rendering.ts
1227
+ function renderForm(experienceId, config) {
1228
+ const form = document.createElement("form");
1229
+ form.className = "xp-modal__form";
1230
+ form.style.cssText = getFormStyles();
1231
+ form.dataset.xpExperienceId = experienceId;
1232
+ form.setAttribute("novalidate", "");
1233
+ config.fields.forEach((field) => {
1234
+ const fieldElement = renderFormField(experienceId, field);
1235
+ form.appendChild(fieldElement);
1236
+ });
1237
+ const submitButton = renderSubmitButton(config.submitButton);
1238
+ form.appendChild(submitButton);
1239
+ return form;
1240
+ }
1241
+ function renderFormField(experienceId, field) {
1242
+ const wrapper = document.createElement("div");
1243
+ wrapper.className = "xp-form__field";
1244
+ wrapper.style.cssText = getFieldStyles();
1245
+ if (field.label) {
1246
+ const label = document.createElement("label");
1247
+ label.className = "xp-form__label";
1248
+ label.style.cssText = getLabelStyles();
1249
+ label.htmlFor = `${experienceId}-${field.name}`;
1250
+ label.textContent = field.label;
1251
+ if (field.required) {
1252
+ const required = document.createElement("span");
1253
+ required.className = "xp-form__required";
1254
+ required.style.cssText = getRequiredStyles();
1255
+ required.textContent = " *";
1256
+ required.setAttribute("aria-label", "required");
1257
+ label.appendChild(required);
1258
+ }
1259
+ wrapper.appendChild(label);
1260
+ }
1261
+ const input = field.type === "textarea" ? document.createElement("textarea") : document.createElement("input");
1262
+ input.className = "xp-form__input";
1263
+ input.style.cssText = getInputStyles();
1264
+ input.id = `${experienceId}-${field.name}`;
1265
+ input.name = field.name;
1266
+ if (input instanceof HTMLInputElement) {
1267
+ input.type = field.type;
1268
+ }
1269
+ if (field.placeholder) {
1270
+ input.placeholder = field.placeholder;
1271
+ }
1272
+ if (field.required) {
1273
+ input.required = true;
1274
+ }
1275
+ if (field.pattern && input instanceof HTMLInputElement) {
1276
+ input.setAttribute("pattern", field.pattern);
1277
+ }
1278
+ input.setAttribute("aria-invalid", "false");
1279
+ input.setAttribute("aria-describedby", `${experienceId}-${field.name}-error`);
1280
+ if (field.className) {
1281
+ input.className += ` ${field.className}`;
1282
+ }
1283
+ if (field.style) {
1284
+ Object.assign(input.style, field.style);
1285
+ }
1286
+ wrapper.appendChild(input);
1287
+ const error = document.createElement("div");
1288
+ error.className = "xp-form__error";
1289
+ error.style.cssText = getErrorMessageStyles();
1290
+ error.id = `${experienceId}-${field.name}-error`;
1291
+ error.setAttribute("role", "alert");
1292
+ error.setAttribute("aria-live", "polite");
1293
+ wrapper.appendChild(error);
1294
+ return wrapper;
1295
+ }
1296
+ function renderSubmitButton(buttonConfig) {
1297
+ const button = document.createElement("button");
1298
+ button.type = "submit";
1299
+ button.className = "xp-form__submit xp-modal__button";
1300
+ button.style.cssText = getSubmitButtonStyles();
1301
+ if (buttonConfig.variant) {
1302
+ button.className += ` xp-modal__button--${buttonConfig.variant}`;
1303
+ }
1304
+ if (buttonConfig.className) {
1305
+ button.className += ` ${buttonConfig.className}`;
1306
+ }
1307
+ button.textContent = buttonConfig.text;
1308
+ const hoverBg = getSubmitButtonHoverBg();
1309
+ button.onmouseover = () => {
1310
+ button.style.backgroundColor = hoverBg;
1311
+ };
1312
+ button.onmouseout = () => {
1313
+ button.style.backgroundColor = "";
1314
+ };
1315
+ if (buttonConfig.style) {
1316
+ Object.assign(button.style, buttonConfig.style);
1317
+ }
1318
+ return button;
1319
+ }
1320
+ function renderFormState(state, stateConfig) {
1321
+ const stateEl = document.createElement("div");
1322
+ stateEl.className = `xp-form__state xp-form__state--${state}`;
1323
+ const baseStyles = getFormStateStyles();
1324
+ const stateStyles = state === "success" ? getSuccessStateStyles() : getErrorStateStyles();
1325
+ stateEl.style.cssText = `${baseStyles}; ${stateStyles}`;
1326
+ if (stateConfig.title) {
1327
+ const title = document.createElement("h3");
1328
+ title.className = "xp-form__state-title";
1329
+ title.style.cssText = getStateTitleStyles();
1330
+ title.textContent = stateConfig.title;
1331
+ stateEl.appendChild(title);
1332
+ }
1333
+ const message = document.createElement("div");
1334
+ message.className = "xp-form__state-message";
1335
+ message.style.cssText = getStateMessageStyles();
1336
+ message.textContent = stateConfig.message;
1337
+ stateEl.appendChild(message);
1338
+ if (stateConfig.buttons && stateConfig.buttons.length > 0) {
1339
+ const buttonContainer = document.createElement("div");
1340
+ buttonContainer.className = "xp-form__state-buttons";
1341
+ buttonContainer.style.cssText = getStateButtonsStyles();
1342
+ stateConfig.buttons.forEach((btnConfig) => {
1343
+ const btn = document.createElement("button");
1344
+ btn.type = "button";
1345
+ btn.className = "xp-modal__button";
1346
+ if (btnConfig.variant) {
1347
+ btn.className += ` xp-modal__button--${btnConfig.variant}`;
1348
+ }
1349
+ if (btnConfig.className) {
1350
+ btn.className += ` ${btnConfig.className}`;
1351
+ }
1352
+ btn.textContent = btnConfig.text;
1353
+ if (btnConfig.style) {
1354
+ Object.assign(btn.style, btnConfig.style);
1355
+ }
1356
+ if (btnConfig.action) {
1357
+ btn.dataset.action = btnConfig.action;
1358
+ }
1359
+ if (btnConfig.dismiss) {
1360
+ btn.dataset.dismiss = "true";
1361
+ }
1362
+ buttonContainer.appendChild(btn);
1363
+ });
1364
+ stateEl.appendChild(buttonContainer);
1365
+ }
1366
+ return stateEl;
1367
+ }
1368
+
1369
+ // src/modal/form-validation.ts
1370
+ function validateField(field, value) {
1371
+ const errors = {};
1372
+ if (field.required && (!value || value.trim() === "")) {
1373
+ errors[field.name] = field.errorMessage || `${field.label || field.name} is required`;
1374
+ return { valid: false, errors };
1375
+ }
1376
+ if (!value || value.trim() === "") {
1377
+ return { valid: true };
1378
+ }
1379
+ switch (field.type) {
1380
+ case "email": {
1381
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1382
+ if (!emailRegex.test(value)) {
1383
+ errors[field.name] = field.errorMessage || "Please enter a valid email address";
1384
+ }
1385
+ break;
1386
+ }
1387
+ case "url": {
1388
+ try {
1389
+ new URL(value);
1390
+ } catch {
1391
+ errors[field.name] = field.errorMessage || "Please enter a valid URL";
1392
+ }
1393
+ break;
1394
+ }
1395
+ case "tel": {
1396
+ const phoneRegex = /^[\d\s\-()+]+$/;
1397
+ if (!phoneRegex.test(value)) {
1398
+ errors[field.name] = field.errorMessage || "Please enter a valid phone number";
1399
+ }
1400
+ break;
1401
+ }
1402
+ case "number": {
1403
+ if (Number.isNaN(Number(value))) {
1404
+ errors[field.name] = field.errorMessage || "Please enter a valid number";
1405
+ }
1406
+ break;
1407
+ }
1408
+ }
1409
+ if (field.pattern && value) {
1410
+ try {
1411
+ const regex = new RegExp(field.pattern);
1412
+ if (!regex.test(value)) {
1413
+ errors[field.name] = field.errorMessage || `Invalid format for ${field.label || field.name}`;
1414
+ }
1415
+ } catch (_error) {
1416
+ console.warn(`Invalid regex pattern for field ${field.name}:`, field.pattern);
1417
+ }
1418
+ }
1419
+ return {
1420
+ valid: Object.keys(errors).length === 0,
1421
+ errors: Object.keys(errors).length > 0 ? errors : void 0
1422
+ };
1423
+ }
1424
+ function validateForm(config, data) {
1425
+ const errors = {};
1426
+ config.fields.forEach((field) => {
1427
+ const value = data[field.name] || "";
1428
+ const result = validateField(field, value);
1429
+ if (!result.valid && result.errors) {
1430
+ Object.assign(errors, result.errors);
1431
+ }
1432
+ });
1433
+ if (config.validate) {
1434
+ try {
1435
+ const customResult = config.validate(data);
1436
+ if (!customResult.valid && customResult.errors) {
1437
+ Object.assign(errors, customResult.errors);
1438
+ }
1439
+ } catch (error) {
1440
+ console.error("Custom validation function threw an error:", error);
1441
+ }
1442
+ }
1443
+ return {
1444
+ valid: Object.keys(errors).length === 0,
1445
+ errors: Object.keys(errors).length > 0 ? errors : void 0
1446
+ };
1447
+ }
1448
+
1449
+ // src/modal/modal-styles.ts
1450
+ function getBackdropStyles() {
1451
+ return `
1452
+ position: absolute;
1453
+ inset: 0;
1454
+ background-color: var(--xp-modal-backdrop-bg, rgba(0, 0, 0, 0.5));
1455
+ `.trim();
1456
+ }
1457
+ function getDialogStyles(params) {
1458
+ return `
1459
+ position: relative;
1460
+ background: var(--xp-modal-dialog-bg, white);
1461
+ border-radius: var(--xp-modal-dialog-radius, ${params.borderRadius});
1462
+ box-shadow: var(--xp-modal-dialog-shadow, 0 4px 6px rgba(0, 0, 0, 0.1));
1463
+ max-width: ${params.width};
1464
+ width: ${params.maxWidth};
1465
+ height: ${params.height};
1466
+ max-height: ${params.maxHeight};
1467
+ overflow-y: auto;
1468
+ padding: ${params.padding};
1469
+ `.trim();
1470
+ }
1471
+ function getHeroImageStyles(params) {
1472
+ return `
1473
+ width: 100%;
1474
+ height: auto;
1475
+ max-height: ${params.maxHeight}px;
1476
+ object-fit: cover;
1477
+ border-radius: ${params.borderRadius};
1478
+ display: block;
1479
+ margin: 0;
1480
+ `.trim();
1481
+ }
1482
+ function getCloseButtonStyles() {
1483
+ return `
1484
+ position: absolute;
1485
+ top: var(--xp-modal-close-top, 16px);
1486
+ right: var(--xp-modal-close-right, 16px);
1487
+ background: none;
1488
+ border: none;
1489
+ font-size: var(--xp-modal-close-size, 24px);
1490
+ line-height: 1;
1491
+ cursor: pointer;
1492
+ padding: var(--xp-modal-close-padding, 4px 8px);
1493
+ color: var(--xp-modal-close-color, #666);
1494
+ opacity: var(--xp-modal-close-opacity, 0.7);
1495
+ transition: opacity 0.2s;
1496
+ `.trim();
1497
+ }
1498
+ function getCloseButtonHoverOpacity() {
1499
+ return "var(--xp-modal-close-hover-opacity, 1)";
1500
+ }
1501
+ function getCloseButtonDefaultOpacity() {
1502
+ return "var(--xp-modal-close-opacity, 0.7)";
1503
+ }
1504
+ function getContentWrapperStyles(padding) {
1505
+ return `padding: ${padding};`;
1506
+ }
1507
+ function getTitleStyles() {
1508
+ return `
1509
+ margin: 0 0 var(--xp-modal-title-margin-bottom, 16px) 0;
1510
+ font-size: var(--xp-modal-title-size, 20px);
1511
+ font-weight: var(--xp-modal-title-weight, 600);
1512
+ color: var(--xp-modal-title-color, #111);
1513
+ `.trim();
1514
+ }
1515
+ function getMessageStyles() {
1516
+ return `
1517
+ margin: 0 0 var(--xp-modal-message-margin-bottom, 20px) 0;
1518
+ font-size: var(--xp-modal-message-size, 14px);
1519
+ line-height: var(--xp-modal-message-line-height, 1.5);
1520
+ color: var(--xp-modal-message-color, #444);
1521
+ `.trim();
1522
+ }
1523
+ function getButtonContainerStyles() {
1524
+ return `
1525
+ display: flex;
1526
+ gap: var(--xp-modal-buttons-gap, 8px);
1527
+ flex-wrap: wrap;
1528
+ `.trim();
1529
+ }
1530
+ function getPrimaryButtonStyles() {
1531
+ return `
1532
+ padding: var(--xp-button-padding, 10px 20px);
1533
+ font-size: var(--xp-button-font-size, 14px);
1534
+ font-weight: var(--xp-button-font-weight, 500);
1535
+ border-radius: var(--xp-button-radius, 6px);
1536
+ cursor: pointer;
1537
+ transition: all 0.2s;
1538
+ border: none;
1539
+ background: var(--xp-button-primary-bg, #2563eb);
1540
+ color: var(--xp-button-primary-color, white);
1541
+ `.trim();
1542
+ }
1543
+ function getPrimaryButtonHoverBg() {
1544
+ return "var(--xp-button-primary-bg-hover, #1d4ed8)";
1545
+ }
1546
+ function getPrimaryButtonDefaultBg() {
1547
+ return "var(--xp-button-primary-bg, #2563eb)";
1548
+ }
1549
+ function getSecondaryButtonStyles() {
1550
+ return `
1551
+ padding: var(--xp-button-padding, 10px 20px);
1552
+ font-size: var(--xp-button-font-size, 14px);
1553
+ font-weight: var(--xp-button-font-weight, 500);
1554
+ border-radius: var(--xp-button-radius, 6px);
1555
+ cursor: pointer;
1556
+ transition: all 0.2s;
1557
+ border: none;
1558
+ background: var(--xp-button-secondary-bg, #f3f4f6);
1559
+ color: var(--xp-button-secondary-color, #374151);
1560
+ `.trim();
1561
+ }
1562
+ function getSecondaryButtonHoverBg() {
1563
+ return "var(--xp-button-secondary-bg-hover, #e5e7eb)";
1564
+ }
1565
+ function getSecondaryButtonDefaultBg() {
1566
+ return "var(--xp-button-secondary-bg, #f3f4f6)";
1567
+ }
1568
+
1569
+ // src/modal/modal.ts
1570
+ var modalPlugin = (plugin, instance) => {
1571
+ plugin.ns("experiences.modal");
1572
+ plugin.defaults({
1573
+ modal: {
1574
+ dismissable: true,
1575
+ backdropDismiss: true,
1576
+ zIndex: 10001,
1577
+ size: "md",
1578
+ mobileFullscreen: false,
1579
+ position: "center",
1580
+ animation: "fade",
1581
+ animationDuration: 200
1582
+ }
1583
+ });
1584
+ const activeModals = /* @__PURE__ */ new Map();
1585
+ const previouslyFocusedElement = /* @__PURE__ */ new Map();
1586
+ const formData = /* @__PURE__ */ new Map();
1587
+ const getFocusableElements = (container) => {
1588
+ const selector = 'a[href], button, textarea, input, select, details, [tabindex]:not([tabindex="-1"])';
1589
+ return Array.from(container.querySelectorAll(selector)).filter(
1590
+ (el) => !el.hasAttribute("disabled")
1591
+ );
1592
+ };
1593
+ const createFocusTrap = (container) => {
1594
+ const focusable = getFocusableElements(container);
1595
+ if (focusable.length === 0) return () => {
1596
+ };
1597
+ const firstFocusable = focusable[0];
1598
+ const lastFocusable = focusable[focusable.length - 1];
1599
+ const handleKeyDown = (e) => {
1600
+ if (e.key !== "Tab") return;
1601
+ if (e.shiftKey) {
1602
+ if (document.activeElement === firstFocusable) {
1603
+ e.preventDefault();
1604
+ lastFocusable.focus();
1605
+ }
1606
+ } else {
1607
+ if (document.activeElement === lastFocusable) {
1608
+ e.preventDefault();
1609
+ firstFocusable.focus();
1610
+ }
1611
+ }
1612
+ };
1613
+ container.addEventListener("keydown", handleKeyDown);
1614
+ firstFocusable.focus();
1615
+ return () => {
1616
+ container.removeEventListener("keydown", handleKeyDown);
1617
+ };
1618
+ };
1619
+ const getSizeWidth = (size) => {
1620
+ switch (size) {
1621
+ case "sm":
1622
+ return "400px";
1623
+ case "md":
1624
+ return "600px";
1625
+ case "lg":
1626
+ return "800px";
1627
+ case "fullscreen":
1628
+ return "100vw";
1629
+ case "auto":
1630
+ return "auto";
1631
+ default:
1632
+ return "600px";
1633
+ }
1634
+ };
1635
+ const isMobile = () => {
1636
+ return typeof window !== "undefined" && window.innerWidth < 640;
1637
+ };
1638
+ const renderModal = (experienceId, content) => {
1639
+ const modalConfig = instance.get("modal") || {};
1640
+ const zIndex = modalConfig.zIndex || 10001;
1641
+ const size = modalConfig.size || "md";
1642
+ const position = modalConfig.position || "center";
1643
+ const animation = modalConfig.animation || "fade";
1644
+ const animationDuration = modalConfig.animationDuration || 200;
1645
+ const mobileFullscreen = modalConfig.mobileFullscreen !== void 0 ? modalConfig.mobileFullscreen : size === "lg";
1646
+ const shouldBeFullscreen = size === "fullscreen" || mobileFullscreen && isMobile();
1647
+ const container = document.createElement("div");
1648
+ const sizeClass = shouldBeFullscreen ? "fullscreen" : size;
1649
+ const positionClass = position === "bottom" ? "xp-modal--bottom" : "xp-modal--center";
1650
+ const animationClass = animation !== "none" ? `xp-modal--${animation}` : "";
1651
+ container.className = `xp-modal xp-modal--${sizeClass} ${positionClass} ${animationClass} ${content.className || ""}`.trim();
1652
+ container.setAttribute("data-xp-id", experienceId);
1653
+ container.setAttribute("role", "dialog");
1654
+ container.setAttribute("aria-modal", "true");
1655
+ if (content.title) {
1656
+ container.setAttribute("aria-labelledby", `xp-modal-title-${experienceId}`);
1657
+ }
1658
+ const alignItems = position === "bottom" ? "flex-end" : "center";
1659
+ container.style.cssText = `position: fixed; inset: 0; z-index: ${zIndex}; display: flex; align-items: ${alignItems}; justify-content: center;`;
1660
+ if (animation !== "none") {
1661
+ container.style.opacity = "0";
1662
+ container.style.transition = `opacity ${animationDuration}ms ease-in-out`;
1663
+ if (animation === "slide-up") {
1664
+ container.style.transform = "translateY(100%)";
1665
+ container.style.transition += `, transform ${animationDuration}ms ease-out`;
1666
+ }
1667
+ }
1668
+ if (content.style) {
1669
+ Object.entries(content.style).forEach(([key, value]) => {
1670
+ container.style.setProperty(key, String(value));
1671
+ });
1672
+ }
1673
+ const backdrop = document.createElement("div");
1674
+ backdrop.className = "xp-modal__backdrop";
1675
+ backdrop.style.cssText = getBackdropStyles();
1676
+ container.appendChild(backdrop);
1677
+ const dialog = document.createElement("div");
1678
+ const dialogWidth = shouldBeFullscreen ? "100%" : size === "auto" ? "none" : getSizeWidth(size);
1679
+ const dialogHeight = shouldBeFullscreen ? "100%" : "auto";
1680
+ const dialogMaxWidth = shouldBeFullscreen ? "100%" : size === "auto" ? "none" : "90%";
1681
+ const dialogBorderRadius = shouldBeFullscreen ? "0" : "8px";
1682
+ const dialogPadding = content.image ? "0" : "24px";
1683
+ dialog.className = `xp-modal__dialog${content.image ? " xp-modal__dialog--has-image" : ""}`;
1684
+ dialog.style.cssText = getDialogStyles({
1685
+ width: dialogWidth,
1686
+ maxWidth: dialogMaxWidth,
1687
+ height: dialogHeight,
1688
+ maxHeight: shouldBeFullscreen ? "100%" : "90vh",
1689
+ borderRadius: dialogBorderRadius,
1690
+ padding: dialogPadding
1691
+ });
1692
+ container.appendChild(dialog);
1693
+ if (content.image) {
1694
+ const img = document.createElement("img");
1695
+ img.className = "xp-modal__hero-image";
1696
+ img.src = content.image.src;
1697
+ img.alt = content.image.alt;
1698
+ img.loading = "lazy";
1699
+ const maxHeight = content.image.maxHeight || (isMobile() ? 200 : 300);
1700
+ img.style.cssText = getHeroImageStyles({
1701
+ maxHeight,
1702
+ borderRadius: shouldBeFullscreen ? "0" : "8px 8px 0 0"
1703
+ });
1704
+ dialog.appendChild(img);
1705
+ }
1706
+ if (modalConfig.dismissable !== false) {
1707
+ const closeButton = document.createElement("button");
1708
+ closeButton.className = "xp-modal__close";
1709
+ closeButton.setAttribute("aria-label", "Close dialog");
1710
+ closeButton.innerHTML = "&times;";
1711
+ closeButton.style.cssText = getCloseButtonStyles();
1712
+ closeButton.onmouseover = () => {
1713
+ closeButton.style.opacity = getCloseButtonHoverOpacity();
1714
+ };
1715
+ closeButton.onmouseout = () => {
1716
+ closeButton.style.opacity = getCloseButtonDefaultOpacity();
1717
+ };
1718
+ closeButton.onclick = () => removeModal(experienceId);
1719
+ dialog.appendChild(closeButton);
1720
+ }
1721
+ const contentWrapper = document.createElement("div");
1722
+ contentWrapper.className = "xp-modal__content";
1723
+ const contentPadding = content.image ? "24px" : "24px 24px 0 24px";
1724
+ contentWrapper.style.cssText = getContentWrapperStyles(contentPadding);
1725
+ if (content.title) {
1726
+ const title = document.createElement("h2");
1727
+ title.id = `xp-modal-title-${experienceId}`;
1728
+ title.className = "xp-modal__title";
1729
+ title.textContent = content.title;
1730
+ title.style.cssText = getTitleStyles();
1731
+ contentWrapper.appendChild(title);
1732
+ }
1733
+ const message = document.createElement("div");
1734
+ message.className = "xp-modal__message";
1735
+ message.innerHTML = sanitizeHTML(content.message);
1736
+ message.style.cssText = getMessageStyles();
1737
+ contentWrapper.appendChild(message);
1738
+ if (content.form) {
1739
+ const form = renderForm(experienceId, content.form);
1740
+ contentWrapper.appendChild(form);
1741
+ container.__formConfig = content.form;
1742
+ const data = {};
1743
+ content.form.fields.forEach((field) => {
1744
+ data[field.name] = "";
1745
+ });
1746
+ formData.set(experienceId, data);
1747
+ content.form.fields.forEach((field) => {
1748
+ const input = form.querySelector(`#${experienceId}-${field.name}`);
1749
+ const errorEl = form.querySelector(`#${experienceId}-${field.name}-error`);
1750
+ if (!input) return;
1751
+ input.addEventListener("input", () => {
1752
+ const currentData = formData.get(experienceId) || {};
1753
+ currentData[field.name] = input.value;
1754
+ formData.set(experienceId, currentData);
1755
+ instance.emit("experiences:modal:form:change", {
1756
+ experienceId,
1757
+ field: field.name,
1758
+ value: input.value,
1759
+ formData: { ...currentData },
1760
+ timestamp: Date.now()
1761
+ });
1762
+ });
1763
+ input.addEventListener("blur", () => {
1764
+ const currentData = formData.get(experienceId) || {};
1765
+ const result = validateField(field, currentData[field.name] || "");
1766
+ if (!result.valid && result.errors) {
1767
+ input.style.cssText += `; ${getInputErrorStyles()}`;
1768
+ input.setAttribute("aria-invalid", "true");
1769
+ errorEl.textContent = result.errors[field.name] || "";
1770
+ instance.emit("experiences:modal:form:validation", {
1771
+ experienceId,
1772
+ field: field.name,
1773
+ valid: false,
1774
+ errors: result.errors,
1775
+ timestamp: Date.now()
1776
+ });
1777
+ } else {
1778
+ input.style.cssText = input.style.cssText.replace(getInputErrorStyles(), "");
1779
+ input.setAttribute("aria-invalid", "false");
1780
+ errorEl.textContent = "";
1781
+ instance.emit("experiences:modal:form:validation", {
1782
+ experienceId,
1783
+ field: field.name,
1784
+ valid: true,
1785
+ timestamp: Date.now()
1786
+ });
1787
+ }
1788
+ });
1789
+ });
1790
+ form.addEventListener("submit", async (e) => {
1791
+ e.preventDefault();
1792
+ if (!content.form) return;
1793
+ const currentData = formData.get(experienceId) || {};
1794
+ const result = validateForm(content.form, currentData);
1795
+ if (!result.valid && result.errors) {
1796
+ content.form.fields.forEach((field) => {
1797
+ if (result.errors?.[field.name]) {
1798
+ const input = form.querySelector(
1799
+ `#${experienceId}-${field.name}`
1800
+ );
1801
+ const errorEl = form.querySelector(
1802
+ `#${experienceId}-${field.name}-error`
1803
+ );
1804
+ if (input) {
1805
+ input.style.cssText += `; ${getInputErrorStyles()}`;
1806
+ input.setAttribute("aria-invalid", "true");
1807
+ }
1808
+ if (errorEl) {
1809
+ errorEl.textContent = result.errors[field.name] || "";
1810
+ }
1811
+ }
1812
+ });
1813
+ instance.emit("experiences:modal:form:validation", {
1814
+ experienceId,
1815
+ valid: false,
1816
+ errors: result.errors,
1817
+ timestamp: Date.now()
1818
+ });
1819
+ return;
1820
+ }
1821
+ const submitButton = form.querySelector('button[type="submit"]');
1822
+ if (submitButton) {
1823
+ submitButton.disabled = true;
1824
+ submitButton.textContent = "Submitting...";
1825
+ }
1826
+ instance.emit("experiences:modal:form:submit", {
1827
+ experienceId,
1828
+ formData: { ...currentData },
1829
+ timestamp: Date.now()
1830
+ });
1831
+ });
1832
+ } else if (content.buttons && content.buttons.length > 0) {
1833
+ const buttonContainer = document.createElement("div");
1834
+ buttonContainer.className = "xp-modal__buttons";
1835
+ buttonContainer.style.cssText = getButtonContainerStyles();
1836
+ content.buttons.forEach((button) => {
1837
+ const btn = document.createElement("button");
1838
+ btn.className = `xp-modal__button xp-modal__button--${button.variant || "secondary"}`;
1839
+ btn.textContent = button.text;
1840
+ if (button.variant === "primary") {
1841
+ btn.style.cssText = getPrimaryButtonStyles();
1842
+ btn.onmouseover = () => {
1843
+ btn.style.background = getPrimaryButtonHoverBg();
1844
+ };
1845
+ btn.onmouseout = () => {
1846
+ btn.style.background = getPrimaryButtonDefaultBg();
1847
+ };
1848
+ } else {
1849
+ btn.style.cssText = getSecondaryButtonStyles();
1850
+ btn.onmouseover = () => {
1851
+ btn.style.background = getSecondaryButtonHoverBg();
1852
+ };
1853
+ btn.onmouseout = () => {
1854
+ btn.style.background = getSecondaryButtonDefaultBg();
1855
+ };
1856
+ }
1857
+ btn.onclick = () => {
1858
+ instance.emit("experiences:action", {
1859
+ experienceId,
1860
+ action: button.action,
1861
+ button,
1862
+ timestamp: Date.now()
1863
+ });
1864
+ if (button.dismiss) {
1865
+ removeModal(experienceId);
1866
+ }
1867
+ if (button.url) {
1868
+ window.location.href = button.url;
1869
+ }
1870
+ };
1871
+ buttonContainer.appendChild(btn);
1872
+ });
1873
+ contentWrapper.appendChild(buttonContainer);
1874
+ }
1875
+ dialog.appendChild(contentWrapper);
1876
+ if (modalConfig.backdropDismiss !== false) {
1877
+ backdrop.onclick = () => removeModal(experienceId);
1878
+ }
1879
+ const handleEscape = (e) => {
1880
+ if (e.key === "Escape" && modalConfig.dismissable !== false) {
1881
+ removeModal(experienceId);
1882
+ }
1883
+ };
1884
+ document.addEventListener("keydown", handleEscape);
1885
+ container.__cleanupEscape = () => {
1886
+ document.removeEventListener("keydown", handleEscape);
1887
+ };
1888
+ return container;
1889
+ };
1890
+ const showModal = (experience) => {
1891
+ const experienceId = experience.id;
1892
+ if (activeModals.has(experienceId)) return;
1893
+ if (activeModals.size > 0) {
1894
+ const existingIds = Array.from(activeModals.keys());
1895
+ for (const id of existingIds) {
1896
+ removeModal(id);
1897
+ }
1898
+ }
1899
+ previouslyFocusedElement.set(experienceId, document.activeElement);
1900
+ const modal = renderModal(experienceId, experience.content);
1901
+ document.body.appendChild(modal);
1902
+ activeModals.set(experienceId, modal);
1903
+ const modalConfig = instance.get("modal") || {};
1904
+ const animation = modalConfig.animation || "fade";
1905
+ if (animation !== "none") {
1906
+ requestAnimationFrame(() => {
1907
+ modal.style.opacity = "1";
1908
+ if (animation === "slide-up") {
1909
+ modal.style.transform = "translateY(0)";
1910
+ }
1911
+ });
1912
+ }
1913
+ const cleanupFocusTrap = createFocusTrap(modal);
1914
+ modal.__cleanupFocusTrap = cleanupFocusTrap;
1915
+ instance.emit("experiences:shown", {
1916
+ experienceId,
1917
+ timestamp: Date.now()
1918
+ });
1919
+ instance.emit("trigger:modal", {
1920
+ experienceId,
1921
+ timestamp: Date.now(),
1922
+ shown: true
1923
+ });
1924
+ };
1925
+ const removeModal = (experienceId) => {
1926
+ const modal = activeModals.get(experienceId);
1927
+ if (!modal) return;
1928
+ if (modal.__cleanupFocusTrap) {
1929
+ modal.__cleanupFocusTrap();
1930
+ }
1931
+ if (modal.__cleanupEscape) {
1932
+ modal.__cleanupEscape();
1933
+ }
1934
+ const previousElement = previouslyFocusedElement.get(experienceId);
1935
+ if (previousElement && document.body.contains(previousElement)) {
1936
+ previousElement.focus();
1937
+ }
1938
+ previouslyFocusedElement.delete(experienceId);
1939
+ modal.remove();
1940
+ activeModals.delete(experienceId);
1941
+ instance.emit("experiences:dismissed", {
1942
+ experienceId,
1943
+ timestamp: Date.now()
1944
+ });
1945
+ };
1946
+ const isShowing = (experienceId) => {
1947
+ if (experienceId) {
1948
+ return activeModals.has(experienceId);
1949
+ }
1950
+ return activeModals.size > 0;
1951
+ };
1952
+ const showFormState = (experienceId, state) => {
1953
+ const modal = activeModals.get(experienceId);
1954
+ if (!modal) return;
1955
+ const form = modal.querySelector(".xp-modal__form");
1956
+ if (!form) return;
1957
+ const formConfig = modal.__formConfig;
1958
+ if (!formConfig) return;
1959
+ const stateConfig = state === "success" ? formConfig.successState : formConfig.errorState;
1960
+ if (!stateConfig) return;
1961
+ const stateEl = renderFormState(state, stateConfig);
1962
+ form.replaceWith(stateEl);
1963
+ instance.emit("experiences:modal:form:state", {
1964
+ experienceId,
1965
+ state,
1966
+ timestamp: Date.now()
1967
+ });
1968
+ };
1969
+ const resetForm = (experienceId) => {
1970
+ const modal = activeModals.get(experienceId);
1971
+ if (!modal) return;
1972
+ const form = modal.querySelector(".xp-modal__form");
1973
+ if (!form) return;
1974
+ form.reset();
1975
+ const data = formData.get(experienceId);
1976
+ if (data) {
1977
+ Object.keys(data).forEach((key) => {
1978
+ data[key] = "";
1979
+ });
1980
+ }
1981
+ const errors = form.querySelectorAll(".xp-form__error");
1982
+ errors.forEach((error) => {
1983
+ error.textContent = "";
1984
+ });
1985
+ const inputs = form.querySelectorAll(".xp-form__input");
1986
+ inputs.forEach((input) => {
1987
+ input.setAttribute("aria-invalid", "false");
1988
+ input.style.cssText = input.style.cssText.replace(getInputErrorStyles(), "");
1989
+ });
1990
+ };
1991
+ const getFormData = (experienceId) => {
1992
+ return formData.get(experienceId) || null;
1993
+ };
1994
+ plugin.expose({
1995
+ modal: {
1996
+ show: showModal,
1997
+ remove: removeModal,
1998
+ isShowing,
1999
+ showFormState,
2000
+ resetForm,
2001
+ getFormData
2002
+ }
2003
+ });
2004
+ instance.on("experiences:evaluated", (data) => {
2005
+ const { decision, experience } = data;
2006
+ if (decision.show && decision.experienceId && experience) {
2007
+ if (experience.type === "modal") {
2008
+ showModal(experience);
2009
+ }
2010
+ }
2011
+ });
2012
+ instance.on("sdk:destroy", () => {
2013
+ activeModals.forEach((_, id) => {
2014
+ removeModal(id);
2015
+ });
2016
+ });
2017
+ };
2018
+ function respectsDNT() {
2019
+ if (typeof navigator === "undefined") return false;
2020
+ return navigator.doNotTrack === "1" || navigator.msDoNotTrack === "1" || window.doNotTrack === "1";
2021
+ }
2022
+ function createVisitsEvent(isFirstVisit, totalVisits, sessionVisits, firstVisitTime, lastVisitTime, timestamp) {
2023
+ return {
2024
+ isFirstVisit,
2025
+ totalVisits,
2026
+ sessionVisits,
2027
+ firstVisitTime,
2028
+ lastVisitTime,
2029
+ timestamp
2030
+ };
2031
+ }
2032
+ var pageVisitsPlugin = (plugin, instance, config) => {
2033
+ plugin.ns("pageVisits");
2034
+ plugin.defaults({
2035
+ pageVisits: {
2036
+ enabled: true,
2037
+ respectDNT: true,
2038
+ sessionKey: "pageVisits:session",
2039
+ totalKey: "pageVisits:total",
2040
+ ttl: void 0,
2041
+ autoIncrement: true
2042
+ }
2043
+ });
2044
+ if (!instance.storage) {
2045
+ console.warn("[PageVisits] Storage plugin not found, auto-loading...");
2046
+ instance.use(storagePlugin);
2047
+ }
2048
+ const sdkInstance = instance;
2049
+ let sessionCount = 0;
2050
+ let totalCount = 0;
2051
+ let firstVisitTime;
2052
+ let lastVisitTime;
2053
+ let isFirstVisitFlag = false;
2054
+ let initialized = false;
2055
+ function loadData() {
2056
+ const sessionKey = config.get("pageVisits.sessionKey") ?? "pageVisits:session";
2057
+ const totalKey = config.get("pageVisits.totalKey") ?? "pageVisits:total";
2058
+ const storedSession = sdkInstance.storage.get(sessionKey, {
2059
+ backend: "sessionStorage"
2060
+ });
2061
+ sessionCount = storedSession ?? 0;
2062
+ const storedTotal = sdkInstance.storage.get(totalKey, {
2063
+ backend: "localStorage"
2064
+ });
2065
+ if (storedTotal) {
2066
+ totalCount = storedTotal.count ?? 0;
2067
+ firstVisitTime = storedTotal.first;
2068
+ lastVisitTime = storedTotal.last;
2069
+ isFirstVisitFlag = false;
2070
+ } else {
2071
+ totalCount = 0;
2072
+ firstVisitTime = void 0;
2073
+ lastVisitTime = void 0;
2074
+ isFirstVisitFlag = true;
2075
+ }
2076
+ }
2077
+ function saveData() {
2078
+ const sessionKey = config.get("pageVisits.sessionKey") ?? "pageVisits:session";
2079
+ const totalKey = config.get("pageVisits.totalKey") ?? "pageVisits:total";
2080
+ const ttl = config.get("pageVisits.ttl");
2081
+ sdkInstance.storage.set(sessionKey, sessionCount, {
2082
+ backend: "sessionStorage"
2083
+ });
2084
+ const totalData = {
2085
+ count: totalCount,
2086
+ first: firstVisitTime ?? Date.now(),
2087
+ last: lastVisitTime ?? Date.now()
2088
+ };
2089
+ sdkInstance.storage.set(totalKey, totalData, {
2090
+ backend: "localStorage",
2091
+ ...ttl && { ttl }
2092
+ });
2093
+ }
2094
+ function increment() {
2095
+ if (!initialized) {
2096
+ loadData();
2097
+ initialized = true;
2098
+ }
2099
+ sessionCount += 1;
2100
+ totalCount += 1;
2101
+ const now = Date.now();
2102
+ if (isFirstVisitFlag) {
2103
+ firstVisitTime = now;
2104
+ }
2105
+ lastVisitTime = now;
2106
+ saveData();
2107
+ const event = createVisitsEvent(
2108
+ isFirstVisitFlag,
2109
+ totalCount,
2110
+ sessionCount,
2111
+ firstVisitTime,
2112
+ lastVisitTime,
2113
+ now
2114
+ );
2115
+ plugin.emit("pageVisits:incremented", event);
2116
+ if (isFirstVisitFlag) {
2117
+ isFirstVisitFlag = false;
2118
+ }
2119
+ }
2120
+ function reset() {
2121
+ const sessionKey = config.get("pageVisits.sessionKey") ?? "pageVisits:session";
2122
+ const totalKey = config.get("pageVisits.totalKey") ?? "pageVisits:total";
2123
+ sdkInstance.storage.remove(sessionKey, { backend: "sessionStorage" });
2124
+ sdkInstance.storage.remove(totalKey, { backend: "localStorage" });
2125
+ sessionCount = 0;
2126
+ totalCount = 0;
2127
+ firstVisitTime = void 0;
2128
+ lastVisitTime = void 0;
2129
+ isFirstVisitFlag = false;
2130
+ initialized = false;
2131
+ plugin.emit("pageVisits:reset");
2132
+ }
2133
+ function getState() {
2134
+ return createVisitsEvent(
2135
+ isFirstVisitFlag,
2136
+ totalCount,
2137
+ sessionCount,
2138
+ firstVisitTime,
2139
+ lastVisitTime,
2140
+ Date.now()
2141
+ );
2142
+ }
2143
+ function initialize() {
2144
+ const enabled = config.get("pageVisits.enabled") ?? true;
2145
+ const respectDNTConfig = config.get("pageVisits.respectDNT") ?? true;
2146
+ const autoIncrement = config.get("pageVisits.autoIncrement") ?? true;
2147
+ if (respectDNTConfig && respectsDNT()) {
2148
+ plugin.emit("pageVisits:disabled", { reason: "dnt" });
2149
+ return;
2150
+ }
2151
+ if (!enabled) {
2152
+ plugin.emit("pageVisits:disabled", { reason: "config" });
2153
+ return;
2154
+ }
2155
+ if (autoIncrement) {
2156
+ increment();
2157
+ }
2158
+ }
2159
+ instance.on("sdk:ready", initialize);
2160
+ plugin.expose({
2161
+ pageVisits: {
2162
+ getTotalCount: () => totalCount,
2163
+ getSessionCount: () => sessionCount,
2164
+ isFirstVisit: () => isFirstVisitFlag,
2165
+ getFirstVisitTime: () => firstVisitTime,
2166
+ getLastVisitTime: () => lastVisitTime,
2167
+ increment,
2168
+ reset,
2169
+ getState
2170
+ }
2171
+ });
2172
+ };
2173
+
2174
+ // src/scroll-depth/scroll-depth.ts
2175
+ function detectDevice() {
2176
+ if (typeof window === "undefined") return "desktop";
2177
+ const ua = navigator.userAgent;
2178
+ const isMobile = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
2179
+ const isTablet = /iPad|Android(?!.*Mobile)/i.test(ua);
2180
+ const width = window.innerWidth;
2181
+ if (width < 768) return "mobile";
2182
+ if (width < 1024) return "tablet";
2183
+ if (isMobile) return "mobile";
2184
+ if (isTablet) return "tablet";
2185
+ return "desktop";
2186
+ }
2187
+ function throttle(func, wait) {
2188
+ let timeout = null;
2189
+ let previous = 0;
2190
+ return function throttled(...args) {
2191
+ const now = Date.now();
2192
+ const remaining = wait - (now - previous);
2193
+ if (remaining <= 0 || remaining > wait) {
2194
+ if (timeout) {
2195
+ clearTimeout(timeout);
2196
+ timeout = null;
2197
+ }
2198
+ previous = now;
2199
+ func(...args);
2200
+ } else if (!timeout) {
2201
+ timeout = setTimeout(() => {
2202
+ previous = Date.now();
2203
+ timeout = null;
2204
+ func(...args);
2205
+ }, remaining);
2206
+ }
2207
+ };
2208
+ }
2209
+ function calculateScrollPercent(includeViewportHeight) {
2210
+ if (typeof document === "undefined") return 0;
2211
+ const scrollingElement = document.scrollingElement || document.documentElement;
2212
+ const scrollTop = scrollingElement.scrollTop;
2213
+ const scrollHeight = scrollingElement.scrollHeight;
2214
+ const clientHeight = scrollingElement.clientHeight;
2215
+ if (scrollHeight <= clientHeight) {
2216
+ return 100;
2217
+ }
2218
+ if (includeViewportHeight) {
2219
+ return Math.min((scrollTop + clientHeight) / scrollHeight * 100, 100);
2220
+ }
2221
+ return Math.min(scrollTop / (scrollHeight - clientHeight) * 100, 100);
2222
+ }
2223
+ function calculateEngagementScore(velocity, fastScrollThreshold, directionChanges, timeScrollingUp, totalTime) {
2224
+ const velocityScore = Math.min(velocity / fastScrollThreshold * 50, 50);
2225
+ const directionScore = Math.min(directionChanges / 5 * 30, 30);
2226
+ const seekingScore = Math.min(timeScrollingUp / totalTime * 20, 20);
2227
+ return Math.max(0, 100 - (velocityScore + directionScore + seekingScore));
2228
+ }
2229
+ var scrollDepthPlugin = (plugin, instance, config) => {
2230
+ plugin.ns("experiences.scrollDepth");
2231
+ plugin.defaults({
2232
+ scrollDepth: {
2233
+ thresholds: [25, 50, 75, 100],
2234
+ throttle: 100,
2235
+ includeViewportHeight: true,
2236
+ recalculateOnResize: true,
2237
+ trackAdvancedMetrics: false,
2238
+ fastScrollVelocityThreshold: 3,
2239
+ disableOnMobile: false
2240
+ }
2241
+ });
2242
+ const scrollConfig = config.get("scrollDepth");
2243
+ if (!scrollConfig) return;
2244
+ const cfg = scrollConfig;
2245
+ const device = detectDevice();
2246
+ if (cfg.disableOnMobile && device === "mobile") {
2247
+ return;
2248
+ }
2249
+ let maxScrollPercent = 0;
2250
+ const triggeredThresholds = /* @__PURE__ */ new Set();
2251
+ const pageLoadTime = Date.now();
2252
+ let lastScrollPosition = 0;
2253
+ let lastScrollTime = Date.now();
2254
+ let lastScrollDirection = null;
2255
+ let directionChangesSinceLastThreshold = 0;
2256
+ let timeScrollingUp = 0;
2257
+ const thresholdTimes = /* @__PURE__ */ new Map();
2258
+ function handleScroll() {
2259
+ const currentPercent = calculateScrollPercent(cfg.includeViewportHeight ?? true);
2260
+ const now = Date.now();
2261
+ const scrollingElement = document.scrollingElement || document.documentElement;
2262
+ const currentPosition = scrollingElement.scrollTop;
2263
+ let velocity = 0;
2264
+ if (cfg.trackAdvancedMetrics) {
2265
+ const timeDelta = now - lastScrollTime;
2266
+ const positionDelta = currentPosition - lastScrollPosition;
2267
+ velocity = timeDelta > 0 ? Math.abs(positionDelta) / timeDelta : 0;
2268
+ const currentDirection = positionDelta > 0 ? "down" : positionDelta < 0 ? "up" : lastScrollDirection;
2269
+ if (currentDirection && lastScrollDirection && currentDirection !== lastScrollDirection) {
2270
+ directionChangesSinceLastThreshold++;
2271
+ }
2272
+ if (currentDirection === "up" && timeDelta > 0) {
2273
+ timeScrollingUp += timeDelta;
2274
+ }
2275
+ lastScrollDirection = currentDirection;
2276
+ lastScrollPosition = currentPosition;
2277
+ lastScrollTime = now;
2278
+ }
2279
+ maxScrollPercent = Math.max(maxScrollPercent, currentPercent);
2280
+ for (const threshold of cfg.thresholds || []) {
2281
+ if (currentPercent >= threshold && !triggeredThresholds.has(threshold)) {
2282
+ triggeredThresholds.add(threshold);
2283
+ if (cfg.trackAdvancedMetrics) {
2284
+ thresholdTimes.set(threshold, now - pageLoadTime);
2285
+ }
2286
+ const eventPayload = {
2287
+ triggered: true,
2288
+ timestamp: now,
2289
+ percent: Math.round(currentPercent * 100) / 100,
2290
+ maxPercent: Math.round(maxScrollPercent * 100) / 100,
2291
+ threshold,
2292
+ thresholdsCrossed: Array.from(triggeredThresholds).sort((a, b) => a - b),
2293
+ device
2294
+ };
2295
+ if (cfg.trackAdvancedMetrics) {
2296
+ const fastScrollThreshold = cfg.fastScrollVelocityThreshold || 3;
2297
+ const isFastScrolling = velocity > fastScrollThreshold;
2298
+ const engagementScore = calculateEngagementScore(
2299
+ velocity,
2300
+ fastScrollThreshold,
2301
+ directionChangesSinceLastThreshold,
2302
+ timeScrollingUp,
2303
+ now - pageLoadTime
2304
+ );
2305
+ eventPayload.advanced = {
2306
+ timeToThreshold: now - pageLoadTime,
2307
+ velocity: Math.round(velocity * 1e3) / 1e3,
2308
+ // Round to 3 decimals
2309
+ isFastScrolling,
2310
+ directionChanges: directionChangesSinceLastThreshold,
2311
+ timeScrollingUp,
2312
+ engagementScore: Math.round(engagementScore)
2313
+ };
2314
+ directionChangesSinceLastThreshold = 0;
2315
+ }
2316
+ instance.emit("trigger:scrollDepth", eventPayload);
2317
+ }
2318
+ }
2319
+ }
2320
+ const throttledScrollHandler = throttle(handleScroll, cfg.throttle || 100);
2321
+ const throttledResizeHandler = throttle(handleScroll, cfg.throttle || 100);
2322
+ function initialize() {
2323
+ if (typeof window === "undefined" || typeof document === "undefined") {
2324
+ return;
2325
+ }
2326
+ window.addEventListener("scroll", throttledScrollHandler, { passive: true });
2327
+ if (cfg.recalculateOnResize) {
2328
+ window.addEventListener("resize", throttledResizeHandler, { passive: true });
2329
+ }
2330
+ }
2331
+ function cleanup() {
2332
+ window.removeEventListener("scroll", throttledScrollHandler);
2333
+ window.removeEventListener("resize", throttledResizeHandler);
2334
+ }
2335
+ instance.on("sdk:destroy", () => {
2336
+ cleanup();
2337
+ });
2338
+ plugin.expose({
2339
+ scrollDepth: {
2340
+ /**
2341
+ * Get the maximum scroll percentage reached during the session
2342
+ */
2343
+ getMaxPercent: () => maxScrollPercent,
2344
+ /**
2345
+ * Get the current scroll percentage
2346
+ */
2347
+ getCurrentPercent: () => calculateScrollPercent(cfg.includeViewportHeight ?? true),
2348
+ /**
2349
+ * Get all thresholds that have been crossed
2350
+ */
2351
+ getThresholdsCrossed: () => Array.from(triggeredThresholds).sort((a, b) => a - b),
2352
+ /**
2353
+ * Get the detected device type
2354
+ */
2355
+ getDevice: () => device,
2356
+ /**
2357
+ * Get advanced metrics (only available when trackAdvancedMetrics is enabled)
2358
+ */
2359
+ getAdvancedMetrics: () => {
2360
+ if (!cfg.trackAdvancedMetrics) return null;
2361
+ const now = Date.now();
2362
+ return {
2363
+ timeOnPage: now - pageLoadTime,
2364
+ directionChanges: directionChangesSinceLastThreshold,
2365
+ timeScrollingUp,
2366
+ thresholdTimes: Object.fromEntries(thresholdTimes)
2367
+ };
2368
+ },
2369
+ /**
2370
+ * Reset scroll depth tracking
2371
+ * Clears all triggered thresholds, max scroll, and advanced metrics
2372
+ */
2373
+ reset: () => {
2374
+ maxScrollPercent = 0;
2375
+ triggeredThresholds.clear();
2376
+ directionChangesSinceLastThreshold = 0;
2377
+ timeScrollingUp = 0;
2378
+ thresholdTimes.clear();
2379
+ lastScrollDirection = null;
2380
+ }
2381
+ }
2382
+ });
2383
+ if (typeof window !== "undefined") {
2384
+ setTimeout(initialize, 0);
2385
+ }
2386
+ return () => {
2387
+ cleanup();
2388
+ };
2389
+ };
2390
+
2391
+ // src/time-delay/time-delay.ts
2392
+ function calculateElapsed(startTime, pausedDuration) {
2393
+ return Date.now() - startTime - pausedDuration;
2394
+ }
2395
+ function isDocumentHidden() {
2396
+ if (typeof document === "undefined") return false;
2397
+ return document.hidden || false;
2398
+ }
2399
+ function createTimeDelayEvent(startTime, pausedDuration, wasPaused, visibilityChanges) {
2400
+ const timestamp = Date.now();
2401
+ const elapsed = timestamp - startTime;
2402
+ const activeElapsed = elapsed - pausedDuration;
2403
+ return {
2404
+ timestamp,
2405
+ elapsed,
2406
+ activeElapsed,
2407
+ wasPaused,
2408
+ visibilityChanges
2409
+ };
2410
+ }
2411
+ var timeDelayPlugin = (plugin, instance, config) => {
2412
+ plugin.ns("experiences.timeDelay");
2413
+ plugin.defaults({
2414
+ timeDelay: {
2415
+ delay: 0,
2416
+ pauseWhenHidden: true
2417
+ }
2418
+ });
2419
+ const timeDelayConfig = config.get("timeDelay");
2420
+ if (!timeDelayConfig) return;
2421
+ const delay = timeDelayConfig.delay ?? 0;
2422
+ const pauseWhenHidden = timeDelayConfig.pauseWhenHidden ?? true;
2423
+ if (delay <= 0) return;
2424
+ const startTime = Date.now();
2425
+ let triggered = false;
2426
+ let paused = false;
2427
+ let pausedDuration = 0;
2428
+ let lastPauseTime = 0;
2429
+ let visibilityChanges = 0;
2430
+ let timer = null;
2431
+ let visibilityListener = null;
2432
+ function trigger() {
2433
+ if (triggered) return;
2434
+ triggered = true;
2435
+ const eventPayload = createTimeDelayEvent(
2436
+ startTime,
2437
+ pausedDuration,
2438
+ visibilityChanges > 0,
2439
+ visibilityChanges
2440
+ );
2441
+ instance.emit("trigger:timeDelay", eventPayload);
2442
+ cleanup();
2443
+ }
2444
+ function scheduleTimer(remainingDelay) {
2445
+ if (timer) {
2446
+ clearTimeout(timer);
2447
+ }
2448
+ timer = setTimeout(() => {
2449
+ trigger();
2450
+ }, remainingDelay);
2451
+ }
2452
+ function handleVisibilityChange() {
2453
+ const hidden = isDocumentHidden();
2454
+ if (hidden && !paused) {
2455
+ paused = true;
2456
+ lastPauseTime = Date.now();
2457
+ visibilityChanges++;
2458
+ if (timer) {
2459
+ clearTimeout(timer);
2460
+ timer = null;
2461
+ }
2462
+ } else if (!hidden && paused) {
2463
+ paused = false;
2464
+ const pauseDuration = Date.now() - lastPauseTime;
2465
+ pausedDuration += pauseDuration;
2466
+ visibilityChanges++;
2467
+ const elapsed = calculateElapsed(startTime, pausedDuration);
2468
+ const remaining = delay - elapsed;
2469
+ if (remaining > 0) {
2470
+ scheduleTimer(remaining);
2471
+ } else {
2472
+ trigger();
2473
+ }
2474
+ }
2475
+ }
2476
+ function cleanup() {
2477
+ if (timer) {
2478
+ clearTimeout(timer);
2479
+ timer = null;
2480
+ }
2481
+ if (visibilityListener && typeof document !== "undefined") {
2482
+ document.removeEventListener("visibilitychange", visibilityListener);
2483
+ visibilityListener = null;
2484
+ }
2485
+ }
2486
+ function initialize() {
2487
+ if (pauseWhenHidden && isDocumentHidden()) {
2488
+ paused = true;
2489
+ lastPauseTime = Date.now();
2490
+ visibilityChanges++;
2491
+ } else {
2492
+ scheduleTimer(delay);
2493
+ }
2494
+ if (pauseWhenHidden && typeof document !== "undefined") {
2495
+ visibilityListener = handleVisibilityChange;
2496
+ document.addEventListener("visibilitychange", visibilityListener);
2497
+ }
2498
+ }
2499
+ plugin.expose({
2500
+ timeDelay: {
2501
+ getElapsed: () => {
2502
+ return Date.now() - startTime;
2503
+ },
2504
+ getActiveElapsed: () => {
2505
+ let currentPausedDuration = pausedDuration;
2506
+ if (paused) {
2507
+ currentPausedDuration += Date.now() - lastPauseTime;
2508
+ }
2509
+ return calculateElapsed(startTime, currentPausedDuration);
2510
+ },
2511
+ getRemaining: () => {
2512
+ if (triggered) return 0;
2513
+ const elapsed = calculateElapsed(startTime, pausedDuration);
2514
+ const remaining = delay - elapsed;
2515
+ return Math.max(0, remaining);
2516
+ },
2517
+ isPaused: () => paused,
2518
+ isTriggered: () => triggered,
2519
+ reset: () => {
2520
+ triggered = false;
2521
+ paused = false;
2522
+ pausedDuration = 0;
2523
+ lastPauseTime = 0;
2524
+ visibilityChanges = 0;
2525
+ cleanup();
2526
+ initialize();
2527
+ }
2528
+ }
2529
+ });
2530
+ initialize();
2531
+ instance.on("sdk:destroy", () => {
2532
+ cleanup();
2533
+ });
2534
+ };
2535
+
2536
+ export { bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, inlinePlugin, insertContent, modalPlugin, pageVisitsPlugin, removeContent, scrollDepthPlugin, timeDelayPlugin };
693
2537
  //# sourceMappingURL=index.js.map
694
2538
  //# sourceMappingURL=index.js.map