veo-sdk 0.5.5 → 0.5.10

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
@@ -654,7 +654,8 @@ function buildButtonBlock(block, doc, callbacks) {
654
654
  btn.textContent = typeof block.text === "string" && block.text ? block.text : "OK";
655
655
  applyBlockStyle(btn, block.style, "button");
656
656
  btn.addEventListener("click", () => {
657
- const action = block.action ?? "dismiss";
657
+ const raw = block.action ?? "dismiss";
658
+ const action = raw === "next" || raw === "url" || raw === "guide" ? raw : "dismiss";
658
659
  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;
659
660
  const meta = {
660
661
  ...typeof block.id === "string" && block.id ? { buttonId: block.id } : {},
@@ -695,6 +696,42 @@ function applyBlockStyle(el, style, kind) {
695
696
  if (typeof s.marginBottom === "number" && Number.isFinite(s.marginBottom)) {
696
697
  el.style.marginBottom = `${clampNum(s.marginBottom, 0, 64)}px`;
697
698
  }
699
+ if (kind !== "image") {
700
+ if (typeof s.fontSize === "number" && Number.isFinite(s.fontSize)) {
701
+ el.style.fontSize = `${clampNum(s.fontSize, 8, 96)}px`;
702
+ }
703
+ if (typeof s.fontFamily === "string" && s.fontFamily && s.fontFamily !== "inherit") {
704
+ el.style.fontFamily = s.fontFamily;
705
+ }
706
+ if (typeof s.fontWeight === "string" && s.fontWeight) el.style.fontWeight = s.fontWeight;
707
+ if (typeof s.textTransform === "string" && s.textTransform && s.textTransform !== "none") {
708
+ el.style.textTransform = s.textTransform;
709
+ }
710
+ if (typeof s.lineHeight === "number" && Number.isFinite(s.lineHeight)) {
711
+ el.style.lineHeight = `${clampNum(s.lineHeight, 50, 300)}%`;
712
+ }
713
+ if (typeof s.letterSpacing === "number" && Number.isFinite(s.letterSpacing)) {
714
+ el.style.letterSpacing = `${clampNum(s.letterSpacing, -5, 20)}px`;
715
+ }
716
+ }
717
+ if (typeof s.background === "string" && s.background) el.style.background = s.background;
718
+ if (typeof s.borderWidth === "number" && s.borderWidth > 0) {
719
+ const bc = typeof s.borderColor === "string" && s.borderColor ? s.borderColor : "#bbbbbb";
720
+ el.style.border = `${clampNum(s.borderWidth, 0, 20)}px solid ${bc}`;
721
+ }
722
+ if (kind !== "image" && typeof s.radius === "number" && Number.isFinite(s.radius)) {
723
+ el.style.borderRadius = `${clampNum(s.radius, 0, 64)}px`;
724
+ }
725
+ for (const side of ["Top", "Right", "Bottom", "Left"]) {
726
+ const p = s[`padding${side}`];
727
+ if (typeof p === "number" && Number.isFinite(p)) {
728
+ el.style.setProperty(`padding-${side.toLowerCase()}`, `${clampNum(p, 0, 128)}px`);
729
+ }
730
+ const m = s[`margin${side}`];
731
+ if (typeof m === "number" && Number.isFinite(m)) {
732
+ el.style.setProperty(`margin-${side.toLowerCase()}`, `${clampNum(m, 0, 128)}px`);
733
+ }
734
+ }
698
735
  }
699
736
  function createCloseButton(doc, onClose, className = "veo-guide-close") {
700
737
  const closeBtn = doc.createElement("button");
@@ -1075,6 +1112,35 @@ var init_styles = __esm({
1075
1112
  animation: veo-fade-in 160ms ease-out;
1076
1113
  }
1077
1114
 
1115
+ /* \u2500\u2500 Modelo unificado (composite) \u2500\u2500 */
1116
+ .veo-composite-overlay {
1117
+ position: fixed; inset: 0;
1118
+ z-index: ${GUIDE_Z_INDEX};
1119
+ pointer-events: none;
1120
+ }
1121
+ /* Backdrop atenuado (solo centro con backdrop activo). */
1122
+ .veo-composite-overlay--dim {
1123
+ background: rgba(0, 0, 0, 0.5);
1124
+ pointer-events: auto;
1125
+ }
1126
+ .veo-composite-anchored {
1127
+ position: fixed;
1128
+ z-index: ${GUIDE_Z_INDEX};
1129
+ }
1130
+ /* Card gen\xE9rica del modelo unificado. El dise\xF1o (bg/borde/padding/\u2026) lo aplica
1131
+ el renderer como estilos inline desde step.design; ac\xE1 van los defaults. */
1132
+ .veo-card {
1133
+ pointer-events: auto;
1134
+ background: var(--veo-bg, #ffffff);
1135
+ color: var(--veo-text, #1f2937);
1136
+ border-radius: var(--veo-radius, 12px);
1137
+ padding: 20px;
1138
+ max-width: 90vw;
1139
+ box-shadow: var(--veo-shadow, 0 12px 40px rgba(0, 0, 0, 0.18));
1140
+ animation: veo-fade-in 160ms ease-out;
1141
+ }
1142
+ .veo-composite-anchored .veo-card, .veo-composite-overlay .veo-card { position: relative; }
1143
+
1078
1144
  .veo-guide-content {
1079
1145
  position: relative;
1080
1146
  display: flex; flex-direction: column;
@@ -2058,91 +2124,58 @@ var init_custom_frame = __esm({
2058
2124
  MAX_HOST_CSS = 800 * 1024;
2059
2125
  }
2060
2126
  });
2061
- function applyFloatingPosition(wrapper, placement) {
2062
- const position = placement.position ?? "bottom-right";
2063
- const ox = `${placement.offsetX ?? DEFAULT_OFFSET}px`;
2064
- const oy = `${placement.offsetY ?? DEFAULT_OFFSET}px`;
2065
- const s = wrapper.style;
2066
- if (position === "center") {
2067
- s.top = "50%";
2068
- s.left = "50%";
2069
- s.transform = "translate(-50%, -50%)";
2070
- return;
2071
- }
2072
- const [vertical, horizontal] = position.split("-");
2073
- if (vertical === "top") s.top = oy;
2074
- else s.bottom = oy;
2075
- if (horizontal === "left") {
2076
- s.left = ox;
2077
- } else if (horizontal === "right") {
2078
- s.right = ox;
2079
- } else {
2080
- s.left = "50%";
2081
- s.transform = "translateX(-50%)";
2127
+
2128
+ // src/plugins/guides/code-block.ts
2129
+ function mountCodeBlock(block, doc, context) {
2130
+ if (block.sandboxed) {
2131
+ const step = { html: block.html ?? "", css: block.css, js: block.js };
2132
+ const { iframe, cleanup } = mountCustomFrame(doc, step, context, { width: "100%" });
2133
+ const wrapper = doc.createElement("div");
2134
+ wrapper.className = "veo-code-sandbox";
2135
+ wrapper.appendChild(iframe);
2136
+ return { node: wrapper, cleanup };
2137
+ }
2138
+ const hostDoc = doc;
2139
+ const injected = [];
2140
+ if (typeof block.css === "string" && block.css.trim()) {
2141
+ const style = hostDoc.createElement("style");
2142
+ style.textContent = block.css;
2143
+ hostDoc.head.appendChild(style);
2144
+ injected.push(style);
2145
+ }
2146
+ let node = null;
2147
+ if (typeof block.html === "string" && block.html.trim()) {
2148
+ node = doc.createElement("div");
2149
+ node.className = "veo-code-html";
2150
+ node.innerHTML = block.html;
2151
+ for (const old of Array.from(node.querySelectorAll("script"))) {
2152
+ injected.push(runScript(old, hostDoc));
2153
+ old.remove();
2154
+ }
2155
+ }
2156
+ if (typeof block.js === "string" && block.js.trim()) {
2157
+ const s = hostDoc.createElement("script");
2158
+ s.textContent = block.js;
2159
+ hostDoc.body.appendChild(s);
2160
+ injected.push(s);
2082
2161
  }
2162
+ return {
2163
+ node,
2164
+ cleanup: () => {
2165
+ for (const n of injected) n.parentNode?.removeChild(n);
2166
+ }
2167
+ };
2083
2168
  }
2084
- var DEFAULT_PLACEMENT, DEFAULT_OFFSET, DEFAULT_WIDTH, CustomRenderer;
2085
- var init_custom_renderer = __esm({
2086
- "src/plugins/guides/renderers/custom-renderer.ts"() {
2087
- init_wait_for_element();
2088
- init_base_renderer();
2169
+ function runScript(old, hostDoc) {
2170
+ const s = hostDoc.createElement("script");
2171
+ for (const attr of Array.from(old.attributes)) s.setAttribute(attr.name, attr.value);
2172
+ if (!old.src) s.textContent = old.textContent;
2173
+ hostDoc.body.appendChild(s);
2174
+ return s;
2175
+ }
2176
+ var init_code_block = __esm({
2177
+ "src/plugins/guides/code-block.ts"() {
2089
2178
  init_custom_frame();
2090
- DEFAULT_PLACEMENT = { mode: "floating", position: "bottom-right" };
2091
- DEFAULT_OFFSET = 16;
2092
- DEFAULT_WIDTH = 360;
2093
- CustomRenderer = class extends BaseRenderer {
2094
- async render(context) {
2095
- const step = context.guide.guideSteps[0];
2096
- if (!step || typeof step.html !== "string" || step.html.length === 0) return;
2097
- const placement = step.placement ?? DEFAULT_PLACEMENT;
2098
- let anchor = null;
2099
- if (placement.mode === "anchored") {
2100
- const selector = placement.selector ?? step.selector ?? context.guide.activationRules.selector;
2101
- if (typeof selector !== "string" || selector.length === 0) return;
2102
- anchor = await waitForElement(selector);
2103
- if (!anchor) return;
2104
- }
2105
- const { root } = this.createHost();
2106
- const doc = root.ownerDocument ?? document;
2107
- const wrapper = doc.createElement("div");
2108
- wrapper.className = placement.mode === "anchored" ? "veo-custom-anchored" : "veo-custom-floating";
2109
- const { iframe, cleanup } = mountCustomFrame(doc, step, context, {
2110
- width: toCssSize(placement.width ?? DEFAULT_WIDTH),
2111
- fixedHeight: placement.height ?? null
2112
- });
2113
- this.registerCleanup(cleanup);
2114
- wrapper.appendChild(iframe);
2115
- root.appendChild(wrapper);
2116
- if (placement.mode === "floating") {
2117
- applyFloatingPosition(wrapper, placement);
2118
- } else if (anchor) {
2119
- this.registerCleanup(await this.setupAnchoredPosition(anchor, wrapper, placement));
2120
- }
2121
- context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
2122
- }
2123
- async setupAnchoredPosition(anchor, wrapper, placement) {
2124
- const side = placement.side ?? "bottom";
2125
- const update = async () => {
2126
- const { x, y } = await dom.computePosition(anchor, wrapper, {
2127
- placement: side,
2128
- strategy: "fixed",
2129
- middleware: [dom.offset(placement.offsetY ?? 8), dom.flip(), dom.shift({ padding: 8 })]
2130
- });
2131
- wrapper.style.left = `${x}px`;
2132
- wrapper.style.top = `${y}px`;
2133
- };
2134
- await update();
2135
- const reposition = () => {
2136
- void update();
2137
- };
2138
- window.addEventListener("scroll", reposition, true);
2139
- window.addEventListener("resize", reposition);
2140
- return () => {
2141
- window.removeEventListener("scroll", reposition, true);
2142
- window.removeEventListener("resize", reposition);
2143
- };
2144
- }
2145
- };
2146
2179
  }
2147
2180
  });
2148
2181
 
@@ -2415,6 +2448,448 @@ var init_form_content = __esm({
2415
2448
  init_block_builder();
2416
2449
  }
2417
2450
  });
2451
+ function inlinePosOf(p) {
2452
+ return p === "before" || p === "prepend" || p === "append" ? p : "after";
2453
+ }
2454
+ function applyViewportPosition(card, position, design) {
2455
+ const s = card.style;
2456
+ s.position = "fixed";
2457
+ const M = "16px";
2458
+ const hAlign = design.hAlign === "left" ? "left" : design.hAlign === "right" ? "right" : null;
2459
+ const v = position.includes("top") ? "top" : position.includes("bottom") ? "bottom" : "middle";
2460
+ const h = hAlign ?? (position.includes("left") ? "left" : position.includes("right") ? "right" : "middle");
2461
+ if (v === "top") s.top = M;
2462
+ else if (v === "bottom") s.bottom = M;
2463
+ else s.top = "50%";
2464
+ if (h === "left") s.left = M;
2465
+ else if (h === "right") s.right = M;
2466
+ else s.left = "50%";
2467
+ const tx = h === "middle" ? "-50%" : "0";
2468
+ const ty = v === "middle" ? "-50%" : "0";
2469
+ s.transform = tx === "0" && ty === "0" ? "" : `translate(${tx}, ${ty})`;
2470
+ }
2471
+ function makeCaret(doc, design) {
2472
+ const caret = doc.createElement("div");
2473
+ caret.className = "veo-composite-caret";
2474
+ const w = typeof design.caretWidth === "number" ? design.caretWidth : 12;
2475
+ const h = typeof design.caretHeight === "number" ? design.caretHeight : 12;
2476
+ const s = caret.style;
2477
+ s.position = "absolute";
2478
+ s.width = `${w}px`;
2479
+ s.height = `${h}px`;
2480
+ s.transform = "rotate(45deg)";
2481
+ s.background = typeof design.background === "string" ? design.background : "#ffffff";
2482
+ if (typeof design.borderWidth === "number" && design.borderWidth > 0) {
2483
+ const bc = typeof design.borderColor === "string" ? design.borderColor : "#bbbbbb";
2484
+ s.border = `${design.borderWidth}px solid ${bc}`;
2485
+ }
2486
+ return caret;
2487
+ }
2488
+ function liftTarget(anchor, zIndex) {
2489
+ const el = anchor;
2490
+ const prevPosition = el.style.position;
2491
+ const prevZ = el.style.zIndex;
2492
+ if (getComputedStyle(el).position === "static") el.style.position = "relative";
2493
+ el.style.zIndex = String(zIndex);
2494
+ return () => {
2495
+ el.style.position = prevPosition;
2496
+ el.style.zIndex = prevZ;
2497
+ };
2498
+ }
2499
+ async function anchorPosition(anchor, wrapper, caret) {
2500
+ const update = async () => {
2501
+ const { x, y, placement, middlewareData } = await dom.computePosition(anchor, wrapper, {
2502
+ placement: "bottom",
2503
+ strategy: "fixed",
2504
+ middleware: [
2505
+ dom.offset(caret ? 10 : 8),
2506
+ dom.flip(),
2507
+ dom.shift({ padding: 8 }),
2508
+ ...caret ? [dom.arrow({ element: caret })] : []
2509
+ ]
2510
+ });
2511
+ wrapper.style.left = `${x}px`;
2512
+ wrapper.style.top = `${y}px`;
2513
+ if (caret) positionArrow(caret, placement, middlewareData.arrow);
2514
+ };
2515
+ await update();
2516
+ const reposition = () => void update();
2517
+ window.addEventListener("scroll", reposition, true);
2518
+ window.addEventListener("resize", reposition);
2519
+ return () => {
2520
+ window.removeEventListener("scroll", reposition, true);
2521
+ window.removeEventListener("resize", reposition);
2522
+ };
2523
+ }
2524
+ function applyCompositeDesign(card, design) {
2525
+ const s = card.style;
2526
+ const px = (v) => typeof v === "number" && Number.isFinite(v) ? `${v}px` : null;
2527
+ const strv = (v) => typeof v === "string" && v ? v : null;
2528
+ if (strv(design.background)) s.background = design.background;
2529
+ const textColor = strv(design.color) ?? strv(design.textColor);
2530
+ if (textColor) s.color = textColor;
2531
+ if (px(design.fontSize)) s.fontSize = px(design.fontSize);
2532
+ if (strv(design.fontWeight)) s.fontWeight = design.fontWeight;
2533
+ if (strv(design.fontFamily) && design.fontFamily !== "inherit") {
2534
+ s.fontFamily = design.fontFamily;
2535
+ }
2536
+ if (strv(design.textTransform) && design.textTransform !== "none") {
2537
+ s.textTransform = design.textTransform;
2538
+ }
2539
+ if (typeof design.letterSpacing === "number") s.letterSpacing = `${design.letterSpacing}px`;
2540
+ if (strv(design.align)) s.textAlign = design.align;
2541
+ if (typeof design.lineHeight === "number") s.lineHeight = `${design.lineHeight}%`;
2542
+ if (px(design.radius)) s.borderRadius = px(design.radius);
2543
+ const bw = px(design.borderWidth);
2544
+ if (bw) s.border = `${bw} solid ${strv(design.borderColor) ?? "#e5e5e5"}`;
2545
+ if (strv(design.shadowColor) || typeof design.shadowBlur === "number") {
2546
+ const n = (k) => typeof design[k] === "number" ? design[k] : 0;
2547
+ s.boxShadow = `${n("shadowX")}px ${n("shadowY")}px ${n("shadowBlur")}px ${n("shadowSpread")}px ${strv(design.shadowColor) ?? "rgba(0,0,0,0.2)"}`;
2548
+ }
2549
+ if (design.fullWidth === true) {
2550
+ s.width = "100%";
2551
+ } else if (design.widthType === "fluid") {
2552
+ s.width = "auto";
2553
+ if (px(design.width)) s.maxWidth = px(design.width);
2554
+ } else if (px(design.width)) {
2555
+ s.width = px(design.width);
2556
+ }
2557
+ for (const side of ["Top", "Right", "Bottom", "Left"]) {
2558
+ const p = px(design[`padding${side}`]);
2559
+ if (p) s.setProperty(`padding-${side.toLowerCase()}`, p);
2560
+ const m = px(design[`margin${side}`]);
2561
+ if (m) s.setProperty(`margin-${side.toLowerCase()}`, m);
2562
+ }
2563
+ }
2564
+ var CompositeRenderer;
2565
+ var init_composite_renderer = __esm({
2566
+ "src/plugins/guides/renderers/composite-renderer.ts"() {
2567
+ init_block_builder();
2568
+ init_code_block();
2569
+ init_form_content();
2570
+ init_inline_host();
2571
+ init_wait_for_element();
2572
+ init_base_renderer();
2573
+ init_floating_arrow();
2574
+ CompositeRenderer = class extends BaseRenderer {
2575
+ async render(context) {
2576
+ const idx = context.nav?.stepIndex ?? 0;
2577
+ const step = context.guide.guideSteps[idx];
2578
+ if (!step) return;
2579
+ const anchorSelector = step.position === "relative-to-element" || step.presentation === "inline" ? step.anchorSelector?.trim() || void 0 : void 0;
2580
+ let anchor = null;
2581
+ if (anchorSelector) {
2582
+ anchor = await waitForElement(anchorSelector);
2583
+ if (!anchor) return;
2584
+ }
2585
+ const { host, root } = this.createHost();
2586
+ const doc = root.ownerDocument ?? document;
2587
+ const design = step.design ?? {};
2588
+ if (typeof design.zIndex === "number") host.style.zIndex = String(design.zIndex);
2589
+ const card = doc.createElement("div");
2590
+ card.className = "veo-card";
2591
+ applyCompositeDesign(card, design);
2592
+ const dismiss = (action, meta) => {
2593
+ context.onInteraction({
2594
+ guideId: context.guide.guideId,
2595
+ stepIndex: idx,
2596
+ action,
2597
+ ...{}
2598
+ });
2599
+ };
2600
+ const readers = [];
2601
+ this.buildContent(card, step.blocks ?? [], doc, context, idx, readers);
2602
+ if (design.closeButton !== false) {
2603
+ const closeBtn = createCloseButton(doc, () => {
2604
+ dismiss("dismissed");
2605
+ this.closeOrSkip(context);
2606
+ });
2607
+ const closeColor = typeof design.closeColor === "string" ? design.closeColor : null;
2608
+ const hoverColor = typeof design.closeHoverColor === "string" ? design.closeHoverColor : null;
2609
+ if (closeColor) closeBtn.style.color = closeColor;
2610
+ if (hoverColor) {
2611
+ closeBtn.addEventListener("mouseenter", () => {
2612
+ closeBtn.style.color = hoverColor;
2613
+ });
2614
+ closeBtn.addEventListener("mouseleave", () => {
2615
+ closeBtn.style.color = closeColor ?? "";
2616
+ });
2617
+ }
2618
+ card.appendChild(closeBtn);
2619
+ }
2620
+ if (step.presentation === "inline" && anchor) {
2621
+ host.style.display = "block";
2622
+ const pos = inlinePosOf(step.inlinePosition);
2623
+ root.appendChild(card);
2624
+ insertHost(anchor, host, pos);
2625
+ this.registerCleanup(keepHostAttached(host, anchorSelector, pos));
2626
+ } else if (step.position === "relative-to-element" && anchor) {
2627
+ if (design.backdrop === true) {
2628
+ const bg = doc.createElement("div");
2629
+ bg.className = "veo-composite-overlay veo-composite-overlay--dim";
2630
+ if (typeof design.backdropColor === "string") bg.style.background = design.backdropColor;
2631
+ const bz = typeof design.backdropZIndex === "number" ? design.backdropZIndex : 2e5;
2632
+ bg.style.zIndex = String(bz);
2633
+ root.appendChild(bg);
2634
+ if (design.targetAbove === true) this.registerCleanup(liftTarget(anchor, bz + 1));
2635
+ }
2636
+ const wrapper = doc.createElement("div");
2637
+ wrapper.className = "veo-composite-anchored";
2638
+ const caretEl = design.caret === false ? null : makeCaret(doc, design);
2639
+ if (caretEl) wrapper.appendChild(caretEl);
2640
+ wrapper.appendChild(card);
2641
+ root.appendChild(wrapper);
2642
+ this.registerCleanup(await anchorPosition(anchor, wrapper, caretEl));
2643
+ } else {
2644
+ const overlay = doc.createElement("div");
2645
+ overlay.className = "veo-composite-overlay";
2646
+ if (step.position === "center" && design.backdrop !== false) {
2647
+ overlay.classList.add("veo-composite-overlay--dim");
2648
+ if (typeof design.backdropColor === "string") {
2649
+ overlay.style.background = design.backdropColor;
2650
+ }
2651
+ if (typeof design.backdropZIndex === "number") {
2652
+ overlay.style.zIndex = String(design.backdropZIndex);
2653
+ }
2654
+ overlay.addEventListener("click", (e) => {
2655
+ if (e.target === overlay) {
2656
+ dismiss("dismissed");
2657
+ this.closeOrSkip(context);
2658
+ }
2659
+ });
2660
+ }
2661
+ applyViewportPosition(card, step.position, design);
2662
+ overlay.appendChild(card);
2663
+ root.appendChild(overlay);
2664
+ }
2665
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: idx, action: "shown" });
2666
+ }
2667
+ /** Recorre los bloques en orden y los pinta en la card. */
2668
+ buildContent(card, blocks, doc, context, idx, readers) {
2669
+ const emit = (block) => {
2670
+ context.onInteraction({
2671
+ guideId: context.guide.guideId,
2672
+ stepIndex: idx,
2673
+ action: "cta_clicked",
2674
+ ...block.id ? { metadata: { buttonId: block.id } } : {}
2675
+ });
2676
+ };
2677
+ let i = 0;
2678
+ while (i < blocks.length) {
2679
+ const b = blocks[i];
2680
+ if (!b) {
2681
+ i++;
2682
+ continue;
2683
+ }
2684
+ if (b.type === "button") {
2685
+ const row = doc.createElement("div");
2686
+ row.className = "veo-guide-actions";
2687
+ while (i < blocks.length && blocks[i]?.type === "button") {
2688
+ row.appendChild(
2689
+ this.buildButton(blocks[i], doc, context, idx, readers, emit)
2690
+ );
2691
+ i++;
2692
+ }
2693
+ card.appendChild(row);
2694
+ continue;
2695
+ }
2696
+ if (b.type === "code") {
2697
+ const { node: node2, cleanup } = mountCodeBlock(b, doc, context);
2698
+ if (node2) card.appendChild(node2);
2699
+ this.registerCleanup(cleanup);
2700
+ i++;
2701
+ continue;
2702
+ }
2703
+ if (b.type === "form") {
2704
+ const form = doc.createElement("div");
2705
+ form.className = "veo-form";
2706
+ for (const field of b.fields ?? []) {
2707
+ const { wrapper, read } = buildField(field, doc);
2708
+ readers.push({ field, read });
2709
+ form.appendChild(wrapper);
2710
+ }
2711
+ card.appendChild(form);
2712
+ i++;
2713
+ continue;
2714
+ }
2715
+ const node = buildContentBlock(b, doc);
2716
+ if (node) card.appendChild(node);
2717
+ i++;
2718
+ }
2719
+ }
2720
+ /** Botón con enrutado de acción (navegación / cierre / url / guía / envío de forms). */
2721
+ buildButton(block, doc, context, idx, readers, emit) {
2722
+ const btn = doc.createElement("button");
2723
+ btn.type = "button";
2724
+ btn.className = "veo-guide-cta";
2725
+ btn.textContent = block.text?.trim() || "OK";
2726
+ const nav = context.nav?.callbacks;
2727
+ btn.addEventListener("click", () => {
2728
+ switch (block.action) {
2729
+ case "next":
2730
+ emit(block);
2731
+ if (nav) nav.onNext();
2732
+ else context.onClose();
2733
+ break;
2734
+ case "prev":
2735
+ emit(block);
2736
+ nav?.onBack();
2737
+ break;
2738
+ case "url":
2739
+ emit(block);
2740
+ if (typeof block.url === "string" && isSafeUrl(block.url)) {
2741
+ window.open(block.url, "_blank", "noopener,noreferrer");
2742
+ }
2743
+ break;
2744
+ case "guide":
2745
+ emit(block);
2746
+ if (block.targetGuideId) context.onOpenGuide?.(block.targetGuideId);
2747
+ this.closeOrSkip(context);
2748
+ break;
2749
+ case "submit-forms":
2750
+ case "submit-forms-advance":
2751
+ case "submit-forms-goto":
2752
+ this.submitForms(readers, context, idx, btn, () => {
2753
+ emit(block);
2754
+ if (block.action === "submit-forms" || !nav) {
2755
+ context.onClose();
2756
+ } else if (block.action === "submit-forms-goto" && typeof block.targetStep === "number" && nav.onGoto) {
2757
+ nav.onGoto(block.targetStep);
2758
+ } else {
2759
+ nav.onNext();
2760
+ }
2761
+ });
2762
+ break;
2763
+ default:
2764
+ emit(block);
2765
+ this.closeOrSkip(context);
2766
+ }
2767
+ });
2768
+ return btn;
2769
+ }
2770
+ /** Recoge respuestas de todos los form-blocks del paso, valida y envía. */
2771
+ submitForms(readers, context, idx, btn, onDone) {
2772
+ const answers = {};
2773
+ for (const { field, read } of readers) {
2774
+ const value = read();
2775
+ const empty = value === void 0 || value === null || value === "";
2776
+ if (empty) {
2777
+ if (field.required) {
2778
+ btn.textContent = `Completa "${field.label}"`;
2779
+ return;
2780
+ }
2781
+ continue;
2782
+ }
2783
+ answers[field.key] = value;
2784
+ }
2785
+ if (readers.length > 0 && Object.keys(answers).length === 0) return;
2786
+ btn.disabled = true;
2787
+ const submitFn = context.onFormSubmit ?? (() => Promise.resolve(true));
2788
+ void submitFn(answers).then((ok) => {
2789
+ btn.disabled = false;
2790
+ if (!ok) return;
2791
+ context.onInteraction({
2792
+ guideId: context.guide.guideId,
2793
+ stepIndex: idx,
2794
+ action: "completed"
2795
+ });
2796
+ onDone();
2797
+ });
2798
+ }
2799
+ closeOrSkip(context) {
2800
+ if (context.nav?.callbacks) context.nav.callbacks.onSkip();
2801
+ else context.onClose();
2802
+ }
2803
+ };
2804
+ }
2805
+ });
2806
+ function applyFloatingPosition(wrapper, placement) {
2807
+ const position = placement.position ?? "bottom-right";
2808
+ const ox = `${placement.offsetX ?? DEFAULT_OFFSET}px`;
2809
+ const oy = `${placement.offsetY ?? DEFAULT_OFFSET}px`;
2810
+ const s = wrapper.style;
2811
+ if (position === "center") {
2812
+ s.top = "50%";
2813
+ s.left = "50%";
2814
+ s.transform = "translate(-50%, -50%)";
2815
+ return;
2816
+ }
2817
+ const [vertical, horizontal] = position.split("-");
2818
+ if (vertical === "top") s.top = oy;
2819
+ else s.bottom = oy;
2820
+ if (horizontal === "left") {
2821
+ s.left = ox;
2822
+ } else if (horizontal === "right") {
2823
+ s.right = ox;
2824
+ } else {
2825
+ s.left = "50%";
2826
+ s.transform = "translateX(-50%)";
2827
+ }
2828
+ }
2829
+ var DEFAULT_PLACEMENT, DEFAULT_OFFSET, DEFAULT_WIDTH, CustomRenderer;
2830
+ var init_custom_renderer = __esm({
2831
+ "src/plugins/guides/renderers/custom-renderer.ts"() {
2832
+ init_wait_for_element();
2833
+ init_base_renderer();
2834
+ init_custom_frame();
2835
+ DEFAULT_PLACEMENT = { mode: "floating", position: "bottom-right" };
2836
+ DEFAULT_OFFSET = 16;
2837
+ DEFAULT_WIDTH = 360;
2838
+ CustomRenderer = class extends BaseRenderer {
2839
+ async render(context) {
2840
+ const step = context.guide.guideSteps[0];
2841
+ if (!step || typeof step.html !== "string" || step.html.length === 0) return;
2842
+ const placement = step.placement ?? DEFAULT_PLACEMENT;
2843
+ let anchor = null;
2844
+ if (placement.mode === "anchored") {
2845
+ const selector = placement.selector ?? step.selector ?? context.guide.activationRules.selector;
2846
+ if (typeof selector !== "string" || selector.length === 0) return;
2847
+ anchor = await waitForElement(selector);
2848
+ if (!anchor) return;
2849
+ }
2850
+ const { root } = this.createHost();
2851
+ const doc = root.ownerDocument ?? document;
2852
+ const wrapper = doc.createElement("div");
2853
+ wrapper.className = placement.mode === "anchored" ? "veo-custom-anchored" : "veo-custom-floating";
2854
+ const { iframe, cleanup } = mountCustomFrame(doc, step, context, {
2855
+ width: toCssSize(placement.width ?? DEFAULT_WIDTH),
2856
+ fixedHeight: placement.height ?? null
2857
+ });
2858
+ this.registerCleanup(cleanup);
2859
+ wrapper.appendChild(iframe);
2860
+ root.appendChild(wrapper);
2861
+ if (placement.mode === "floating") {
2862
+ applyFloatingPosition(wrapper, placement);
2863
+ } else if (anchor) {
2864
+ this.registerCleanup(await this.setupAnchoredPosition(anchor, wrapper, placement));
2865
+ }
2866
+ context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "shown" });
2867
+ }
2868
+ async setupAnchoredPosition(anchor, wrapper, placement) {
2869
+ const side = placement.side ?? "bottom";
2870
+ const update = async () => {
2871
+ const { x, y } = await dom.computePosition(anchor, wrapper, {
2872
+ placement: side,
2873
+ strategy: "fixed",
2874
+ middleware: [dom.offset(placement.offsetY ?? 8), dom.flip(), dom.shift({ padding: 8 })]
2875
+ });
2876
+ wrapper.style.left = `${x}px`;
2877
+ wrapper.style.top = `${y}px`;
2878
+ };
2879
+ await update();
2880
+ const reposition = () => {
2881
+ void update();
2882
+ };
2883
+ window.addEventListener("scroll", reposition, true);
2884
+ window.addEventListener("resize", reposition);
2885
+ return () => {
2886
+ window.removeEventListener("scroll", reposition, true);
2887
+ window.removeEventListener("resize", reposition);
2888
+ };
2889
+ }
2890
+ };
2891
+ }
2892
+ });
2418
2893
 
2419
2894
  // src/plugins/guides/renderers/form-renderer.ts
2420
2895
  var FormRenderer;
@@ -2880,8 +3355,12 @@ function createSingleStepRenderer(type) {
2880
3355
  return new InlineFormRenderer();
2881
3356
  case "badge":
2882
3357
  return new BadgeRenderer();
3358
+ case "composite":
3359
+ return new CompositeRenderer();
2883
3360
  case "walkthrough":
2884
3361
  return null;
3362
+ default:
3363
+ return null;
2885
3364
  }
2886
3365
  }
2887
3366
  function safeDestroy(renderer) {
@@ -2897,6 +3376,7 @@ var init_guide_preview = __esm({
2897
3376
  init_badge_renderer();
2898
3377
  init_banner_renderer();
2899
3378
  init_base_renderer();
3379
+ init_composite_renderer();
2900
3380
  init_custom_renderer();
2901
3381
  init_form_renderer();
2902
3382
  init_inline_custom_renderer();
@@ -2930,6 +3410,13 @@ var init_guide_preview = __esm({
2930
3410
  this.input = input;
2931
3411
  const guide = normalize(input);
2932
3412
  const start = typeof input.startStepIndex === "number" ? input.startStepIndex : 0;
3413
+ if (guide.guideType === "composite") {
3414
+ const s = Math.min(Math.max(0, start), guide.guideSteps.length - 1);
3415
+ const active2 = guide.guideSteps[s];
3416
+ if (active2) guide.guideSteps = [active2];
3417
+ const ready2 = this.renderSingleStep(guide);
3418
+ return { close: () => this.close(), ready: ready2, host: () => this.activeHost() };
3419
+ }
2933
3420
  const ready = guide.guideType === "walkthrough" ? this.startWalkthrough(guide, start) : this.renderSingleStep(guide);
2934
3421
  return { close: () => this.close(), ready, host: () => this.activeHost() };
2935
3422
  }
@@ -5434,6 +5921,7 @@ var GuideTrackerQueue = class {
5434
5921
  // src/plugins/guides/guides-controller.ts
5435
5922
  init_badge_renderer();
5436
5923
  init_banner_renderer();
5924
+ init_composite_renderer();
5437
5925
  init_custom_renderer();
5438
5926
  init_form_renderer();
5439
5927
  init_inline_custom_renderer();
@@ -5443,6 +5931,26 @@ init_modal_renderer();
5443
5931
  init_tooltip_renderer();
5444
5932
  init_walkthrough_renderer();
5445
5933
 
5934
+ // src/plugins/guides/url-match.ts
5935
+ function matchesUrl(matcher, pageUrl) {
5936
+ switch (matcher.type) {
5937
+ case "exact":
5938
+ return matcher.pattern === pageUrl;
5939
+ case "prefix":
5940
+ return pageUrl.startsWith(matcher.pattern);
5941
+ case "regex":
5942
+ try {
5943
+ return new RegExp(matcher.pattern).test(pageUrl);
5944
+ } catch {
5945
+ return false;
5946
+ }
5947
+ }
5948
+ }
5949
+ function stepMatchesPage(loc, pageUrl) {
5950
+ if (!loc || loc.scope === "sitewide" || !loc.pattern) return true;
5951
+ return matchesUrl({ type: loc.matchType ?? "prefix", pattern: loc.pattern }, pageUrl);
5952
+ }
5953
+
5446
5954
  // src/plugins/guides/walkthrough-state.ts
5447
5955
  init_constants2();
5448
5956
  var WalkthroughStateManager = class {
@@ -5597,6 +6105,12 @@ var GuidesController = class {
5597
6105
  * resolve fresco manda) y se limpian en `destroy()`.
5598
6106
  */
5599
6107
  this.armedTriggers = /* @__PURE__ */ new Map();
6108
+ /**
6109
+ * Tour composite ESPERANDO navegación: el usuario avanzó a un paso cuya
6110
+ * `pageLocation` apunta a otra página. Se resume en `checkGuides` cuando llega
6111
+ * a esa página (tours multi-página estilo Pendo).
6112
+ */
6113
+ this.pendingComposite = null;
5600
6114
  this.debug = config.debug === true;
5601
6115
  this.apiUrl = config.apiUrl;
5602
6116
  this.apiKey = config.apiKey;
@@ -5646,6 +6160,7 @@ var GuidesController = class {
5646
6160
  if (await this.tryResumeWalkthrough(endUserId, sessionId, pageUrl)) {
5647
6161
  return;
5648
6162
  }
6163
+ this.tryResumeComposite(sessionId, pageUrl);
5649
6164
  let guides;
5650
6165
  try {
5651
6166
  guides = await this.resolveGuides(endUserId, pageUrl);
@@ -5687,6 +6202,10 @@ var GuidesController = class {
5687
6202
  await this.startWalkthrough(guide, endUserId, sessionId, pageUrl);
5688
6203
  return;
5689
6204
  }
6205
+ if (guide.guideType === "composite") {
6206
+ this.startComposite(guide, sessionId, pageUrl);
6207
+ return;
6208
+ }
5690
6209
  this.runSingleStepRender(guide, sessionId, pageUrl);
5691
6210
  }
5692
6211
  /**
@@ -5794,6 +6313,91 @@ ${pageUrl}`;
5794
6313
  delayMs
5795
6314
  );
5796
6315
  }
6316
+ /**
6317
+ * Camino del modelo unificado (`composite`). Elige el PRIMER paso cuya
6318
+ * `pageLocation` matchea la URL actual (targeting por-paso) y lo pinta con el
6319
+ * `CompositeRenderer`. Si ningún paso aplica a esta página, no muestra nada.
6320
+ */
6321
+ startComposite(guide, sessionId, pageUrl) {
6322
+ if (this.pendingComposite?.guide.guideId === guide.guideId) return;
6323
+ const steps = guide.guideSteps;
6324
+ const start = steps.findIndex((s) => stepMatchesPage(s.pageLocation, pageUrl));
6325
+ if (start === -1) return;
6326
+ this.renderCompositeStep(guide, start, sessionId, pageUrl);
6327
+ }
6328
+ /**
6329
+ * Resume un tour composite que quedó esperando navegación: si el paso
6330
+ * pendiente aplica a la página actual, lo pinta. Devuelve `true` si resumió.
6331
+ */
6332
+ tryResumeComposite(sessionId, pageUrl) {
6333
+ const pending = this.pendingComposite;
6334
+ if (!pending) return false;
6335
+ const steps = pending.guide.guideSteps;
6336
+ const step = steps[pending.stepIndex];
6337
+ if (!step) {
6338
+ this.pendingComposite = null;
6339
+ return false;
6340
+ }
6341
+ if (!stepMatchesPage(step.pageLocation, pageUrl)) return false;
6342
+ this.pendingComposite = null;
6343
+ this.renderCompositeStep(pending.guide, pending.stepIndex, sessionId, pageUrl);
6344
+ return true;
6345
+ }
6346
+ /**
6347
+ * Pinta UN paso composite. En guías multi-paso pasa `nav` al renderer para
6348
+ * que los botones next/prev avancen (misma página); en single-step los
6349
+ * botones cierran. Cada avance destruye el paso anterior y monta el siguiente.
6350
+ */
6351
+ renderCompositeStep(guide, idx, sessionId, pageUrl) {
6352
+ const steps = guide.guideSteps;
6353
+ const renderer = new CompositeRenderer();
6354
+ this.activeRenderers.add(renderer);
6355
+ this.activeByGuideId.set(guide.guideId, renderer);
6356
+ const cleanup = () => {
6357
+ renderer.destroy();
6358
+ this.activeRenderers.delete(renderer);
6359
+ if (this.activeByGuideId.get(guide.guideId) === renderer) {
6360
+ this.activeByGuideId.delete(guide.guideId);
6361
+ }
6362
+ };
6363
+ const goToStep = (target) => {
6364
+ cleanup();
6365
+ if (target < 0 || target >= steps.length) return;
6366
+ const step = steps[target];
6367
+ if (step && stepMatchesPage(step.pageLocation, pageUrl)) {
6368
+ this.renderCompositeStep(guide, target, sessionId, pageUrl);
6369
+ } else {
6370
+ this.pendingComposite = { guide, stepIndex: target };
6371
+ }
6372
+ };
6373
+ const nav = steps.length > 1 ? {
6374
+ stepIndex: idx,
6375
+ totalSteps: steps.length,
6376
+ callbacks: {
6377
+ onNext: () => goToStep(idx + 1),
6378
+ onBack: () => goToStep(idx - 1),
6379
+ onGoto: (target) => goToStep(target),
6380
+ onSkip: cleanup,
6381
+ onComplete: cleanup
6382
+ }
6383
+ } : void 0;
6384
+ try {
6385
+ void renderer.render({
6386
+ guide,
6387
+ onInteraction: (e) => this.handleInteraction(guide, sessionId, pageUrl, e),
6388
+ onClose: cleanup,
6389
+ onTrack: (name, props) => this.client.track(name, props),
6390
+ onFormSubmit: (answers) => this.submitFormResponse(guide.guideId, answers),
6391
+ onOpenGuide: (targetGuideId) => {
6392
+ void this.openGuideById(targetGuideId, sessionId, pageUrl);
6393
+ },
6394
+ ...nav ? { nav } : {}
6395
+ });
6396
+ } catch (err) {
6397
+ if (this.debug) console.error("[veo] composite renderer threw:", err);
6398
+ cleanup();
6399
+ }
6400
+ }
5797
6401
  mountSingleStepRenderer(guide, renderer, sessionId, pageUrl) {
5798
6402
  this.activeRenderers.add(renderer);
5799
6403
  this.activeByGuideId.set(guide.guideId, renderer);
@@ -6108,8 +6712,12 @@ ${pageUrl}`;
6108
6712
  return new InlineFormRenderer();
6109
6713
  case "badge":
6110
6714
  return new BadgeRenderer();
6715
+ case "composite":
6716
+ return new CompositeRenderer();
6111
6717
  case "walkthrough":
6112
6718
  return null;
6719
+ default:
6720
+ return null;
6113
6721
  }
6114
6722
  }
6115
6723
  /**