@xenia-martech/sdk-js 1.0.0-dev.26 → 1.0.0-dev.28

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 (37) hide show
  1. package/CSS_VARIABLES.md +7 -0
  2. package/README.md +71 -2
  3. package/dist/{actions-DFLoSXO_.d.cts → actions-CcBYV--3.d.cts} +242 -6
  4. package/dist/{actions-DFLoSXO_.d.ts → actions-CcBYV--3.d.ts} +242 -6
  5. package/dist/{chunk-3KYKKIIG.cjs → chunk-WYM4CQVG.cjs} +126 -4
  6. package/dist/chunk-WYM4CQVG.cjs.map +1 -0
  7. package/dist/{chunk-D37RD3UC.js → chunk-XXNDHYAS.js} +125 -5
  8. package/dist/chunk-XXNDHYAS.js.map +1 -0
  9. package/dist/client/index.cjs +613 -35
  10. package/dist/client/index.cjs.map +1 -1
  11. package/dist/client/index.d.cts +112 -3
  12. package/dist/client/index.d.ts +112 -3
  13. package/dist/client/index.js +610 -36
  14. package/dist/client/index.js.map +1 -1
  15. package/dist/client/react/index.cjs +609 -35
  16. package/dist/client/react/index.cjs.map +1 -1
  17. package/dist/client/react/index.d.cts +2 -2
  18. package/dist/client/react/index.d.ts +2 -2
  19. package/dist/client/react/index.js +609 -35
  20. package/dist/client/react/index.js.map +1 -1
  21. package/dist/{cookie-names-LgNDAreS.d.cts → cookie-names-B3b0XNc2.d.cts} +187 -1
  22. package/dist/{cookie-names-LgNDAreS.d.ts → cookie-names-B3b0XNc2.d.ts} +187 -1
  23. package/dist/index.cjs +23 -19
  24. package/dist/index.d.cts +23 -3
  25. package/dist/index.d.ts +23 -3
  26. package/dist/index.js +1 -1
  27. package/dist/server/index.cjs +53 -22
  28. package/dist/server/index.cjs.map +1 -1
  29. package/dist/server/index.d.cts +31 -11
  30. package/dist/server/index.d.ts +31 -11
  31. package/dist/server/index.js +43 -12
  32. package/dist/server/index.js.map +1 -1
  33. package/dist/xenia-sdk.iife.js +37 -3
  34. package/dist/xenia-sdk.iife.js.map +1 -1
  35. package/package.json +1 -1
  36. package/dist/chunk-3KYKKIIG.cjs.map +0 -1
  37. package/dist/chunk-D37RD3UC.js.map +0 -1
@@ -15,6 +15,7 @@ var MESSAGE_POSITIONS = /* @__PURE__ */ new Set([
15
15
  "top",
16
16
  "bottom"
17
17
  ]);
18
+ var WIDGET_TYPES = /* @__PURE__ */ new Set(["standard", "spin-wheel"]);
18
19
  var ALL_TYPES = /* @__PURE__ */ new Set([
19
20
  "swap",
20
21
  "hide",
@@ -61,15 +62,67 @@ function toAction(raw) {
61
62
  message: asString(raw.message),
62
63
  imageUrl: asString(raw.imageUrl),
63
64
  html: asString(raw.html),
64
- cta: cta && cta.label ? cta : void 0,
65
+ // A link-only CTA (empty label, e.g. a spin-wheel's data-xenia-wheel-cta —
66
+ // label is meaningless there, the winHtml/lossHtml design supplies its own button text)
67
+ // must still parse through — gating on `cta.label` alone silently dropped it.
68
+ cta: cta && (cta.label || cta.url) ? cta : void 0,
65
69
  dismissAfterMs: asNumber(raw.dismissAfterMs),
66
- position: asMessagePosition(raw.position)
70
+ position: asMessagePosition(raw.position),
71
+ widgetType: asWidgetType(raw.widgetType),
72
+ segments: asSegments(raw.segments),
73
+ formSubmit: asFormSubmit(raw.formSubmit),
74
+ style: asWidgetStyle(raw.style),
75
+ spinnerHtml: asString(raw.spinnerHtml),
76
+ winHtml: asString(raw.winHtml),
77
+ lossHtml: asString(raw.lossHtml)
67
78
  };
68
79
  }
69
80
  function asMessagePosition(v) {
70
81
  const s = asString(v);
71
82
  return s && MESSAGE_POSITIONS.has(s) ? s : void 0;
72
83
  }
84
+ function asWidgetType(v) {
85
+ const s = asString(v);
86
+ return s && WIDGET_TYPES.has(s) ? s : void 0;
87
+ }
88
+ function asSegments(v) {
89
+ if (!Array.isArray(v)) return void 0;
90
+ const segments = [];
91
+ for (const entry of v) {
92
+ if (!isObject(entry)) continue;
93
+ const id = asString(entry.id);
94
+ const label = asString(entry.label);
95
+ if (!id || !label) continue;
96
+ segments.push({
97
+ id,
98
+ label,
99
+ colorHex: asString(entry.colorHex),
100
+ isLoss: entry.isLoss === true
101
+ });
102
+ }
103
+ return segments.length ? segments : void 0;
104
+ }
105
+ function asFormSubmit(v) {
106
+ if (!isObject(v)) return void 0;
107
+ const mode = asString(v.mode);
108
+ if (!mode) return void 0;
109
+ return {
110
+ requiresResolve: v.requiresResolve === true,
111
+ mode,
112
+ flowRef: asString(v.flowRef),
113
+ bindingRef: asString(v.bindingRef),
114
+ successDisplayMs: asNumber(v.successDisplayMs)
115
+ };
116
+ }
117
+ function asWidgetStyle(v) {
118
+ if (!isObject(v)) return void 0;
119
+ const style = {
120
+ hideCloseButton: v.hideCloseButton === true,
121
+ hideOverlay: v.hideOverlay === true
122
+ };
123
+ const hasAny = style.hideCloseButton || style.hideOverlay;
124
+ return hasAny ? style : void 0;
125
+ }
73
126
  function parseActions(body) {
74
127
  let list;
75
128
  if (Array.isArray(body)) {
@@ -252,6 +305,12 @@ var TokenProvider = class {
252
305
  };
253
306
 
254
307
  // src/core/api.ts
308
+ function isJsonObject(v) {
309
+ return typeof v === "object" && v !== null && !Array.isArray(v);
310
+ }
311
+ function asWidgetPrize(v) {
312
+ return v !== void 0 && isJsonObject(v) ? v : void 0;
313
+ }
255
314
  function compact(obj) {
256
315
  const out = {};
257
316
  for (const [k, v] of Object.entries(obj)) {
@@ -321,6 +380,39 @@ var XeniaApi = class {
321
380
  receivedAt: Date.now()
322
381
  };
323
382
  }
383
+ /**
384
+ * Widget form submission — `POST /widget-submissions?ref={bindingRef}`. Used whenever the
385
+ * action's `formSubmit.requiresResolve` is true (spin-wheel, which must have a
386
+ * server-resolved outcome before anything renders) or `mode === "endpoint"` — the common
387
+ * plain-popup case (`mode === "journey" && !requiresResolve`) bypasses this entirely and calls
388
+ * {@link webhook} against `flowRef` instead (see `XeniaClient.submitWidgetForm`'s endpoint
389
+ * selection).
390
+ *
391
+ * Not retried (`noRetry: true`), styled like {@link webhook} — a retried POST must never
392
+ * double-fire a journey or double-resolve a prize server-side.
393
+ */
394
+ async submitWidget(ref, body) {
395
+ const { response, data: rawBody } = await this.http.request({
396
+ method: "POST",
397
+ path: "/widget-submissions",
398
+ query: { ref },
399
+ headers: await this.authHeaders(),
400
+ body: body ?? {},
401
+ noRetry: true
402
+ });
403
+ const obj = isJsonObject(rawBody) ? rawBody : {};
404
+ return {
405
+ status: response.status,
406
+ body: rawBody,
407
+ actions: parseActions(rawBody),
408
+ prize: asWidgetPrize(obj.prize),
409
+ alreadyPlayed: obj.alreadyPlayed === true,
410
+ dispatched: obj.dispatched === true,
411
+ dispatchError: typeof obj.dispatchError === "string" ? obj.dispatchError : void 0,
412
+ widgetType: typeof obj.widgetType === "string" ? obj.widgetType : void 0,
413
+ receivedAt: Date.now()
414
+ };
415
+ }
324
416
  /**
325
417
  * Unified event batch (max 100/request per the backend) — `POST /api/events/v1`.
326
418
  * Records analytics for every event and matches/advances a journey for any
@@ -966,6 +1058,40 @@ var Identity = class {
966
1058
  return null;
967
1059
  }
968
1060
  }
1061
+ // ── Customer id / email ──────────────────────────────────────────────────
1062
+ //
1063
+ // Ambient, persisted the same way as the visitor id, so a value set once
1064
+ // (typically alongside `identify()`) rides on every later `resolve()` call
1065
+ // without the caller having to pass it again — the SDK-provided-fields
1066
+ // parity list on the Personalization Resolve trigger includes both.
1067
+ setCustomerId(customerId) {
1068
+ try {
1069
+ if (customerId) localStorage.setItem(this.key("customer_id"), customerId);
1070
+ else localStorage.removeItem(this.key("customer_id"));
1071
+ } catch {
1072
+ }
1073
+ }
1074
+ getCustomerId() {
1075
+ try {
1076
+ return localStorage.getItem(this.key("customer_id"));
1077
+ } catch {
1078
+ return null;
1079
+ }
1080
+ }
1081
+ setEmail(email) {
1082
+ try {
1083
+ if (email) localStorage.setItem(this.key("email"), email);
1084
+ else localStorage.removeItem(this.key("email"));
1085
+ } catch {
1086
+ }
1087
+ }
1088
+ getEmail() {
1089
+ try {
1090
+ return localStorage.getItem(this.key("email"));
1091
+ } catch {
1092
+ return null;
1093
+ }
1094
+ }
969
1095
  // ── Session id ───────────────────────────────────────────────────────────────
970
1096
  /**
971
1097
  * Resolve the current session id: reused while activity continues within
@@ -1054,6 +1180,316 @@ function resolveCookieConfig(opt) {
1054
1180
  return { sameSite, secure, maxAgeDays: o.maxAgeDays ?? 365, domain: o.domain };
1055
1181
  }
1056
1182
 
1183
+ // src/client/widgets/form.ts
1184
+ function collectFields(root) {
1185
+ const fields = {};
1186
+ root.querySelectorAll("[data-xenia-field]").forEach((el) => {
1187
+ const name = el.getAttribute("data-xenia-field");
1188
+ if (!name) return;
1189
+ fields[name] = fieldValue(el);
1190
+ });
1191
+ return fields;
1192
+ }
1193
+ function fieldValue(el) {
1194
+ if (el instanceof HTMLInputElement) {
1195
+ if (el.type === "checkbox") return el.checked ? el.value || "on" : "";
1196
+ if (el.type === "radio") return el.checked ? el.value : "";
1197
+ return el.value;
1198
+ }
1199
+ if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return el.value;
1200
+ return el.textContent?.trim() ?? "";
1201
+ }
1202
+ function setDisabled(el, disabled) {
1203
+ if (!el) return;
1204
+ if (el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement || el instanceof HTMLFieldSetElement) {
1205
+ el.disabled = disabled;
1206
+ } else if (disabled) {
1207
+ el.setAttribute("aria-disabled", "true");
1208
+ } else {
1209
+ el.removeAttribute("aria-disabled");
1210
+ }
1211
+ }
1212
+ function showError(root, message) {
1213
+ const el = root.querySelector("[data-xenia-error]");
1214
+ if (el) el.textContent = message;
1215
+ }
1216
+ var SUBMIT_STYLE_ID = "xenia-form-submit-style";
1217
+ function ensureSubmitStyles(root) {
1218
+ if (root.querySelector(`#${SUBMIT_STYLE_ID}`)) return;
1219
+ const style = document.createElement("style");
1220
+ style.id = SUBMIT_STYLE_ID;
1221
+ style.textContent = "@keyframes xenia-submit-spin{to{transform:rotate(360deg)}}[data-xenia-submitting]::after{content:'';display:inline-block;width:0.85em;height:0.85em;margin-left:0.5em;vertical-align:-0.15em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:xenia-submit-spin 0.6s linear infinite}";
1222
+ root.prepend(style);
1223
+ }
1224
+ function setSubmitting(el, submitting) {
1225
+ if (!el) return;
1226
+ if (submitting) el.setAttribute("data-xenia-submitting", "");
1227
+ else el.removeAttribute("data-xenia-submitting");
1228
+ }
1229
+ function fadeOutAndDismiss(root, dismiss) {
1230
+ if (!dismiss) return;
1231
+ const host = root instanceof ShadowRoot ? root.host : root;
1232
+ if (host instanceof HTMLElement) {
1233
+ host.style.transition = "opacity 220ms ease, transform 220ms ease";
1234
+ host.style.opacity = "0";
1235
+ host.style.transform = "scale(0.96)";
1236
+ window.setTimeout(dismiss, 220);
1237
+ } else {
1238
+ dismiss();
1239
+ }
1240
+ }
1241
+ var DEFAULT_SUCCESS_DISPLAY_MS = 900;
1242
+ function revealSuccess(root, form) {
1243
+ const successEl = root.querySelector("[data-xenia-success]");
1244
+ if (!successEl) return false;
1245
+ if (successEl instanceof HTMLElement) successEl.hidden = false;
1246
+ else successEl.removeAttribute("hidden");
1247
+ if (form) {
1248
+ form.hidden = true;
1249
+ } else {
1250
+ root.querySelectorAll("[data-xenia-field], [data-xenia-submit]").forEach((el) => {
1251
+ el.hidden = true;
1252
+ });
1253
+ }
1254
+ return true;
1255
+ }
1256
+ function wireFormSubmit(root, action, ctx) {
1257
+ const form = root.querySelector("form");
1258
+ let submitting = false;
1259
+ ensureSubmitStyles(root);
1260
+ const run = (control, fields) => {
1261
+ if (submitting) return;
1262
+ if (!action.formSubmit) {
1263
+ showError(root, "This form isn't connected yet.");
1264
+ return;
1265
+ }
1266
+ submitting = true;
1267
+ showError(root, "");
1268
+ setDisabled(control, true);
1269
+ setSubmitting(control, true);
1270
+ const startedAt = Date.now();
1271
+ const minDisplayMs = action.formSubmit.successDisplayMs ?? DEFAULT_SUCCESS_DISPLAY_MS;
1272
+ ctx.submitWidgetForm(fields).then(() => {
1273
+ setSubmitting(control, false);
1274
+ revealSuccess(root, form);
1275
+ const wait = Math.max(0, minDisplayMs - (Date.now() - startedAt));
1276
+ window.setTimeout(() => fadeOutAndDismiss(root, ctx.dismiss), wait);
1277
+ }).catch(() => {
1278
+ setSubmitting(control, false);
1279
+ setDisabled(control, false);
1280
+ showError(root, "Something went wrong. Please try again.");
1281
+ }).finally(() => {
1282
+ submitting = false;
1283
+ });
1284
+ };
1285
+ if (form) {
1286
+ form.addEventListener("submit", (ev) => {
1287
+ ev.preventDefault();
1288
+ const formFields = {};
1289
+ new FormData(form).forEach((value, key) => {
1290
+ if (typeof value === "string") formFields[key] = value;
1291
+ });
1292
+ const submitter = ev.submitter ?? form.querySelector('[type="submit"], button:not([type])');
1293
+ run(submitter, { ...collectFields(root), ...formFields });
1294
+ });
1295
+ }
1296
+ root.querySelectorAll("[data-xenia-submit]").forEach((el) => {
1297
+ if (form?.contains(el)) return;
1298
+ el.addEventListener("click", () => run(el, collectFields(root)));
1299
+ });
1300
+ }
1301
+
1302
+ // src/client/widgets/types.ts
1303
+ function escapeHtml(s) {
1304
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1305
+ }
1306
+ function escapeAttr(s) {
1307
+ return escapeHtml(s).replace(/"/g, "&quot;");
1308
+ }
1309
+ function wireCloseAffordances(root, dismiss) {
1310
+ if (dismiss) {
1311
+ const closeEls = root.querySelectorAll(
1312
+ ".xenia-close, [data-xenia-close], [class*='close' i], [id*='close' i], [class*='backdrop' i], [id*='backdrop' i]"
1313
+ );
1314
+ closeEls.forEach((el) => {
1315
+ el.removeAttribute("onclick");
1316
+ el.addEventListener("click", dismiss);
1317
+ });
1318
+ }
1319
+ root.querySelectorAll("[class*='backdrop' i], [id*='backdrop' i]").forEach((el) => {
1320
+ if (el instanceof HTMLElement && el.children.length === 0 && !el.textContent?.trim()) {
1321
+ el.style.opacity = "0.5";
1322
+ }
1323
+ });
1324
+ }
1325
+
1326
+ // src/client/widgets/spin-wheel.ts
1327
+ var WHEEL_SIZE = 260;
1328
+ var RADIUS = WHEEL_SIZE / 2;
1329
+ var CENTER = WHEEL_SIZE / 2;
1330
+ var DEFAULT_COLORS = ["#7c3aed", "#2563eb", "#059669", "#d97706", "#dc2626", "#0891b2"];
1331
+ var SPIN_TRANSITION = "transform 3.2s cubic-bezier(0.15, 0.85, 0.25, 1)";
1332
+ var WHEEL_POINTER_HTML = '<div class="xenia-wheel-pointer" aria-hidden="true"></div>';
1333
+ var REVEAL_TRANSITION_MS = 320;
1334
+ var REVEAL_TRANSITION = `opacity ${REVEAL_TRANSITION_MS}ms ease, transform ${REVEAL_TRANSITION_MS}ms ease`;
1335
+ var REVEAL_PERSPECTIVE_PX = 900;
1336
+ var revealTransform = (scale, depthPx) => `perspective(${REVEAL_PERSPECTIVE_PX}px) translateZ(${depthPx}px) scale(${scale})`;
1337
+ function polarPoint(angleDeg, radius) {
1338
+ const rad = angleDeg * Math.PI / 180;
1339
+ return { x: CENTER + radius * Math.sin(rad), y: CENTER - radius * Math.cos(rad) };
1340
+ }
1341
+ function segmentPath(startAngle, endAngle) {
1342
+ const start = polarPoint(startAngle, RADIUS);
1343
+ const end = polarPoint(endAngle, RADIUS);
1344
+ const largeArc = endAngle - startAngle > 180 ? 1 : 0;
1345
+ return `M ${CENTER},${CENTER} L ${start.x},${start.y} A ${RADIUS},${RADIUS} 0 ${largeArc} 1 ${end.x},${end.y} Z`;
1346
+ }
1347
+ function computeSpinRotation(segmentIndex, segmentCount, extraSpins = 0) {
1348
+ if (segmentCount <= 0) return 0;
1349
+ const segmentAngle = 360 / segmentCount;
1350
+ const centerAngle = segmentIndex * segmentAngle + segmentAngle / 2;
1351
+ const landingAngle = (360 - centerAngle) % 360;
1352
+ return extraSpins * 360 + landingAngle;
1353
+ }
1354
+ function crossFadeReveal(outgoing, incoming) {
1355
+ const outContent = outgoing ? Array.from(outgoing.children) : [];
1356
+ const inContent = incoming ? Array.from(incoming.children) : [];
1357
+ if (incoming) incoming.hidden = false;
1358
+ inContent.forEach((el) => {
1359
+ if (!(el instanceof HTMLElement)) return;
1360
+ el.style.transition = "none";
1361
+ el.style.opacity = "0";
1362
+ el.style.transform = revealTransform(0.94, -60);
1363
+ });
1364
+ nextFrame(() => {
1365
+ outContent.forEach((el) => {
1366
+ if (!(el instanceof HTMLElement)) return;
1367
+ el.style.transition = REVEAL_TRANSITION;
1368
+ el.style.opacity = "0";
1369
+ el.style.transform = revealTransform(0.94, -60);
1370
+ });
1371
+ inContent.forEach((el) => {
1372
+ if (!(el instanceof HTMLElement)) return;
1373
+ el.style.transition = REVEAL_TRANSITION;
1374
+ el.style.opacity = "1";
1375
+ el.style.transform = revealTransform(1, 0);
1376
+ });
1377
+ });
1378
+ if (outgoing) {
1379
+ setTimeout(() => {
1380
+ outgoing.hidden = true;
1381
+ }, REVEAL_TRANSITION_MS);
1382
+ }
1383
+ }
1384
+ function nextFrame(fn) {
1385
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(fn);
1386
+ else setTimeout(fn, 0);
1387
+ }
1388
+ function wheelSvg(segments) {
1389
+ const n = segments.length || 1;
1390
+ const segmentAngle = 360 / n;
1391
+ const slices = segments.map((seg, i) => {
1392
+ const start = i * segmentAngle;
1393
+ const end = start + segmentAngle;
1394
+ const color = seg.colorHex || DEFAULT_COLORS[i % DEFAULT_COLORS.length];
1395
+ const labelAngle = start + segmentAngle / 2;
1396
+ const labelPoint = polarPoint(labelAngle, RADIUS * 0.62);
1397
+ return `<path d="${segmentPath(start, end)}" fill="${escapeAttr(color)}" stroke="#fff" stroke-width="1"></path><text x="${labelPoint.x}" y="${labelPoint.y}" fill="#fff" font-size="12" text-anchor="middle" transform="rotate(${labelAngle} ${labelPoint.x} ${labelPoint.y})">${escapeHtml(seg.label)}</text>`;
1398
+ }).join("");
1399
+ return `<svg class="xenia-wheel-svg" viewBox="0 0 ${WHEEL_SIZE} ${WHEEL_SIZE}" width="${WHEEL_SIZE}" height="${WHEEL_SIZE}"><g class="xenia-wheel-dial">${slices}</g><circle class="xenia-wheel-hub" cx="${CENTER}" cy="${CENTER}" r="6"></circle></svg>`;
1400
+ }
1401
+ var spinWheelRenderer = {
1402
+ html(action) {
1403
+ return `<div class="xenia-spin-wheel"><div data-xenia-wheel-face>${action.spinnerHtml ?? ""}</div><div class="xenia-widget-result" data-xenia-wheel-result hidden>${action.winHtml ?? ""}</div><div class="xenia-widget-result" data-xenia-wheel-loss hidden>${action.lossHtml ?? ""}</div></div>`;
1404
+ },
1405
+ wire(root, action, ctx) {
1406
+ wireCloseAffordances(root, ctx.dismiss);
1407
+ const segments = action.segments ?? [];
1408
+ const wheelFace = root.querySelector("[data-xenia-wheel-face]");
1409
+ const spinBtn = root.querySelector("[data-xenia-spin]");
1410
+ const winEl = root.querySelector("[data-xenia-wheel-result]");
1411
+ const lossEl = root.querySelector("[data-xenia-wheel-loss]");
1412
+ const errorEl = root.querySelector("[data-xenia-error]");
1413
+ const slotEl = root.querySelector("[data-xenia-wheel-slot]");
1414
+ if (slotEl) slotEl.insertAdjacentHTML("afterbegin", wheelSvg(segments) + WHEEL_POINTER_HTML);
1415
+ const dial = root.querySelector(".xenia-wheel-dial");
1416
+ if (!spinBtn || !action.formSubmit) return;
1417
+ root.querySelectorAll("[data-xenia-wheel-cta]").forEach((btn) => {
1418
+ btn.addEventListener("click", () => ctx.fireCta());
1419
+ });
1420
+ const revealResult = (prizeLabel, isLoss, canRetry) => {
1421
+ root.querySelectorAll("[data-xenia-wheel-prize]").forEach((el) => {
1422
+ el.textContent = prizeLabel;
1423
+ });
1424
+ const shown = isLoss ? lossEl : winEl;
1425
+ const other = isLoss ? winEl : lossEl;
1426
+ if (other) other.hidden = true;
1427
+ crossFadeReveal(wheelFace, shown);
1428
+ if (!canRetry) {
1429
+ playsExhausted = true;
1430
+ root.querySelectorAll("[data-xenia-spin-again]").forEach((btn) => {
1431
+ btn.hidden = true;
1432
+ });
1433
+ }
1434
+ };
1435
+ let spinning = false;
1436
+ let playsExhausted = false;
1437
+ root.querySelectorAll("[data-xenia-spin-again]").forEach((btn) => {
1438
+ btn.addEventListener("click", () => {
1439
+ if (playsExhausted) return;
1440
+ const shown = winEl && !winEl.hidden ? winEl : lossEl && !lossEl.hidden ? lossEl : null;
1441
+ if (!shown) return;
1442
+ spinning = false;
1443
+ spinBtn.disabled = false;
1444
+ if (errorEl) errorEl.textContent = "";
1445
+ crossFadeReveal(shown, wheelFace);
1446
+ });
1447
+ });
1448
+ spinBtn.addEventListener("click", () => {
1449
+ if (spinning) return;
1450
+ spinning = true;
1451
+ spinBtn.disabled = true;
1452
+ if (errorEl) errorEl.textContent = "";
1453
+ const fields = collectFields(root);
1454
+ ctx.submitWidgetForm(fields).then((response) => {
1455
+ const index = segments.findIndex((s) => s.id === response.prize?.segmentId);
1456
+ const prizeLabel = response.prize?.label ?? segments[index]?.label ?? "";
1457
+ const isLoss = index >= 0 && !!segments[index]?.isLoss;
1458
+ const canRetry = response.alreadyPlayed !== true;
1459
+ if (index < 0 || !dial) {
1460
+ revealResult(prizeLabel, isLoss, canRetry);
1461
+ return;
1462
+ }
1463
+ const extraSpins = 3 + Math.floor(Math.random() * 3);
1464
+ const rotation = computeSpinRotation(index, segments.length || 1, extraSpins);
1465
+ const priorMatch = /rotate\(([-\d.]+)deg\)/.exec(dial.style.transform);
1466
+ const priorRotation = priorMatch ? parseFloat(priorMatch[1]) : 0;
1467
+ dial.style.transition = "none";
1468
+ dial.style.transform = `rotate(${priorRotation % 360}deg)`;
1469
+ void dial.getBoundingClientRect();
1470
+ dial.style.transition = SPIN_TRANSITION;
1471
+ const onTransitionEnd = () => {
1472
+ dial.removeEventListener("transitionend", onTransitionEnd);
1473
+ revealResult(prizeLabel, isLoss, canRetry);
1474
+ };
1475
+ dial.addEventListener("transitionend", onTransitionEnd);
1476
+ nextFrame(() => {
1477
+ dial.style.transform = `rotate(${rotation}deg)`;
1478
+ });
1479
+ }).catch(() => {
1480
+ spinning = false;
1481
+ spinBtn.disabled = false;
1482
+ if (errorEl) errorEl.textContent = "Something went wrong. Please try again.";
1483
+ });
1484
+ });
1485
+ }
1486
+ };
1487
+
1488
+ // src/client/widgets/index.ts
1489
+ var WIDGET_RENDERERS = {
1490
+ "spin-wheel": spinWheelRenderer
1491
+ };
1492
+
1057
1493
  // src/client/renderer.ts
1058
1494
  function resolveTarget(target, root = document) {
1059
1495
  if (!target) return null;
@@ -1111,6 +1547,7 @@ var ActionRenderer = class {
1111
1547
  messageTemplate;
1112
1548
  messageStyles;
1113
1549
  messageComponent;
1550
+ submitWidgetFormFn;
1114
1551
  /** Unmount callbacks for currently-mounted framework components, keyed by their container. */
1115
1552
  mounted = /* @__PURE__ */ new Map();
1116
1553
  constructor(onCta, opts) {
@@ -1118,6 +1555,7 @@ var ActionRenderer = class {
1118
1555
  this.messageTemplate = opts?.messageTemplate;
1119
1556
  this.messageStyles = opts?.messageStyles;
1120
1557
  this.messageComponent = opts?.messageComponent;
1558
+ this.submitWidgetFormFn = opts?.submitWidgetForm;
1121
1559
  }
1122
1560
  /** Apply a list of actions. Returns the count successfully applied. */
1123
1561
  apply(actions) {
@@ -1200,7 +1638,8 @@ ${this.messageStyles}` : STYLES;
1200
1638
  const wrap = document.createElement("div");
1201
1639
  const posClass = this.positionClass(action.type, action.position);
1202
1640
  const customBgClass = action.html !== void 0 ? " xenia-custom-bg" : "";
1203
- wrap.className = `xenia-msg xenia-${action.type}${posClass ? ` ${posClass}` : ""}${customBgClass}`;
1641
+ const noOverlayClass = action.style?.hideOverlay ? " xenia-no-overlay" : "";
1642
+ wrap.className = `xenia-msg xenia-${action.type}${posClass ? ` ${posClass}` : ""}${customBgClass}${noOverlayClass}`;
1204
1643
  shadow.appendChild(wrap);
1205
1644
  const dismiss = () => {
1206
1645
  this.unmount(wrap);
@@ -1213,7 +1652,10 @@ ${this.messageStyles}` : STYLES;
1213
1652
  this.wireCard(wrap, action, dismiss);
1214
1653
  }
1215
1654
  if (action.dismissAfterMs && action.dismissAfterMs > 0) {
1216
- setTimeout(dismiss, action.dismissAfterMs);
1655
+ const timer = setTimeout(dismiss, action.dismissAfterMs);
1656
+ const cancelAutoDismiss = () => clearTimeout(timer);
1657
+ wrap.addEventListener("click", cancelAutoDismiss, { once: true });
1658
+ wrap.addEventListener("focusin", cancelAutoDismiss, { once: true });
1217
1659
  }
1218
1660
  return true;
1219
1661
  }
@@ -1257,38 +1699,32 @@ ${this.messageStyles}` : STYLES;
1257
1699
  return "";
1258
1700
  }
1259
1701
  cardHtml(action, variant) {
1702
+ const widget = action.widgetType ? WIDGET_RENDERERS[action.widgetType] : void 0;
1703
+ if (widget) return widget.html(action, variant);
1260
1704
  if (action.html !== void 0) return wrapCustomHtml(action.html, variant);
1261
1705
  if (this.messageTemplate) {
1262
1706
  const ctx = {
1263
1707
  action,
1264
1708
  variant,
1265
- escapeHtml,
1266
- escapeAttr
1709
+ escapeHtml: escapeHtml2,
1710
+ escapeAttr: escapeAttr2
1267
1711
  };
1268
1712
  return this.messageTemplate(ctx);
1269
1713
  }
1270
- const img = action.imageUrl ? `<img class="xenia-img" src="${escapeAttr(action.imageUrl)}" alt="" />` : "";
1271
- const title = action.title ? `<div class="xenia-title">${escapeHtml(action.title)}</div>` : "";
1272
- const msg = action.message ? `<div class="xenia-body">${escapeHtml(action.message)}</div>` : "";
1273
- const cta = action.cta ? `<button class="xenia-cta" type="button">${escapeHtml(action.cta.label)}</button>` : "";
1714
+ const img = action.imageUrl ? `<img class="xenia-img" src="${escapeAttr2(action.imageUrl)}" alt="" />` : "";
1715
+ const title = action.title ? `<div class="xenia-title">${escapeHtml2(action.title)}</div>` : "";
1716
+ const msg = action.message ? `<div class="xenia-body">${escapeHtml2(action.message)}</div>` : "";
1717
+ const cta = action.cta ? `<button class="xenia-cta" type="button">${escapeHtml2(action.cta.label)}</button>` : "";
1274
1718
  const close = variant === "inline" ? "" : `<button class="xenia-close" type="button" aria-label="Close">\xD7</button>`;
1275
1719
  return `<div class="xenia-card">${close}${img}<div class="xenia-content">${title}${msg}${cta}</div></div>`;
1276
1720
  }
1277
1721
  wireCard(root, action, dismiss) {
1278
- if (dismiss) {
1279
- const closeEls = root.querySelectorAll(
1280
- ".xenia-close, [data-xenia-close], [class*='close' i], [id*='close' i], [class*='backdrop' i], [id*='backdrop' i]"
1281
- );
1282
- closeEls.forEach((el) => {
1283
- el.removeAttribute("onclick");
1284
- el.addEventListener("click", dismiss);
1285
- });
1722
+ const widget = action.widgetType ? WIDGET_RENDERERS[action.widgetType] : void 0;
1723
+ if (widget) {
1724
+ widget.wire(root, action, this.buildWidgetCtx(action, dismiss));
1725
+ return;
1286
1726
  }
1287
- root.querySelectorAll("[class*='backdrop' i], [id*='backdrop' i]").forEach((el) => {
1288
- if (el instanceof HTMLElement && el.children.length === 0 && !el.textContent?.trim()) {
1289
- el.style.opacity = "0.5";
1290
- }
1291
- });
1727
+ wireCloseAffordances(root, dismiss);
1292
1728
  const ctaBtn = root.querySelector(".xenia-cta, [data-xenia-cta]");
1293
1729
  if (ctaBtn && action.cta) {
1294
1730
  ctaBtn.addEventListener("click", () => {
@@ -1297,6 +1733,29 @@ ${this.messageStyles}` : STYLES;
1297
1733
  dismiss?.();
1298
1734
  });
1299
1735
  }
1736
+ wireFormSubmit(root, action, this.buildWidgetCtx(action, dismiss));
1737
+ }
1738
+ /** Build the imperative hooks (`dismiss`/`fireCta`/`submitWidgetForm`) handed to a widget's `wire()`. */
1739
+ buildWidgetCtx(action, dismiss) {
1740
+ const idempotencyKey = generateUlid();
1741
+ return {
1742
+ dismiss,
1743
+ fireCta: () => {
1744
+ if (action.cta?.event) this.onCta?.(action.cta.event, action.cta);
1745
+ if (action.cta?.url) window.location.assign(action.cta.url);
1746
+ dismiss?.();
1747
+ },
1748
+ submitWidgetForm: (fields) => {
1749
+ if (!action.formSubmit || !this.submitWidgetFormFn) {
1750
+ return Promise.reject(
1751
+ new Error(
1752
+ "xenia: this action has no formSubmit config, or no submitWidgetForm handler is wired (construct the ActionRenderer via XeniaClient to get one)"
1753
+ )
1754
+ );
1755
+ }
1756
+ return this.submitWidgetFormFn(action.formSubmit, fields, { idempotencyKey });
1757
+ }
1758
+ };
1300
1759
  }
1301
1760
  };
1302
1761
  function wrapCustomHtml(html, variant) {
@@ -1305,11 +1764,11 @@ function wrapCustomHtml(html, variant) {
1305
1764
  }
1306
1765
  return `<div class="xenia-custom-html"><style>.xenia-custom-html{position:relative}.xenia-custom-html>*{position:static!important;inset:auto!important;top:auto!important;right:auto!important;bottom:auto!important;left:auto!important}</style>${html}</div>`;
1307
1766
  }
1308
- function escapeHtml(s) {
1767
+ function escapeHtml2(s) {
1309
1768
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1310
1769
  }
1311
- function escapeAttr(s) {
1312
- return escapeHtml(s).replace(/"/g, "&quot;");
1770
+ function escapeAttr2(s) {
1771
+ return escapeHtml2(s).replace(/"/g, "&quot;");
1313
1772
  }
1314
1773
  var STYLES = `
1315
1774
  :host { all: initial; }
@@ -1326,12 +1785,14 @@ var STYLES = `
1326
1785
  .xenia-modal { inset: 0; display: flex; align-items: center; justify-content: center;
1327
1786
  background: var(--xenia-modal-backdrop, rgba(0,0,0,.12)); }
1328
1787
  .xenia-modal.xenia-custom-bg { background: transparent; }
1788
+ .xenia-modal.xenia-no-overlay { background: transparent; }
1329
1789
  .xenia-card { position: relative; background: var(--xenia-card-bg, #fff); color: var(--xenia-card-color, #111);
1330
1790
  border-radius: var(--xenia-card-radius, 12px); box-shadow: var(--xenia-card-shadow, 0 10px 40px rgba(0,0,0,.18));
1331
1791
  padding: var(--xenia-card-padding, 16px 18px); display: flex; gap: var(--xenia-card-gap, 12px);
1332
1792
  align-items: center; }
1333
1793
  .xenia-banner .xenia-card { border-radius: var(--xenia-banner-card-radius, 0); justify-content: center; }
1334
1794
  .xenia-modal .xenia-card { max-width: var(--xenia-modal-max-width, 440px); width: calc(100% - 32px); }
1795
+ .xenia-card.xenia-widget { flex-direction: column; align-items: center; text-align: center; }
1335
1796
  .xenia-img { width: var(--xenia-img-size, 56px); height: var(--xenia-img-size, 56px); object-fit: cover;
1336
1797
  border-radius: var(--xenia-img-radius, 8px); }
1337
1798
  .xenia-title { font-weight: var(--xenia-title-weight, 600); font-size: var(--xenia-title-size, 15px);
@@ -1343,6 +1804,38 @@ var STYLES = `
1343
1804
  font-size: var(--xenia-cta-size, 14px); cursor: pointer; }
1344
1805
  .xenia-close { position: absolute; top: 6px; right: 8px; background: none; border: 0;
1345
1806
  font-size: var(--xenia-close-size, 20px); line-height: 1; cursor: pointer; color: var(--xenia-close-color, #999); }
1807
+ .xenia-wheel-dial { transform-box: fill-box; transform-origin: center; }
1808
+ .xenia-wheel-hub { fill: var(--xenia-wheel-pointer-color, #111); }
1809
+ .xenia-wheel-wrap { position: relative; display: inline-block; }
1810
+ .xenia-wheel-pointer { position: absolute; top: -6px; left: 50%; transform: translateX(-50%);
1811
+ width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent;
1812
+ border-top: 14px solid var(--xenia-wheel-pointer-color, #111); pointer-events: none; }
1813
+ .xenia-wheel-spin { margin-top: 10px; background: var(--xenia-cta-bg, #111); color: var(--xenia-cta-color, #fff);
1814
+ border: 0; border-radius: var(--xenia-cta-radius, 8px); padding: var(--xenia-cta-padding, 8px 14px);
1815
+ font-size: var(--xenia-cta-size, 14px); font-family: inherit; cursor: pointer; }
1816
+ .xenia-wheel-spin:disabled { opacity: 0.6; cursor: default; }
1817
+ /* No animation/transform here \u2014 spin-wheel's win/loss screens are ALWAYS full
1818
+ custom HTML now (winHtml/lossHtml), never this class's own generated icon/title/prize/cta
1819
+ content (those sub-classes below are unreachable dead CSS, kept only for backwards-compat with
1820
+ any already-saved artifact that might still reference them). A transform on THIS wrapper
1821
+ would make it the CSS containing block for any fixed/absolute-positioned descendant the
1822
+ author's own HTML uses (a real, common case \u2014 a "full-screen overlay" win/loss design) \u2014 the
1823
+ fixed child would then size/position itself relative to this wrapper's own small, in-flow,
1824
+ often off-center box instead of the true browser viewport, visibly popping in from/toward
1825
+ whatever corner the wrapper happens to sit at instead of scaling from the real screen center.
1826
+ spin-wheel.ts's crossFadeReveal owns the actual reveal transition (applied to the author's own
1827
+ top-level element, never this wrapper) \u2014 this class stays a bare, transform-free layout box. */
1828
+ .xenia-widget-result { display: flex; flex-direction: column; align-items: center; gap: 4px;
1829
+ padding: 8px 4px 2px; text-align: center; }
1830
+ .xenia-widget-result[hidden] { display: none; }
1831
+ .xenia-widget-result-icon { font-size: 40px; line-height: 1; }
1832
+ .xenia-widget-result-title { font-weight: var(--xenia-title-weight, 700); font-size: var(--xenia-title-size, 17px); }
1833
+ .xenia-widget-result-prize { font-weight: 600; font-size: 15px; color: var(--xenia-cta-bg, #111);
1834
+ background: var(--xenia-result-prize-bg, rgba(0,0,0,.06)); border-radius: var(--xenia-cta-radius, 8px);
1835
+ padding: 6px 14px; }
1836
+ .xenia-widget-result-cta { margin-top: 8px; background: var(--xenia-cta-bg, #111); color: var(--xenia-cta-color, #fff);
1837
+ border: 0; border-radius: var(--xenia-cta-radius, 8px); padding: var(--xenia-cta-padding, 8px 14px);
1838
+ font-size: var(--xenia-cta-size, 14px); font-family: inherit; cursor: pointer; }
1346
1839
  `;
1347
1840
 
1348
1841
  // src/client/xenia-client.ts
@@ -1367,7 +1860,8 @@ var XeniaClient = class {
1367
1860
  this.renderer = new ActionRenderer((event, cta) => this.onCtaEvent(event, cta), {
1368
1861
  messageTemplate: options.messageTemplate,
1369
1862
  messageStyles: options.messageStyles,
1370
- messageComponent: options.messageComponent
1863
+ messageComponent: options.messageComponent,
1864
+ submitWidgetForm: (submit, fields, subOpts) => this.submitWidgetForm(submit, fields, subOpts)
1371
1865
  });
1372
1866
  this.consentState = { ...options.consent };
1373
1867
  this.analyticsQueue = new AnalyticsQueue({
@@ -1429,10 +1923,16 @@ var XeniaClient = class {
1429
1923
  * Persist a visitor id, attached to every subsequent event, and bind it to
1430
1924
  * a known user server-side (`POST /api/events/v1/identify`, fire-and-
1431
1925
  * forget — resolves to an `"identify"` client event carrying `personId`).
1926
+ *
1927
+ * `extra.customerId`/`extra.email`, when supplied, are also persisted
1928
+ * ambiently (like the visitor id) so they ride on every later `resolve()`
1929
+ * call automatically — part of the SDK-provided-fields parity list.
1432
1930
  */
1433
- identify(visitorId) {
1931
+ identify(visitorId, extra) {
1434
1932
  this.identity.identify(visitorId);
1435
- void this.api.identify({ xeniaVisitorId: visitorId }).then((res) => this.emit("identify", res)).catch((err) => this.emitError(err));
1933
+ if (extra?.customerId !== void 0) this.identity.setCustomerId(extra.customerId);
1934
+ if (extra?.email !== void 0) this.identity.setEmail(extra.email);
1935
+ void this.api.identify({ xeniaVisitorId: visitorId, userId: extra?.customerId, email: extra?.email }).then((res) => this.emit("identify", res)).catch((err) => this.emitError(err));
1436
1936
  }
1437
1937
  /** Override the customer-journey UUID (else derived from utm_id). */
1438
1938
  setJourneyId(id) {
@@ -1550,15 +2050,40 @@ var XeniaClient = class {
1550
2050
  return this.identity.getVisitorId() ?? this.identity.getDeviceId() ?? void 0;
1551
2051
  }
1552
2052
  /**
1553
- * Stamp the SDK-owned visitor id into the resolve `data` under the reserved
1554
- * `_xenia_vid` key so the backend can bucket holdout experiments and record
1555
- * exposure per visitor (it reads visitor identity from `data`). Set last so a
1556
- * caller can't override it; a no-op when no visitor id is resolvable yet.
2053
+ * Stamp the full set of SDK-provided fields into the resolve `data` —
2054
+ * `path`, `query`, `deviceId`, `visitorId`, `customerId`, `email`, `url`,
2055
+ * `referrer`, and the five `utm*` fields — the same field-name/type parity
2056
+ * the webhook trigger's "SDK-provided fields" already promises, now native
2057
+ * (always attached, no marketer toggle) for the Personalization Resolve
2058
+ * trigger too. Plus the reserved `_xenia_vid` key the backend reads for
2059
+ * holdout/exposure bucketing. All of these are spread LAST (after the
2060
+ * caller's own `data`) so none can be overridden per call — the same
2061
+ * override-protection guarantee `_xenia_vid` alone used to have, now
2062
+ * widened to the full set. A no-op (returns `opts` unchanged) when nothing
2063
+ * is resolvable yet (e.g. called before `init()` on a fresh visit with no
2064
+ * page context available).
1557
2065
  */
1558
2066
  withVisitor(opts) {
2067
+ const ctx = this.identity.collectContext();
1559
2068
  const vid = this.resolveVisitorId();
1560
- if (!vid) return opts;
1561
- return { ...opts, data: { ...opts?.data ?? {}, _xenia_vid: vid } };
2069
+ const sdkFields = compact({
2070
+ path: ctx.path,
2071
+ query: ctx.query,
2072
+ deviceId: this.identity.getDeviceId() ?? void 0,
2073
+ visitorId: vid,
2074
+ customerId: this.identity.getCustomerId() ?? void 0,
2075
+ email: this.identity.getEmail() ?? void 0,
2076
+ url: ctx.url,
2077
+ referrer: ctx.referrer,
2078
+ utmSource: ctx.utmSource,
2079
+ utmMedium: ctx.utmMedium,
2080
+ utmCampaign: ctx.utmCampaign,
2081
+ utmContent: ctx.utmContent,
2082
+ utmId: ctx.utmId
2083
+ });
2084
+ if (vid) sdkFields._xenia_vid = vid;
2085
+ if (Object.keys(sdkFields).length === 0) return opts;
2086
+ return { ...opts, data: { ...opts?.data ?? {}, ...sdkFields } };
1562
2087
  }
1563
2088
  dntBlocked() {
1564
2089
  if (this.options.respectDnt === false) return false;
@@ -1608,6 +2133,55 @@ var XeniaClient = class {
1608
2133
  getResponse(key) {
1609
2134
  return this.responses.get(key);
1610
2135
  }
2136
+ /**
2137
+ * Submit a widget's captured form values (standard/custom popup submit, or a spin-wheel/
2138
+ * spin-wheel's resolve request) and apply any returned `xeniaActions`, exactly like
2139
+ * {@link request}.
2140
+ *
2141
+ * **Endpoint selection is config-driven, not SDK-decided** (per the cross-repo contract): a
2142
+ * `flowRef` (present only when `mode === "journey" && !requiresResolve` — the common
2143
+ * plain-popup case) bypasses the resolve endpoint entirely and POSTs straight to the existing
2144
+ * public `/flow` fast path; otherwise this POSTs to `/widget-submissions` via `bindingRef`. The
2145
+ * SDK is agnostic to *why* — it just follows whichever ref field the backend populated.
2146
+ *
2147
+ * The full ambient-context parity list {@link request}/`withVisitor()` already build
2148
+ * (visitorId, deviceId, url, referrer, path, query, `utm*`) rides on every call, plus `fields`
2149
+ * and `submit.mode`/the resolved ref, relayed verbatim/uninterpreted — the SDK never branches
2150
+ * on `mode`'s value itself, that's server-side.
2151
+ *
2152
+ * `opts.idempotencyKey` defaults to a freshly generated ULID; pass the SAME key back in on a
2153
+ * retried submit of the same interaction so a network retry can't double-fire a journey or
2154
+ * double-resolve a prize server-side.
2155
+ */
2156
+ async submitWidgetForm(submit, fields, opts) {
2157
+ const ctx = this.identity.collectContext();
2158
+ const idempotencyKey = opts?.idempotencyKey ?? generateUlid();
2159
+ const ref = submit.flowRef ?? submit.bindingRef;
2160
+ const ambient = {
2161
+ visitorId: this.resolveVisitorId(),
2162
+ deviceId: this.identity.getDeviceId() ?? void 0,
2163
+ idempotencyKey,
2164
+ mode: submit.mode,
2165
+ ref,
2166
+ url: ctx.url,
2167
+ referrer: ctx.referrer,
2168
+ path: ctx.path,
2169
+ query: ctx.query,
2170
+ utmSource: ctx.utmSource,
2171
+ utmMedium: ctx.utmMedium,
2172
+ utmCampaign: ctx.utmCampaign,
2173
+ utmContent: ctx.utmContent,
2174
+ utmId: ctx.utmId
2175
+ };
2176
+ const response = submit.flowRef ? await this.api.webhook(submit.flowRef, compact({ ...ambient, ...fields }), "POST") : await this.api.submitWidget(
2177
+ submit.bindingRef ?? "",
2178
+ compact({ ...ambient, fields })
2179
+ );
2180
+ this.emit("response", { triggerId: ref, response });
2181
+ const apply = opts?.apply ?? this.options.autoApplyActions !== false;
2182
+ if (apply && response.actions.length) this.applyActions(response.actions);
2183
+ return response;
2184
+ }
1611
2185
  // ── Imperative render helpers ─────────────────────────────────────────────────
1612
2186
  swapContent(target, content) {
1613
2187
  return swapContent(target, content);
@@ -1722,7 +2296,10 @@ exports.AnalyticsQueue = AnalyticsQueue;
1722
2296
  exports.AutoTracker = AutoTracker;
1723
2297
  exports.Identity = Identity;
1724
2298
  exports.TokenProvider = TokenProvider;
2299
+ exports.WIDGET_RENDERERS = WIDGET_RENDERERS;
1725
2300
  exports.XeniaClient = XeniaClient;
2301
+ exports.collectFields = collectFields;
2302
+ exports.computeSpinRotation = computeSpinRotation;
1726
2303
  exports.createXeniaClient = createXeniaClient;
1727
2304
  exports.extractContentSwaps = extractContentSwaps;
1728
2305
  exports.hideContent = hideContent;
@@ -1733,5 +2310,6 @@ exports.resolveTarget = resolveTarget;
1733
2310
  exports.showContent = showContent;
1734
2311
  exports.spliceSlots = spliceSlots;
1735
2312
  exports.swapContent = swapContent;
2313
+ exports.wireFormSubmit = wireFormSubmit;
1736
2314
  //# sourceMappingURL=index.cjs.map
1737
2315
  //# sourceMappingURL=index.cjs.map