@prosdevlab/experience-sdk-plugins 0.2.0 → 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.
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) {
@@ -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
  `;
@@ -726,10 +730,9 @@ var exitIntentPlugin = (plugin, instance, config) => {
726
730
  }
727
731
  });
728
732
  initialize();
729
- const destroyHandler = () => {
733
+ instance.on("sdk:destroy", () => {
730
734
  cleanup();
731
- };
732
- instance.on("destroy", destroyHandler);
735
+ });
733
736
  };
734
737
  var frequencyPlugin = (plugin, instance, config) => {
735
738
  plugin.ns("frequency");
@@ -858,6 +861,1160 @@ var frequencyPlugin = (plugin, instance, config) => {
858
861
  });
859
862
  }
860
863
  };
864
+
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
+ };
861
2018
  function respectsDNT() {
862
2019
  if (typeof navigator === "undefined") return false;
863
2020
  return navigator.doNotTrack === "1" || navigator.msDoNotTrack === "1" || window.doNotTrack === "1";
@@ -1175,10 +2332,9 @@ var scrollDepthPlugin = (plugin, instance, config) => {
1175
2332
  window.removeEventListener("scroll", throttledScrollHandler);
1176
2333
  window.removeEventListener("resize", throttledResizeHandler);
1177
2334
  }
1178
- const destroyHandler = () => {
2335
+ instance.on("sdk:destroy", () => {
1179
2336
  cleanup();
1180
- };
1181
- instance.on("destroy", destroyHandler);
2337
+ });
1182
2338
  plugin.expose({
1183
2339
  scrollDepth: {
1184
2340
  /**
@@ -1229,7 +2385,6 @@ var scrollDepthPlugin = (plugin, instance, config) => {
1229
2385
  }
1230
2386
  return () => {
1231
2387
  cleanup();
1232
- instance.off("destroy", destroyHandler);
1233
2388
  };
1234
2389
  };
1235
2390
 
@@ -1373,12 +2528,11 @@ var timeDelayPlugin = (plugin, instance, config) => {
1373
2528
  }
1374
2529
  });
1375
2530
  initialize();
1376
- const destroyHandler = () => {
2531
+ instance.on("sdk:destroy", () => {
1377
2532
  cleanup();
1378
- };
1379
- instance.on("destroy", destroyHandler);
2533
+ });
1380
2534
  };
1381
2535
 
1382
- export { bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, pageVisitsPlugin, scrollDepthPlugin, timeDelayPlugin };
2536
+ export { bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, inlinePlugin, insertContent, modalPlugin, pageVisitsPlugin, removeContent, scrollDepthPlugin, timeDelayPlugin };
1383
2537
  //# sourceMappingURL=index.js.map
1384
2538
  //# sourceMappingURL=index.js.map