veo-sdk 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,8 +34,8 @@ interface CustomPlacement {
34
34
  height?: number | string;
35
35
  }
36
36
  type UrlMatchType = 'exact' | 'prefix' | 'regex';
37
- type ActivationTrigger = 'immediate' | 'on_element' | 'on_event' | 'delayed';
38
- type CtaAction = 'dismiss' | 'next' | 'url';
37
+ type ActivationTrigger = 'immediate' | 'on_element' | 'on_event' | 'delayed' | 'on_click' | 'on_hover';
38
+ type CtaAction = 'dismiss' | 'next' | 'url' | 'guide';
39
39
  interface UrlMatcher {
40
40
  type: UrlMatchType;
41
41
  pattern: string;
@@ -76,6 +76,8 @@ interface StepBlock {
76
76
  url?: string | null;
77
77
  /** button: qué hace al hacer click. */
78
78
  action?: CtaAction | null;
79
+ /** button con acción `guide`: id de la guía que abre (encadenado). */
80
+ targetGuideId?: string | null;
79
81
  /** Estilo por-bloque (align/color/fontSize/margin…). Se aplica inline. */
80
82
  style?: Record<string, unknown> | null;
81
83
  }
@@ -34,8 +34,8 @@ interface CustomPlacement {
34
34
  height?: number | string;
35
35
  }
36
36
  type UrlMatchType = 'exact' | 'prefix' | 'regex';
37
- type ActivationTrigger = 'immediate' | 'on_element' | 'on_event' | 'delayed';
38
- type CtaAction = 'dismiss' | 'next' | 'url';
37
+ type ActivationTrigger = 'immediate' | 'on_element' | 'on_event' | 'delayed' | 'on_click' | 'on_hover';
38
+ type CtaAction = 'dismiss' | 'next' | 'url' | 'guide';
39
39
  interface UrlMatcher {
40
40
  type: UrlMatchType;
41
41
  pattern: string;
@@ -76,6 +76,8 @@ interface StepBlock {
76
76
  url?: string | null;
77
77
  /** button: qué hace al hacer click. */
78
78
  action?: CtaAction | null;
79
+ /** button con acción `guide`: id de la guía que abre (encadenado). */
80
+ targetGuideId?: string | null;
79
81
  /** Estilo por-bloque (align/color/fontSize/margin…). Se aplica inline. */
80
82
  style?: Record<string, unknown> | null;
81
83
  }
package/dist/index.cjs CHANGED
@@ -615,12 +615,12 @@ function buildButtonBlock(block, doc, callbacks) {
615
615
  applyBlockStyle(btn, block.style, "button");
616
616
  btn.addEventListener("click", () => {
617
617
  const action = block.action ?? "dismiss";
618
- const url = action === "url" && typeof block.url === "string" && isSafeUrl(block.url) ? block.url : void 0;
618
+ const target = action === "url" && typeof block.url === "string" && isSafeUrl(block.url) ? block.url : action === "guide" && typeof block.targetGuideId === "string" && block.targetGuideId ? block.targetGuideId : void 0;
619
619
  const meta = {
620
620
  ...typeof block.id === "string" && block.id ? { buttonId: block.id } : {},
621
621
  ...typeof block.text === "string" && block.text ? { buttonText: block.text } : {}
622
622
  };
623
- callbacks.onCtaClick(action, url, Object.keys(meta).length ? meta : void 0);
623
+ callbacks.onCtaClick(action, target, Object.keys(meta).length ? meta : void 0);
624
624
  });
625
625
  return btn;
626
626
  }
@@ -1554,15 +1554,16 @@ var init_badge_renderer = __esm({
1554
1554
  });
1555
1555
  };
1556
1556
  const buildTooltipContent = (s) => buildStepContent(s, ownerDocument, {
1557
- onCtaClick: (action, url, meta) => {
1557
+ onCtaClick: (action, target, meta) => {
1558
1558
  emit("cta_clicked", meta);
1559
- if (action === "url" && url) window.open(url, "_blank", "noopener,noreferrer");
1559
+ if (action === "url" && target) window.open(target, "_blank", "noopener,noreferrer");
1560
1560
  if (action === "dismiss") {
1561
1561
  emit("dismissed");
1562
1562
  context.onClose();
1563
1563
  return;
1564
1564
  }
1565
1565
  this.closeTooltip();
1566
+ if (action === "guide" && target) context.onOpenGuide?.(target);
1566
1567
  },
1567
1568
  onDismiss: () => {
1568
1569
  emit("dismissed");
@@ -1820,7 +1821,10 @@ var init_banner_renderer = __esm({
1820
1821
  ownerDocument,
1821
1822
  context.nav.callbacks
1822
1823
  ) : buildStepContent(step, ownerDocument, {
1823
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1824
+ onCtaClick: (action, target, meta) => {
1825
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
1826
+ if (action === "guide" && target) context.onOpenGuide?.(target);
1827
+ },
1824
1828
  onDismiss: () => dismiss("dismissed")
1825
1829
  });
1826
1830
  banner.appendChild(content);
@@ -1835,7 +1839,10 @@ var init_banner_renderer = __esm({
1835
1839
  this.liveContainer = banner;
1836
1840
  this.liveContent = content;
1837
1841
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
1838
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1842
+ onCtaClick: (action, target, meta) => {
1843
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
1844
+ if (action === "guide" && target) context.onOpenGuide?.(target);
1845
+ },
1839
1846
  onDismiss: () => dismiss("dismissed")
1840
1847
  });
1841
1848
  this.liveKey = this.liveKeyOf(step);
@@ -2496,7 +2503,10 @@ var init_inline_renderer = __esm({
2496
2503
  context.onClose();
2497
2504
  };
2498
2505
  const content = buildStepContent(step, ownerDocument, {
2499
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2506
+ onCtaClick: (action, target, meta) => {
2507
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2508
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2509
+ },
2500
2510
  onDismiss: () => dismiss("dismissed")
2501
2511
  });
2502
2512
  card.appendChild(content);
@@ -2505,7 +2515,10 @@ var init_inline_renderer = __esm({
2505
2515
  this.liveContent = content;
2506
2516
  this.liveKey = `${selector}|${position}`;
2507
2517
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2508
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2518
+ onCtaClick: (action, target, meta) => {
2519
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2520
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2521
+ },
2509
2522
  onDismiss: () => dismiss("dismissed")
2510
2523
  });
2511
2524
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
@@ -2565,7 +2578,10 @@ var init_modal_renderer = __esm({
2565
2578
  ownerDocument,
2566
2579
  context.nav.callbacks
2567
2580
  ) : buildStepContent(step, ownerDocument, {
2568
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2581
+ onCtaClick: (action, target, meta) => {
2582
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2583
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2584
+ },
2569
2585
  onDismiss: () => dismiss("dismissed")
2570
2586
  });
2571
2587
  card.appendChild(content);
@@ -2575,7 +2591,10 @@ var init_modal_renderer = __esm({
2575
2591
  this.liveContainer = card;
2576
2592
  this.liveContent = content;
2577
2593
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2578
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2594
+ onCtaClick: (action, target, meta) => {
2595
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2596
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2597
+ },
2579
2598
  onDismiss: () => dismiss("dismissed")
2580
2599
  });
2581
2600
  }
@@ -2640,8 +2659,9 @@ var init_tooltip_renderer = __esm({
2640
2659
  context.onClose();
2641
2660
  };
2642
2661
  const content = buildStepContent(step, ownerDocument, {
2643
- onCtaClick: (action, url, meta) => {
2644
- dismiss("cta_clicked", action === "url" ? url : void 0, meta);
2662
+ onCtaClick: (action, target, meta) => {
2663
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2664
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2645
2665
  },
2646
2666
  onDismiss: () => dismiss("dismissed")
2647
2667
  });
@@ -2674,7 +2694,10 @@ var init_tooltip_renderer = __esm({
2674
2694
  this.liveContent = content;
2675
2695
  this.liveKey = selector;
2676
2696
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2677
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2697
+ onCtaClick: (action, target, meta) => {
2698
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2699
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2700
+ },
2678
2701
  onDismiss: () => dismiss("dismissed")
2679
2702
  });
2680
2703
  context.onInteraction({
@@ -2891,6 +2914,10 @@ var init_guide_preview = __esm({
2891
2914
  onClose: () => this.close(),
2892
2915
  onTrack: () => {
2893
2916
  },
2917
+ // Encadenado en preview: no-op — el editor no puede montar otra guía
2918
+ // guardada; el click igual cierra la preview actual (vía onCtaClick).
2919
+ onOpenGuide: () => {
2920
+ },
2894
2921
  // Preview de forms: por defecto simula el envío SIN escribir props;
2895
2922
  // el caller puede pasar `formSubmit` real (link de preview).
2896
2923
  onFormSubmit: this.input?.formSubmit ?? (() => Promise.resolve(true)),
@@ -5333,6 +5360,12 @@ var GuidesController = class {
5333
5360
  this.resolveCache = null;
5334
5361
  /** Resolución idéntica en vuelo, para coalescer llamadas concurrentes. */
5335
5362
  this.inflightResolve = null;
5363
+ /**
5364
+ * Disparadores por interacción armados (`on_click`/`on_hover`): guideId →
5365
+ * desarme del listener. Se re-arman desde cero en cada `checkGuides` (el
5366
+ * resolve fresco manda) y se limpian en `destroy()`.
5367
+ */
5368
+ this.armedTriggers = /* @__PURE__ */ new Map();
5336
5369
  this.debug = config.debug === true;
5337
5370
  this.apiUrl = config.apiUrl;
5338
5371
  this.apiKey = config.apiKey;
@@ -5390,26 +5423,80 @@ var GuidesController = class {
5390
5423
  return;
5391
5424
  }
5392
5425
  if (this.debug) console.log(`[veo] guides: ${guides.length} eligible at ${pageUrl}`);
5426
+ this.disarmTriggers();
5393
5427
  for (const guide of guides) {
5394
- if (this.frequencyCache.shouldFilter(guide.guideId, guide.displayFrequency)) {
5395
- if (this.debug) console.log(`[veo] guides: filtered locally guideId=${guide.guideId}`);
5396
- continue;
5397
- }
5398
- if (this.activeByGuideId.has(guide.guideId)) {
5399
- if (this.debug) console.log(`[veo] guides: already shown guideId=${guide.guideId}`);
5428
+ const trigger = guide.activationRules.trigger;
5429
+ if ((trigger === "on_click" || trigger === "on_hover") && guide.activationRules.selector) {
5430
+ this.armInteractionTrigger(guide, endUserId, sessionId, pageUrl);
5400
5431
  continue;
5401
5432
  }
5402
- if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
5403
- if (this.walkthroughState.hasActive()) {
5404
- if (this.debug)
5405
- console.log(`[veo] guides: walkthrough ${guide.guideId} skipped (another active)`);
5406
- continue;
5407
- }
5408
- await this.startWalkthrough(guide, endUserId, sessionId, pageUrl);
5409
- continue;
5433
+ await this.showGuide(guide, endUserId, sessionId, pageUrl);
5434
+ }
5435
+ }
5436
+ /**
5437
+ * Muestra una guía elegible (filtro de frecuencia + dedupe + dispatch por
5438
+ * tipo). Cuerpo original del loop de `checkGuides`, extraído para que los
5439
+ * disparadores por interacción pasen por LOS MISMOS chequeos al disparar.
5440
+ */
5441
+ async showGuide(guide, endUserId, sessionId, pageUrl) {
5442
+ if (this.frequencyCache.shouldFilter(guide.guideId, guide.displayFrequency)) {
5443
+ if (this.debug) console.log(`[veo] guides: filtered locally guideId=${guide.guideId}`);
5444
+ return;
5445
+ }
5446
+ if (this.activeByGuideId.has(guide.guideId)) {
5447
+ if (this.debug) console.log(`[veo] guides: already shown guideId=${guide.guideId}`);
5448
+ return;
5449
+ }
5450
+ if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
5451
+ if (this.walkthroughState.hasActive()) {
5452
+ if (this.debug)
5453
+ console.log(`[veo] guides: walkthrough ${guide.guideId} skipped (another active)`);
5454
+ return;
5410
5455
  }
5411
- this.runSingleStepRender(guide, sessionId, pageUrl);
5456
+ await this.startWalkthrough(guide, endUserId, sessionId, pageUrl);
5457
+ return;
5412
5458
  }
5459
+ this.runSingleStepRender(guide, sessionId, pageUrl);
5460
+ }
5461
+ /**
5462
+ * Arma un disparador por interacción: la guía NO se muestra al cargar, sino
5463
+ * cuando el usuario hace click / pasa el mouse sobre `activationRules.selector`.
5464
+ * Listener DELEGADO en `document` (fase captura, sin preventDefault — el click
5465
+ * original de la app sigue funcionando). Al disparar se desarma y la guía pasa
5466
+ * por `showGuide` (mismos chequeos de frecuencia/dedupe). Tras cerrarse, el
5467
+ * próximo pageview/identify la re-arma — hover/click vuelve a mostrarla
5468
+ * mientras la frecuencia lo permita.
5469
+ */
5470
+ armInteractionTrigger(guide, endUserId, sessionId, pageUrl) {
5471
+ if (this.frequencyCache.shouldFilter(guide.guideId, guide.displayFrequency)) return;
5472
+ if (this.activeByGuideId.has(guide.guideId)) return;
5473
+ if (this.armedTriggers.has(guide.guideId)) return;
5474
+ const selector = guide.activationRules.selector;
5475
+ const eventName = guide.activationRules.trigger === "on_hover" ? "mouseover" : "click";
5476
+ const onFire = (e) => {
5477
+ const target = e.target;
5478
+ if (!(target instanceof Element)) return;
5479
+ try {
5480
+ if (!target.closest(selector)) return;
5481
+ } catch {
5482
+ return;
5483
+ }
5484
+ disarm();
5485
+ void this.showGuide(guide, endUserId, sessionId, pageUrl);
5486
+ };
5487
+ const disarm = () => {
5488
+ document.removeEventListener(eventName, onFire, true);
5489
+ this.armedTriggers.delete(guide.guideId);
5490
+ };
5491
+ document.addEventListener(eventName, onFire, true);
5492
+ this.armedTriggers.set(guide.guideId, disarm);
5493
+ if (this.debug)
5494
+ console.log(`[veo] guides: armed ${eventName} trigger for ${guide.guideId} on ${selector}`);
5495
+ }
5496
+ /** Desarma todos los disparadores por interacción pendientes. */
5497
+ disarmTriggers() {
5498
+ for (const disarm of this.armedTriggers.values()) disarm();
5499
+ this.armedTriggers.clear();
5413
5500
  }
5414
5501
  /**
5415
5502
  * `resolve` con cache de corta duración + coalescing:
@@ -5460,6 +5547,7 @@ ${pageUrl}`;
5460
5547
  this.activeWalkthroughRenderer = null;
5461
5548
  this.activeWalkthroughGuide = null;
5462
5549
  }
5550
+ this.disarmTriggers();
5463
5551
  this.dispatched.clear();
5464
5552
  this.resolveCache = null;
5465
5553
  this.inflightResolve = null;
@@ -5492,8 +5580,11 @@ ${pageUrl}`;
5492
5580
  this.client.track(eventName, properties);
5493
5581
  };
5494
5582
  const onFormSubmit = (answers) => this.submitFormResponse(guide.guideId, answers);
5583
+ const onOpenGuide = (targetGuideId) => {
5584
+ void this.openGuideById(targetGuideId, sessionId, pageUrl);
5585
+ };
5495
5586
  try {
5496
- void renderer.render({ guide, onInteraction, onClose, onTrack, onFormSubmit });
5587
+ void renderer.render({ guide, onInteraction, onClose, onTrack, onFormSubmit, onOpenGuide });
5497
5588
  } catch (err) {
5498
5589
  if (this.debug) console.error("[veo] renderer.render threw:", err);
5499
5590
  this.activeRenderers.delete(renderer);
@@ -5502,6 +5593,28 @@ ${pageUrl}`;
5502
5593
  }
5503
5594
  }
5504
5595
  }
5596
+ /**
5597
+ * Abre una guía POR ID, disparada desde el botón de otra guía (encadenado).
5598
+ * Ignora las URL rules y el trigger (el autor lo pidió explícitamente desde
5599
+ * un botón); respeta el dedupe de guías activas y el flujo normal de
5600
+ * tracking. 404/red → no-op (la guía destino pudo borrarse).
5601
+ */
5602
+ async openGuideById(guideId, sessionId, pageUrl) {
5603
+ if (this.activeByGuideId.has(guideId)) return;
5604
+ const guide = await this.resolver.fetchById(guideId);
5605
+ if (!guide) {
5606
+ if (this.debug) console.warn(`[veo] guides: chained guide ${guideId} not found`);
5607
+ return;
5608
+ }
5609
+ const endUserId = this.client.getEndUserId() ?? this.client.getAnonymousId();
5610
+ if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
5611
+ if (!this.walkthroughState.hasActive()) {
5612
+ await this.startWalkthrough(guide, endUserId, sessionId, pageUrl);
5613
+ }
5614
+ return;
5615
+ }
5616
+ this.runSingleStepRender(guide, sessionId, pageUrl);
5617
+ }
5505
5618
  /**
5506
5619
  * Si hay un walkthrough persistido, intenta pintar el step actual en
5507
5620
  * la página actual. Retorna `true` si lo intentó (sea éxito o cancel),