@syntrologie/runtime-sdk 2.8.0-canary.8 → 2.8.0-canary.80

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 (63) hide show
  1. package/CAPABILITIES.md +299 -167
  2. package/README.md +2 -0
  3. package/dist/actions/schema.d.ts +783 -783
  4. package/dist/actions/schema.js +3 -3
  5. package/dist/actions/types.d.ts +1 -1
  6. package/dist/actions/validation-core.d.ts +24 -0
  7. package/dist/actions/validation-rules.d.ts +74 -0
  8. package/dist/actions/validation.d.ts +5 -11
  9. package/dist/bootstrap-init.d.ts +33 -0
  10. package/dist/bootstrap-runtime.d.ts +7 -0
  11. package/dist/bootstrap-types.d.ts +90 -0
  12. package/dist/bootstrap.d.ts +19 -83
  13. package/dist/{chunk-R5DNAIRI.js → chunk-77TNZ66J.js} +4 -4
  14. package/dist/{chunk-R5DNAIRI.js.map → chunk-77TNZ66J.js.map} +2 -2
  15. package/dist/{chunk-OB5AKUQT.js → chunk-EELCEGS7.js} +1868 -731
  16. package/dist/chunk-EELCEGS7.js.map +7 -0
  17. package/dist/{chunk-XDYJ64IN.js → chunk-IR6UOR63.js} +4 -4
  18. package/dist/chunk-IR6UOR63.js.map +7 -0
  19. package/dist/chunk-YLLWLUQX.js +241 -0
  20. package/dist/chunk-YLLWLUQX.js.map +7 -0
  21. package/dist/components/ShadowCanvasOverlay.d.ts +1 -2
  22. package/dist/components/TileIcon.d.ts +2 -2
  23. package/dist/components/emojiToIcon.d.ts +24 -0
  24. package/dist/config/schema.d.ts +147 -136
  25. package/dist/config/schema.js +2 -2
  26. package/dist/decisions/schema.d.ts +47 -47
  27. package/dist/decisions/schema.js +1 -1
  28. package/dist/events/EventBus.d.ts +27 -1
  29. package/dist/events/history.d.ts +9 -0
  30. package/dist/events/index.d.ts +3 -0
  31. package/dist/events/normalizers/posthog.d.ts +4 -50
  32. package/dist/events/types.d.ts +30 -23
  33. package/dist/events/validation.d.ts +7 -0
  34. package/dist/fetchers/experimentsFetcher.d.ts +3 -3
  35. package/dist/fetchers/mergeConfigs.d.ts +7 -7
  36. package/dist/index.d.ts +0 -2
  37. package/dist/index.js +1529 -212
  38. package/dist/index.js.map +4 -4
  39. package/dist/overlays/runtime/overlay/overlay-runner.d.ts +4 -0
  40. package/dist/overlays/runtime/overlay/overlay-state.d.ts +21 -0
  41. package/dist/overlays/types.d.ts +3 -1
  42. package/dist/react.js +6 -4
  43. package/dist/react.js.map +2 -2
  44. package/dist/smart-canvas.esm.js +115 -65
  45. package/dist/smart-canvas.esm.js.map +4 -4
  46. package/dist/smart-canvas.js +5982 -3081
  47. package/dist/smart-canvas.js.map +4 -4
  48. package/dist/smart-canvas.min.js +115 -65
  49. package/dist/smart-canvas.min.js.map +4 -4
  50. package/dist/telemetry/InterventionTracker.d.ts +23 -0
  51. package/dist/telemetry/adapters/posthog.d.ts +30 -4
  52. package/dist/telemetry/index.d.ts +1 -0
  53. package/dist/test/setup.d.ts +1 -0
  54. package/dist/token.d.ts +2 -0
  55. package/dist/version.d.ts +1 -1
  56. package/package.json +23 -28
  57. package/schema/canvas-config.schema.json +1974 -10925
  58. package/scripts/syntroReactPlugin.mjs +3 -0
  59. package/scripts/validate-config.mjs +42 -0
  60. package/dist/chunk-BU4Z6PD7.js +0 -218
  61. package/dist/chunk-BU4Z6PD7.js.map +0 -7
  62. package/dist/chunk-OB5AKUQT.js.map +0 -7
  63. package/dist/chunk-XDYJ64IN.js.map +0 -7
@@ -3,7 +3,7 @@ import {
3
3
  __privateGet,
4
4
  __privateSet,
5
5
  __publicField
6
- } from "./chunk-BU4Z6PD7.js";
6
+ } from "./chunk-YLLWLUQX.js";
7
7
 
8
8
  // ../adaptives/adaptive-content/dist/reconciliation-guard.js
9
9
  function guardAgainstReconciliation(container, anchor, reinsertFn, opts) {
@@ -34,6 +34,11 @@ function guardAgainstReconciliation(container, anchor, reinsertFn, opts) {
34
34
  debounceTimer = setTimeout(() => {
35
35
  if (disconnected)
36
36
  return;
37
+ if (!anchor.isConnected) {
38
+ observer.disconnect();
39
+ disconnected = true;
40
+ return;
41
+ }
37
42
  retries++;
38
43
  try {
39
44
  reinsertFn();
@@ -75,7 +80,16 @@ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
75
80
  "sup",
76
81
  "sub",
77
82
  "a",
78
- "button"
83
+ "button",
84
+ // SVG elements (for inline Lucide icons in config HTML)
85
+ "svg",
86
+ "path",
87
+ "circle",
88
+ "line",
89
+ "polyline",
90
+ "polygon",
91
+ "rect",
92
+ "g"
79
93
  ]);
80
94
  function sanitizeHtml(html) {
81
95
  var _a2;
@@ -113,6 +127,12 @@ function sanitizeHtml(html) {
113
127
  }
114
128
  }
115
129
  }
130
+ const svgs = Array.from(root.querySelectorAll("svg"));
131
+ for (const svg of svgs) {
132
+ if (toRemove.some((el) => svg.contains(el) && el.tagName.toLowerCase() === "script")) {
133
+ toRemove.push(svg);
134
+ }
135
+ }
116
136
  for (const el of toRemove) {
117
137
  while (el.firstChild) {
118
138
  (_a2 = el.parentNode) == null ? void 0 : _a2.insertBefore(el.firstChild, el);
@@ -213,15 +233,20 @@ var executeInsertHtml = async (action, context) => {
213
233
  container.removeEventListener("click", deepLinkHandler);
214
234
  }
215
235
  guardCleanup();
216
- if (action.position === "replace" && originalContent !== null) {
217
- const restoredEl = document.createElement(anchorEl.tagName);
218
- restoredEl.innerHTML = originalContent;
219
- Array.from(anchorEl.attributes).forEach((attr) => {
220
- restoredEl.setAttribute(attr.name, attr.value);
221
- });
222
- container.replaceWith(restoredEl);
223
- } else {
224
- container.remove();
236
+ if (!container.isConnected)
237
+ return;
238
+ try {
239
+ if (action.position === "replace" && originalContent !== null) {
240
+ const restoredEl = document.createElement(anchorEl.tagName);
241
+ restoredEl.innerHTML = originalContent;
242
+ Array.from(anchorEl.attributes).forEach((attr) => {
243
+ restoredEl.setAttribute(attr.name, attr.value);
244
+ });
245
+ container.replaceWith(restoredEl);
246
+ } else {
247
+ container.remove();
248
+ }
249
+ } catch {
225
250
  }
226
251
  },
227
252
  updateFn: (changes) => {
@@ -251,6 +276,8 @@ var executeSetText = async (action, context) => {
251
276
  });
252
277
  return {
253
278
  cleanup: () => {
279
+ if (!anchorEl.isConnected)
280
+ return;
254
281
  anchorEl.textContent = originalText;
255
282
  },
256
283
  updateFn: (changes) => {
@@ -292,6 +319,8 @@ var executeSetAttr = async (action, context) => {
292
319
  });
293
320
  return {
294
321
  cleanup: () => {
322
+ if (!anchorEl.isConnected)
323
+ return;
295
324
  if (hadAttribute && originalValue !== null) {
296
325
  anchorEl.setAttribute(action.attr, originalValue);
297
326
  } else {
@@ -325,6 +354,8 @@ var executeAddClass = async (action, context) => {
325
354
  });
326
355
  return {
327
356
  cleanup: () => {
357
+ if (!anchorEl.isConnected)
358
+ return;
328
359
  if (!hadClass) {
329
360
  anchorEl.classList.remove(action.className);
330
361
  }
@@ -351,6 +382,8 @@ var executeRemoveClass = async (action, context) => {
351
382
  });
352
383
  return {
353
384
  cleanup: () => {
385
+ if (!anchorEl.isConnected)
386
+ return;
354
387
  if (hadClass) {
355
388
  anchorEl.classList.add(action.className);
356
389
  }
@@ -383,6 +416,8 @@ var executeSetStyle = async (action, context) => {
383
416
  });
384
417
  return {
385
418
  cleanup: () => {
419
+ if (!anchorEl.isConnected)
420
+ return;
386
421
  for (const [prop, originalValue] of originalStyles) {
387
422
  if (originalValue) {
388
423
  anchorEl.style.setProperty(prop, originalValue);
@@ -726,6 +761,10 @@ var CelebrationEngine = class {
726
761
  this.container = null;
727
762
  }
728
763
  start(container, effect, config) {
764
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
765
+ if (prefersReducedMotion) {
766
+ return;
767
+ }
729
768
  this.container = container;
730
769
  this.effect = effect;
731
770
  this.duration = config.duration;
@@ -1369,7 +1408,7 @@ var alert = {
1369
1408
  var tag = {
1370
1409
  content: slateGrey[10],
1371
1410
  border: slateGrey[4],
1372
- background: "#1c2124"
1411
+ background: slateGrey[3]
1373
1412
  };
1374
1413
  var menu = {
1375
1414
  backgroundDefault: slateGrey[2],
@@ -1558,10 +1597,10 @@ function showHighlight(anchorEl, overlayRoot, opts) {
1558
1597
  return;
1559
1598
  }
1560
1599
  const rect = anchorEl.getBoundingClientRect();
1561
- const x = Math.max(0, rect.left - padding);
1562
- const y = Math.max(0, rect.top - padding);
1563
- const w = Math.min(window.innerWidth, rect.width + padding * 2);
1564
- const h = Math.min(window.innerHeight, rect.height + padding * 2);
1600
+ const x = rect.left - padding;
1601
+ const y = rect.top - padding;
1602
+ const w = rect.width + padding * 2;
1603
+ const h = rect.height + padding * 2;
1565
1604
  Object.assign(ring.style, {
1566
1605
  left: `${x}px`,
1567
1606
  top: `${y}px`,
@@ -1610,17 +1649,22 @@ function showHighlight(anchorEl, overlayRoot, opts) {
1610
1649
  window.addEventListener("scroll", onScroll, true);
1611
1650
  window.addEventListener("resize", onResize);
1612
1651
  const onKey = (e) => {
1613
- if (e.key === "Escape" && onEsc)
1652
+ var _a3;
1653
+ if (e.key === "Escape" && onEsc) {
1654
+ (_a3 = opts == null ? void 0 : opts.onDismiss) == null ? void 0 : _a3.call(opts);
1614
1655
  handle.destroy();
1656
+ }
1615
1657
  };
1616
1658
  if (onEsc) {
1617
1659
  window.addEventListener("keydown", onKey);
1618
1660
  }
1619
1661
  const onClick = (event) => {
1662
+ var _a3;
1620
1663
  if (blocking) {
1621
1664
  event.preventDefault();
1622
1665
  event.stopPropagation();
1623
1666
  } else if (onClickOutside) {
1667
+ (_a3 = opts == null ? void 0 : opts.onDismiss) == null ? void 0 : _a3.call(opts);
1624
1668
  handle.destroy();
1625
1669
  }
1626
1670
  };
@@ -1637,8 +1681,14 @@ function showHighlight(anchorEl, overlayRoot, opts) {
1637
1681
  scrim.style.pointerEvents = "none";
1638
1682
  scrim.style.opacity = "0";
1639
1683
  setTimeout(() => {
1640
- scrim.remove();
1641
- ring.remove();
1684
+ try {
1685
+ scrim.remove();
1686
+ } catch {
1687
+ }
1688
+ try {
1689
+ ring.remove();
1690
+ } catch {
1691
+ }
1642
1692
  }, 220);
1643
1693
  }
1644
1694
  };
@@ -1666,7 +1716,16 @@ var ALLOWED_TAGS2 = /* @__PURE__ */ new Set([
1666
1716
  "sup",
1667
1717
  "sub",
1668
1718
  "a",
1669
- "button"
1719
+ "button",
1720
+ // SVG elements (for inline Lucide icons in config HTML)
1721
+ "svg",
1722
+ "path",
1723
+ "circle",
1724
+ "line",
1725
+ "polyline",
1726
+ "polygon",
1727
+ "rect",
1728
+ "g"
1670
1729
  ]);
1671
1730
  function sanitizeHtml2(html) {
1672
1731
  var _a2;
@@ -1875,8 +1934,14 @@ var executeModal = async (action, context) => {
1875
1934
  modal2.style.transform = "translate(-50%, -50%) scale(0.95)";
1876
1935
  scrimEl.style.opacity = "0";
1877
1936
  setTimeout(() => {
1878
- modal2.remove();
1879
- scrimEl.remove();
1937
+ try {
1938
+ modal2.remove();
1939
+ } catch {
1940
+ }
1941
+ try {
1942
+ scrimEl.remove();
1943
+ } catch {
1944
+ }
1880
1945
  }, 200);
1881
1946
  context.publishEvent("action.modal_dismissed", {
1882
1947
  actionClicked
@@ -1923,10 +1988,12 @@ function getAnchorReference(anchorEl) {
1923
1988
  }
1924
1989
  function showTooltip(anchorEl, overlayRoot, opts) {
1925
1990
  var _a2;
1926
- const rect = anchorEl.getBoundingClientRect();
1927
- const isLargeElement = rect.width > window.innerWidth * 0.8 || rect.height > window.innerHeight * 0.8;
1928
- if (!isLargeElement) {
1929
- anchorEl.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
1991
+ if (!opts.trigger || opts.trigger === "immediate") {
1992
+ const rect = anchorEl.getBoundingClientRect();
1993
+ const isLargeElement = rect.width > window.innerWidth * 0.8 || rect.height > window.innerHeight * 0.8;
1994
+ if (!isLargeElement) {
1995
+ anchorEl.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
1996
+ }
1930
1997
  }
1931
1998
  const div = document.createElement("div");
1932
1999
  div.className = "syntro-tooltip";
@@ -2136,7 +2203,12 @@ function showTooltip(anchorEl, overlayRoot, opts) {
2136
2203
  }
2137
2204
  div.style.pointerEvents = "none";
2138
2205
  div.style.opacity = "0";
2139
- setTimeout(() => div.remove(), 200);
2206
+ setTimeout(() => {
2207
+ try {
2208
+ div.remove();
2209
+ } catch {
2210
+ }
2211
+ }, 200);
2140
2212
  }
2141
2213
  };
2142
2214
  return handle;
@@ -2169,7 +2241,7 @@ function WorkflowTracker({ workflows, expanded, onStepClick, onDismiss }) {
2169
2241
  }
2170
2242
 
2171
2243
  // ../adaptives/adaptive-overlays/dist/WorkflowWidget.js
2172
- function showWorkflowToast(container, notification) {
2244
+ function showWorkflowToast(notification) {
2173
2245
  const toast = document.createElement("div");
2174
2246
  toast.setAttribute("data-testid", "workflow-toast");
2175
2247
  toast.className = "se-fixed se-bottom-4 se-right-4 se-z-50";
@@ -2203,7 +2275,7 @@ function showWorkflowToast(container, notification) {
2203
2275
  bodyEl.textContent = notification.body;
2204
2276
  toast.appendChild(bodyEl);
2205
2277
  }
2206
- container.appendChild(toast);
2278
+ document.body.appendChild(toast);
2207
2279
  let removeTimer;
2208
2280
  const fadeTimer = setTimeout(() => {
2209
2281
  toast.style.opacity = "0";
@@ -2221,7 +2293,7 @@ function extractWorkflowsFromActive(activeActions) {
2221
2293
  const workflows = /* @__PURE__ */ new Map();
2222
2294
  for (const entry of activeActions) {
2223
2295
  const action = entry.action;
2224
- if (action.kind === "core:tour" && action.workflow && action.tourId) {
2296
+ if (action.kind === "overlays:tour" && action.workflow && action.tourId) {
2225
2297
  const meta = action.workflow;
2226
2298
  const rawSteps = action.steps || [];
2227
2299
  const steps = rawSteps.map((s) => {
@@ -2302,10 +2374,10 @@ function WorkflowWidgetInner({ runtime: runtime3 }) {
2302
2374
  return newEntries.length > 0 ? [...prev, ...newEntries] : prev;
2303
2375
  });
2304
2376
  for (const [tourId, { meta }] of tourWorkflows) {
2305
- if (!notifiedRef.current.has(tourId) && meta.notification && containerRef.current && !dismissed.includes(tourId) && !completed[tourId]) {
2377
+ if (!notifiedRef.current.has(tourId) && meta.notification && !dismissed.includes(tourId) && !completed[tourId]) {
2306
2378
  notifiedRef.current.add(tourId);
2307
2379
  (_c = stateNs == null ? void 0 : stateNs.set) == null ? void 0 : _c.call(stateNs, "notified", [...notifiedRef.current]);
2308
- const cleanup = showWorkflowToast(containerRef.current, meta.notification);
2380
+ const cleanup = showWorkflowToast(meta.notification);
2309
2381
  toastCleanupsRef.current.push(cleanup);
2310
2382
  }
2311
2383
  }
@@ -2338,8 +2410,8 @@ function WorkflowWidgetInner({ runtime: runtime3 }) {
2338
2410
  notifiedRef.current.add(tourId);
2339
2411
  (_c = stateNs == null ? void 0 : stateNs.set) == null ? void 0 : _c.call(stateNs, "notified", [...notifiedRef.current]);
2340
2412
  const workflow = currentWorkflows.get(tourId);
2341
- if ((workflow == null ? void 0 : workflow.meta.notification) && containerRef.current) {
2342
- const cleanup = showWorkflowToast(containerRef.current, workflow.meta.notification);
2413
+ if (workflow == null ? void 0 : workflow.meta.notification) {
2414
+ const cleanup = showWorkflowToast(workflow.meta.notification);
2343
2415
  toastCleanupsRef.current.push(cleanup);
2344
2416
  }
2345
2417
  }
@@ -2443,6 +2515,10 @@ var executeHighlight = async (action, context) => {
2443
2515
  return { cleanup: () => {
2444
2516
  } };
2445
2517
  }
2518
+ if (anchorEl.getAttribute("data-syntro-highlight-dismissed")) {
2519
+ return { cleanup: () => {
2520
+ } };
2521
+ }
2446
2522
  const existing = anchorEl.getAttribute("data-syntro-highlight");
2447
2523
  if (existing) {
2448
2524
  const prev = context.overlayRoot.querySelectorAll(".syntro-spotlight-scrim, .syntro-spotlight-ring");
@@ -2465,7 +2541,10 @@ var executeHighlight = async (action, context) => {
2465
2541
  ringColor,
2466
2542
  blocking: (_i = action.blocking) != null ? _i : false,
2467
2543
  onClickOutside: (_j = action.onClickOutside) != null ? _j : true,
2468
- onEsc: (_k = action.onEsc) != null ? _k : true
2544
+ onEsc: (_k = action.onEsc) != null ? _k : true,
2545
+ onDismiss: () => {
2546
+ anchorEl.setAttribute("data-syntro-highlight-dismissed", "true");
2547
+ }
2469
2548
  });
2470
2549
  context.publishEvent("action.applied", {
2471
2550
  id: context.generateId(),
@@ -2476,6 +2555,7 @@ var executeHighlight = async (action, context) => {
2476
2555
  cleanup: () => {
2477
2556
  handle.destroy();
2478
2557
  anchorEl.removeAttribute("data-syntro-highlight");
2558
+ anchorEl.removeAttribute("data-syntro-highlight-dismissed");
2479
2559
  }
2480
2560
  };
2481
2561
  };
@@ -2565,6 +2645,8 @@ var executePulse = async (action, context) => {
2565
2645
  return {
2566
2646
  cleanup: () => {
2567
2647
  clearTimeout(timeoutId);
2648
+ if (!anchorEl.isConnected)
2649
+ return;
2568
2650
  anchorEl.style.animation = originalAnimation;
2569
2651
  anchorEl.removeAttribute("data-syntro-pulse");
2570
2652
  }
@@ -2634,7 +2716,12 @@ var executeBadge = async (action, context) => {
2634
2716
  });
2635
2717
  return {
2636
2718
  cleanup: () => {
2637
- badge2.remove();
2719
+ try {
2720
+ badge2.remove();
2721
+ } catch {
2722
+ }
2723
+ if (!anchorEl.isConnected)
2724
+ return;
2638
2725
  if (originalPosition !== void 0) {
2639
2726
  anchorEl.style.position = originalPosition;
2640
2727
  }
@@ -2729,7 +2816,7 @@ var executors2 = [
2729
2816
  { kind: "overlays:badge", executor: executeBadge },
2730
2817
  { kind: "overlays:tooltip", executor: executeTooltip },
2731
2818
  { kind: "overlays:modal", executor: executeModal },
2732
- { kind: "core:tour", executor: executeTour },
2819
+ { kind: "overlays:tour", executor: executeTour },
2733
2820
  { kind: "overlays:celebrate", executor: executeCelebrate }
2734
2821
  ];
2735
2822
  var runtime2 = {
@@ -2849,7 +2936,15 @@ var CORE_ACTION_KINDS = /* @__PURE__ */ new Set([
2849
2936
  "core:wait",
2850
2937
  "core:sequence",
2851
2938
  "core:parallel",
2852
- "core:tour"
2939
+ "overlays:tour"
2940
+ ]);
2941
+ var BUNDLED_APP_IDS = /* @__PURE__ */ new Set([
2942
+ "adaptive-chatbot",
2943
+ "adaptive-content",
2944
+ "adaptive-faq",
2945
+ "adaptive-gamification",
2946
+ "adaptive-nav",
2947
+ "adaptive-overlays"
2853
2948
  ]);
2854
2949
  var NAMESPACE_TO_APP_ID = {
2855
2950
  faq: "adaptive-faq",
@@ -3003,6 +3098,9 @@ function createAppLoader(options) {
3003
3098
  }
3004
3099
  }
3005
3100
  }
3101
+ for (const bundled of BUNDLED_APP_IDS) {
3102
+ appIds.delete(bundled);
3103
+ }
3006
3104
  return Array.from(appIds);
3007
3105
  }
3008
3106
  async function loadAppsForConfig(config) {
@@ -3358,14 +3456,20 @@ function getAntiFlickerSnippet(config = {}) {
3358
3456
  }
3359
3457
 
3360
3458
  // src/version.ts
3361
- var SDK_VERSION = "2.8.0-canary.8";
3459
+ var SDK_VERSION = "2.8.0-canary.80";
3362
3460
 
3363
3461
  // src/types.ts
3364
3462
  var SDK_SCHEMA_VERSION = "2.0";
3365
3463
 
3366
3464
  // src/fetchers/mergeConfigs.ts
3367
- function resolveVariantConfigs(client, keys, _strategy = "first-match") {
3368
- var _a2, _b, _c, _d;
3465
+ function resolveVariantConfigs(client, keys, strategy = "merge") {
3466
+ if (strategy === "first-match") {
3467
+ return resolveFirstMatch(client, keys);
3468
+ }
3469
+ return resolveMerge(client, keys);
3470
+ }
3471
+ function resolveFirstMatch(client, keys) {
3472
+ var _a2;
3369
3473
  for (const key of keys) {
3370
3474
  const value = (_a2 = client.getFeatureValue) == null ? void 0 : _a2.call(client, key, null);
3371
3475
  if (!value || typeof value !== "object") continue;
@@ -3373,20 +3477,60 @@ function resolveVariantConfigs(client, keys, _strategy = "first-match") {
3373
3477
  const hasTiles = variant.tiles && variant.tiles.length > 0;
3374
3478
  const hasActions = variant.actions && variant.actions.length > 0;
3375
3479
  if (hasTiles || hasActions) {
3376
- return {
3377
- tiles: (_b = variant.tiles) != null ? _b : [],
3378
- actions: (_c = variant.actions) != null ? _c : [],
3379
- fetchedAt: (_d = variant.fetchedAt) != null ? _d : (/* @__PURE__ */ new Date()).toISOString(),
3380
- ...variant.schemaVersion && { schemaVersion: variant.schemaVersion },
3381
- ...variant.configVersion && { configVersion: variant.configVersion },
3382
- ...variant.canvasTitle && { canvasTitle: variant.canvasTitle },
3383
- ...variant.theme && { theme: variant.theme },
3384
- ...variant.launcher && { launcher: variant.launcher }
3385
- };
3480
+ return buildConfig([variant]);
3386
3481
  }
3387
3482
  }
3388
3483
  return null;
3389
3484
  }
3485
+ function resolveMerge(client, keys) {
3486
+ var _a2;
3487
+ const variants = [];
3488
+ for (const key of keys) {
3489
+ const value = (_a2 = client.getFeatureValue) == null ? void 0 : _a2.call(client, key, null);
3490
+ if (!value || typeof value !== "object") continue;
3491
+ const variant = value;
3492
+ const hasTiles = variant.tiles && variant.tiles.length > 0;
3493
+ const hasActions = variant.actions && variant.actions.length > 0;
3494
+ if (hasTiles || hasActions) {
3495
+ variants.push(variant);
3496
+ }
3497
+ }
3498
+ if (variants.length === 0) return null;
3499
+ return buildConfig(variants);
3500
+ }
3501
+ function buildConfig(variants) {
3502
+ const allTiles = [];
3503
+ const allActions = [];
3504
+ let fetchedAt;
3505
+ let schemaVersion;
3506
+ let configVersion;
3507
+ let canvasTitle;
3508
+ let theme;
3509
+ let launcher;
3510
+ let meta;
3511
+ for (const variant of variants) {
3512
+ if (variant.tiles) allTiles.push(...variant.tiles);
3513
+ if (variant.actions) allActions.push(...variant.actions);
3514
+ fetchedAt != null ? fetchedAt : fetchedAt = variant.fetchedAt;
3515
+ schemaVersion != null ? schemaVersion : schemaVersion = variant.schemaVersion;
3516
+ configVersion != null ? configVersion : configVersion = variant.configVersion;
3517
+ canvasTitle != null ? canvasTitle : canvasTitle = variant.canvasTitle;
3518
+ theme != null ? theme : theme = variant.theme;
3519
+ launcher != null ? launcher : launcher = variant.launcher;
3520
+ meta != null ? meta : meta = variant.meta;
3521
+ }
3522
+ return {
3523
+ tiles: allTiles,
3524
+ actions: allActions,
3525
+ fetchedAt: fetchedAt != null ? fetchedAt : (/* @__PURE__ */ new Date()).toISOString(),
3526
+ ...schemaVersion && { schemaVersion },
3527
+ ...configVersion && { configVersion },
3528
+ ...canvasTitle && { canvasTitle },
3529
+ ...theme && { theme },
3530
+ ...launcher && { launcher },
3531
+ ...meta && { meta }
3532
+ };
3533
+ }
3390
3534
 
3391
3535
  // src/logger.ts
3392
3536
  var debugEnabled = false;
@@ -3509,12 +3653,14 @@ function getCachedConfig(sdkVersion) {
3509
3653
  var resolveConfigUri = ({
3510
3654
  configUri,
3511
3655
  experiments,
3512
- featureKey = "smart-canvas-config-uri"
3656
+ featureKey
3513
3657
  }) => {
3514
3658
  var _a2;
3515
3659
  if (configUri) return configUri;
3516
- const fromFeature = (_a2 = experiments == null ? void 0 : experiments.getFeatureValue) == null ? void 0 : _a2.call(experiments, featureKey, null);
3517
- if (fromFeature) return fromFeature;
3660
+ if (experiments && featureKey) {
3661
+ const fromFeature = (_a2 = experiments.getFeatureValue) == null ? void 0 : _a2.call(experiments, featureKey, null);
3662
+ if (fromFeature) return fromFeature;
3663
+ }
3518
3664
  return void 0;
3519
3665
  };
3520
3666
  function getVariantFlagKeys(experiments, manifestKey, variantFlagPrefix) {
@@ -3540,7 +3686,7 @@ var createCanvasConfigFetcher = ({
3540
3686
  experiments,
3541
3687
  featureKey,
3542
3688
  credentials,
3543
- configFeatureKey = "smart-canvas-config",
3689
+ configFeatureKey,
3544
3690
  manifestKey,
3545
3691
  variantFlagPrefix,
3546
3692
  sdkVersion
@@ -3641,7 +3787,8 @@ function registerFromTriggerWhen(triggerWhen, accumulator) {
3641
3787
  if (cond.type === "event_count" && cond.key) {
3642
3788
  const counter = cond.counter;
3643
3789
  const predicate = counter ? buildPredicate(counter) : () => true;
3644
- accumulator.register(cond.key, predicate);
3790
+ const needsTimestamps = typeof cond.withinMs === "number";
3791
+ accumulator.register(cond.key, predicate, needsTimestamps);
3645
3792
  }
3646
3793
  }
3647
3794
  }
@@ -3734,113 +3881,666 @@ function useShadowRoot() {
3734
3881
  return ctx;
3735
3882
  }
3736
3883
 
3737
- // src/events/types.ts
3884
+ // ../event-processor/dist/types.js
3885
+ var EVENT_SCHEMA_VERSION = "1.0.0";
3738
3886
  var StandardEvents = {
3739
- // UI events (from PostHog autocapture)
3740
3887
  UI_CLICK: "ui.click",
3741
3888
  UI_SCROLL: "ui.scroll",
3742
3889
  UI_INPUT: "ui.input",
3743
3890
  UI_CHANGE: "ui.change",
3744
3891
  UI_SUBMIT: "ui.submit",
3745
- // Navigation events
3746
3892
  NAV_PAGE_VIEW: "nav.page_view",
3747
3893
  NAV_PAGE_LEAVE: "nav.page_leave",
3748
- // Canvas events
3749
- CANVAS_OPENED: "canvas.opened",
3750
- CANVAS_CLOSED: "canvas.closed",
3751
- TILE_VIEWED: "tile.viewed",
3752
- TILE_EXPANDED: "tile.expanded",
3753
- TILE_COLLAPSED: "tile.collapsed",
3754
- TILE_ACTION: "tile.action",
3755
- // Overlay/tour events
3756
- OVERLAY_STARTED: "overlay.started",
3757
- OVERLAY_COMPLETED: "overlay.completed",
3758
- OVERLAY_DISMISSED: "overlay.dismissed",
3759
- OVERLAY_STEP_VIEWED: "overlay.step_viewed",
3760
- // Derived behavioral signals (Phase 3)
3761
- BEHAVIOR_RAGE_CLICK: "behavior.rage_click",
3762
- BEHAVIOR_HESITATION: "behavior.hesitation",
3763
- BEHAVIOR_CONFUSION: "behavior.confusion",
3764
- // Action events
3765
- ACTION_APPLIED: "action.applied",
3766
- ACTION_REVERTED: "action.reverted",
3767
- ACTION_FAILED: "action.failed",
3768
- ACTION_CTA_CLICKED: "action.cta_clicked",
3769
- // Notification events
3770
- NOTIFICATION_SHOWN: "notification.shown",
3771
- NOTIFICATION_CLICKED: "notification.clicked",
3772
- NOTIFICATION_DISMISSED: "notification.dismissed",
3773
- NOTIFICATION_DEEP_LINK: "notification.deep_link",
3774
- // Surface events
3775
- SURFACE_MOUNTED: "surface.mounted",
3776
- SURFACE_UNMOUNTED: "surface.unmounted"
3894
+ UI_HESITATE: "ui.hesitate",
3895
+ UI_RAGE_CLICK: "ui.rage_click",
3896
+ UI_SCROLL_THRASH: "ui.scroll_thrash",
3897
+ UI_FOCUS_BOUNCE: "ui.focus_bounce",
3898
+ UI_IDLE: "ui.idle",
3899
+ UI_HOVER: "ui.hover"
3900
+ };
3901
+ var RRWebSource = {
3902
+ Mutation: 0,
3903
+ MouseMove: 1,
3904
+ MouseInteraction: 2,
3905
+ Scroll: 3,
3906
+ ViewportResize: 4,
3907
+ Input: 5,
3908
+ TouchMove: 6,
3909
+ MediaInteraction: 7,
3910
+ Drag: 12
3911
+ };
3912
+ var RRWebMouseInteraction = {
3913
+ MouseUp: 0,
3914
+ MouseDown: 1,
3915
+ Click: 2,
3916
+ ContextMenu: 3,
3917
+ DblClick: 4,
3918
+ Focus: 5,
3919
+ Blur: 6,
3920
+ TouchStart: 7,
3921
+ TouchEnd: 9
3922
+ };
3923
+ var DEFAULT_DETECTOR_CONFIG = {
3924
+ hesitationMs: 3e3,
3925
+ hesitationRadiusPx: 10,
3926
+ rageClickCount: 3,
3927
+ rageClickWindowMs: 1e3,
3928
+ rageClickRadiusPx: 30,
3929
+ scrollThrashReversals: 3,
3930
+ scrollThrashWindowMs: 2e3,
3931
+ focusBounceMaxInputs: 0,
3932
+ idleMs: 5e3,
3933
+ hoverSampleMs: 100
3777
3934
  };
3778
- var EVENT_SCHEMA_VERSION = "1.0.0";
3779
3935
 
3780
- // src/events/normalizers/canvas.ts
3781
- function createCanvasEvent(name, props) {
3782
- return {
3783
- ts: Date.now(),
3784
- name,
3785
- source: "canvas",
3786
- props,
3787
- schemaVersion: EVENT_SCHEMA_VERSION
3788
- };
3789
- }
3790
- function canvasOpened(surface) {
3791
- return createCanvasEvent(StandardEvents.CANVAS_OPENED, { surface });
3792
- }
3793
- function canvasClosed(surface) {
3794
- return createCanvasEvent(StandardEvents.CANVAS_CLOSED, { surface });
3795
- }
3796
- function tileViewed(tileId, surface) {
3797
- return createCanvasEvent(StandardEvents.TILE_VIEWED, { tileId, surface });
3798
- }
3799
- function tileExpanded(tileId, surface) {
3800
- return createCanvasEvent(StandardEvents.TILE_EXPANDED, { tileId, surface });
3801
- }
3802
- function tileCollapsed(tileId, surface) {
3803
- return createCanvasEvent(StandardEvents.TILE_COLLAPSED, { tileId, surface });
3936
+ // ../event-processor/dist/normalizers/posthog.js
3937
+ var POSTHOG_EVENT_MAP = {
3938
+ // NOTE: $autocapture is intentionally NOT in this map.
3939
+ // It's handled below in getEventName() with $event_type refinement
3940
+ // so that change/submit events aren't all mapped to ui.click.
3941
+ $click: StandardEvents.UI_CLICK,
3942
+ $scroll: StandardEvents.UI_SCROLL,
3943
+ $input: StandardEvents.UI_INPUT,
3944
+ $change: StandardEvents.UI_CHANGE,
3945
+ $submit: StandardEvents.UI_SUBMIT,
3946
+ // Navigation events
3947
+ $pageview: StandardEvents.NAV_PAGE_VIEW,
3948
+ $pageleave: StandardEvents.NAV_PAGE_LEAVE,
3949
+ // Session events
3950
+ $session_start: "session.start",
3951
+ // Identify events
3952
+ $identify: "user.identify"
3953
+ };
3954
+ function getEventName(phEvent) {
3955
+ var _a2, _b;
3956
+ const eventName = phEvent.event;
3957
+ if (typeof eventName !== "string") {
3958
+ return "posthog.unknown";
3959
+ }
3960
+ if (POSTHOG_EVENT_MAP[eventName]) {
3961
+ return POSTHOG_EVENT_MAP[eventName];
3962
+ }
3963
+ if (eventName === "$autocapture") {
3964
+ const tagName = (_a2 = phEvent.properties) == null ? void 0 : _a2.$tag_name;
3965
+ const eventType = (_b = phEvent.properties) == null ? void 0 : _b.$event_type;
3966
+ if (eventType === "submit")
3967
+ return StandardEvents.UI_SUBMIT;
3968
+ if (eventType === "change")
3969
+ return StandardEvents.UI_CHANGE;
3970
+ if (tagName === "input" || tagName === "textarea")
3971
+ return StandardEvents.UI_INPUT;
3972
+ return StandardEvents.UI_CLICK;
3973
+ }
3974
+ if (!eventName.startsWith("$")) {
3975
+ return `posthog.${eventName}`;
3976
+ }
3977
+ return eventName.replace("$", "posthog.");
3804
3978
  }
3805
- function tileAction(tileId, actionId, surface) {
3806
- return createCanvasEvent(StandardEvents.TILE_ACTION, {
3807
- tileId,
3808
- actionId,
3809
- surface
3979
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["a", "button", "input", "select", "textarea"]);
3980
+ function parseElementsChain(chain) {
3981
+ if (!chain)
3982
+ return void 0;
3983
+ return chain.split(";").map((segment) => {
3984
+ const el = {};
3985
+ const colonIdx = segment.indexOf(":");
3986
+ const tagPart = colonIdx >= 0 ? segment.slice(0, colonIdx) : segment;
3987
+ const attrPart = colonIdx >= 0 ? segment.slice(colonIdx + 1) : "";
3988
+ const dotIdx = tagPart.indexOf(".");
3989
+ if (dotIdx >= 0) {
3990
+ el.tag_name = tagPart.slice(0, dotIdx);
3991
+ el.attr__class = tagPart.slice(dotIdx + 1).replace(/\./g, " ");
3992
+ } else {
3993
+ el.tag_name = tagPart;
3994
+ }
3995
+ const attrRegex = /([\w$]+)="([^"]*)"/g;
3996
+ let match;
3997
+ while ((match = attrRegex.exec(attrPart)) !== null) {
3998
+ const [, key, value] = match;
3999
+ if (key === "nth-child" || key === "nth-of-type")
4000
+ continue;
4001
+ el[key] = value;
4002
+ }
4003
+ if (el.text) {
4004
+ el.$el_text = el.text;
4005
+ delete el.text;
4006
+ }
4007
+ return el;
3810
4008
  });
3811
4009
  }
3812
- function overlayStarted(recipeId, recipeName) {
3813
- return createCanvasEvent(StandardEvents.OVERLAY_STARTED, {
3814
- recipeId,
3815
- recipeName
3816
- });
4010
+ function resolveInteractiveTag(elements, directTag) {
4011
+ if (directTag && INTERACTIVE_TAGS.has(directTag))
4012
+ return directTag;
4013
+ if (!elements)
4014
+ return directTag;
4015
+ for (const el of elements) {
4016
+ const tag2 = el.tag_name;
4017
+ if (tag2 && INTERACTIVE_TAGS.has(tag2))
4018
+ return tag2;
4019
+ }
4020
+ return directTag;
3817
4021
  }
3818
- function overlayCompleted(recipeId, recipeName) {
3819
- return createCanvasEvent(StandardEvents.OVERLAY_COMPLETED, {
3820
- recipeId,
3821
- recipeName
3822
- });
4022
+ function extractProps(phEvent) {
4023
+ var _a2, _b, _c;
4024
+ const props = {};
4025
+ const phProps = phEvent.properties || {};
4026
+ const elements = (_a2 = phProps.$elements) != null ? _a2 : typeof phProps.$elements_chain === "string" ? parseElementsChain(phProps.$elements_chain) : void 0;
4027
+ const directTag = (_c = phProps.$tag_name) != null ? _c : (_b = elements == null ? void 0 : elements[0]) == null ? void 0 : _b.tag_name;
4028
+ const isClickEvent = phEvent.event === "$autocapture" || phEvent.event === "$click";
4029
+ props.tagName = isClickEvent ? resolveInteractiveTag(elements, directTag) : directTag;
4030
+ if (phProps.$el_text)
4031
+ props.elementText = phProps.$el_text;
4032
+ if (elements)
4033
+ props.elements = elements;
4034
+ if (isClickEvent && !elements) {
4035
+ console.warn(`[PostHogNormalizer] $autocapture click has no element chain. PostHog may have changed wire format. Properties: $elements=${!!phProps.$elements}, $elements_chain=${typeof phProps.$elements_chain}`);
4036
+ }
4037
+ if (phProps.$current_url)
4038
+ props.url = phProps.$current_url;
4039
+ if (phProps.$pathname)
4040
+ props.pathname = phProps.$pathname;
4041
+ if (phProps.$host)
4042
+ props.host = phProps.$host;
4043
+ if (phProps.$viewport_width)
4044
+ props.viewportWidth = phProps.$viewport_width;
4045
+ if (phProps.$viewport_height)
4046
+ props.viewportHeight = phProps.$viewport_height;
4047
+ if (phProps.$session_id)
4048
+ props.sessionId = phProps.$session_id;
4049
+ if (phProps.$scroll_depth)
4050
+ props.scrollDepth = phProps.$scroll_depth;
4051
+ if (phProps.$scroll_percentage)
4052
+ props.scrollPercentage = phProps.$scroll_percentage;
4053
+ props.originalEvent = phEvent.event;
4054
+ return props;
3823
4055
  }
3824
- function overlayDismissed(recipeId, recipeName, stepIndex) {
3825
- return createCanvasEvent(StandardEvents.OVERLAY_DISMISSED, {
3826
- recipeId,
3827
- recipeName,
3828
- stepIndex
3829
- });
4056
+ function normalizePostHogEvent(phEvent) {
4057
+ let ts;
4058
+ if (typeof phEvent.timestamp === "number") {
4059
+ ts = phEvent.timestamp;
4060
+ } else if (typeof phEvent.timestamp === "string") {
4061
+ ts = new Date(phEvent.timestamp).getTime();
4062
+ } else {
4063
+ ts = Date.now();
4064
+ }
4065
+ return {
4066
+ ts,
4067
+ name: getEventName(phEvent),
4068
+ source: "posthog",
4069
+ props: extractProps(phEvent),
4070
+ schemaVersion: EVENT_SCHEMA_VERSION
4071
+ };
3830
4072
  }
3831
- function overlayStepViewed(recipeId, stepIndex, stepTitle) {
3832
- return createCanvasEvent(StandardEvents.OVERLAY_STEP_VIEWED, {
3833
- recipeId,
3834
- stepIndex,
3835
- stepTitle
3836
- });
4073
+ function shouldNormalizeEvent(phEvent) {
4074
+ const eventName = phEvent.event;
4075
+ if (typeof eventName !== "string")
4076
+ return false;
4077
+ const skipEvents = [
4078
+ "$feature_flag_called",
4079
+ "$feature_flags",
4080
+ "$groups",
4081
+ "$groupidentify",
4082
+ "$set",
4083
+ "$set_once",
4084
+ "$unset",
4085
+ "$create_alias",
4086
+ "$capture_metrics",
4087
+ "$performance_event",
4088
+ "$web_vitals",
4089
+ "$exception",
4090
+ "$dead_click",
4091
+ "$heatmap"
4092
+ ];
4093
+ if (skipEvents.includes(eventName)) {
4094
+ return false;
4095
+ }
4096
+ return true;
3837
4097
  }
3838
- function customCanvasEvent(name, props) {
3839
- const eventName = name.startsWith("canvas.") ? name : `canvas.${name}`;
3840
- return createCanvasEvent(eventName, props);
4098
+ function createPostHogNormalizer(publishFn) {
4099
+ return (eventName, properties) => {
4100
+ if (typeof eventName !== "string")
4101
+ return;
4102
+ const phEvent = {
4103
+ event: eventName,
4104
+ properties,
4105
+ timestamp: Date.now()
4106
+ };
4107
+ if (shouldNormalizeEvent(phEvent)) {
4108
+ const normalizedEvent = normalizePostHogEvent(phEvent);
4109
+ publishFn(normalizedEvent);
4110
+ }
4111
+ };
3841
4112
  }
3842
- var CanvasEvents = {
3843
- canvasOpened,
4113
+
4114
+ // ../event-processor/dist/detectors/focus-bounce.js
4115
+ var FocusBounceDetector = class {
4116
+ constructor(config, emit2) {
4117
+ this.config = config;
4118
+ this.emit = emit2;
4119
+ this.focused = /* @__PURE__ */ new Map();
4120
+ }
4121
+ ingest(raw) {
4122
+ var _a2, _b;
4123
+ if (raw.type !== 3)
4124
+ return;
4125
+ const ts = raw.timestamp;
4126
+ if (raw.data.source === RRWebSource.MouseInteraction) {
4127
+ const id = (_a2 = raw.data.id) != null ? _a2 : 0;
4128
+ if (raw.data.type === RRWebMouseInteraction.Focus) {
4129
+ this.focused.set(id, { id, focusTs: ts, inputCount: 0 });
4130
+ } else if (raw.data.type === RRWebMouseInteraction.Blur) {
4131
+ const entry = this.focused.get(id);
4132
+ if (entry && entry.inputCount <= this.config.focusBounceMaxInputs) {
4133
+ this.emit({
4134
+ ts,
4135
+ name: StandardEvents.UI_FOCUS_BOUNCE,
4136
+ source: "rrweb",
4137
+ schemaVersion: EVENT_SCHEMA_VERSION,
4138
+ props: {
4139
+ elementId: id,
4140
+ duration_ms: ts - entry.focusTs
4141
+ }
4142
+ });
4143
+ }
4144
+ this.focused.delete(id);
4145
+ }
4146
+ } else if (raw.data.source === RRWebSource.Input) {
4147
+ const id = (_b = raw.data.id) != null ? _b : 0;
4148
+ const entry = this.focused.get(id);
4149
+ if (entry) {
4150
+ entry.inputCount++;
4151
+ }
4152
+ }
4153
+ }
4154
+ };
4155
+
4156
+ // ../event-processor/dist/detectors/hesitation.js
4157
+ var HesitationDetector = class {
4158
+ constructor(config, emit2) {
4159
+ this.config = config;
4160
+ this.emit = emit2;
4161
+ this.anchorX = 0;
4162
+ this.anchorY = 0;
4163
+ this.anchorTs = 0;
4164
+ this.emitted = false;
4165
+ this.hasPosition = false;
4166
+ }
4167
+ ingest(raw) {
4168
+ var _a2;
4169
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.MouseMove)
4170
+ return;
4171
+ const positions = raw.data.positions;
4172
+ if (!positions || positions.length === 0)
4173
+ return;
4174
+ const last = positions[positions.length - 1];
4175
+ const ts = raw.timestamp + ((_a2 = last.timeOffset) != null ? _a2 : 0);
4176
+ if (!this.hasPosition) {
4177
+ this.anchorX = last.x;
4178
+ this.anchorY = last.y;
4179
+ this.anchorTs = ts;
4180
+ this.hasPosition = true;
4181
+ this.emitted = false;
4182
+ return;
4183
+ }
4184
+ const dx = last.x - this.anchorX;
4185
+ const dy = last.y - this.anchorY;
4186
+ const dist = Math.sqrt(dx * dx + dy * dy);
4187
+ if (dist > this.config.hesitationRadiusPx) {
4188
+ this.anchorX = last.x;
4189
+ this.anchorY = last.y;
4190
+ this.anchorTs = ts;
4191
+ this.emitted = false;
4192
+ }
4193
+ }
4194
+ tick(now) {
4195
+ if (!this.hasPosition || this.emitted)
4196
+ return;
4197
+ const elapsed = now - this.anchorTs;
4198
+ if (elapsed >= this.config.hesitationMs) {
4199
+ this.emit({
4200
+ ts: now,
4201
+ name: StandardEvents.UI_HESITATE,
4202
+ source: "rrweb",
4203
+ schemaVersion: EVENT_SCHEMA_VERSION,
4204
+ props: {
4205
+ x: this.anchorX,
4206
+ y: this.anchorY,
4207
+ duration_ms: elapsed
4208
+ }
4209
+ });
4210
+ this.emitted = true;
4211
+ }
4212
+ }
4213
+ };
4214
+
4215
+ // ../event-processor/dist/detectors/hover.js
4216
+ var HoverTracker = class {
4217
+ constructor(config, emit2, elementResolver) {
4218
+ this.config = config;
4219
+ this.emit = emit2;
4220
+ this.elementResolver = elementResolver;
4221
+ this.currentElement = null;
4222
+ this.hoverStartTs = null;
4223
+ this.lastX = 0;
4224
+ this.lastY = 0;
4225
+ this.lastSampleTs = -Infinity;
4226
+ }
4227
+ ingest(raw) {
4228
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.MouseMove)
4229
+ return;
4230
+ const positions = raw.data.positions;
4231
+ if (!positions || positions.length === 0)
4232
+ return;
4233
+ const last = positions[positions.length - 1];
4234
+ this.lastX = last.x;
4235
+ this.lastY = last.y;
4236
+ }
4237
+ tick(now) {
4238
+ if (!this.elementResolver)
4239
+ return;
4240
+ if (now - this.lastSampleTs < this.config.hoverSampleMs)
4241
+ return;
4242
+ this.lastSampleTs = now;
4243
+ const newElement = this.elementResolver(this.lastX, this.lastY);
4244
+ const newKey = newElement ? elementKey(newElement) : null;
4245
+ const currentKey = this.currentElement ? elementKey(this.currentElement) : null;
4246
+ if (newKey !== currentKey) {
4247
+ if (this.currentElement && this.hoverStartTs !== null) {
4248
+ this.emit({
4249
+ ts: now,
4250
+ name: StandardEvents.UI_HOVER,
4251
+ source: "rrweb",
4252
+ schemaVersion: EVENT_SCHEMA_VERSION,
4253
+ props: {
4254
+ x: this.lastX,
4255
+ y: this.lastY,
4256
+ duration_ms: now - this.hoverStartTs,
4257
+ element: this.currentElement
4258
+ }
4259
+ });
4260
+ }
4261
+ this.currentElement = newElement;
4262
+ this.hoverStartTs = now;
4263
+ }
4264
+ }
4265
+ };
4266
+ function elementKey(el) {
4267
+ var _a2, _b, _c;
4268
+ return `${(_a2 = el.tag_name) != null ? _a2 : ""}|${(_b = el.attr__id) != null ? _b : ""}|${((_c = el.classes) != null ? _c : []).join(",")}`;
4269
+ }
4270
+
4271
+ // ../event-processor/dist/detectors/idle.js
4272
+ var IdleDetector = class {
4273
+ constructor(config, emit2) {
4274
+ this.config = config;
4275
+ this.emit = emit2;
4276
+ this.lastActivityTs = null;
4277
+ this.emitted = false;
4278
+ }
4279
+ ingest(raw) {
4280
+ if (raw.type !== 3)
4281
+ return;
4282
+ const src = raw.data.source;
4283
+ if (src === RRWebSource.MouseMove || src === RRWebSource.MouseInteraction || src === RRWebSource.Scroll) {
4284
+ this.lastActivityTs = raw.timestamp;
4285
+ this.emitted = false;
4286
+ }
4287
+ }
4288
+ tick(now) {
4289
+ if (this.lastActivityTs === null || this.emitted)
4290
+ return;
4291
+ if (now - this.lastActivityTs >= this.config.idleMs) {
4292
+ this.emit({
4293
+ ts: now,
4294
+ name: StandardEvents.UI_IDLE,
4295
+ source: "rrweb",
4296
+ schemaVersion: EVENT_SCHEMA_VERSION,
4297
+ props: {
4298
+ idle_ms: now - this.lastActivityTs
4299
+ }
4300
+ });
4301
+ this.emitted = true;
4302
+ }
4303
+ }
4304
+ };
4305
+
4306
+ // ../event-processor/dist/detectors/rage-click.js
4307
+ var RageClickDetector = class {
4308
+ constructor(config, emit2) {
4309
+ this.config = config;
4310
+ this.emit = emit2;
4311
+ this.clicks = [];
4312
+ }
4313
+ ingest(raw) {
4314
+ var _a2, _b;
4315
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.MouseInteraction)
4316
+ return;
4317
+ if (raw.data.type !== RRWebMouseInteraction.Click)
4318
+ return;
4319
+ const x = (_a2 = raw.data.x) != null ? _a2 : 0;
4320
+ const y = (_b = raw.data.y) != null ? _b : 0;
4321
+ const ts = raw.timestamp;
4322
+ const cutoff = ts - this.config.rageClickWindowMs;
4323
+ this.clicks = this.clicks.filter((c) => c.ts >= cutoff);
4324
+ this.clicks.push({ ts, x, y });
4325
+ const nearby = this.clicks.filter((c) => {
4326
+ const dx = c.x - x;
4327
+ const dy = c.y - y;
4328
+ return Math.sqrt(dx * dx + dy * dy) <= this.config.rageClickRadiusPx;
4329
+ });
4330
+ if (nearby.length >= this.config.rageClickCount) {
4331
+ this.emit({
4332
+ ts,
4333
+ name: StandardEvents.UI_RAGE_CLICK,
4334
+ source: "rrweb",
4335
+ schemaVersion: EVENT_SCHEMA_VERSION,
4336
+ props: {
4337
+ x,
4338
+ y,
4339
+ clickCount: nearby.length,
4340
+ duration_ms: ts - nearby[0].ts
4341
+ }
4342
+ });
4343
+ this.clicks = [];
4344
+ }
4345
+ }
4346
+ };
4347
+
4348
+ // ../event-processor/dist/detectors/scroll-thrash.js
4349
+ var ScrollThrashDetector = class {
4350
+ constructor(config, emit2) {
4351
+ this.config = config;
4352
+ this.emit = emit2;
4353
+ this.lastY = null;
4354
+ this.lastDirection = null;
4355
+ this.reversals = [];
4356
+ }
4357
+ ingest(raw) {
4358
+ var _a2;
4359
+ if (raw.type !== 3 || raw.data.source !== RRWebSource.Scroll)
4360
+ return;
4361
+ const y = (_a2 = raw.data.y) != null ? _a2 : 0;
4362
+ const ts = raw.timestamp;
4363
+ if (this.lastY !== null) {
4364
+ const direction = y > this.lastY ? "down" : y < this.lastY ? "up" : this.lastDirection;
4365
+ if (direction && direction !== this.lastDirection) {
4366
+ const cutoff = ts - this.config.scrollThrashWindowMs;
4367
+ this.reversals = this.reversals.filter((t) => t > cutoff);
4368
+ this.reversals.push(ts);
4369
+ if (this.reversals.length >= this.config.scrollThrashReversals) {
4370
+ this.emit({
4371
+ ts,
4372
+ name: StandardEvents.UI_SCROLL_THRASH,
4373
+ source: "rrweb",
4374
+ schemaVersion: EVENT_SCHEMA_VERSION,
4375
+ props: {
4376
+ reversals: this.reversals.length,
4377
+ duration_ms: ts - this.reversals[0]
4378
+ }
4379
+ });
4380
+ this.reversals = [];
4381
+ }
4382
+ }
4383
+ this.lastDirection = direction;
4384
+ }
4385
+ this.lastY = y;
4386
+ }
4387
+ };
4388
+
4389
+ // ../event-processor/dist/processor.js
4390
+ function createEventProcessor(options) {
4391
+ const config = { ...DEFAULT_DETECTOR_CONFIG, ...options == null ? void 0 : options.config };
4392
+ const listeners = [];
4393
+ function emit2(event) {
4394
+ for (const cb of listeners)
4395
+ cb(event);
4396
+ }
4397
+ const hesitation = new HesitationDetector(config, emit2);
4398
+ const rageClick = new RageClickDetector(config, emit2);
4399
+ const scrollThrash = new ScrollThrashDetector(config, emit2);
4400
+ const focusBounce = new FocusBounceDetector(config, emit2);
4401
+ const idle = new IdleDetector(config, emit2);
4402
+ const hover = new HoverTracker(config, emit2, options == null ? void 0 : options.elementResolver);
4403
+ const clickAttrBuffer = [];
4404
+ return {
4405
+ ingest(raw) {
4406
+ if (raw.kind === "posthog") {
4407
+ const { kind: _, ...phEvent } = raw;
4408
+ if (shouldNormalizeEvent(phEvent)) {
4409
+ emit2(normalizePostHogEvent(phEvent));
4410
+ }
4411
+ } else if (raw.kind === "rrweb") {
4412
+ hesitation.ingest(raw);
4413
+ rageClick.ingest(raw);
4414
+ scrollThrash.ingest(raw);
4415
+ focusBounce.ingest(raw);
4416
+ idle.ingest(raw);
4417
+ hover.ingest(raw);
4418
+ }
4419
+ },
4420
+ onEvent(callback) {
4421
+ listeners.push(callback);
4422
+ },
4423
+ tick(timestamp) {
4424
+ hesitation.tick(timestamp);
4425
+ idle.tick(timestamp);
4426
+ hover.tick(timestamp);
4427
+ },
4428
+ enrichClickAttributes(timestamp, elements) {
4429
+ clickAttrBuffer.push({ ts: timestamp, elements });
4430
+ const cutoff = timestamp - 500;
4431
+ while (clickAttrBuffer.length > 0 && clickAttrBuffer[0].ts < cutoff) {
4432
+ clickAttrBuffer.shift();
4433
+ }
4434
+ }
4435
+ };
4436
+ }
4437
+
4438
+ // src/events/types.ts
4439
+ var StandardEvents2 = {
4440
+ // UI events (from PostHog autocapture)
4441
+ UI_CLICK: "ui.click",
4442
+ UI_SCROLL: "ui.scroll",
4443
+ UI_INPUT: "ui.input",
4444
+ UI_CHANGE: "ui.change",
4445
+ UI_SUBMIT: "ui.submit",
4446
+ // Navigation events
4447
+ NAV_PAGE_VIEW: "nav.page_view",
4448
+ NAV_PAGE_LEAVE: "nav.page_leave",
4449
+ // Canvas events
4450
+ CANVAS_OPENED: "canvas.opened",
4451
+ CANVAS_CLOSED: "canvas.closed",
4452
+ TILE_VIEWED: "tile.viewed",
4453
+ TILE_EXPANDED: "tile.expanded",
4454
+ TILE_COLLAPSED: "tile.collapsed",
4455
+ TILE_ACTION: "tile.action",
4456
+ // Overlay/tour events
4457
+ OVERLAY_STARTED: "overlay.started",
4458
+ OVERLAY_COMPLETED: "overlay.completed",
4459
+ OVERLAY_DISMISSED: "overlay.dismissed",
4460
+ OVERLAY_STEP_VIEWED: "overlay.step_viewed",
4461
+ // Derived behavioral signals (Phase 3)
4462
+ BEHAVIOR_RAGE_CLICK: "behavior.rage_click",
4463
+ BEHAVIOR_HESITATION: "behavior.hesitation",
4464
+ BEHAVIOR_CONFUSION: "behavior.confusion",
4465
+ // Action events
4466
+ ACTION_APPLIED: "action.applied",
4467
+ ACTION_REVERTED: "action.reverted",
4468
+ ACTION_FAILED: "action.failed",
4469
+ ACTION_CTA_CLICKED: "action.cta_clicked",
4470
+ // Notification events
4471
+ NOTIFICATION_SHOWN: "notification.shown",
4472
+ NOTIFICATION_CLICKED: "notification.clicked",
4473
+ NOTIFICATION_DISMISSED: "notification.dismissed",
4474
+ NOTIFICATION_DEEP_LINK: "notification.deep_link",
4475
+ // Surface events
4476
+ SURFACE_MOUNTED: "surface.mounted",
4477
+ SURFACE_UNMOUNTED: "surface.unmounted"
4478
+ };
4479
+
4480
+ // src/events/normalizers/canvas.ts
4481
+ function createCanvasEvent(name, props) {
4482
+ return {
4483
+ ts: Date.now(),
4484
+ name,
4485
+ source: "canvas",
4486
+ props,
4487
+ schemaVersion: EVENT_SCHEMA_VERSION
4488
+ };
4489
+ }
4490
+ function canvasOpened(surface) {
4491
+ return createCanvasEvent(StandardEvents2.CANVAS_OPENED, { surface });
4492
+ }
4493
+ function canvasClosed(surface) {
4494
+ return createCanvasEvent(StandardEvents2.CANVAS_CLOSED, { surface });
4495
+ }
4496
+ function tileViewed(tileId, surface) {
4497
+ return createCanvasEvent(StandardEvents2.TILE_VIEWED, { tileId, surface });
4498
+ }
4499
+ function tileExpanded(tileId, surface) {
4500
+ return createCanvasEvent(StandardEvents2.TILE_EXPANDED, { tileId, surface });
4501
+ }
4502
+ function tileCollapsed(tileId, surface) {
4503
+ return createCanvasEvent(StandardEvents2.TILE_COLLAPSED, { tileId, surface });
4504
+ }
4505
+ function tileAction(tileId, actionId, surface) {
4506
+ return createCanvasEvent(StandardEvents2.TILE_ACTION, {
4507
+ tileId,
4508
+ actionId,
4509
+ surface
4510
+ });
4511
+ }
4512
+ function overlayStarted(recipeId, recipeName) {
4513
+ return createCanvasEvent(StandardEvents2.OVERLAY_STARTED, {
4514
+ recipeId,
4515
+ recipeName
4516
+ });
4517
+ }
4518
+ function overlayCompleted(recipeId, recipeName) {
4519
+ return createCanvasEvent(StandardEvents2.OVERLAY_COMPLETED, {
4520
+ recipeId,
4521
+ recipeName
4522
+ });
4523
+ }
4524
+ function overlayDismissed(recipeId, recipeName, stepIndex) {
4525
+ return createCanvasEvent(StandardEvents2.OVERLAY_DISMISSED, {
4526
+ recipeId,
4527
+ recipeName,
4528
+ stepIndex
4529
+ });
4530
+ }
4531
+ function overlayStepViewed(recipeId, stepIndex, stepTitle) {
4532
+ return createCanvasEvent(StandardEvents2.OVERLAY_STEP_VIEWED, {
4533
+ recipeId,
4534
+ stepIndex,
4535
+ stepTitle
4536
+ });
4537
+ }
4538
+ function customCanvasEvent(name, props) {
4539
+ const eventName = name.startsWith("canvas.") ? name : `canvas.${name}`;
4540
+ return createCanvasEvent(eventName, props);
4541
+ }
4542
+ var CanvasEvents = {
4543
+ canvasOpened,
3844
4544
  canvasClosed,
3845
4545
  tileViewed,
3846
4546
  tileExpanded,
@@ -3853,8 +4553,65 @@ var CanvasEvents = {
3853
4553
  custom: customCanvasEvent
3854
4554
  };
3855
4555
 
4556
+ // src/components/emojiToIcon.tsx
4557
+ import {
4558
+ AlertTriangle,
4559
+ ArrowRight,
4560
+ Banknote,
4561
+ Bell,
4562
+ BookOpen,
4563
+ CheckCircle,
4564
+ ClipboardList,
4565
+ Compass,
4566
+ FileText,
4567
+ Gamepad2,
4568
+ HelpCircle,
4569
+ Landmark,
4570
+ Layers,
4571
+ Lightbulb,
4572
+ MessageCircle,
4573
+ SkipForward,
4574
+ Sparkles,
4575
+ Timer,
4576
+ Trophy
4577
+ } from "lucide-react";
4578
+ import { jsx as jsx2 } from "react/jsx-runtime";
4579
+ var EMOJI_ICON_MAP = {
4580
+ "\u2753": HelpCircle,
4581
+ "\u{1F9ED}": Compass,
4582
+ "\u{1F4DD}": FileText,
4583
+ "\u{1F3AF}": Layers,
4584
+ "\u{1F3C6}": Trophy,
4585
+ "\u2728": Sparkles,
4586
+ "\u{1F4AC}": MessageCircle,
4587
+ "\u{1F3AE}": Gamepad2,
4588
+ "\u{1F4A1}": Lightbulb,
4589
+ "\u{1F4B0}": Banknote,
4590
+ "\u{1F4CB}": ClipboardList,
4591
+ "\u2705": CheckCircle,
4592
+ "\u26A0\uFE0F": AlertTriangle,
4593
+ "\u{1F4B5}": Banknote,
4594
+ "\u{1F3DB}\uFE0F": Landmark,
4595
+ "\u23ED\uFE0F": SkipForward,
4596
+ "\u27A1\uFE0F": ArrowRight,
4597
+ "\u23F1\uFE0F": Timer,
4598
+ "\u{1F4D6}": BookOpen,
4599
+ "\u{1F514}": Bell
4600
+ };
4601
+ function EmojiIcon({
4602
+ emoji,
4603
+ size = 14,
4604
+ color = "currentColor"
4605
+ }) {
4606
+ const Icon = EMOJI_ICON_MAP[emoji];
4607
+ if (!Icon) {
4608
+ return /* @__PURE__ */ jsx2("span", { children: emoji });
4609
+ }
4610
+ return /* @__PURE__ */ jsx2(Icon, { size, color });
4611
+ }
4612
+
3856
4613
  // src/notifications/NotificationToastStack.tsx
3857
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
4614
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
3858
4615
  var TOAST_STYLES_ID = "syntro-toast-styles";
3859
4616
  var TOAST_CSS = `
3860
4617
  @keyframes syntro-toast-slide-in {
@@ -3891,7 +4648,7 @@ function NotificationToastStack({
3891
4648
  const { shadowRoot } = useShadowRoot();
3892
4649
  ensureToastStyles(shadowRoot);
3893
4650
  if (notifications.length === 0) return null;
3894
- return /* @__PURE__ */ jsx2(
4651
+ return /* @__PURE__ */ jsx3(
3895
4652
  "div",
3896
4653
  {
3897
4654
  "data-testid": "notification-toast-stack",
@@ -3946,7 +4703,7 @@ function NotificationToastStack({
3946
4703
  padding: "10px 12px"
3947
4704
  },
3948
4705
  children: [
3949
- /* @__PURE__ */ jsx2(
4706
+ /* @__PURE__ */ jsx3(
3950
4707
  "div",
3951
4708
  {
3952
4709
  style: {
@@ -3960,11 +4717,11 @@ function NotificationToastStack({
3960
4717
  flexShrink: 0,
3961
4718
  fontSize: "14px"
3962
4719
  },
3963
- children: (_a2 = notif.icon) != null ? _a2 : "\u{1F514}"
4720
+ children: /* @__PURE__ */ jsx3(EmojiIcon, { emoji: (_a2 = notif.icon) != null ? _a2 : "\u{1F514}", size: 14 })
3964
4721
  }
3965
4722
  ),
3966
4723
  /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
3967
- /* @__PURE__ */ jsx2(
4724
+ /* @__PURE__ */ jsx3(
3968
4725
  "div",
3969
4726
  {
3970
4727
  style: {
@@ -3979,7 +4736,7 @@ function NotificationToastStack({
3979
4736
  children: notif.title
3980
4737
  }
3981
4738
  ),
3982
- notif.body && /* @__PURE__ */ jsx2(
4739
+ notif.body && /* @__PURE__ */ jsx3(
3983
4740
  "div",
3984
4741
  {
3985
4742
  style: {
@@ -3995,7 +4752,7 @@ function NotificationToastStack({
3995
4752
  }
3996
4753
  )
3997
4754
  ] }),
3998
- /* @__PURE__ */ jsx2(
4755
+ /* @__PURE__ */ jsx3(
3999
4756
  "button",
4000
4757
  {
4001
4758
  type: "button",
@@ -4020,7 +4777,7 @@ function NotificationToastStack({
4020
4777
  ]
4021
4778
  }
4022
4779
  ),
4023
- /* @__PURE__ */ jsx2("div", { style: { height: "2px", background: "rgba(0, 0, 0, 0.08)" }, children: /* @__PURE__ */ jsx2(
4780
+ /* @__PURE__ */ jsx3("div", { style: { height: "2px", background: "rgba(0, 0, 0, 0.08)" }, children: /* @__PURE__ */ jsx3(
4024
4781
  "div",
4025
4782
  {
4026
4783
  className: "syntro-toast-progress",
@@ -4098,7 +4855,7 @@ function useNotifications(eventBus, tiles) {
4098
4855
  const timerIds = useRef2(/* @__PURE__ */ new Map());
4099
4856
  const publishDismissed = useCallback2(
4100
4857
  (notif) => {
4101
- eventBus == null ? void 0 : eventBus.publish(StandardEvents.NOTIFICATION_DISMISSED, {
4858
+ eventBus == null ? void 0 : eventBus.publish(StandardEvents2.NOTIFICATION_DISMISSED, {
4102
4859
  notificationId: notif.id,
4103
4860
  tileId: notif.tileId,
4104
4861
  itemId: notif.itemId
@@ -4163,7 +4920,7 @@ function useNotifications(eventBus, tiles) {
4163
4920
  }
4164
4921
  return next;
4165
4922
  });
4166
- eventBus.publish(StandardEvents.NOTIFICATION_SHOWN, {
4923
+ eventBus.publish(StandardEvents2.NOTIFICATION_SHOWN, {
4167
4924
  notificationId: matched.id,
4168
4925
  tileId: matched.tileId,
4169
4926
  itemId: matched.itemId,
@@ -4234,7 +4991,7 @@ function useNotifyWatcher(runtime3, tiles, appRegistry2) {
4234
4991
 
4235
4992
  // src/RuntimeProvider.tsx
4236
4993
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect4, useMemo as useMemo2, useState as useState3 } from "react";
4237
- import { jsx as jsx3 } from "react/jsx-runtime";
4994
+ import { jsx as jsx4 } from "react/jsx-runtime";
4238
4995
  var RuntimeReactContext = createContext2({
4239
4996
  runtime: null,
4240
4997
  context: null
@@ -4252,7 +5009,7 @@ function RuntimeProvider({ runtime: runtime3, children }) {
4252
5009
  return unsubscribe;
4253
5010
  }, [runtime3]);
4254
5011
  const value = useMemo2(() => ({ runtime: runtime3, context }), [runtime3, context]);
4255
- return /* @__PURE__ */ jsx3(RuntimeReactContext.Provider, { value, children });
5012
+ return /* @__PURE__ */ jsx4(RuntimeReactContext.Provider, { value, children });
4256
5013
  }
4257
5014
  function useRuntime() {
4258
5015
  const { runtime: runtime3 } = useContext2(RuntimeReactContext);
@@ -4326,41 +5083,17 @@ import {
4326
5083
  } from "react";
4327
5084
 
4328
5085
  // src/components/TileIcon.tsx
4329
- import {
4330
- Compass,
4331
- FileText,
4332
- Gamepad2,
4333
- HelpCircle,
4334
- Layers,
4335
- MessageCircle,
4336
- Sparkles,
4337
- Trophy
4338
- } from "lucide-react";
4339
- import { jsx as jsx4 } from "react/jsx-runtime";
4340
- var ICON_MAP = {
4341
- "\u2753": HelpCircle,
4342
- "\u{1F9ED}": Compass,
4343
- "\u{1F4DD}": FileText,
4344
- "\u{1F3AF}": Layers,
4345
- "\u{1F3C6}": Trophy,
4346
- "\u2728": Sparkles,
4347
- "\u{1F4AC}": MessageCircle,
4348
- "\u{1F3AE}": Gamepad2
4349
- };
5086
+ import { jsx as jsx5 } from "react/jsx-runtime";
4350
5087
  function TileIcon({
4351
5088
  emoji,
4352
5089
  size = 18,
4353
5090
  color = "currentColor"
4354
5091
  }) {
4355
- const Icon = ICON_MAP[emoji];
4356
- if (!Icon) {
4357
- return /* @__PURE__ */ jsx4("span", { children: emoji });
4358
- }
4359
- return /* @__PURE__ */ jsx4(Icon, { size, color });
5092
+ return /* @__PURE__ */ jsx5(EmojiIcon, { emoji, size, color });
4360
5093
  }
4361
5094
 
4362
5095
  // src/components/TileCard.tsx
4363
- import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
5096
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
4364
5097
  function WidgetMount({ widgetId, props }) {
4365
5098
  var _a2;
4366
5099
  const runtime3 = useRuntime();
@@ -4390,7 +5123,6 @@ function WidgetMount({ widgetId, props }) {
4390
5123
  return () => {
4391
5124
  handle.unmount();
4392
5125
  handleRef.current = null;
4393
- container.remove();
4394
5126
  };
4395
5127
  }, [registry, widgetId, widgetAvailable]);
4396
5128
  const propsJson = JSON.stringify(props);
@@ -4401,7 +5133,7 @@ function WidgetMount({ widgetId, props }) {
4401
5133
  prevPropsJsonRef.current = propsJson;
4402
5134
  (_a3 = handleRef.current) == null ? void 0 : _a3.update(propsRef.current);
4403
5135
  }, [propsJson]);
4404
- if (!registry || !registry.has(widgetId)) {
5136
+ if (!(registry == null ? void 0 : registry.has(widgetId))) {
4405
5137
  return /* @__PURE__ */ jsxs2(
4406
5138
  "div",
4407
5139
  {
@@ -4418,8 +5150,21 @@ function WidgetMount({ widgetId, props }) {
4418
5150
  }
4419
5151
  );
4420
5152
  }
4421
- return /* @__PURE__ */ jsx5("div", { ref: parentRef });
4422
- }
5153
+ return /* @__PURE__ */ jsx6("div", { ref: parentRef });
5154
+ }
5155
+ var INTERACTION_PATTERNS = [
5156
+ ":toggled",
5157
+ ":clicked",
5158
+ ":feedback",
5159
+ ":navigate",
5160
+ ":expanded",
5161
+ ":collapsed",
5162
+ ":dismissed",
5163
+ ":submitted",
5164
+ ":interacted",
5165
+ ":tip_clicked",
5166
+ ":tip_focused"
5167
+ ];
4423
5168
  function TileCard({
4424
5169
  config,
4425
5170
  surface: _surface,
@@ -4428,10 +5173,42 @@ function TileCard({
4428
5173
  }) {
4429
5174
  const { title, subtitle, widget, props, icon } = config;
4430
5175
  const [, setTick] = useState4(0);
5176
+ const articleRef = useRef4(null);
4431
5177
  const runtime3 = useRuntime();
4432
5178
  useEffect5(() => {
4433
5179
  if (runtime3) setTick((t) => t + 1);
4434
5180
  }, [runtime3]);
5181
+ useEffect5(() => {
5182
+ var _a2;
5183
+ const tracker = typeof window !== "undefined" ? (_a2 = window.SynOS) == null ? void 0 : _a2.interventionTracker : null;
5184
+ if (!articleRef.current || !tracker) return;
5185
+ const observer = new IntersectionObserver(
5186
+ ([entry]) => {
5187
+ var _a3;
5188
+ if (entry.isIntersecting) {
5189
+ tracker.trackSeen(config.id, (_a3 = config.widget) != null ? _a3 : "unknown");
5190
+ observer.disconnect();
5191
+ }
5192
+ },
5193
+ { threshold: 0.5 }
5194
+ );
5195
+ observer.observe(articleRef.current);
5196
+ return () => observer.disconnect();
5197
+ }, [config.id, config.widget]);
5198
+ useEffect5(() => {
5199
+ var _a2;
5200
+ const tracker = typeof window !== "undefined" ? (_a2 = window.SynOS) == null ? void 0 : _a2.interventionTracker : null;
5201
+ if (!(runtime3 == null ? void 0 : runtime3.events) || !tracker) return;
5202
+ return runtime3.events.subscribe((event) => {
5203
+ var _a3, _b;
5204
+ if (!INTERACTION_PATTERNS.some((p) => {
5205
+ var _a4;
5206
+ return (_a4 = event.name) == null ? void 0 : _a4.includes(p);
5207
+ })) return;
5208
+ if (((_a3 = event.props) == null ? void 0 : _a3.instanceId) !== config.id) return;
5209
+ tracker.trackInteracted(config.id, (_b = config.widget) != null ? _b : "unknown", event.name);
5210
+ });
5211
+ }, [runtime3 == null ? void 0 : runtime3.events, config.id, config.widget]);
4435
5212
  const registration = useMemo3(
4436
5213
  () => {
4437
5214
  var _a2, _b;
@@ -4485,15 +5262,16 @@ function TileCard({
4485
5262
  return /* @__PURE__ */ jsxs2(
4486
5263
  "article",
4487
5264
  {
5265
+ ref: articleRef,
4488
5266
  "data-shadow-canvas-id": `tile-${config.id}`,
4489
5267
  style: cardStyle,
4490
5268
  onMouseEnter,
4491
5269
  onMouseLeave,
4492
5270
  children: [
4493
5271
  /* @__PURE__ */ jsxs2("div", { style: headerStyle, children: [
4494
- /* @__PURE__ */ jsx5("div", { style: iconStyle, children: /* @__PURE__ */ jsx5(TileIcon, { emoji: resolvedIcon, size: resolvedSubtitle ? 36 : 24 }) }),
5272
+ /* @__PURE__ */ jsx6("div", { style: iconStyle, children: /* @__PURE__ */ jsx6(TileIcon, { emoji: resolvedIcon, size: resolvedSubtitle ? 36 : 24 }) }),
4495
5273
  /* @__PURE__ */ jsxs2("div", { style: { flex: 1, minWidth: 0 }, children: [
4496
- /* @__PURE__ */ jsx5(
5274
+ /* @__PURE__ */ jsx6(
4497
5275
  "h3",
4498
5276
  {
4499
5277
  style: {
@@ -4508,7 +5286,7 @@ function TileCard({
4508
5286
  children: title != null ? title : widget
4509
5287
  }
4510
5288
  ),
4511
- resolvedSubtitle && /* @__PURE__ */ jsx5(
5289
+ resolvedSubtitle && /* @__PURE__ */ jsx6(
4512
5290
  "p",
4513
5291
  {
4514
5292
  style: {
@@ -4525,14 +5303,14 @@ function TileCard({
4525
5303
  )
4526
5304
  ] })
4527
5305
  ] }),
4528
- /* @__PURE__ */ jsx5(
5306
+ /* @__PURE__ */ jsx6(
4529
5307
  "div",
4530
5308
  {
4531
5309
  style: {
4532
5310
  padding: "var(--sc-tile-body-padding, 0 0.75rem 0.5rem)",
4533
5311
  borderTop: "1px solid rgba(255, 255, 255, 0.06)"
4534
5312
  },
4535
- children: /* @__PURE__ */ jsx5("div", { style: { paddingTop: "var(--sc-tile-gap, 0.25rem)" }, children: /* @__PURE__ */ jsx5(WidgetMount, { widgetId: widget, props: { ...props, instanceId: config.id } }) })
5313
+ children: /* @__PURE__ */ jsx6("div", { style: { paddingTop: "var(--sc-tile-gap, 0.25rem)" }, children: /* @__PURE__ */ jsx6(WidgetMount, { widgetId: widget, props: { ...props, instanceId: config.id } }) })
4536
5314
  }
4537
5315
  )
4538
5316
  ]
@@ -4940,7 +5718,7 @@ function flattenThemeConfig(config) {
4940
5718
 
4941
5719
  // src/theme/ThemeProvider.tsx
4942
5720
  import { createContext as createContext3, useContext as useContext3, useEffect as useEffect6, useMemo as useMemo4 } from "react";
4943
- import { jsx as jsx6 } from "react/jsx-runtime";
5721
+ import { jsx as jsx7 } from "react/jsx-runtime";
4944
5722
  var ThemeContext = createContext3(null);
4945
5723
  function ThemeProvider({
4946
5724
  children,
@@ -4971,7 +5749,7 @@ ${cssRules}
4971
5749
  mode: merged.mode,
4972
5750
  cssVariables
4973
5751
  };
4974
- return /* @__PURE__ */ jsx6(ThemeContext.Provider, { value, children });
5752
+ return /* @__PURE__ */ jsx7(ThemeContext.Provider, { value, children });
4975
5753
  }
4976
5754
  function useTheme() {
4977
5755
  const context = useContext3(ThemeContext);
@@ -4982,7 +5760,7 @@ function useTheme() {
4982
5760
  }
4983
5761
 
4984
5762
  // src/components/ShadowCanvasOverlay.tsx
4985
- import { Fragment, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
5763
+ import { Fragment, jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
4986
5764
  var LAUNCHER_STYLES_ID = "syntro-launcher-styles";
4987
5765
  function ensureLauncherStyles(target, css) {
4988
5766
  if (target.querySelector(`#${LAUNCHER_STYLES_ID}`)) return;
@@ -4996,7 +5774,6 @@ function ShadowCanvasOverlay({
4996
5774
  onToggle,
4997
5775
  telemetry,
4998
5776
  launcherLabel: _launcherLabel = "Adaptives",
4999
- launcherIcon,
5000
5777
  launcherAnimate = false,
5001
5778
  launcherAnimationStyle: _launcherAnimationStyle = "pulse",
5002
5779
  notificationCount: _notificationCount,
@@ -5007,7 +5784,7 @@ function ShadowCanvasOverlay({
5007
5784
  canvasTitle,
5008
5785
  displayMode = "standard"
5009
5786
  }) {
5010
- var _a2, _b, _c, _d;
5787
+ var _a2, _b, _c, _d, _e;
5011
5788
  const [mounted, setMounted] = useState5(false);
5012
5789
  const [launcherPos, setLauncherPos] = useState5(null);
5013
5790
  const dragRef = useRef5(null);
@@ -5022,7 +5799,7 @@ function ShadowCanvasOverlay({
5022
5799
  const handleNotificationClick = useCallback4(
5023
5800
  (notif) => {
5024
5801
  if (runtime3) {
5025
- runtime3.events.publish(StandardEvents.NOTIFICATION_CLICKED, {
5802
+ runtime3.events.publish(StandardEvents2.NOTIFICATION_CLICKED, {
5026
5803
  notificationId: notif.id,
5027
5804
  tileId: notif.tileId,
5028
5805
  itemId: notif.itemId
@@ -5032,7 +5809,7 @@ function ShadowCanvasOverlay({
5032
5809
  onToggle();
5033
5810
  }
5034
5811
  if (runtime3 && notif.tileId) {
5035
- runtime3.events.publish(StandardEvents.NOTIFICATION_DEEP_LINK, {
5812
+ runtime3.events.publish(StandardEvents2.NOTIFICATION_DEEP_LINK, {
5036
5813
  tileId: notif.tileId,
5037
5814
  itemId: notif.itemId
5038
5815
  });
@@ -5105,6 +5882,17 @@ function ShadowCanvasOverlay({
5105
5882
  }
5106
5883
  onToggle();
5107
5884
  }, [isOpen, telemetry, runtime3, onToggle]);
5885
+ useEffect7(() => {
5886
+ if (!isOpen) return;
5887
+ const handleOutsideClick = (e) => {
5888
+ const path = e.composedPath();
5889
+ if (containerRef.current && !path.includes(containerRef.current) && launcherRef.current && !path.includes(launcherRef.current)) {
5890
+ toggle2();
5891
+ }
5892
+ };
5893
+ document.addEventListener("mousedown", handleOutsideClick);
5894
+ return () => document.removeEventListener("mousedown", handleOutsideClick);
5895
+ }, [isOpen, toggle2]);
5108
5896
  const onLauncherPointerDown = useCallback4((e) => {
5109
5897
  const rect = e.currentTarget.getBoundingClientRect();
5110
5898
  dragRef.current = {
@@ -5129,7 +5917,7 @@ function ShadowCanvasOverlay({
5129
5917
  }
5130
5918
  }, []);
5131
5919
  const onLauncherPointerUp = useCallback4(
5132
- (_e) => {
5920
+ (_e2) => {
5133
5921
  const drag = dragRef.current;
5134
5922
  dragRef.current = null;
5135
5923
  if (drag && !drag.dragged) {
@@ -5143,6 +5931,7 @@ function ShadowCanvasOverlay({
5143
5931
  const isPush = config.canvas.layout === "push";
5144
5932
  const canvasBorder = (_b = config.canvas.border) != null ? _b : "none";
5145
5933
  const containerRef = useRef5(null);
5934
+ const launcherRef = useRef5(null);
5146
5935
  const zIndex = 2147483600;
5147
5936
  useEffect7(() => {
5148
5937
  var _a3, _b2, _c2, _d2;
@@ -5223,19 +6012,19 @@ function ShadowCanvasOverlay({
5223
6012
  pointerEvents: "none",
5224
6013
  padding: "0"
5225
6014
  };
5226
- const content = /* @__PURE__ */ jsx7(
6015
+ const content = /* @__PURE__ */ jsx8(
5227
6016
  "div",
5228
6017
  {
5229
6018
  "data-shadow-canvas-id": "overlay-root",
5230
6019
  style: {
5231
6020
  position: "fixed",
5232
6021
  inset: 0,
5233
- pointerEvents: isOpen ? "auto" : "none",
6022
+ pointerEvents: "none",
5234
6023
  zIndex
5235
6024
  },
5236
6025
  children: /* @__PURE__ */ jsxs3("div", { style: wrapperStyle, children: [
5237
6026
  /* @__PURE__ */ jsxs3("div", { ref: containerRef, "data-shadow-canvas-id": "overlay-container", style: containerStyle, children: [
5238
- isFocused && canvasTitle && /* @__PURE__ */ jsx7("header", { style: { color: "white", padding: "1.5rem 1.5rem 0" }, children: /* @__PURE__ */ jsx7(
6027
+ isFocused && canvasTitle && /* @__PURE__ */ jsx8("header", { style: { color: "white", padding: "1.5rem 1.5rem 0" }, children: /* @__PURE__ */ jsx8(
5239
6028
  "p",
5240
6029
  {
5241
6030
  style: {
@@ -5248,7 +6037,7 @@ function ShadowCanvasOverlay({
5248
6037
  children: canvasTitle
5249
6038
  }
5250
6039
  ) }),
5251
- /* @__PURE__ */ jsx7("div", { style: { flex: 1, overflowY: "auto", padding: isFocused ? "0" : "1rem" }, children: isLoading ? /* @__PURE__ */ jsx7(
6040
+ /* @__PURE__ */ jsx8("div", { style: { flex: 1, overflowY: "auto", padding: isFocused ? "0" : "1rem" }, children: isLoading ? /* @__PURE__ */ jsx8(
5252
6041
  "div",
5253
6042
  {
5254
6043
  style: { color: "var(--sc-overlay-text-color)", padding: isFocused ? "1rem" : "0" },
@@ -5268,7 +6057,7 @@ function ShadowCanvasOverlay({
5268
6057
  }
5269
6058
  ) : isFocused ? (
5270
6059
  /* Focused Mode: Render first tile full size */
5271
- tiles.length > 0 ? /* @__PURE__ */ jsx7(
6060
+ tiles.length > 0 ? /* @__PURE__ */ jsx8(
5272
6061
  TileCard,
5273
6062
  {
5274
6063
  config: tiles[0],
@@ -5279,7 +6068,7 @@ function ShadowCanvasOverlay({
5279
6068
  ) : null
5280
6069
  ) : (
5281
6070
  /* Standard Mode: Stacked cards — widgets always visible */
5282
- /* @__PURE__ */ jsx7(
6071
+ /* @__PURE__ */ jsx8(
5283
6072
  "div",
5284
6073
  {
5285
6074
  style: {
@@ -5288,7 +6077,7 @@ function ShadowCanvasOverlay({
5288
6077
  gap: "0.75rem",
5289
6078
  width: "100%"
5290
6079
  },
5291
- children: tiles.map((tile) => /* @__PURE__ */ jsx7(
6080
+ children: tiles.map((tile) => /* @__PURE__ */ jsx8(
5292
6081
  TileCard,
5293
6082
  {
5294
6083
  config: tile,
@@ -5303,17 +6092,7 @@ function ShadowCanvasOverlay({
5303
6092
  ) }),
5304
6093
  footerSlot
5305
6094
  ] }),
5306
- /* @__PURE__ */ jsx7(
5307
- "div",
5308
- {
5309
- onClick: toggle2,
5310
- style: {
5311
- flex: "1 1 auto",
5312
- pointerEvents: isOpen ? "auto" : "none",
5313
- cursor: "default"
5314
- }
5315
- }
5316
- )
6095
+ /* @__PURE__ */ jsx8("div", { style: { flex: "1 1 auto" } })
5317
6096
  ] })
5318
6097
  }
5319
6098
  );
@@ -5331,7 +6110,7 @@ function ShadowCanvasOverlay({
5331
6110
  zIndex: zIndex + 47
5332
6111
  },
5333
6112
  children: [
5334
- /* @__PURE__ */ jsx7(
6113
+ /* @__PURE__ */ jsx8(
5335
6114
  NotificationToastStack,
5336
6115
  {
5337
6116
  notifications,
@@ -5343,6 +6122,7 @@ function ShadowCanvasOverlay({
5343
6122
  /* @__PURE__ */ jsxs3(
5344
6123
  "button",
5345
6124
  {
6125
+ ref: launcherRef,
5346
6126
  type: "button",
5347
6127
  "aria-label": "Toggle shadow canvas",
5348
6128
  className: launcherAnimate && !isOpen ? "syntro-launcher-animate" : void 0,
@@ -5401,14 +6181,14 @@ function ShadowCanvasOverlay({
5401
6181
  focusable: "false",
5402
6182
  style: { transition: "transform 200ms ease" },
5403
6183
  children: [
5404
- /* @__PURE__ */ jsx7("path", { d: "M18 6L6 18" }),
5405
- /* @__PURE__ */ jsx7("path", { d: "M6 6l12 12" })
6184
+ /* @__PURE__ */ jsx8("path", { d: "M18 6L6 18" }),
6185
+ /* @__PURE__ */ jsx8("path", { d: "M6 6l12 12" })
5406
6186
  ]
5407
6187
  }
5408
- ) : launcherIcon ? /* @__PURE__ */ jsx7(
6188
+ ) : ((_e = config.launcher) == null ? void 0 : _e.icon) ? /* @__PURE__ */ jsx8(
5409
6189
  "img",
5410
6190
  {
5411
- src: launcherIcon,
6191
+ src: config.launcher.icon,
5412
6192
  alt: "",
5413
6193
  "aria-hidden": "true",
5414
6194
  style: {
@@ -5433,16 +6213,16 @@ function ShadowCanvasOverlay({
5433
6213
  focusable: "false",
5434
6214
  style: { transition: "transform 200ms ease" },
5435
6215
  children: [
5436
- /* @__PURE__ */ jsx7("path", { d: "M12 3l1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3Z" }),
5437
- /* @__PURE__ */ jsx7("path", { d: "M5 3v4" }),
5438
- /* @__PURE__ */ jsx7("path", { d: "M3 5h4" }),
5439
- /* @__PURE__ */ jsx7("path", { d: "M19 17v4" }),
5440
- /* @__PURE__ */ jsx7("path", { d: "M17 19h4" })
6216
+ /* @__PURE__ */ jsx8("path", { d: "M12 3l1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3Z" }),
6217
+ /* @__PURE__ */ jsx8("path", { d: "M5 3v4" }),
6218
+ /* @__PURE__ */ jsx8("path", { d: "M3 5h4" }),
6219
+ /* @__PURE__ */ jsx8("path", { d: "M19 17v4" }),
6220
+ /* @__PURE__ */ jsx8("path", { d: "M17 19h4" })
5441
6221
  ]
5442
6222
  }
5443
6223
  ),
5444
6224
  !isOpen && notifications.length > 0 && /* @__PURE__ */ jsxs3("div", { style: { position: "absolute", top: -2, right: -2, pointerEvents: "none" }, children: [
5445
- /* @__PURE__ */ jsx7(
6225
+ /* @__PURE__ */ jsx8(
5446
6226
  "span",
5447
6227
  {
5448
6228
  className: "syntro-badge-ping",
@@ -5454,7 +6234,7 @@ function ShadowCanvasOverlay({
5454
6234
  }
5455
6235
  }
5456
6236
  ),
5457
- /* @__PURE__ */ jsx7(
6237
+ /* @__PURE__ */ jsx8(
5458
6238
  "span",
5459
6239
  {
5460
6240
  className: "syntro-badge-glow",
@@ -5465,7 +6245,7 @@ function ShadowCanvasOverlay({
5465
6245
  }
5466
6246
  }
5467
6247
  ),
5468
- /* @__PURE__ */ jsx7(
6248
+ /* @__PURE__ */ jsx8(
5469
6249
  "span",
5470
6250
  {
5471
6251
  className: "syntro-badge-bounce",
@@ -5504,6 +6284,14 @@ var sortTiles = (tiles) => [...tiles].sort((a, b) => {
5504
6284
  var _a2, _b;
5505
6285
  return ((_a2 = b.priority) != null ? _a2 : 0) - ((_b = a.priority) != null ? _b : 0);
5506
6286
  });
6287
+ function fireTriggeredForTiles(tiles) {
6288
+ var _a2, _b;
6289
+ const tracker = typeof window !== "undefined" ? (_a2 = window.SynOS) == null ? void 0 : _a2.interventionTracker : null;
6290
+ if (!tracker) return;
6291
+ for (const tile of tiles) {
6292
+ tracker.trackTriggered(tile.id, (_b = tile.widget) != null ? _b : "unknown");
6293
+ }
6294
+ }
5507
6295
  function useShadowCanvasConfig({
5508
6296
  fetcher,
5509
6297
  experiments,
@@ -5526,6 +6314,7 @@ function useShadowCanvasConfig({
5526
6314
  if (experiments) {
5527
6315
  tiles = tiles.filter((tile) => experiments.shouldRenderRectangle(tile));
5528
6316
  }
6317
+ fireTriggeredForTiles(tiles);
5529
6318
  setState((prev) => ({ ...prev, tiles: sortTiles(tiles) }));
5530
6319
  }, [runtime3, experiments]);
5531
6320
  const load = useCallback5(async () => {
@@ -5543,6 +6332,7 @@ function useShadowCanvasConfig({
5543
6332
  } else if (experiments) {
5544
6333
  tiles = tiles.filter((tile) => experiments.shouldRenderRectangle(tile));
5545
6334
  }
6335
+ fireTriggeredForTiles(tiles);
5546
6336
  debug("SmartCanvas Config", `Tile count after filtering: ${tiles.length}`);
5547
6337
  const newActions = response.actions || [];
5548
6338
  const newActionsJson = JSON.stringify(newActions);
@@ -5600,13 +6390,13 @@ function useShadowCanvasConfig({
5600
6390
 
5601
6391
  // src/SmartCanvasApp.tsx
5602
6392
  import { useEffect as useEffect9, useMemo as useMemo7, useRef as useRef7, useState as useState7 } from "react";
5603
- import { jsx as jsx8 } from "react/jsx-runtime";
6393
+ import { jsx as jsx9 } from "react/jsx-runtime";
5604
6394
  function SmartCanvasApp({
5605
6395
  controller,
5606
6396
  fetcher,
5607
6397
  configUri,
5608
- configUriFeatureKey = "smart-canvas-config-uri",
5609
- configFeatureKey = "smart-canvas-config",
6398
+ configUriFeatureKey,
6399
+ configFeatureKey,
5610
6400
  fetchCredentials = "include",
5611
6401
  pollIntervalMs,
5612
6402
  experiments,
@@ -5623,7 +6413,7 @@ function SmartCanvasApp({
5623
6413
  workspaceTheme
5624
6414
  }) {
5625
6415
  if (runtime3) {
5626
- return /* @__PURE__ */ jsx8(RuntimeProvider, { runtime: runtime3, children: /* @__PURE__ */ jsx8(
6416
+ return /* @__PURE__ */ jsx9(RuntimeProvider, { runtime: runtime3, children: /* @__PURE__ */ jsx9(
5627
6417
  SmartCanvasAppInner,
5628
6418
  {
5629
6419
  controller,
@@ -5648,7 +6438,7 @@ function SmartCanvasApp({
5648
6438
  }
5649
6439
  ) });
5650
6440
  }
5651
- return /* @__PURE__ */ jsx8(
6441
+ return /* @__PURE__ */ jsx9(
5652
6442
  SmartCanvasAppInner,
5653
6443
  {
5654
6444
  controller,
@@ -5676,10 +6466,10 @@ function SmartCanvasAppInner({
5676
6466
  controller,
5677
6467
  fetcher,
5678
6468
  configUri,
5679
- configUriFeatureKey = "smart-canvas-config-uri",
5680
- configFeatureKey = "smart-canvas-config",
6469
+ configUriFeatureKey,
6470
+ configFeatureKey,
5681
6471
  fetchCredentials = "include",
5682
- pollIntervalMs,
6472
+ pollIntervalMs: _pollIntervalMs,
5683
6473
  experiments,
5684
6474
  telemetry,
5685
6475
  runtime: runtime3,
@@ -5693,7 +6483,7 @@ function SmartCanvasAppInner({
5693
6483
  initialBatchHandle,
5694
6484
  workspaceTheme
5695
6485
  }) {
5696
- var _a2, _b, _c, _d, _e, _f, _g;
6486
+ var _a2, _b, _c, _d, _e, _f;
5697
6487
  const [open, setOpen] = useState7(controller.getState().open);
5698
6488
  const pageContext = usePageContext();
5699
6489
  const [localUrl, setLocalUrl] = useState7(
@@ -5749,6 +6539,7 @@ function SmartCanvasAppInner({
5749
6539
  const batchHandleRef = useRef7(initialBatchHandle != null ? initialBatchHandle : null);
5750
6540
  const adoptedInitialRef = useRef7(!!initialBatchHandle);
5751
6541
  const runVersionRef = useRef7(0);
6542
+ const pendingRevertRef = useRef7(null);
5752
6543
  useEffect9(() => {
5753
6544
  if (!(runtime3 == null ? void 0 : runtime3.actions)) return;
5754
6545
  if (adoptedInitialRef.current) {
@@ -5760,6 +6551,10 @@ function SmartCanvasAppInner({
5760
6551
  const version = ++runVersionRef.current;
5761
6552
  const stale = () => version !== runVersionRef.current;
5762
6553
  const run = async () => {
6554
+ if (pendingRevertRef.current) {
6555
+ await pendingRevertRef.current;
6556
+ pendingRevertRef.current = null;
6557
+ }
5763
6558
  if (batchHandleRef.current) {
5764
6559
  try {
5765
6560
  await batchHandleRef.current.revertAll();
@@ -5787,7 +6582,7 @@ function SmartCanvasAppInner({
5787
6582
  run();
5788
6583
  return () => {
5789
6584
  if (batchHandleRef.current) {
5790
- batchHandleRef.current.revertAll().catch((err) => {
6585
+ pendingRevertRef.current = batchHandleRef.current.revertAll().catch((err) => {
5791
6586
  console.error("[SmartCanvasApp] Failed to revert actions on cleanup:", err);
5792
6587
  });
5793
6588
  batchHandleRef.current = null;
@@ -5803,16 +6598,13 @@ function SmartCanvasAppInner({
5803
6598
  }, [runtime3, controller]);
5804
6599
  const { shadowRoot } = useShadowRoot();
5805
6600
  const themeConfig = configState.theme;
5806
- if (!configState.isLoading && !hasContent) {
5807
- return null;
5808
- }
5809
- return /* @__PURE__ */ jsx8(
6601
+ return /* @__PURE__ */ jsx9(
5810
6602
  ThemeProvider,
5811
6603
  {
5812
6604
  themeConfig,
5813
6605
  workspaceTheme,
5814
6606
  shadowRoot,
5815
- children: /* @__PURE__ */ jsx8(
6607
+ children: !configState.isLoading && !hasContent ? null : /* @__PURE__ */ jsx9(
5816
6608
  ShadowCanvasOverlay,
5817
6609
  {
5818
6610
  tiles: configState.tiles,
@@ -5821,10 +6613,9 @@ function SmartCanvasAppInner({
5821
6613
  canvasTitle: configState.canvasTitle,
5822
6614
  telemetry,
5823
6615
  launcherLabel: launcherLabel != null ? launcherLabel : (_b = configState.launcher) == null ? void 0 : _b.label,
5824
- launcherIcon: (_c = configState.launcher) == null ? void 0 : _c.icon,
5825
- launcherAnimate: (_d = configState.launcher) == null ? void 0 : _d.animate,
5826
- launcherAnimationStyle: (_e = configState.launcher) == null ? void 0 : _e.animationStyle,
5827
- notificationCount: (_g = (_f = configState.launcher) == null ? void 0 : _f.notificationCount) != null ? _g : configState.tiles.length,
6616
+ launcherAnimate: (_c = configState.launcher) == null ? void 0 : _c.animate,
6617
+ launcherAnimationStyle: (_d = configState.launcher) == null ? void 0 : _d.animationStyle,
6618
+ notificationCount: (_f = (_e = configState.launcher) == null ? void 0 : _e.notificationCount) != null ? _f : configState.tiles.length,
5828
6619
  footerSlot,
5829
6620
  isOpen: open,
5830
6621
  onToggle: () => controller.toggle(),
@@ -5837,7 +6628,7 @@ function SmartCanvasAppInner({
5837
6628
 
5838
6629
  // src/SmartCanvasElement.tsx
5839
6630
  import { createRoot as createRoot2 } from "react-dom/client";
5840
- import { jsx as jsx9 } from "react/jsx-runtime";
6631
+ import { jsx as jsx10 } from "react/jsx-runtime";
5841
6632
  var TAG_NAME = "smart-canvas";
5842
6633
  var BASE_CSS = `
5843
6634
  :host {
@@ -5938,13 +6729,13 @@ var SmartCanvasElement = class extends HTMLElement {
5938
6729
  __privateSet(this, _root, createRoot2(__privateGet(this, _mount)));
5939
6730
  }
5940
6731
  __privateGet(this, _root).render(
5941
- /* @__PURE__ */ jsx9(
6732
+ /* @__PURE__ */ jsx10(
5942
6733
  ShadowRootProvider,
5943
6734
  {
5944
6735
  shadowRoot: __privateGet(this, _shadow),
5945
6736
  portalRoot: __privateGet(this, _portalRoot),
5946
6737
  overlayContainer: __privateGet(this, _overlayContainer),
5947
- children: /* @__PURE__ */ jsx9(SmartCanvasApp, { ...__privateGet(this, _lastAppProps), controller: __privateGet(this, _controller), canvasHost: this })
6738
+ children: /* @__PURE__ */ jsx10(SmartCanvasApp, { ...__privateGet(this, _lastAppProps), controller: __privateGet(this, _controller), canvasHost: this })
5948
6739
  }
5949
6740
  )
5950
6741
  );
@@ -6513,7 +7304,7 @@ var createSmartCanvas = async (config = {}) => {
6513
7304
  console.log(
6514
7305
  "[SmartCanvas] Actions to apply:",
6515
7306
  canvasConfig.actions.map(
6516
- (a, i) => `[${i}] ${a.kind}${a.anchorId ? ` anchor="${typeof a.anchorId === "string" ? a.anchorId : a.anchorId.selector}"` : ""}${a.label ? ` "${a.label}"` : ""}`
7307
+ (a, i) => `[${i}] ${a.kind}${a.anchorId ? ` anchor="${a.anchorId.selector}"` : ""}${a.label ? ` "${a.label}"` : ""}`
6517
7308
  ).join(", ")
6518
7309
  );
6519
7310
  }
@@ -6964,17 +7755,18 @@ var PostHogAdapter = class {
6964
7755
  __publicField(this, "client");
6965
7756
  __publicField(this, "featureFlagsCallback");
6966
7757
  __publicField(this, "captureCallback");
6967
- __publicField(this, "consentUnsub");
7758
+ __publicField(this, "rrwebCallback");
6968
7759
  this.client = options.client;
6969
7760
  this.featureFlagsCallback = options.onFeatureFlagsLoaded;
6970
7761
  this.captureCallback = options.onCapture;
7762
+ this.rrwebCallback = options.onRRWebEvent;
6971
7763
  if (!this.client && options.consent && options.requireExplicitConsent && typeof window !== "undefined" && options.apiKey) {
6972
7764
  const consent = options.consent;
6973
7765
  const currentStatus = consent.getStatus();
6974
7766
  if (currentStatus === "granted") {
6975
7767
  this.initPostHog();
6976
7768
  }
6977
- this.consentUnsub = consent.subscribe((status) => {
7769
+ consent.subscribe((status) => {
6978
7770
  if (status === "granted") {
6979
7771
  if (!this.client) {
6980
7772
  this.initPostHog();
@@ -7001,68 +7793,117 @@ var PostHogAdapter = class {
7001
7793
  if (!options.apiKey) return;
7002
7794
  const enableFeatureFlags = (_a2 = options.enableFeatureFlags) != null ? _a2 : true;
7003
7795
  const instanceName = `syntro_${options.apiKey.slice(-6) || "sdk"}`;
7004
- this.client = posthog.init(
7005
- options.apiKey,
7006
- {
7007
- api_host: (_b = options.apiHost) != null ? _b : "https://telemetry.syntrologie.com",
7008
- // Feature flags for segment membership (in_segment_* flags)
7009
- // When enabled, /decide is called to get segment flags
7010
- advanced_disable_feature_flags: !enableFeatureFlags,
7011
- advanced_disable_feature_flags_on_first_load: !enableFeatureFlags,
7012
- // Full-page tracking - all ON by default
7013
- autocapture: (_c = options.autocapture) != null ? _c : true,
7014
- capture_pageview: (_d = options.capturePageview) != null ? _d : true,
7015
- capture_pageleave: (_e = options.capturePageleave) != null ? _e : true,
7016
- disable_session_recording: !((_f = options.sessionRecording) != null ? _f : true),
7017
- // CRITICAL: Disable user agent filtering to allow headless Chrome
7018
- // PostHog blocks "HeadlessChrome" user agents by default as bot detection
7019
- // This enables session recording in Playwright/crawler sessions
7020
- opt_out_useragent_filter: true,
7021
- // Cross-domain iframe recording for embeds
7022
- session_recording: {
7023
- recordCrossDomainIFrames: true
7024
- },
7025
- // Capture performance metrics
7026
- capture_performance: true,
7027
- // Enable web vitals
7028
- enable_recording_console_log: true,
7029
- // Bootstrap callback for when flags are loaded
7030
- loaded: (ph) => {
7031
- if (enableFeatureFlags && this.featureFlagsCallback) {
7032
- ph.onFeatureFlags(() => {
7033
- const allFlags = this.getAllFeatureFlags();
7034
- if (allFlags && this.featureFlagsCallback) {
7035
- this.featureFlagsCallback(allFlags);
7036
- }
7037
- });
7038
- const existingFlags = this.getAllFeatureFlags();
7039
- if (existingFlags && Object.keys(existingFlags).length > 0) {
7040
- this.featureFlagsCallback(existingFlags);
7041
- }
7042
- }
7043
- if (this.captureCallback) {
7044
- ph.on("eventCaptured", (data) => {
7045
- var _a3;
7046
- const eventName = typeof data === "string" ? data : data == null ? void 0 : data.event;
7047
- const properties = typeof data === "string" ? void 0 : data == null ? void 0 : data.properties;
7048
- if (typeof eventName === "string") {
7049
- (_a3 = this.captureCallback) == null ? void 0 : _a3.call(this, eventName, properties);
7050
- }
7051
- });
7052
- }
7053
- }
7796
+ const initOptions = {
7797
+ api_host: (_b = options.apiHost) != null ? _b : "https://telemetry.syntrologie.com",
7798
+ // Feature flags for segment membership (in_segment_* flags)
7799
+ // When enabled, /decide is called to get segment flags
7800
+ advanced_disable_feature_flags: !enableFeatureFlags,
7801
+ advanced_disable_feature_flags_on_first_load: !enableFeatureFlags,
7802
+ // Full-page tracking - all ON by default
7803
+ autocapture: (_c = options.autocapture) != null ? _c : true,
7804
+ capture_pageview: (_d = options.capturePageview) != null ? _d : "history_change",
7805
+ capture_pageleave: (_e = options.capturePageleave) != null ? _e : true,
7806
+ disable_session_recording: !((_f = options.sessionRecording) != null ? _f : true),
7807
+ // CRITICAL: Disable user agent filtering to allow headless Chrome
7808
+ // PostHog blocks "HeadlessChrome" user agents by default as bot detection
7809
+ // This enables session recording in Playwright/crawler sessions
7810
+ opt_out_useragent_filter: true,
7811
+ // Cross-domain iframe recording for embeds
7812
+ session_recording: {
7813
+ recordCrossDomainIFrames: true
7054
7814
  },
7815
+ // Capture performance metrics
7816
+ capture_performance: true,
7817
+ // Enable web vitals
7818
+ enable_recording_console_log: true
7819
+ };
7820
+ const result = posthog.init(
7821
+ options.apiKey,
7822
+ initOptions,
7055
7823
  instanceName
7056
7824
  );
7825
+ if (result) {
7826
+ this.client = result;
7827
+ }
7828
+ if (this.captureCallback && this.client) {
7829
+ this.client.on("eventCaptured", (...args) => {
7830
+ var _a3;
7831
+ const data = args[0];
7832
+ const eventName = typeof data === "string" ? data : data == null ? void 0 : data.event;
7833
+ const properties = typeof data === "string" ? void 0 : data == null ? void 0 : data.properties;
7834
+ if (typeof eventName === "string") {
7835
+ (_a3 = this.captureCallback) == null ? void 0 : _a3.call(this, eventName, properties);
7836
+ }
7837
+ });
7838
+ }
7839
+ if (enableFeatureFlags && this.featureFlagsCallback && this.client) {
7840
+ this.client.onFeatureFlags(() => {
7841
+ const allFlags = this.getAllFeatureFlags();
7842
+ if (allFlags && this.featureFlagsCallback) {
7843
+ this.featureFlagsCallback(allFlags);
7844
+ }
7845
+ });
7846
+ const existingFlags = this.getAllFeatureFlags();
7847
+ if (existingFlags && Object.keys(existingFlags).length > 0) {
7848
+ this.featureFlagsCallback(existingFlags);
7849
+ }
7850
+ }
7851
+ if (this.rrwebCallback && this.client) {
7852
+ this.setupRRWebIntercept();
7853
+ }
7854
+ }
7855
+ /**
7856
+ * Set up rrweb event interception on PostHog's session recording.
7857
+ *
7858
+ * PostHog lazy-loads the rrweb recorder. The SessionRecording wrapper has
7859
+ * an `onRRwebEmit` method, but rrweb delivers events directly to the
7860
+ * lazy-loaded recorder instance's `onRRwebEmit`, bypassing the wrapper.
7861
+ * We must find and patch the recorder instance, not the wrapper.
7862
+ *
7863
+ * The recorder instance is stored on a minified property of SessionRecording.
7864
+ * We detect it by looking for an object with both `onRRwebEmit` and `start` methods.
7865
+ */
7866
+ setupRRWebIntercept(retries = 30) {
7867
+ var _a2;
7868
+ const sr = (_a2 = this.client) == null ? void 0 : _a2.sessionRecording;
7869
+ if (!sr) {
7870
+ if (retries > 0) {
7871
+ setTimeout(() => this.setupRRWebIntercept(retries - 1), 500);
7872
+ }
7873
+ return;
7874
+ }
7875
+ let recorder = null;
7876
+ const srRecord = sr;
7877
+ for (const key of Object.getOwnPropertyNames(srRecord)) {
7878
+ const val = srRecord[key];
7879
+ if (val && typeof val === "object" && typeof val.onRRwebEmit === "function" && typeof val.start === "function" && val !== sr) {
7880
+ recorder = val;
7881
+ break;
7882
+ }
7883
+ }
7884
+ if (!recorder) {
7885
+ if (retries > 0) {
7886
+ setTimeout(() => this.setupRRWebIntercept(retries - 1), 500);
7887
+ }
7888
+ return;
7889
+ }
7890
+ const originalEmit = recorder.onRRwebEmit.bind(recorder);
7891
+ recorder.onRRwebEmit = (rawEvent) => {
7892
+ var _a3;
7893
+ (_a3 = this.rrwebCallback) == null ? void 0 : _a3.call(this, { kind: "rrweb", ...rawEvent });
7894
+ originalEmit(rawEvent);
7895
+ };
7896
+ if (typeof window !== "undefined") {
7897
+ window.__RRWEB_INTERCEPT_READY__ = true;
7898
+ }
7057
7899
  }
7058
7900
  /**
7059
7901
  * Get all feature flags from PostHog.
7060
7902
  * Used to extract segment membership flags (in_segment_*).
7061
7903
  */
7062
7904
  getAllFeatureFlags() {
7063
- var _a2, _b, _c;
7064
- const flags = (_c = (_b = (_a2 = this.client) == null ? void 0 : _a2.featureFlags) == null ? void 0 : _b.getFlagVariants) == null ? void 0 : _c.call(_b);
7065
- return flags;
7905
+ var _a2, _b;
7906
+ return (_b = (_a2 = this.client) == null ? void 0 : _a2.featureFlags) == null ? void 0 : _b.getFlagVariants();
7066
7907
  }
7067
7908
  /**
7068
7909
  * Get segment membership flags (in_segment_*) from PostHog.
@@ -7143,6 +7984,59 @@ function createPostHogClient(options = {}) {
7143
7984
  return new PostHogAdapter(options);
7144
7985
  }
7145
7986
 
7987
+ // src/telemetry/InterventionTracker.ts
7988
+ var InterventionTracker = class {
7989
+ constructor(telemetry, variantId) {
7990
+ __publicField(this, "telemetry");
7991
+ __publicField(this, "variantId");
7992
+ __publicField(this, "seenSet", /* @__PURE__ */ new Set());
7993
+ __publicField(this, "triggeredSet", /* @__PURE__ */ new Set());
7994
+ this.telemetry = telemetry;
7995
+ this.variantId = variantId;
7996
+ }
7997
+ trackServed(tiles, actions) {
7998
+ var _a2, _b;
7999
+ (_b = (_a2 = this.telemetry).track) == null ? void 0 : _b.call(_a2, "syntro_config_served", {
8000
+ variant_id: this.variantId,
8001
+ tiles,
8002
+ actions
8003
+ });
8004
+ }
8005
+ trackSeen(interventionId, interventionKind) {
8006
+ var _a2, _b;
8007
+ if (this.seenSet.has(interventionId)) return;
8008
+ this.seenSet.add(interventionId);
8009
+ (_b = (_a2 = this.telemetry).track) == null ? void 0 : _b.call(_a2, "syntro_intervention_seen", {
8010
+ variant_id: this.variantId,
8011
+ intervention_id: interventionId,
8012
+ intervention_kind: interventionKind
8013
+ });
8014
+ }
8015
+ trackTriggered(interventionId, interventionKind) {
8016
+ var _a2, _b;
8017
+ if (this.triggeredSet.has(interventionId)) return;
8018
+ this.triggeredSet.add(interventionId);
8019
+ (_b = (_a2 = this.telemetry).track) == null ? void 0 : _b.call(_a2, "syntro_intervention_triggered", {
8020
+ variant_id: this.variantId,
8021
+ intervention_id: interventionId,
8022
+ intervention_kind: interventionKind
8023
+ });
8024
+ }
8025
+ trackInteracted(interventionId, interventionKind, interactionType) {
8026
+ var _a2, _b;
8027
+ (_b = (_a2 = this.telemetry).track) == null ? void 0 : _b.call(_a2, "syntro_intervention_interacted", {
8028
+ variant_id: this.variantId,
8029
+ intervention_id: interventionId,
8030
+ intervention_kind: interventionKind,
8031
+ interaction_type: interactionType
8032
+ });
8033
+ }
8034
+ resetPage() {
8035
+ this.seenSet.clear();
8036
+ this.triggeredSet.clear();
8037
+ }
8038
+ };
8039
+
7146
8040
  // src/actions/executors/core-flow.ts
7147
8041
  var executeSequence = async (action, context) => {
7148
8042
  const handles = [];
@@ -7413,158 +8307,7 @@ function hasExecutor(kind) {
7413
8307
  return executorRegistry.has(kind);
7414
8308
  }
7415
8309
 
7416
- // src/actions/validation.ts
7417
- var DANGEROUS_ATTRS = /* @__PURE__ */ new Set([
7418
- "onclick",
7419
- "onerror",
7420
- "onload",
7421
- "onmouseover",
7422
- "onfocus",
7423
- "onblur",
7424
- "onchange",
7425
- "onsubmit",
7426
- "onkeydown",
7427
- "onkeyup",
7428
- "onkeypress"
7429
- ]);
7430
- var MAX_HTML_LENGTH = 5e4;
7431
- var MAX_STYLE_COUNT = 50;
7432
- function validateAction(action) {
7433
- const errors = [];
7434
- const warnings = [];
7435
- if (!action || typeof action !== "object") {
7436
- errors.push({
7437
- code: "INVALID_ACTION",
7438
- message: "Action must be an object"
7439
- });
7440
- return { valid: false, errors, warnings };
7441
- }
7442
- const { kind } = action;
7443
- if (!kind || typeof kind !== "string") {
7444
- errors.push({
7445
- code: "MISSING_KIND",
7446
- message: "Action must have a 'kind' property"
7447
- });
7448
- return { valid: false, errors, warnings };
7449
- }
7450
- if (!hasExecutor(kind) && kind !== "core:mountWidget") {
7451
- const registered = executorRegistry.list();
7452
- console.error(
7453
- `[ActionValidation] Unknown action kind: "${kind}". Registered kinds (${registered.length}): [${registered.join(", ")}]. This usually means the app that provides "${kind}" hasn't been activated yet, or the executor was never registered in ExecutorRegistry.`
7454
- );
7455
- errors.push({
7456
- code: "UNKNOWN_KIND",
7457
- message: `Unknown action kind: ${kind}`,
7458
- field: "kind"
7459
- });
7460
- return { valid: false, errors, warnings };
7461
- }
7462
- switch (kind) {
7463
- case "overlays:highlight":
7464
- case "overlays:pulse":
7465
- case "navigation:scrollTo":
7466
- validateAnchorAction(action, errors, warnings);
7467
- break;
7468
- case "overlays:badge":
7469
- validateAnchorAction(action, errors, warnings);
7470
- validateBadgeAction(action, errors, warnings);
7471
- break;
7472
- case "overlays:tooltip":
7473
- validateAnchorAction(action, errors, warnings);
7474
- validateTooltipAction(action, errors, warnings);
7475
- break;
7476
- case "overlays:modal":
7477
- validateModalAction(action, errors, warnings);
7478
- break;
7479
- case "content:insertHtml":
7480
- validateAnchorAction(action, errors, warnings);
7481
- validateInsertHtmlAction(action, errors, warnings);
7482
- break;
7483
- case "content:setText":
7484
- validateAnchorAction(action, errors, warnings);
7485
- validateSetTextAction(action, errors, warnings);
7486
- break;
7487
- case "content:setAttr":
7488
- validateAnchorAction(action, errors, warnings);
7489
- validateSetAttrAction(action, errors, warnings);
7490
- break;
7491
- case "content:addClass":
7492
- case "content:removeClass":
7493
- validateAnchorAction(action, errors, warnings);
7494
- validateClassAction(action, errors, warnings);
7495
- break;
7496
- case "content:setStyle":
7497
- validateAnchorAction(action, errors, warnings);
7498
- validateSetStyleAction(action, errors, warnings);
7499
- break;
7500
- case "core:mountWidget":
7501
- validateMountWidgetAction(action, errors, warnings);
7502
- break;
7503
- case "core:wait":
7504
- validateWaitAction(action, errors, warnings);
7505
- break;
7506
- case "core:sequence":
7507
- validateSequenceAction(action, errors, warnings);
7508
- break;
7509
- case "core:parallel":
7510
- validateParallelAction(action, errors, warnings);
7511
- break;
7512
- case "core:tour":
7513
- validateTourAction(action, errors, warnings);
7514
- break;
7515
- case "navigation:navigate":
7516
- validateNavigateAction(action, errors, warnings);
7517
- break;
7518
- }
7519
- return {
7520
- valid: errors.length === 0,
7521
- errors,
7522
- warnings
7523
- };
7524
- }
7525
- function validateAnchorAction(action, errors, warnings) {
7526
- const anchorId = action.anchorId;
7527
- if (!anchorId || typeof anchorId !== "object") {
7528
- errors.push({
7529
- code: "MISSING_ANCHOR_ID",
7530
- message: "Action requires an 'anchorId' object with a 'selector' string",
7531
- field: "anchorId"
7532
- });
7533
- return;
7534
- }
7535
- if (!anchorId.selector || typeof anchorId.selector !== "string") {
7536
- errors.push({
7537
- code: "MISSING_ANCHOR_SELECTOR",
7538
- message: "anchorId requires a 'selector' string",
7539
- field: "anchorId.selector"
7540
- });
7541
- } else if (anchorId.selector.length > 200) {
7542
- warnings.push({
7543
- code: "LONG_ANCHOR_ID",
7544
- message: "Anchor selector is unusually long",
7545
- suggestion: "Consider using a shorter, more descriptive selector"
7546
- });
7547
- }
7548
- if (anchorId.route === void 0 || anchorId.route === null) {
7549
- errors.push({
7550
- code: "MISSING_ANCHOR_ROUTE",
7551
- message: `anchorId requires a 'route' (string or array of strings). Use "**" for all routes.`,
7552
- field: "anchorId.route"
7553
- });
7554
- } else {
7555
- const routes = Array.isArray(anchorId.route) ? anchorId.route : [anchorId.route];
7556
- for (const route of routes) {
7557
- if (typeof route !== "string") {
7558
- errors.push({
7559
- code: "INVALID_ANCHOR_ROUTE",
7560
- message: "anchorId.route must be a string or array of strings",
7561
- field: "anchorId.route"
7562
- });
7563
- break;
7564
- }
7565
- }
7566
- }
7567
- }
8310
+ // src/actions/validation-rules.ts
7568
8311
  function validateBadgeAction(action, errors, warnings) {
7569
8312
  if (!action.content || typeof action.content !== "string") {
7570
8313
  errors.push({
@@ -7951,29 +8694,182 @@ function validateTourAction(action, errors, warnings) {
7951
8694
  }
7952
8695
  }
7953
8696
  }
7954
- function validateActions(actions) {
7955
- var _a2;
8697
+
8698
+ // src/actions/validation-core.ts
8699
+ var DANGEROUS_ATTRS = /* @__PURE__ */ new Set([
8700
+ "onclick",
8701
+ "onerror",
8702
+ "onload",
8703
+ "onmouseover",
8704
+ "onfocus",
8705
+ "onblur",
8706
+ "onchange",
8707
+ "onsubmit",
8708
+ "onkeydown",
8709
+ "onkeyup",
8710
+ "onkeypress"
8711
+ ]);
8712
+ var MAX_HTML_LENGTH = 5e4;
8713
+ var MAX_STYLE_COUNT = 50;
8714
+ function validateAction(action) {
7956
8715
  const errors = [];
7957
8716
  const warnings = [];
7958
- if (!Array.isArray(actions)) {
7959
- console.error("[ActionValidation] validateActions called with non-array:", typeof actions);
8717
+ if (!action || typeof action !== "object") {
7960
8718
  errors.push({
7961
- code: "INVALID_ACTIONS",
7962
- message: "Actions must be an array"
8719
+ code: "INVALID_ACTION",
8720
+ message: "Action must be an object"
7963
8721
  });
7964
8722
  return { valid: false, errors, warnings };
7965
8723
  }
7966
- for (let i = 0; i < actions.length; i++) {
7967
- const result = validateAction(actions[i]);
7968
- if (!result.valid) {
7969
- const action = actions[i];
7970
- console.error(
7971
- `[ActionValidation] Action [${i}] failed validation:`,
7972
- `kind="${(_a2 = action == null ? void 0 : action.kind) != null ? _a2 : "undefined"}"`,
7973
- result.errors.map((e) => `${e.code}: ${e.message}`).join("; "),
7974
- action
7975
- );
7976
- }
8724
+ const { kind } = action;
8725
+ if (!kind || typeof kind !== "string") {
8726
+ errors.push({
8727
+ code: "MISSING_KIND",
8728
+ message: "Action must have a 'kind' property"
8729
+ });
8730
+ return { valid: false, errors, warnings };
8731
+ }
8732
+ if (!hasExecutor(kind) && kind !== "core:mountWidget") {
8733
+ const registered = executorRegistry.list();
8734
+ console.error(
8735
+ `[ActionValidation] Unknown action kind: "${kind}". Registered kinds (${registered.length}): [${registered.join(", ")}]. This usually means the app that provides "${kind}" hasn't been activated yet, or the executor was never registered in ExecutorRegistry.`
8736
+ );
8737
+ errors.push({
8738
+ code: "UNKNOWN_KIND",
8739
+ message: `Unknown action kind: ${kind}`,
8740
+ field: "kind"
8741
+ });
8742
+ return { valid: false, errors, warnings };
8743
+ }
8744
+ switch (kind) {
8745
+ case "overlays:highlight":
8746
+ case "overlays:pulse":
8747
+ case "navigation:scrollTo":
8748
+ validateAnchorAction(action, errors, warnings);
8749
+ break;
8750
+ case "overlays:badge":
8751
+ validateAnchorAction(action, errors, warnings);
8752
+ validateBadgeAction(action, errors, warnings);
8753
+ break;
8754
+ case "overlays:tooltip":
8755
+ validateAnchorAction(action, errors, warnings);
8756
+ validateTooltipAction(action, errors, warnings);
8757
+ break;
8758
+ case "overlays:modal":
8759
+ validateModalAction(action, errors, warnings);
8760
+ break;
8761
+ case "content:insertHtml":
8762
+ validateAnchorAction(action, errors, warnings);
8763
+ validateInsertHtmlAction(action, errors, warnings);
8764
+ break;
8765
+ case "content:setText":
8766
+ validateAnchorAction(action, errors, warnings);
8767
+ validateSetTextAction(action, errors, warnings);
8768
+ break;
8769
+ case "content:setAttr":
8770
+ validateAnchorAction(action, errors, warnings);
8771
+ validateSetAttrAction(action, errors, warnings);
8772
+ break;
8773
+ case "content:addClass":
8774
+ case "content:removeClass":
8775
+ validateAnchorAction(action, errors, warnings);
8776
+ validateClassAction(action, errors, warnings);
8777
+ break;
8778
+ case "content:setStyle":
8779
+ validateAnchorAction(action, errors, warnings);
8780
+ validateSetStyleAction(action, errors, warnings);
8781
+ break;
8782
+ case "core:mountWidget":
8783
+ validateMountWidgetAction(action, errors, warnings);
8784
+ break;
8785
+ case "core:wait":
8786
+ validateWaitAction(action, errors, warnings);
8787
+ break;
8788
+ case "core:sequence":
8789
+ validateSequenceAction(action, errors, warnings);
8790
+ break;
8791
+ case "core:parallel":
8792
+ validateParallelAction(action, errors, warnings);
8793
+ break;
8794
+ case "overlays:tour":
8795
+ validateTourAction(action, errors, warnings);
8796
+ break;
8797
+ case "navigation:navigate":
8798
+ validateNavigateAction(action, errors, warnings);
8799
+ break;
8800
+ }
8801
+ return {
8802
+ valid: errors.length === 0,
8803
+ errors,
8804
+ warnings
8805
+ };
8806
+ }
8807
+ function validateAnchorAction(action, errors, warnings) {
8808
+ const anchorId = action.anchorId;
8809
+ if (!anchorId || typeof anchorId !== "object") {
8810
+ errors.push({
8811
+ code: "MISSING_ANCHOR_ID",
8812
+ message: "Action requires an 'anchorId' object with a 'selector' string",
8813
+ field: "anchorId"
8814
+ });
8815
+ return;
8816
+ }
8817
+ if (!anchorId.selector || typeof anchorId.selector !== "string") {
8818
+ errors.push({
8819
+ code: "MISSING_ANCHOR_SELECTOR",
8820
+ message: "anchorId requires a 'selector' string",
8821
+ field: "anchorId.selector"
8822
+ });
8823
+ } else if (anchorId.selector.length > 200) {
8824
+ warnings.push({
8825
+ code: "LONG_ANCHOR_ID",
8826
+ message: "Anchor selector is unusually long",
8827
+ suggestion: "Consider using a shorter, more descriptive selector"
8828
+ });
8829
+ }
8830
+ if (anchorId.route === void 0 || anchorId.route === null) {
8831
+ errors.push({
8832
+ code: "MISSING_ANCHOR_ROUTE",
8833
+ message: `anchorId requires a 'route' (string or array of strings). Use "**" for all routes.`,
8834
+ field: "anchorId.route"
8835
+ });
8836
+ } else {
8837
+ const routes = Array.isArray(anchorId.route) ? anchorId.route : [anchorId.route];
8838
+ for (const route of routes) {
8839
+ if (typeof route !== "string") {
8840
+ errors.push({
8841
+ code: "INVALID_ANCHOR_ROUTE",
8842
+ message: "anchorId.route must be a string or array of strings",
8843
+ field: "anchorId.route"
8844
+ });
8845
+ break;
8846
+ }
8847
+ }
8848
+ }
8849
+ }
8850
+ function validateActions(actions) {
8851
+ var _a2;
8852
+ const errors = [];
8853
+ const warnings = [];
8854
+ if (!Array.isArray(actions)) {
8855
+ console.error("[ActionValidation] validateActions called with non-array:", typeof actions);
8856
+ errors.push({
8857
+ code: "INVALID_ACTIONS",
8858
+ message: "Actions must be an array"
8859
+ });
8860
+ return { valid: false, errors, warnings };
8861
+ }
8862
+ for (let i = 0; i < actions.length; i++) {
8863
+ const result = validateAction(actions[i]);
8864
+ if (!result.valid) {
8865
+ const action = actions[i];
8866
+ console.error(
8867
+ `[ActionValidation] Action [${i}] failed validation:`,
8868
+ `kind="${(_a2 = action == null ? void 0 : action.kind) != null ? _a2 : "undefined"}"`,
8869
+ result.errors.map((e) => `${e.code}: ${e.message}`).join("; "),
8870
+ action
8871
+ );
8872
+ }
7977
8873
  for (const error2 of result.errors) {
7978
8874
  errors.push({
7979
8875
  ...error2,
@@ -8097,7 +8993,7 @@ function createActionEngine(options) {
8097
8993
  }
8098
8994
  return executor(action, context);
8099
8995
  }
8100
- function subscribeForReeval(id, action, triggerWhen, handle) {
8996
+ function subscribeForReeval(id, action, triggerWhen, _handle) {
8101
8997
  if (!runtime3) return;
8102
8998
  const unsubs = [];
8103
8999
  const onReeval = async () => {
@@ -8241,13 +9137,9 @@ function createActionEngine(options) {
8241
9137
  entry2.state = "reverted";
8242
9138
  publishEvent("action.reverted", { id, kind: action.kind });
8243
9139
  } catch (error2) {
8244
- entry2.state = "failed";
8245
- publishEvent("action.failed", {
8246
- id,
8247
- kind: action.kind,
8248
- error: String(error2)
8249
- });
8250
- throw error2;
9140
+ console.warn(`[ActionEngine] Cleanup error for ${action.kind} (${id}), ignoring:`, error2);
9141
+ entry2.state = "reverted";
9142
+ publishEvent("action.reverted", { id, kind: action.kind });
8251
9143
  } finally {
8252
9144
  activeActions.delete(id);
8253
9145
  }
@@ -8291,7 +9183,7 @@ function createActionEngine(options) {
8291
9183
  errorMessages,
8292
9184
  "\nActions:",
8293
9185
  actions.map(
8294
- (a, i) => ` [${i}] ${a.kind} ${a.anchorId ? `anchor="${typeof a.anchorId === "string" ? a.anchorId : a.anchorId.selector}"` : ""} ${a.label ? `label="${a.label}"` : ""}`
9186
+ (a, i) => ` [${i}] ${a.kind} ${a.anchorId ? `anchor="${a.anchorId.selector}"` : ""} ${a.label ? `label="${a.label}"` : ""}`
8295
9187
  ).join("\n")
8296
9188
  );
8297
9189
  throw new Error(`Batch validation failed: ${errorMessages}`);
@@ -8300,15 +9192,30 @@ function createActionEngine(options) {
8300
9192
  const handles = [];
8301
9193
  const appliedHandles = [];
8302
9194
  try {
8303
- for (const action of actions) {
8304
- const handle = await apply(action);
8305
- handles.push(handle);
8306
- appliedHandles.push(handle);
9195
+ const results = await Promise.allSettled(actions.map((action) => apply(action)));
9196
+ const errors = [];
9197
+ for (const result of results) {
9198
+ if (result.status === "fulfilled") {
9199
+ handles.push(result.value);
9200
+ appliedHandles.push(result.value);
9201
+ } else {
9202
+ errors.push(
9203
+ result.reason instanceof Error ? result.reason : new Error(String(result.reason))
9204
+ );
9205
+ }
9206
+ }
9207
+ if (errors.length > 0 && appliedHandles.length === 0) {
9208
+ throw errors[0];
9209
+ }
9210
+ if (errors.length > 0) {
9211
+ console.warn(
9212
+ `[ActionEngine] ${errors.length}/${actions.length} action(s) failed in batch:`,
9213
+ errors.map((e) => e.message).join("; ")
9214
+ );
8307
9215
  }
8308
9216
  } catch (error2) {
8309
9217
  console.error(
8310
- `[ActionEngine] Batch apply FAILED at action ${appliedHandles.length + 1}/${actions.length}.`,
8311
- `Successfully applied: ${appliedHandles.length}. Rolling back...`,
9218
+ `[ActionEngine] Batch apply FAILED. Successfully applied: ${appliedHandles.length}. Rolling back...`,
8312
9219
  error2
8313
9220
  );
8314
9221
  for (const handle of appliedHandles) {
@@ -8391,6 +9298,7 @@ function createAnchorResolver(opts) {
8391
9298
  function resolve(selector) {
8392
9299
  if (!root) return null;
8393
9300
  try {
9301
+ if (root.matches(selector)) return root;
8394
9302
  return root.querySelector(selector);
8395
9303
  } catch {
8396
9304
  return null;
@@ -8736,7 +9644,7 @@ function createContextManager(options) {
8736
9644
 
8737
9645
  // src/decisions/strategies/rules.ts
8738
9646
  function evaluateCondition(condition, evalContext) {
8739
- var _a2, _b, _c, _d, _e, _f, _g;
9647
+ var _a2, _b, _c, _d, _e;
8740
9648
  const { context, state, events } = evalContext;
8741
9649
  switch (condition.type) {
8742
9650
  case "page_url": {
@@ -8750,8 +9658,7 @@ function evaluateCondition(condition, evalContext) {
8750
9658
  return context.page.routeId === condition.routeId;
8751
9659
  }
8752
9660
  case "anchor_visible": {
8753
- const condSelector = typeof condition.anchorId === "string" ? condition.anchorId : (_b = (_a2 = condition.anchorId) == null ? void 0 : _a2.selector) != null ? _b : "";
8754
- const anchor = (_c = context.anchors) == null ? void 0 : _c.find((a) => a.anchorId === condSelector);
9661
+ const anchor = (_a2 = context.anchors) == null ? void 0 : _a2.find((a) => a.anchorId === condition.anchorId);
8755
9662
  switch (condition.state) {
8756
9663
  case "visible":
8757
9664
  return (anchor == null ? void 0 : anchor.visible) === true;
@@ -8765,7 +9672,7 @@ function evaluateCondition(condition, evalContext) {
8765
9672
  }
8766
9673
  case "event_occurred": {
8767
9674
  if (!events) return false;
8768
- const withinMs = (_d = condition.withinMs) != null ? _d : 6e4;
9675
+ const withinMs = (_b = condition.withinMs) != null ? _b : 6e4;
8769
9676
  return events.hasRecentEvent(condition.eventName, withinMs);
8770
9677
  }
8771
9678
  case "state_equals": {
@@ -8801,17 +9708,17 @@ function evaluateCondition(condition, evalContext) {
8801
9708
  }
8802
9709
  }
8803
9710
  case "dismissed": {
8804
- if (!state) return (_e = condition.inverted) != null ? _e : false;
9711
+ if (!state) return (_c = condition.inverted) != null ? _c : false;
8805
9712
  const isDismissed = state.isDismissed(condition.key);
8806
9713
  return condition.inverted ? !isDismissed : isDismissed;
8807
9714
  }
8808
9715
  case "cooldown_active": {
8809
- if (!state) return (_f = condition.inverted) != null ? _f : false;
9716
+ if (!state) return (_d = condition.inverted) != null ? _d : false;
8810
9717
  const isActive = state.isCooldownActive(condition.key);
8811
9718
  return condition.inverted ? !isActive : isActive;
8812
9719
  }
8813
9720
  case "frequency_limit": {
8814
- if (!state) return (_g = condition.inverted) != null ? _g : false;
9721
+ if (!state) return (_e = condition.inverted) != null ? _e : false;
8815
9722
  const count = state.getFrequencyCount(condition.key);
8816
9723
  const limitReached = count >= condition.limit;
8817
9724
  return condition.inverted ? !limitReached : limitReached;
@@ -9071,6 +9978,66 @@ function createEventAccumulator(options) {
9071
9978
  };
9072
9979
  }
9073
9980
 
9981
+ // src/events/validation.ts
9982
+ var APP_PREFIX = "app:";
9983
+ var RESERVED_PREFIX = "syntro:";
9984
+ var SEGMENT_PATTERN = /^[a-z][a-z0-9_]*$/;
9985
+ function validateEventName(name) {
9986
+ if (!name) {
9987
+ return { valid: false, reason: "Event name cannot be empty" };
9988
+ }
9989
+ if (name.startsWith(RESERVED_PREFIX)) {
9990
+ return { valid: false, reason: '"syntro:" prefix is reserved for internal SDK events' };
9991
+ }
9992
+ if (!name.startsWith(APP_PREFIX)) {
9993
+ return { valid: false, reason: `Custom events must start with "app:" prefix. Got: "${name}"` };
9994
+ }
9995
+ const segments = name.slice(APP_PREFIX.length).split(":");
9996
+ if (segments.length < 2) {
9997
+ return {
9998
+ valid: false,
9999
+ reason: `Event name must have at least 2 segments after "app:" (app:{category}:{action}). Got: "${name}"`
10000
+ };
10001
+ }
10002
+ for (const segment of segments) {
10003
+ if (!SEGMENT_PATTERN.test(segment)) {
10004
+ return {
10005
+ valid: false,
10006
+ reason: `Segment "${segment}" must be lowercase alphanumeric + underscores. Got: "${name}"`
10007
+ };
10008
+ }
10009
+ }
10010
+ return { valid: true };
10011
+ }
10012
+ function isSerializable(value) {
10013
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
10014
+ }
10015
+ function checkDepth(obj, maxDepth, current = 0) {
10016
+ if (current >= maxDepth) return false;
10017
+ for (const value of Object.values(obj)) {
10018
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
10019
+ if (!checkDepth(value, maxDepth, current + 1)) return false;
10020
+ }
10021
+ }
10022
+ return true;
10023
+ }
10024
+ function validateProps(props) {
10025
+ if (props === void 0) return { valid: true };
10026
+ if (props === null || typeof props !== "object" || Array.isArray(props)) {
10027
+ return { valid: false, reason: "Props must be a plain object" };
10028
+ }
10029
+ if (!checkDepth(props, 2)) {
10030
+ return { valid: false, reason: "Props nesting depth exceeds 2 levels" };
10031
+ }
10032
+ const stripped = [];
10033
+ for (const [key, value] of Object.entries(props)) {
10034
+ if (value !== null && typeof value === "object") continue;
10035
+ if (!isSerializable(value)) stripped.push(key);
10036
+ }
10037
+ if (stripped.length > 0) return { valid: true, stripped };
10038
+ return { valid: true };
10039
+ }
10040
+
9074
10041
  // src/events/EventBus.ts
9075
10042
  function matchesFilter(event, filter) {
9076
10043
  if (!filter) return true;
@@ -9102,8 +10069,16 @@ var EventBus = class {
9102
10069
  __publicField(this, "subscriptions", /* @__PURE__ */ new Set());
9103
10070
  __publicField(this, "history", []);
9104
10071
  __publicField(this, "maxHistorySize");
9105
- var _a2;
10072
+ __publicField(this, "debug");
10073
+ __publicField(this, "emitHistory");
10074
+ __publicField(this, "posthogCapture");
10075
+ __publicField(this, "testMode");
10076
+ var _a2, _b, _c, _d, _e;
9106
10077
  this.maxHistorySize = (_a2 = options.maxHistorySize) != null ? _a2 : 100;
10078
+ this.debug = (_b = options.debug) != null ? _b : false;
10079
+ this.emitHistory = (_c = options.history) != null ? _c : null;
10080
+ this.posthogCapture = (_d = options.posthogCapture) != null ? _d : null;
10081
+ this.testMode = (_e = options.testMode) != null ? _e : false;
9107
10082
  }
9108
10083
  /**
9109
10084
  * Subscribe to events matching an optional filter.
@@ -9156,6 +10131,83 @@ var EventBus = class {
9156
10131
  }
9157
10132
  }
9158
10133
  }
10134
+ /**
10135
+ * Emit a validated custom event from the host application.
10136
+ *
10137
+ * Custom events must use the `app:` prefix (e.g. `app:cart:abandoned`).
10138
+ * In debug mode, returns an EmitResult with delivery details.
10139
+ * In production mode, returns undefined.
10140
+ */
10141
+ emit(name, props) {
10142
+ const nameResult = validateEventName(name);
10143
+ if (!nameResult.valid) {
10144
+ console.warn(`[EventBus] emit() rejected: ${nameResult.reason}`);
10145
+ return this.debug ? { delivered: false, matchedRules: [], posthogCaptured: false, listenersNotified: 0 } : void 0;
10146
+ }
10147
+ const propsResult = validateProps(props);
10148
+ if (!propsResult.valid) {
10149
+ console.warn(`[EventBus] emit() rejected props: ${propsResult.reason}`);
10150
+ return this.debug ? { delivered: false, matchedRules: [], posthogCaptured: false, listenersNotified: 0 } : void 0;
10151
+ }
10152
+ const event = {
10153
+ ts: Date.now(),
10154
+ name,
10155
+ source: "custom",
10156
+ props,
10157
+ schemaVersion: EVENT_SCHEMA_VERSION
10158
+ };
10159
+ this.history.push(event);
10160
+ if (this.history.length > this.maxHistorySize) {
10161
+ this.history.shift();
10162
+ }
10163
+ let listenersNotified = 0;
10164
+ for (const subscription of this.subscriptions) {
10165
+ if (matchesFilter(event, subscription.filter)) {
10166
+ try {
10167
+ subscription.callback(event);
10168
+ listenersNotified++;
10169
+ } catch (err) {
10170
+ console.error("[EventBus] Subscriber error:", err);
10171
+ listenersNotified++;
10172
+ }
10173
+ }
10174
+ }
10175
+ let posthogCaptured = false;
10176
+ if (this.posthogCapture && !this.testMode) {
10177
+ try {
10178
+ this.posthogCapture(name, props);
10179
+ posthogCaptured = true;
10180
+ } catch (err) {
10181
+ console.error("[EventBus] PostHog capture error:", err);
10182
+ }
10183
+ }
10184
+ if (this.emitHistory) {
10185
+ this.emitHistory.record({
10186
+ name,
10187
+ props,
10188
+ source: "custom",
10189
+ timestamp: event.ts,
10190
+ matchedRules: []
10191
+ });
10192
+ }
10193
+ if (this.debug) {
10194
+ console.debug("[EventBus] emit()", { name, props, listenersNotified, posthogCaptured });
10195
+ return {
10196
+ delivered: true,
10197
+ matchedRules: [],
10198
+ posthogCaptured,
10199
+ listenersNotified
10200
+ };
10201
+ }
10202
+ return void 0;
10203
+ }
10204
+ /**
10205
+ * Set the PostHog capture function after construction.
10206
+ * Used by bootstrap to wire PostHog after the EventBus is created.
10207
+ */
10208
+ setPosthogCapture(fn) {
10209
+ this.posthogCapture = fn;
10210
+ }
9159
10211
  /**
9160
10212
  * Get recent events matching an optional filter.
9161
10213
  */
@@ -9206,124 +10258,26 @@ function createEventBus(options = {}) {
9206
10258
  return new EventBus(options);
9207
10259
  }
9208
10260
 
9209
- // src/events/normalizers/posthog.ts
9210
- var POSTHOG_EVENT_MAP = {
9211
- // Autocapture events
9212
- $autocapture: "ui.click",
9213
- // Default autocapture is usually clicks
9214
- $click: StandardEvents.UI_CLICK,
9215
- $scroll: StandardEvents.UI_SCROLL,
9216
- $input: StandardEvents.UI_INPUT,
9217
- $change: StandardEvents.UI_CHANGE,
9218
- $submit: StandardEvents.UI_SUBMIT,
9219
- // Navigation events
9220
- $pageview: StandardEvents.NAV_PAGE_VIEW,
9221
- $pageleave: StandardEvents.NAV_PAGE_LEAVE,
9222
- // Session events
9223
- $session_start: "session.start",
9224
- // Identify events
9225
- $identify: "user.identify"
9226
- };
9227
- function getEventName(phEvent) {
9228
- var _a2, _b;
9229
- const eventName = phEvent.event;
9230
- if (typeof eventName !== "string") {
9231
- return "posthog.unknown";
9232
- }
9233
- if (POSTHOG_EVENT_MAP[eventName]) {
9234
- return POSTHOG_EVENT_MAP[eventName];
9235
- }
9236
- if (eventName === "$autocapture") {
9237
- const tagName = (_a2 = phEvent.properties) == null ? void 0 : _a2.$tag_name;
9238
- const eventType = (_b = phEvent.properties) == null ? void 0 : _b.$event_type;
9239
- if (eventType === "submit") return StandardEvents.UI_SUBMIT;
9240
- if (eventType === "change") return StandardEvents.UI_CHANGE;
9241
- if (tagName === "input" || tagName === "textarea") return StandardEvents.UI_INPUT;
9242
- return StandardEvents.UI_CLICK;
9243
- }
9244
- if (!eventName.startsWith("$")) {
9245
- return `posthog.${eventName}`;
10261
+ // src/events/history.ts
10262
+ var EventHistory = class {
10263
+ constructor(maxSize = 100) {
10264
+ __publicField(this, "entries", []);
10265
+ __publicField(this, "maxSize");
10266
+ this.maxSize = maxSize;
9246
10267
  }
9247
- return eventName.replace("$", "posthog.");
9248
- }
9249
- function extractProps(phEvent) {
9250
- var _a2;
9251
- const props = {};
9252
- const phProps = phEvent.properties || {};
9253
- const elements = phProps.$elements;
9254
- if (phProps.$tag_name) {
9255
- props.tagName = phProps.$tag_name;
9256
- } else if ((_a2 = elements == null ? void 0 : elements[0]) == null ? void 0 : _a2.tag_name) {
9257
- props.tagName = elements[0].tag_name;
9258
- }
9259
- if (phProps.$el_text) props.elementText = phProps.$el_text;
9260
- if (elements) props.elements = elements;
9261
- if (phProps.$current_url) props.url = phProps.$current_url;
9262
- if (phProps.$pathname) props.pathname = phProps.$pathname;
9263
- if (phProps.$host) props.host = phProps.$host;
9264
- if (phProps.$viewport_width) props.viewportWidth = phProps.$viewport_width;
9265
- if (phProps.$viewport_height) props.viewportHeight = phProps.$viewport_height;
9266
- if (phProps.$session_id) props.sessionId = phProps.$session_id;
9267
- if (phProps.$scroll_depth) props.scrollDepth = phProps.$scroll_depth;
9268
- if (phProps.$scroll_percentage) props.scrollPercentage = phProps.$scroll_percentage;
9269
- props.originalEvent = phEvent.event;
9270
- return props;
9271
- }
9272
- function normalizePostHogEvent(phEvent) {
9273
- let ts;
9274
- if (typeof phEvent.timestamp === "number") {
9275
- ts = phEvent.timestamp;
9276
- } else if (typeof phEvent.timestamp === "string") {
9277
- ts = new Date(phEvent.timestamp).getTime();
9278
- } else {
9279
- ts = Date.now();
10268
+ record(entry) {
10269
+ this.entries.push(entry);
10270
+ if (this.maxSize > 0 && this.entries.length > this.maxSize) {
10271
+ this.entries.shift();
10272
+ }
9280
10273
  }
9281
- return {
9282
- ts,
9283
- name: getEventName(phEvent),
9284
- source: "posthog",
9285
- props: extractProps(phEvent),
9286
- schemaVersion: EVENT_SCHEMA_VERSION
9287
- };
9288
- }
9289
- function shouldNormalizeEvent(phEvent) {
9290
- const eventName = phEvent.event;
9291
- if (typeof eventName !== "string") return false;
9292
- const skipEvents = [
9293
- "$feature_flag_called",
9294
- "$feature_flags",
9295
- "$groups",
9296
- "$groupidentify",
9297
- "$set",
9298
- "$set_once",
9299
- "$unset",
9300
- "$create_alias",
9301
- "$capture_metrics",
9302
- "$performance_event",
9303
- "$web_vitals",
9304
- "$exception",
9305
- "$dead_click",
9306
- "$heatmap"
9307
- ];
9308
- if (skipEvents.includes(eventName)) {
9309
- return false;
10274
+ getAll() {
10275
+ return [...this.entries];
9310
10276
  }
9311
- return true;
9312
- }
9313
- function createPostHogNormalizer(publishFn) {
9314
- return (eventName, properties) => {
9315
- if (typeof eventName !== "string") return;
9316
- const phEvent = {
9317
- event: eventName,
9318
- properties,
9319
- timestamp: Date.now()
9320
- };
9321
- if (shouldNormalizeEvent(phEvent)) {
9322
- const normalizedEvent = normalizePostHogEvent(phEvent);
9323
- publishFn(normalizedEvent);
9324
- }
9325
- };
9326
- }
10277
+ clear() {
10278
+ this.entries = [];
10279
+ }
10280
+ };
9327
10281
 
9328
10282
  // src/navigation/NavigationMonitor.ts
9329
10283
  var NavigationMonitor = class {
@@ -10124,9 +11078,18 @@ function createSurfaces(options) {
10124
11078
  }
10125
11079
  async function unmountEntry(entry) {
10126
11080
  var _a2;
10127
- await playExitAnimation(entry.container, entry.options.animation);
10128
- (_a2 = entry.cleanup) == null ? void 0 : _a2.call(entry);
10129
- entry.container.remove();
11081
+ if (entry.container.isConnected) {
11082
+ await playExitAnimation(entry.container, entry.options.animation);
11083
+ }
11084
+ try {
11085
+ (_a2 = entry.cleanup) == null ? void 0 : _a2.call(entry);
11086
+ } catch (error2) {
11087
+ console.warn("[Surfaces] Cleanup error during unmount, ignoring:", error2);
11088
+ }
11089
+ try {
11090
+ entry.container.remove();
11091
+ } catch {
11092
+ }
10130
11093
  mounts.delete(entry.slot);
10131
11094
  publishEvent("surface.unmounted", {
10132
11095
  slot: entry.slot,
@@ -10309,7 +11272,9 @@ var WidgetRegistry = class {
10309
11272
  var _a2;
10310
11273
  const mounted = this.mountedWidgets.get(mountId);
10311
11274
  if (mounted) {
10312
- (_a2 = mounted.cleanup) == null ? void 0 : _a2.call(mounted);
11275
+ if (container.isConnected) {
11276
+ (_a2 = mounted.cleanup) == null ? void 0 : _a2.call(mounted);
11277
+ }
10313
11278
  this.mountedWidgets.delete(mountId);
10314
11279
  container.removeAttribute("data-widget-mount-id");
10315
11280
  container.removeAttribute("data-widget-id");
@@ -10425,7 +11390,16 @@ function matchesAnchorRoute(anchorId) {
10425
11390
  const pathname = typeof window !== "undefined" ? window.location.pathname : "/";
10426
11391
  const normalizedPath = pathname.replace(/\/$/, "") || "/";
10427
11392
  const routes = Array.isArray(anchorId.route) ? anchorId.route : [anchorId.route];
10428
- return routes.some((pattern) => matchRoutePattern(normalizedPath, pattern));
11393
+ return routes.some((pattern) => {
11394
+ let normalized = pattern;
11395
+ if (/^https?:\/\//.test(normalized)) {
11396
+ try {
11397
+ normalized = new URL(normalized).pathname;
11398
+ } catch {
11399
+ }
11400
+ }
11401
+ return matchRoutePattern(normalizedPath, normalized);
11402
+ });
10429
11403
  }
10430
11404
  function createSmartCanvasRuntime(options = {}) {
10431
11405
  var _a2, _b, _c, _d;
@@ -10597,6 +11571,142 @@ function encodeToken(payload) {
10597
11571
  return TOKEN_PREFIX + base64;
10598
11572
  }
10599
11573
 
11574
+ // src/bootstrap-init.ts
11575
+ function getEnvVar(name) {
11576
+ if (typeof process !== "undefined" && process.env) {
11577
+ return process.env[name];
11578
+ }
11579
+ try {
11580
+ const meta = (0, eval)("import.meta");
11581
+ if (meta == null ? void 0 : meta.env) {
11582
+ return meta.env[name];
11583
+ }
11584
+ } catch {
11585
+ }
11586
+ return void 0;
11587
+ }
11588
+ var SEGMENT_CACHE_KEY = "syntro_segment_attributes";
11589
+ function loadCachedSegmentAttributes() {
11590
+ if (typeof window === "undefined") return {};
11591
+ try {
11592
+ const cached = localStorage.getItem(SEGMENT_CACHE_KEY);
11593
+ if (cached) {
11594
+ const attrs = JSON.parse(cached);
11595
+ debug("Syntro Bootstrap", "Loaded cached segment attributes:", attrs);
11596
+ return attrs;
11597
+ }
11598
+ } catch (err) {
11599
+ warn("Syntro Bootstrap", "Failed to load cached segment attributes:", err);
11600
+ }
11601
+ return {};
11602
+ }
11603
+ function cacheSegmentAttributes(attrs) {
11604
+ if (typeof window === "undefined") return;
11605
+ try {
11606
+ localStorage.setItem(SEGMENT_CACHE_KEY, JSON.stringify(attrs));
11607
+ debug("Syntro Bootstrap", "Cached segment attributes:", attrs);
11608
+ } catch (err) {
11609
+ warn("Syntro Bootstrap", "Failed to cache segment attributes:", err);
11610
+ }
11611
+ }
11612
+ function extractSegmentFlags(allFlags) {
11613
+ if (!allFlags) return {};
11614
+ const segmentFlags = {};
11615
+ for (const [key, value] of Object.entries(allFlags)) {
11616
+ if (key.startsWith("in_segment_")) {
11617
+ segmentFlags[key] = value === true;
11618
+ }
11619
+ }
11620
+ return segmentFlags;
11621
+ }
11622
+ function collectBrowserMetadata() {
11623
+ var _a2;
11624
+ if (typeof window === "undefined") return {};
11625
+ const attrs = {};
11626
+ try {
11627
+ const params = new URLSearchParams(window.location.search);
11628
+ for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]) {
11629
+ const val = params.get(key);
11630
+ if (val) attrs[key] = val;
11631
+ }
11632
+ } catch {
11633
+ }
11634
+ try {
11635
+ if (navigator.language) attrs.browser_language = navigator.language;
11636
+ if ((_a2 = navigator.languages) == null ? void 0 : _a2.length) attrs.browser_languages = [...navigator.languages];
11637
+ } catch {
11638
+ }
11639
+ try {
11640
+ const w = window.innerWidth;
11641
+ attrs.device_type = w < 768 ? "mobile" : w < 1024 ? "tablet" : "desktop";
11642
+ } catch {
11643
+ }
11644
+ try {
11645
+ if (document.referrer) {
11646
+ attrs.referrer = document.referrer;
11647
+ try {
11648
+ attrs.referrer_hostname = new URL(document.referrer).hostname;
11649
+ } catch {
11650
+ }
11651
+ }
11652
+ } catch {
11653
+ }
11654
+ try {
11655
+ const ua = navigator.userAgent;
11656
+ if (ua.includes("Edg/")) attrs.browser = "Edge";
11657
+ else if (ua.includes("OPR/") || ua.includes("Opera")) attrs.browser = "Opera";
11658
+ else if (ua.includes("Chrome/") && !ua.includes("Chromium")) attrs.browser = "Chrome";
11659
+ else if (ua.includes("Safari/") && !ua.includes("Chrome")) attrs.browser = "Safari";
11660
+ else if (ua.includes("Firefox/")) attrs.browser = "Firefox";
11661
+ if (ua.includes("Windows")) attrs.os = "Windows";
11662
+ else if (ua.includes("iPhone") || ua.includes("iPad")) attrs.os = "iOS";
11663
+ else if (ua.includes("Mac OS X") || ua.includes("Macintosh")) attrs.os = "macOS";
11664
+ else if (ua.includes("Android")) attrs.os = "Android";
11665
+ else if (ua.includes("Linux")) attrs.os = "Linux";
11666
+ } catch {
11667
+ }
11668
+ try {
11669
+ attrs.page_url = window.location.href;
11670
+ attrs.page_path = window.location.pathname;
11671
+ attrs.page_host = window.location.hostname;
11672
+ if (window.location.search) attrs.page_query = window.location.search;
11673
+ } catch {
11674
+ }
11675
+ return attrs;
11676
+ }
11677
+ var GEO_CACHE_KEY = "syntro_geo";
11678
+ var GEO_DEFAULT_HOST = "https://geo.syntrologie.com";
11679
+ async function fetchGeo(geoHost) {
11680
+ if (typeof window === "undefined") return {};
11681
+ try {
11682
+ const cached = localStorage.getItem(GEO_CACHE_KEY);
11683
+ if (cached) {
11684
+ const parsed = JSON.parse(cached);
11685
+ debug("Syntro Bootstrap", "Geo: using cached data:", parsed);
11686
+ return parsed;
11687
+ }
11688
+ } catch {
11689
+ }
11690
+ try {
11691
+ const res = await fetch(geoHost, { signal: AbortSignal.timeout(2e3) });
11692
+ if (res.ok) {
11693
+ const geo = await res.json();
11694
+ const cleaned = {};
11695
+ for (const [k, v] of Object.entries(geo)) {
11696
+ if (typeof v === "string" && v) cleaned[k] = v;
11697
+ }
11698
+ try {
11699
+ localStorage.setItem(GEO_CACHE_KEY, JSON.stringify(cleaned));
11700
+ } catch {
11701
+ }
11702
+ debug("Syntro Bootstrap", "Geo: fetched from worker:", cleaned);
11703
+ return cleaned;
11704
+ }
11705
+ } catch {
11706
+ }
11707
+ return {};
11708
+ }
11709
+
10600
11710
  // src/experiments/registry.ts
10601
11711
  var adapters = {
10602
11712
  growthbook: (config) => createGrowthBookClient({
@@ -10703,9 +11813,8 @@ var ExperimentsFetcher = class {
10703
11813
  __publicField(this, "featureKey");
10704
11814
  __publicField(this, "manifestKey");
10705
11815
  __publicField(this, "variantFlagPrefix");
10706
- var _a2;
10707
11816
  this.client = options.client;
10708
- this.featureKey = (_a2 = options.featureKey) != null ? _a2 : "smart-canvas-config";
11817
+ this.featureKey = options.featureKey;
10709
11818
  this.manifestKey = options.manifestKey;
10710
11819
  this.variantFlagPrefix = options.variantFlagPrefix;
10711
11820
  }
@@ -10734,6 +11843,11 @@ var ExperimentsFetcher = class {
10734
11843
  };
10735
11844
  }
10736
11845
  }
11846
+ if (!this.featureKey) {
11847
+ throw new Error(
11848
+ "[SmartCanvas] No featureKey configured and no variant flags found. Ensure at least one config feature flag exists in your experiment platform."
11849
+ );
11850
+ }
10737
11851
  const config = (_b = (_a2 = this.client).getFeatureValue) == null ? void 0 : _b.call(_a2, this.featureKey, null);
10738
11852
  if (!config || typeof config !== "object") {
10739
11853
  throw new Error(
@@ -10845,95 +11959,9 @@ function createTelemetryClient(provider, config) {
10845
11959
  return factory(config);
10846
11960
  }
10847
11961
 
10848
- // src/bootstrap.ts
10849
- function getEnvVar(name) {
10850
- if (typeof process !== "undefined" && process.env) {
10851
- return process.env[name];
10852
- }
10853
- try {
10854
- const meta = (0, eval)("import.meta");
10855
- if (meta == null ? void 0 : meta.env) {
10856
- return meta.env[name];
10857
- }
10858
- } catch {
10859
- }
10860
- return void 0;
10861
- }
10862
- var SEGMENT_CACHE_KEY = "syntro_segment_attributes";
10863
- function loadCachedSegmentAttributes() {
10864
- if (typeof window === "undefined") return {};
10865
- try {
10866
- const cached = localStorage.getItem(SEGMENT_CACHE_KEY);
10867
- if (cached) {
10868
- const attrs = JSON.parse(cached);
10869
- debug("Syntro Bootstrap", "Loaded cached segment attributes:", attrs);
10870
- return attrs;
10871
- }
10872
- } catch (err) {
10873
- warn("Syntro Bootstrap", "Failed to load cached segment attributes:", err);
10874
- }
10875
- return {};
10876
- }
10877
- function cacheSegmentAttributes(attrs) {
10878
- if (typeof window === "undefined") return;
10879
- try {
10880
- localStorage.setItem(SEGMENT_CACHE_KEY, JSON.stringify(attrs));
10881
- debug("Syntro Bootstrap", "Cached segment attributes:", attrs);
10882
- } catch (err) {
10883
- warn("Syntro Bootstrap", "Failed to cache segment attributes:", err);
10884
- }
10885
- }
10886
- function extractSegmentFlags(allFlags) {
10887
- if (!allFlags) return {};
10888
- const segmentFlags = {};
10889
- for (const [key, value] of Object.entries(allFlags)) {
10890
- if (key.startsWith("in_segment_")) {
10891
- segmentFlags[key] = value === true;
10892
- }
10893
- }
10894
- return segmentFlags;
10895
- }
10896
- function collectBrowserMetadata() {
10897
- var _a2;
10898
- if (typeof window === "undefined") return {};
10899
- const attrs = {};
10900
- try {
10901
- const params = new URLSearchParams(window.location.search);
10902
- for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]) {
10903
- const val = params.get(key);
10904
- if (val) attrs[key] = val;
10905
- }
10906
- } catch {
10907
- }
10908
- try {
10909
- if (navigator.language) attrs.browser_language = navigator.language;
10910
- if ((_a2 = navigator.languages) == null ? void 0 : _a2.length) attrs.browser_languages = [...navigator.languages];
10911
- } catch {
10912
- }
10913
- try {
10914
- const w = window.innerWidth;
10915
- attrs.device_type = w < 768 ? "mobile" : w < 1024 ? "tablet" : "desktop";
10916
- } catch {
10917
- }
10918
- try {
10919
- if (document.referrer) {
10920
- attrs.referrer = document.referrer;
10921
- try {
10922
- attrs.referrer_hostname = new URL(document.referrer).hostname;
10923
- } catch {
10924
- }
10925
- }
10926
- } catch {
10927
- }
10928
- try {
10929
- attrs.page_url = window.location.href;
10930
- attrs.page_path = window.location.pathname;
10931
- } catch {
10932
- }
10933
- return attrs;
10934
- }
10935
- async function init(options) {
10936
- var _a2, _b, _c, _d, _e, _f;
11962
+ // src/bootstrap-runtime.ts
11963
+ async function _initCore(options) {
11964
+ var _a2, _b, _c, _d, _e, _f, _g;
10937
11965
  initLogger();
10938
11966
  debug("Syntro Bootstrap", "====== INIT ======");
10939
11967
  debug("Syntro Bootstrap", "Options:", {
@@ -10998,17 +12026,59 @@ async function init(options) {
10998
12026
  const experimentHost = getEnvVar("NEXT_PUBLIC_SYNTRO_EXPERIMENT_HOST") || getEnvVar("VITE_SYNTRO_EXPERIMENT_HOST") || (payload == null ? void 0 : payload.eh);
10999
12027
  const telemetryHost = getEnvVar("NEXT_PUBLIC_SYNTRO_TELEMETRY_HOST") || getEnvVar("VITE_SYNTRO_TELEMETRY_HOST") || (payload == null ? void 0 : payload.th);
11000
12028
  const editorUrl = getEnvVar("NEXT_PUBLIC_SYNTRO_EDITOR_URL") || getEnvVar("VITE_SYNTRO_EDITOR_URL") || ((_b = options.canvas) == null ? void 0 : _b.editorUrl);
12029
+ const geoHost = (payload == null ? void 0 : payload.g) || getEnvVar("NEXT_PUBLIC_SYNTRO_GEO_HOST") || getEnvVar("VITE_SYNTRO_GEO_HOST") || GEO_DEFAULT_HOST;
11001
12030
  const cachedSegmentAttrs = loadCachedSegmentAttributes();
11002
12031
  const browserMetadata = collectBrowserMetadata();
11003
12032
  const phaseOneAttrs = { ...browserMetadata, ...cachedSegmentAttrs };
11004
12033
  debug("Syntro Bootstrap", "Phase 1: Browser metadata:", browserMetadata);
11005
12034
  debug("Syntro Bootstrap", "Phase 1: Cached segment attributes:", cachedSegmentAttrs);
12035
+ const geoPromise = fetchGeo(geoHost);
11006
12036
  let experiments;
11007
- const events = createEventBus();
12037
+ const isDebugOrTest = options.debug || options.testMode;
12038
+ const events = createEventBus({
12039
+ debug: options.debug,
12040
+ history: isDebugOrTest ? new EventHistory(options.testMode ? 0 : 100) : void 0,
12041
+ testMode: options.testMode
12042
+ });
11008
12043
  console.log("[Syntro Bootstrap] EventBus created");
11009
- const postHogNormalizer = createPostHogNormalizer((event) => {
11010
- events.publishEvent(event);
12044
+ const processor = createEventProcessor({
12045
+ elementResolver: typeof window !== "undefined" ? (x, y) => {
12046
+ const el = document.elementFromPoint(x, y);
12047
+ if (!el) return null;
12048
+ const info = { tag_name: el.tagName.toLowerCase() };
12049
+ if (el.id) info.attr__id = el.id;
12050
+ if (el.className && typeof el.className === "string") {
12051
+ info.classes = el.className.split(" ").filter(Boolean);
12052
+ }
12053
+ for (const attr of el.attributes) {
12054
+ if (attr.name.startsWith("data-")) info[`attr__${attr.name}`] = attr.value;
12055
+ }
12056
+ return info;
12057
+ } : void 0
11011
12058
  });
12059
+ processor.onEvent((event) => events.publishEvent(event));
12060
+ if (typeof window !== "undefined") {
12061
+ setInterval(() => processor.tick(Date.now()), 1e3);
12062
+ document.addEventListener(
12063
+ "click",
12064
+ (e) => {
12065
+ const chain = [];
12066
+ let el = e.target;
12067
+ while (el && el !== document.body) {
12068
+ const info = { tag_name: el.tagName.toLowerCase() };
12069
+ for (const attr of el.attributes) {
12070
+ if (attr.name.startsWith("data-") || attr.name === "id" || attr.name === "class" || attr.name === "aria-label") {
12071
+ info[`attr__${attr.name}`] = attr.value;
12072
+ }
12073
+ }
12074
+ chain.push(info);
12075
+ el = el.parentElement;
12076
+ }
12077
+ processor.enrichClickAttributes(Date.now(), chain);
12078
+ },
12079
+ true
12080
+ );
12081
+ }
11012
12082
  const onFeatureFlagsLoaded = (allFlags) => {
11013
12083
  var _a3, _b2, _c2;
11014
12084
  debug("Syntro Bootstrap", "Phase 2: PostHog feature flags loaded");
@@ -11031,12 +12101,26 @@ async function init(options) {
11031
12101
  // undefined falls back to adapter default
11032
12102
  // Enable PostHog feature flags for segment membership
11033
12103
  enableFeatureFlags: true,
12104
+ // Disable session recording in debug/dev mode (mock telemetry doesn't
12105
+ // support the PostHog recorder extension, causing console errors)
12106
+ sessionRecording: !payload.d,
11034
12107
  // Wire up callback for when flags are loaded (Phase 2)
11035
12108
  onFeatureFlagsLoaded,
11036
- // Wire up event capture to feed into EventBus
11037
- onCapture: postHogNormalizer
12109
+ // Wire up event capture to feed into event processor
12110
+ onCapture: (eventName, properties) => {
12111
+ processor.ingest({ kind: "posthog", event: eventName, properties, timestamp: Date.now() });
12112
+ },
12113
+ // Wire rrweb events for behavioral signal detection
12114
+ onRRWebEvent: (event) => {
12115
+ processor.ingest(event);
12116
+ }
11038
12117
  });
11039
12118
  console.log(`[Syntro Bootstrap] Telemetry client created (${provider}) with EventBus wiring`);
12119
+ const telemetryForCapture = telemetry;
12120
+ events.setPosthogCapture((name, props) => {
12121
+ var _a3;
12122
+ (_a3 = telemetryForCapture.track) == null ? void 0 : _a3.call(telemetryForCapture, name, props);
12123
+ });
11040
12124
  }
11041
12125
  let sessionMetrics;
11042
12126
  if (payload == null ? void 0 : payload.e) {
@@ -11129,11 +12213,17 @@ async function init(options) {
11129
12213
  warn("Syntro Bootstrap", "Failed to load GrowthBook features:", err);
11130
12214
  }
11131
12215
  }
12216
+ const geoData = await geoPromise;
12217
+ if (experiments && Object.keys(geoData).length > 0) {
12218
+ const mergedAttrs = { ...browserMetadata, ...geoData };
12219
+ debug("Syntro Bootstrap", "Merging geo data into GrowthBook attributes:", geoData);
12220
+ (_f = experiments.setAttributes) == null ? void 0 : _f.call(experiments, mergedAttrs);
12221
+ }
11132
12222
  let baseFetcher;
11133
12223
  if (options.fetcher) {
11134
12224
  baseFetcher = options.fetcher;
11135
12225
  } else if (payload == null ? void 0 : payload.f) {
11136
- const configFetcher = createConfigFetcher(payload.f, (_f = payload.o) != null ? _f : {});
12226
+ const configFetcher = createConfigFetcher(payload.f, (_g = payload.o) != null ? _g : {});
11137
12227
  baseFetcher = async () => {
11138
12228
  var _a3;
11139
12229
  const result = await configFetcher.fetch();
@@ -11147,19 +12237,39 @@ async function init(options) {
11147
12237
  }
11148
12238
  const warnedAppFailures = /* @__PURE__ */ new Set();
11149
12239
  const appLoadingFetcher = baseFetcher ? async () => {
11150
- var _a3, _b2, _c2, _d2, _e2, _f2, _g;
12240
+ var _a3, _b2, _c2, _d2, _e2, _f2, _g2, _h, _i, _j, _k, _l;
11151
12241
  const config = await baseFetcher();
12242
+ const tileCount = (_b2 = (_a3 = config.tiles) == null ? void 0 : _a3.length) != null ? _b2 : 0;
12243
+ const actionCount = (_d2 = (_c2 = config.actions) == null ? void 0 : _c2.length) != null ? _d2 : 0;
12244
+ const variantId = (_e2 = config.meta) == null ? void 0 : _e2.variant_id;
12245
+ if (tileCount > 0 || actionCount > 0) {
12246
+ if (!variantId) {
12247
+ console.warn(
12248
+ "[Syntro] Config has content but no meta.variant_id \u2014 intervention tracking disabled"
12249
+ );
12250
+ }
12251
+ }
12252
+ if (telemetry && variantId) {
12253
+ const tracker = new InterventionTracker(telemetry, variantId);
12254
+ tracker.trackServed(tileCount, actionCount);
12255
+ if (typeof window !== "undefined") {
12256
+ window.SynOS.interventionTracker = tracker;
12257
+ }
12258
+ runtime3.navigation.subscribe(() => {
12259
+ tracker.resetPage();
12260
+ });
12261
+ }
11152
12262
  console.log(
11153
12263
  "[Syntro Bootstrap] Config fetched:",
11154
- `tiles=${(_b2 = (_a3 = config.tiles) == null ? void 0 : _a3.length) != null ? _b2 : 0},`,
11155
- `actions=${(_d2 = (_c2 = config.actions) == null ? void 0 : _c2.length) != null ? _d2 : 0},`,
11156
- `theme=${(_f2 = (_e2 = config.theme) == null ? void 0 : _e2.name) != null ? _f2 : "none"}`
12264
+ `tiles=${(_g2 = (_f2 = config.tiles) == null ? void 0 : _f2.length) != null ? _g2 : 0},`,
12265
+ `actions=${(_i = (_h = config.actions) == null ? void 0 : _h.length) != null ? _i : 0},`,
12266
+ `theme=${(_k = (_j = config.theme) == null ? void 0 : _j.name) != null ? _k : "none"}`
11157
12267
  );
11158
- if (((_g = config.actions) == null ? void 0 : _g.length) > 0) {
12268
+ if (((_l = config.actions) == null ? void 0 : _l.length) > 0) {
11159
12269
  console.log(
11160
12270
  "[Syntro Bootstrap] Actions in config:",
11161
12271
  config.actions.map(
11162
- (a, i) => `[${i}] ${a.kind}${a.anchorId ? ` anchor="${typeof a.anchorId === "string" ? a.anchorId : a.anchorId.selector}"` : ""}${a.label ? ` "${a.label}"` : ""}`
12272
+ (a, i) => `[${i}] ${a.kind}${a.anchorId ? ` anchor="${a.anchorId.selector}"` : ""}${a.label ? ` "${a.label}"` : ""}`
11163
12273
  ).join(", ")
11164
12274
  );
11165
12275
  }
@@ -11218,10 +12328,33 @@ async function init(options) {
11218
12328
  });
11219
12329
  return { canvas, runtime: runtime3, experiments, telemetry, sessionMetrics, appLoader };
11220
12330
  }
12331
+
12332
+ // src/bootstrap.ts
12333
+ async function init(options) {
12334
+ var _a2;
12335
+ try {
12336
+ return await _initCore(options);
12337
+ } catch (err) {
12338
+ const message = err instanceof Error ? err.message : String(err);
12339
+ console.warn("[Syntrologie] SDK initialization failed:", message);
12340
+ if (typeof document !== "undefined") {
12341
+ (_a2 = document.getElementById("syntrologie-anti-flicker")) == null ? void 0 : _a2.remove();
12342
+ }
12343
+ return void 0;
12344
+ }
12345
+ }
12346
+ function emit(eventName, props = {}) {
12347
+ var _a2, _b;
12348
+ if (typeof window === "undefined") return;
12349
+ const runtime3 = (_a2 = window.SynOS) == null ? void 0 : _a2.runtime;
12350
+ if (!((_b = runtime3 == null ? void 0 : runtime3.events) == null ? void 0 : _b.publish)) return;
12351
+ runtime3.events.publish({ name: eventName, source: "custom", props });
12352
+ }
11221
12353
  var Syntro = {
11222
12354
  init,
11223
12355
  encodeToken,
11224
- decodeToken
12356
+ decodeToken,
12357
+ events: { emit }
11225
12358
  };
11226
12359
  if (typeof window !== "undefined") {
11227
12360
  window.Syntro = Syntro;
@@ -11252,8 +12385,11 @@ export {
11252
12385
  createSmartCanvasController,
11253
12386
  ShadowRootProvider,
11254
12387
  useShadowRoot,
11255
- StandardEvents,
11256
12388
  EVENT_SCHEMA_VERSION,
12389
+ normalizePostHogEvent,
12390
+ shouldNormalizeEvent,
12391
+ createPostHogNormalizer,
12392
+ StandardEvents2 as StandardEvents,
11257
12393
  CanvasEvents,
11258
12394
  NotificationToastStack,
11259
12395
  MAX_VISIBLE_TOASTS,
@@ -11284,6 +12420,7 @@ export {
11284
12420
  createSessionMetricTracker,
11285
12421
  createNoopClient,
11286
12422
  createPostHogClient,
12423
+ InterventionTracker,
11287
12424
  ExecutorRegistry,
11288
12425
  executorRegistry,
11289
12426
  getExecutor,
@@ -11302,11 +12439,11 @@ export {
11302
12439
  evaluateSync,
11303
12440
  createDecisionEngine,
11304
12441
  createEventAccumulator,
12442
+ validateEventName,
12443
+ validateProps,
11305
12444
  EventBus,
11306
12445
  createEventBus,
11307
- normalizePostHogEvent,
11308
- shouldNormalizeEvent,
11309
- createPostHogNormalizer,
12446
+ EventHistory,
11310
12447
  NavigationMonitor,
11311
12448
  StateStore,
11312
12449
  createStateStore,
@@ -11330,4 +12467,4 @@ export {
11330
12467
  encodeToken,
11331
12468
  Syntro
11332
12469
  };
11333
- //# sourceMappingURL=chunk-OB5AKUQT.js.map
12470
+ //# sourceMappingURL=chunk-EELCEGS7.js.map