veo-sdk 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
  }
@@ -1259,10 +1259,22 @@ var init_styles = __esm({
1259
1259
  padding: 3px 9px;
1260
1260
  white-space: nowrap;
1261
1261
  }
1262
+ .veo-badge--text {
1263
+ color: var(--veo-primary);
1264
+ font-weight: 600;
1265
+ white-space: nowrap;
1266
+ text-decoration: underline dotted;
1267
+ text-underline-offset: 3px;
1268
+ }
1262
1269
  .veo-badge--image img { display: block; border-radius: 4px; object-fit: cover; }
1263
1270
  /* El tooltip del badge usa strategy fixed (el host vive dentro del flujo del
1264
- cliente; un absoluto se recortar\xEDa con overflow de ancestros). */
1265
- .veo-badge-tooltip { position: fixed; }
1271
+ cliente; un absoluto se recortar\xEDa con overflow de ancestros). Su ancho es
1272
+ personalizable v\xEDa var dedicada (--veo-width no sirve: el :host la define
1273
+ en 420px siempre y pisar\xEDa el default de 300px de los tooltips). */
1274
+ .veo-badge-tooltip {
1275
+ position: fixed;
1276
+ max-width: var(--veo-badge-tip-width, 300px);
1277
+ }
1266
1278
  @keyframes veo-badge-pulse {
1267
1279
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--veo-primary) 45%, transparent); }
1268
1280
  70% { box-shadow: 0 0 0 7px transparent; }
@@ -1439,7 +1451,7 @@ function readBadgeConfig(step) {
1439
1451
  const raw = step.style?.badge;
1440
1452
  if (!raw || typeof raw !== "object") return { kind: "icon" };
1441
1453
  const b = raw;
1442
- const kind = b.kind === "dot" || b.kind === "pill" || b.kind === "image" || b.kind === "icon" ? b.kind : "icon";
1454
+ const kind = b.kind === "dot" || b.kind === "pill" || b.kind === "text" || b.kind === "image" || b.kind === "icon" ? b.kind : "icon";
1443
1455
  return {
1444
1456
  kind,
1445
1457
  icon: typeof b.icon === "string" ? b.icon : null,
@@ -1470,6 +1482,12 @@ function buildBadgeElement(config, doc) {
1470
1482
  if (config.color) btn.style.background = config.color;
1471
1483
  break;
1472
1484
  }
1485
+ case "text": {
1486
+ btn.textContent = config.text?.slice(0, 32) || "Nuevo";
1487
+ btn.style.fontSize = `${clamp2(Math.round(size * 0.7), 9, 28)}px`;
1488
+ if (config.color) btn.style.color = config.color;
1489
+ break;
1490
+ }
1473
1491
  case "image": {
1474
1492
  if (config.imageUrl && isSafeUrl(config.imageUrl)) {
1475
1493
  const img = doc.createElement("img");
@@ -1531,6 +1549,13 @@ var init_badge_renderer = __esm({
1531
1549
  const { host, root } = this.createHost();
1532
1550
  const ownerDocument = root.ownerDocument ?? document;
1533
1551
  this.applyDesign(step.style);
1552
+ const tipWidth = step.style?.width;
1553
+ if (typeof tipWidth === "number" && Number.isFinite(tipWidth)) {
1554
+ host.style.setProperty(
1555
+ "--veo-badge-tip-width",
1556
+ `${Math.min(720, Math.max(220, tipWidth))}px`
1557
+ );
1558
+ }
1534
1559
  this.trigger = step.style?.badgeTrigger === "click" ? "click" : "hover";
1535
1560
  const rawSide = step.style?.tooltipPlacement;
1536
1561
  this.side = rawSide === "bottom" || rawSide === "left" || rawSide === "right" ? rawSide : "top";
@@ -1554,20 +1579,14 @@ var init_badge_renderer = __esm({
1554
1579
  });
1555
1580
  };
1556
1581
  const buildTooltipContent = (s) => buildStepContent(s, ownerDocument, {
1557
- onCtaClick: (action, url, meta) => {
1582
+ onCtaClick: (action, target, meta) => {
1558
1583
  emit("cta_clicked", meta);
1559
- if (action === "url" && url) window.open(url, "_blank", "noopener,noreferrer");
1560
- if (action === "dismiss") {
1561
- emit("dismissed");
1562
- context.onClose();
1563
- return;
1564
- }
1584
+ if (action === "url" && target) window.open(target, "_blank", "noopener,noreferrer");
1565
1585
  this.closeTooltip();
1586
+ if (action === "guide" && target) context.onOpenGuide?.(target);
1566
1587
  },
1567
- onDismiss: () => {
1568
- emit("dismissed");
1569
- context.onClose();
1570
- }
1588
+ // La ✕ también cierra solo el tooltip (nunca el badge, nunca dismissed).
1589
+ onDismiss: () => this.closeTooltip()
1571
1590
  });
1572
1591
  const content = buildTooltipContent(step);
1573
1592
  tooltip.appendChild(content);
@@ -1820,7 +1839,10 @@ var init_banner_renderer = __esm({
1820
1839
  ownerDocument,
1821
1840
  context.nav.callbacks
1822
1841
  ) : buildStepContent(step, ownerDocument, {
1823
- 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
+ },
1824
1846
  onDismiss: () => dismiss("dismissed")
1825
1847
  });
1826
1848
  banner.appendChild(content);
@@ -1835,7 +1857,10 @@ var init_banner_renderer = __esm({
1835
1857
  this.liveContainer = banner;
1836
1858
  this.liveContent = content;
1837
1859
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
1838
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
1860
+ onCtaClick: (action, target, meta) => {
1861
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
1862
+ if (action === "guide" && target) context.onOpenGuide?.(target);
1863
+ },
1839
1864
  onDismiss: () => dismiss("dismissed")
1840
1865
  });
1841
1866
  this.liveKey = this.liveKeyOf(step);
@@ -2496,7 +2521,10 @@ var init_inline_renderer = __esm({
2496
2521
  context.onClose();
2497
2522
  };
2498
2523
  const content = buildStepContent(step, ownerDocument, {
2499
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2524
+ onCtaClick: (action, target, meta) => {
2525
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2526
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2527
+ },
2500
2528
  onDismiss: () => dismiss("dismissed")
2501
2529
  });
2502
2530
  card.appendChild(content);
@@ -2505,7 +2533,10 @@ var init_inline_renderer = __esm({
2505
2533
  this.liveContent = content;
2506
2534
  this.liveKey = `${selector}|${position}`;
2507
2535
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2508
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2536
+ onCtaClick: (action, target, meta) => {
2537
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2538
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2539
+ },
2509
2540
  onDismiss: () => dismiss("dismissed")
2510
2541
  });
2511
2542
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
@@ -2565,7 +2596,10 @@ var init_modal_renderer = __esm({
2565
2596
  ownerDocument,
2566
2597
  context.nav.callbacks
2567
2598
  ) : buildStepContent(step, ownerDocument, {
2568
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2599
+ onCtaClick: (action, target, meta) => {
2600
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2601
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2602
+ },
2569
2603
  onDismiss: () => dismiss("dismissed")
2570
2604
  });
2571
2605
  card.appendChild(content);
@@ -2575,7 +2609,10 @@ var init_modal_renderer = __esm({
2575
2609
  this.liveContainer = card;
2576
2610
  this.liveContent = content;
2577
2611
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2578
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2612
+ onCtaClick: (action, target, meta) => {
2613
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2614
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2615
+ },
2579
2616
  onDismiss: () => dismiss("dismissed")
2580
2617
  });
2581
2618
  }
@@ -2640,8 +2677,9 @@ var init_tooltip_renderer = __esm({
2640
2677
  context.onClose();
2641
2678
  };
2642
2679
  const content = buildStepContent(step, ownerDocument, {
2643
- onCtaClick: (action, url, meta) => {
2644
- dismiss("cta_clicked", action === "url" ? url : void 0, meta);
2680
+ onCtaClick: (action, target, meta) => {
2681
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2682
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2645
2683
  },
2646
2684
  onDismiss: () => dismiss("dismissed")
2647
2685
  });
@@ -2674,7 +2712,10 @@ var init_tooltip_renderer = __esm({
2674
2712
  this.liveContent = content;
2675
2713
  this.liveKey = selector;
2676
2714
  this.liveBuild = (s) => buildStepContent(s, ownerDocument, {
2677
- onCtaClick: (action, url, meta) => dismiss("cta_clicked", action === "url" ? url : void 0, meta),
2715
+ onCtaClick: (action, target, meta) => {
2716
+ dismiss("cta_clicked", action === "url" ? target : void 0, meta);
2717
+ if (action === "guide" && target) context.onOpenGuide?.(target);
2718
+ },
2678
2719
  onDismiss: () => dismiss("dismissed")
2679
2720
  });
2680
2721
  context.onInteraction({
@@ -2891,6 +2932,10 @@ var init_guide_preview = __esm({
2891
2932
  onClose: () => this.close(),
2892
2933
  onTrack: () => {
2893
2934
  },
2935
+ // Encadenado en preview: no-op — el editor no puede montar otra guía
2936
+ // guardada; el click igual cierra la preview actual (vía onCtaClick).
2937
+ onOpenGuide: () => {
2938
+ },
2894
2939
  // Preview de forms: por defecto simula el envío SIN escribir props;
2895
2940
  // el caller puede pasar `formSubmit` real (link de preview).
2896
2941
  onFormSubmit: this.input?.formSubmit ?? (() => Promise.resolve(true)),
@@ -5333,6 +5378,12 @@ var GuidesController = class {
5333
5378
  this.resolveCache = null;
5334
5379
  /** Resolución idéntica en vuelo, para coalescer llamadas concurrentes. */
5335
5380
  this.inflightResolve = null;
5381
+ /**
5382
+ * Disparadores por interacción armados (`on_click`/`on_hover`): guideId →
5383
+ * desarme del listener. Se re-arman desde cero en cada `checkGuides` (el
5384
+ * resolve fresco manda) y se limpian en `destroy()`.
5385
+ */
5386
+ this.armedTriggers = /* @__PURE__ */ new Map();
5336
5387
  this.debug = config.debug === true;
5337
5388
  this.apiUrl = config.apiUrl;
5338
5389
  this.apiKey = config.apiKey;
@@ -5390,26 +5441,80 @@ var GuidesController = class {
5390
5441
  return;
5391
5442
  }
5392
5443
  if (this.debug) console.log(`[veo] guides: ${guides.length} eligible at ${pageUrl}`);
5444
+ this.disarmTriggers();
5393
5445
  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}`);
5446
+ const trigger = guide.activationRules.trigger;
5447
+ if ((trigger === "on_click" || trigger === "on_hover") && guide.activationRules.selector) {
5448
+ this.armInteractionTrigger(guide, endUserId, sessionId, pageUrl);
5400
5449
  continue;
5401
5450
  }
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;
5451
+ await this.showGuide(guide, endUserId, sessionId, pageUrl);
5452
+ }
5453
+ }
5454
+ /**
5455
+ * Muestra una guía elegible (filtro de frecuencia + dedupe + dispatch por
5456
+ * tipo). Cuerpo original del loop de `checkGuides`, extraído para que los
5457
+ * disparadores por interacción pasen por LOS MISMOS chequeos al disparar.
5458
+ */
5459
+ async showGuide(guide, endUserId, sessionId, pageUrl) {
5460
+ if (this.frequencyCache.shouldFilter(guide.guideId, guide.displayFrequency)) {
5461
+ if (this.debug) console.log(`[veo] guides: filtered locally guideId=${guide.guideId}`);
5462
+ return;
5463
+ }
5464
+ if (this.activeByGuideId.has(guide.guideId)) {
5465
+ if (this.debug) console.log(`[veo] guides: already shown guideId=${guide.guideId}`);
5466
+ return;
5467
+ }
5468
+ if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
5469
+ if (this.walkthroughState.hasActive()) {
5470
+ if (this.debug)
5471
+ console.log(`[veo] guides: walkthrough ${guide.guideId} skipped (another active)`);
5472
+ return;
5410
5473
  }
5411
- this.runSingleStepRender(guide, sessionId, pageUrl);
5474
+ await this.startWalkthrough(guide, endUserId, sessionId, pageUrl);
5475
+ return;
5412
5476
  }
5477
+ this.runSingleStepRender(guide, sessionId, pageUrl);
5478
+ }
5479
+ /**
5480
+ * Arma un disparador por interacción: la guía NO se muestra al cargar, sino
5481
+ * cuando el usuario hace click / pasa el mouse sobre `activationRules.selector`.
5482
+ * Listener DELEGADO en `document` (fase captura, sin preventDefault — el click
5483
+ * original de la app sigue funcionando). Al disparar se desarma y la guía pasa
5484
+ * por `showGuide` (mismos chequeos de frecuencia/dedupe). Tras cerrarse, el
5485
+ * próximo pageview/identify la re-arma — hover/click vuelve a mostrarla
5486
+ * mientras la frecuencia lo permita.
5487
+ */
5488
+ armInteractionTrigger(guide, endUserId, sessionId, pageUrl) {
5489
+ if (this.frequencyCache.shouldFilter(guide.guideId, guide.displayFrequency)) return;
5490
+ if (this.activeByGuideId.has(guide.guideId)) return;
5491
+ if (this.armedTriggers.has(guide.guideId)) return;
5492
+ const selector = guide.activationRules.selector;
5493
+ const eventName = guide.activationRules.trigger === "on_hover" ? "mouseover" : "click";
5494
+ const onFire = (e) => {
5495
+ const target = e.target;
5496
+ if (!(target instanceof Element)) return;
5497
+ try {
5498
+ if (!target.closest(selector)) return;
5499
+ } catch {
5500
+ return;
5501
+ }
5502
+ disarm();
5503
+ void this.showGuide(guide, endUserId, sessionId, pageUrl);
5504
+ };
5505
+ const disarm = () => {
5506
+ document.removeEventListener(eventName, onFire, true);
5507
+ this.armedTriggers.delete(guide.guideId);
5508
+ };
5509
+ document.addEventListener(eventName, onFire, true);
5510
+ this.armedTriggers.set(guide.guideId, disarm);
5511
+ if (this.debug)
5512
+ console.log(`[veo] guides: armed ${eventName} trigger for ${guide.guideId} on ${selector}`);
5513
+ }
5514
+ /** Desarma todos los disparadores por interacción pendientes. */
5515
+ disarmTriggers() {
5516
+ for (const disarm of this.armedTriggers.values()) disarm();
5517
+ this.armedTriggers.clear();
5413
5518
  }
5414
5519
  /**
5415
5520
  * `resolve` con cache de corta duración + coalescing:
@@ -5460,6 +5565,7 @@ ${pageUrl}`;
5460
5565
  this.activeWalkthroughRenderer = null;
5461
5566
  this.activeWalkthroughGuide = null;
5462
5567
  }
5568
+ this.disarmTriggers();
5463
5569
  this.dispatched.clear();
5464
5570
  this.resolveCache = null;
5465
5571
  this.inflightResolve = null;
@@ -5492,8 +5598,11 @@ ${pageUrl}`;
5492
5598
  this.client.track(eventName, properties);
5493
5599
  };
5494
5600
  const onFormSubmit = (answers) => this.submitFormResponse(guide.guideId, answers);
5601
+ const onOpenGuide = (targetGuideId) => {
5602
+ void this.openGuideById(targetGuideId, sessionId, pageUrl);
5603
+ };
5495
5604
  try {
5496
- void renderer.render({ guide, onInteraction, onClose, onTrack, onFormSubmit });
5605
+ void renderer.render({ guide, onInteraction, onClose, onTrack, onFormSubmit, onOpenGuide });
5497
5606
  } catch (err) {
5498
5607
  if (this.debug) console.error("[veo] renderer.render threw:", err);
5499
5608
  this.activeRenderers.delete(renderer);
@@ -5502,6 +5611,28 @@ ${pageUrl}`;
5502
5611
  }
5503
5612
  }
5504
5613
  }
5614
+ /**
5615
+ * Abre una guía POR ID, disparada desde el botón de otra guía (encadenado).
5616
+ * Ignora las URL rules y el trigger (el autor lo pidió explícitamente desde
5617
+ * un botón); respeta el dedupe de guías activas y el flujo normal de
5618
+ * tracking. 404/red → no-op (la guía destino pudo borrarse).
5619
+ */
5620
+ async openGuideById(guideId, sessionId, pageUrl) {
5621
+ if (this.activeByGuideId.has(guideId)) return;
5622
+ const guide = await this.resolver.fetchById(guideId);
5623
+ if (!guide) {
5624
+ if (this.debug) console.warn(`[veo] guides: chained guide ${guideId} not found`);
5625
+ return;
5626
+ }
5627
+ const endUserId = this.client.getEndUserId() ?? this.client.getAnonymousId();
5628
+ if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
5629
+ if (!this.walkthroughState.hasActive()) {
5630
+ await this.startWalkthrough(guide, endUserId, sessionId, pageUrl);
5631
+ }
5632
+ return;
5633
+ }
5634
+ this.runSingleStepRender(guide, sessionId, pageUrl);
5635
+ }
5505
5636
  /**
5506
5637
  * Si hay un walkthrough persistido, intenta pintar el step actual en
5507
5638
  * la página actual. Retorna `true` si lo intentó (sea éxito o cancel),