veo-sdk 0.5.34 → 0.6.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.
package/dist/builder.cjs CHANGED
@@ -417,6 +417,125 @@ function rebuildNode(node, doc, depth, budget) {
417
417
  return rebuilt;
418
418
  }
419
419
 
420
+ // src/plugins/guides/video-block.ts
421
+ function videoEmbedUrl(provider, url) {
422
+ let u;
423
+ try {
424
+ u = new URL(url);
425
+ } catch {
426
+ return null;
427
+ }
428
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
429
+ const host = u.hostname.replace(/^www\./, "").replace(/^m\./, "");
430
+ const id = (re, from) => {
431
+ const m = from.match(re);
432
+ return m?.[1] ?? null;
433
+ };
434
+ switch (provider) {
435
+ case "youtube": {
436
+ let v = null;
437
+ if (host === "youtube.com" || host === "youtube-nocookie.com") {
438
+ v = u.searchParams.get("v") ?? id(/^\/(?:embed|shorts|live)\/([A-Za-z0-9_-]{6,})/, u.pathname);
439
+ } else if (host === "youtu.be") {
440
+ v = id(/^\/([A-Za-z0-9_-]{6,})/, u.pathname);
441
+ }
442
+ if (!v || !/^[A-Za-z0-9_-]{6,}$/.test(v)) return null;
443
+ return `https://www.youtube-nocookie.com/embed/${v}`;
444
+ }
445
+ case "vimeo": {
446
+ if (host !== "vimeo.com" && host !== "player.vimeo.com") return null;
447
+ const v = id(/(\d{6,})/, u.pathname);
448
+ return v ? `https://player.vimeo.com/video/${v}` : null;
449
+ }
450
+ case "loom": {
451
+ if (host !== "loom.com") return null;
452
+ const v = id(/^\/(?:share|embed)\/([A-Za-z0-9]{16,})/, u.pathname);
453
+ return v ? `https://www.loom.com/embed/${v}` : null;
454
+ }
455
+ case "wistia": {
456
+ if (!under(host, "wistia.com") && !under(host, "wistia.net")) return null;
457
+ const v = id(/\/(?:medias|embed\/iframe)\/([A-Za-z0-9]{6,})/, u.pathname);
458
+ return v ? `https://fast.wistia.net/embed/iframe/${v}` : null;
459
+ }
460
+ case "msstream": {
461
+ if (!under(host, "microsoftstream.com")) return null;
462
+ const v = id(/\/(?:embed\/)?video\/([0-9a-f-]{36})/i, u.pathname);
463
+ return v ? `https://web.microsoftstream.com/embed/video/${v}?autoplay=false&showinfo=true` : null;
464
+ }
465
+ case "vidyard": {
466
+ if (!under(host, "vidyard.com")) return null;
467
+ const v = id(/\/watch\/([A-Za-z0-9_-]{8,})/, u.pathname) ?? (host === "play.vidyard.com" ? id(/^\/([A-Za-z0-9_-]{8,})/, u.pathname) : null);
468
+ return v ? `https://play.vidyard.com/${v}` : null;
469
+ }
470
+ case "sharepoint":
471
+ return u.protocol === "https:" && under(host, "sharepoint.com") ? u.href : null;
472
+ case "brightcove":
473
+ return u.protocol === "https:" && host === "players.brightcove.net" && u.searchParams.get("videoId") ? u.href : null;
474
+ case "kaltura":
475
+ return u.protocol === "https:" && under(host, "kaltura.com") && u.pathname.startsWith("/p/") ? u.href : null;
476
+ default:
477
+ return null;
478
+ }
479
+ }
480
+ function under(host, domain) {
481
+ return host === domain || host.endsWith(`.${domain}`);
482
+ }
483
+ function fileVideoUrl(url) {
484
+ try {
485
+ const u = new URL(url);
486
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
487
+ return /\.(mp4|webm|mov)$/i.test(u.pathname) ? u.href : null;
488
+ } catch {
489
+ return null;
490
+ }
491
+ }
492
+ function buildVideoBlock(block, doc, opts = {}) {
493
+ const isFile = block.provider === "file";
494
+ const src = typeof block.url === "string" ? isFile ? fileVideoUrl(block.url) : videoEmbedUrl(block.provider, block.url) : null;
495
+ if (!src) return null;
496
+ const s = block.style ?? {};
497
+ const wrap = doc.createElement("div");
498
+ wrap.className = "veo-guide-video";
499
+ const ratio = s.aspect === "4:3" ? "4 / 3" : "16 / 9";
500
+ wrap.style.aspectRatio = ratio;
501
+ if (s.widthMode === "fill") {
502
+ wrap.style.width = "100%";
503
+ } else {
504
+ const w = typeof s.width === "number" && Number.isFinite(s.width) ? s.width : 400;
505
+ wrap.style.width = `${Math.max(120, Math.min(1600, w))}px`;
506
+ wrap.style.maxWidth = "100%";
507
+ }
508
+ const align = s.align === "center" ? "center" : s.align === "right" ? "flex-end" : "flex-start";
509
+ wrap.style.alignSelf = align;
510
+ if (isFile) {
511
+ const video = doc.createElement("video");
512
+ video.src = src;
513
+ video.controls = true;
514
+ video.setAttribute("playsinline", "");
515
+ video.preload = "metadata";
516
+ video.setAttribute(
517
+ "aria-label",
518
+ typeof block.alt === "string" && block.alt.trim() ? block.alt.trim() : "Video"
519
+ );
520
+ if (opts.editing) video.style.pointerEvents = "none";
521
+ wrap.appendChild(video);
522
+ return wrap;
523
+ }
524
+ const frame = doc.createElement("iframe");
525
+ frame.src = src;
526
+ frame.title = typeof block.alt === "string" && block.alt.trim() ? block.alt.trim() : "Video";
527
+ frame.setAttribute("loading", "lazy");
528
+ frame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
529
+ frame.setAttribute(
530
+ "allow",
531
+ `accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture${opts.editing ? "" : "; fullscreen"}`
532
+ );
533
+ if (!opts.editing) frame.setAttribute("allowfullscreen", "");
534
+ if (opts.editing) frame.style.pointerEvents = "none";
535
+ wrap.appendChild(frame);
536
+ return wrap;
537
+ }
538
+
420
539
  // src/plugins/guides/block-builder.ts
421
540
  function buildStepContent(step, doc, callbacks) {
422
541
  const container = doc.createElement("div");
@@ -541,8 +660,12 @@ function renderBlocks(container, blocks, doc, callbacks) {
541
660
  i++;
542
661
  }
543
662
  }
544
- function buildContentBlock(block, doc) {
663
+ function buildContentBlock(block, doc, opts = {}) {
545
664
  switch (block.type) {
665
+ case "video":
666
+ return buildVideoBlock(block, doc, opts);
667
+ case "divider":
668
+ return buildDividerBlock(block, doc);
546
669
  case "title": {
547
670
  const el2 = doc.createElement("h2");
548
671
  el2.className = "veo-guide-title";
@@ -566,7 +689,7 @@ function buildContentBlock(block, doc) {
566
689
  const el2 = doc.createElement("img");
567
690
  el2.className = "veo-guide-image";
568
691
  el2.src = block.url;
569
- el2.alt = "";
692
+ el2.alt = typeof block.alt === "string" ? block.alt : "";
570
693
  applyBlockStyle(el2, block.style, "image");
571
694
  return el2;
572
695
  }
@@ -574,6 +697,32 @@ function buildContentBlock(block, doc) {
574
697
  return null;
575
698
  }
576
699
  }
700
+ var DIVIDER_LINE_STYLES = /* @__PURE__ */ new Set(["solid", "dotted", "dashed"]);
701
+ function buildDividerBlock(block, doc) {
702
+ const s = block.style ?? {};
703
+ const wrap = doc.createElement("div");
704
+ wrap.className = "veo-guide-divider";
705
+ wrap.setAttribute("role", "separator");
706
+ const hr = doc.createElement("hr");
707
+ const custom = s.preset === "custom";
708
+ const color = custom && typeof s.color === "string" && s.color ? s.color : s.colorMode === "dark" ? "rgba(255, 255, 255, 0.35)" : "#e5e7eb";
709
+ const thickness = custom && typeof s.thickness === "number" && Number.isFinite(s.thickness) ? clampNum(Math.round(s.thickness), 1, 12) : 1;
710
+ const lineStyle = typeof s.lineStyle === "string" && DIVIDER_LINE_STYLES.has(s.lineStyle) ? s.lineStyle : "solid";
711
+ hr.style.borderTop = `${thickness}px ${lineStyle} ${color}`;
712
+ wrap.appendChild(hr);
713
+ for (const [k, prop] of [
714
+ ["marginTop", "marginTop"],
715
+ ["marginRight", "marginRight"],
716
+ ["marginBottom", "marginBottom"],
717
+ ["marginLeft", "marginLeft"]
718
+ ]) {
719
+ const v = s[k];
720
+ if (typeof v === "number" && Number.isFinite(v)) {
721
+ wrap.style[prop] = `${clampNum(v, 0, 64)}px`;
722
+ }
723
+ }
724
+ return wrap;
725
+ }
577
726
  function buildButtonBlock(block, doc, callbacks) {
578
727
  const btn = doc.createElement("button");
579
728
  btn.type = "button";
@@ -600,6 +749,12 @@ var BLOCK_ALIGN_SELF = {
600
749
  function clampNum(n, min, max) {
601
750
  return Math.min(max, Math.max(min, n));
602
751
  }
752
+ var TEXT_PRESETS = {
753
+ title: { size: 18, weight: 600, lineHeight: 1.3 },
754
+ subtitle: { size: 16, weight: 600, lineHeight: 1.35 },
755
+ paragraph: { size: 14, weight: 400, lineHeight: 1.5 },
756
+ small: { size: 12, weight: 400, lineHeight: 1.5 }
757
+ };
603
758
  function applyBlockStyle(el2, style, kind) {
604
759
  if (!style || typeof style !== "object") return;
605
760
  const s = style;
@@ -609,14 +764,34 @@ function applyBlockStyle(el2, style, kind) {
609
764
  }
610
765
  const align = typeof s.align === "string" ? s.align : null;
611
766
  if (align && (align === "left" || align === "center" || align === "right")) {
612
- if (kind === "text") el2.style.textAlign = align;
613
- else if (kind === "image") el2.style.alignSelf = BLOCK_ALIGN_SELF[align];
767
+ if (kind === "text") {
768
+ el2.style.textAlign = align;
769
+ el2.style.alignSelf = BLOCK_ALIGN_SELF[align];
770
+ } else if (kind === "image") el2.style.alignSelf = BLOCK_ALIGN_SELF[align];
771
+ }
772
+ if (kind === "text" && typeof s.preset === "string" && s.preset in TEXT_PRESETS) {
773
+ const preset = TEXT_PRESETS[s.preset];
774
+ if (preset) {
775
+ el2.style.fontSize = `${preset.size}px`;
776
+ el2.style.fontWeight = String(preset.weight);
777
+ el2.style.lineHeight = String(preset.lineHeight);
778
+ }
779
+ }
780
+ if (kind !== "image" && s.colorMode === "dark") {
781
+ el2.style.background = "#1f1d1b";
782
+ el2.style.color = "#f5f0eb";
783
+ el2.style.borderRadius = el2.style.borderRadius || "8px";
614
784
  }
615
785
  if (kind !== "image" && typeof s.color === "string") el2.style.color = s.color;
616
786
  if (kind === "text" && typeof s.fontSize === "number" && Number.isFinite(s.fontSize)) {
617
787
  el2.style.fontSize = `${clampNum(s.fontSize, 8, 72)}px`;
618
788
  }
619
- if (kind === "image" && typeof s.width === "number" && Number.isFinite(s.width)) {
789
+ if (kind === "image" && s.naturalSize === true) {
790
+ el2.style.width = "auto";
791
+ el2.style.maxWidth = "100%";
792
+ el2.style.maxHeight = "none";
793
+ el2.style.objectFit = "contain";
794
+ } else if (kind === "image" && typeof s.width === "number" && Number.isFinite(s.width)) {
620
795
  el2.style.width = `${clampNum(s.width, 10, 100)}%`;
621
796
  }
622
797
  if (kind === "image" && typeof s.radius === "number" && Number.isFinite(s.radius)) {
@@ -696,67 +871,326 @@ function readCtaUrl(step) {
696
871
  return isSafeUrl(candidate) ? candidate : void 0;
697
872
  }
698
873
 
699
- // src/plugins/guides/guide-design.ts
700
- var DARK_THEME = {
701
- "--veo-bg": "#1f2937",
702
- "--veo-text": "#f9fafb",
703
- "--veo-text-secondary": "#d1d5db",
704
- "--veo-shadow": "0 20px 60px rgba(0, 0, 0, 0.55)"
705
- };
706
- var ALIGN_JUSTIFY = {
707
- left: "flex-start",
708
- center: "center",
709
- right: "flex-end"
710
- };
711
- var ALIGN_SELF = {
712
- left: "flex-start",
713
- center: "center",
714
- right: "flex-end"
715
- };
716
- var ELEMENT_ALIGN_MODE = {
717
- title: "text",
718
- text: "text",
719
- image: "self"
720
- };
721
- var SHADOW = {
722
- none: "none",
723
- soft: "0 4px 14px rgba(0, 0, 0, 0.10)",
724
- medium: "0 8px 30px rgba(0, 0, 0, 0.18)",
725
- strong: "0 24px 60px rgba(0, 0, 0, 0.32)"
726
- };
727
- var PRESET_POS = {
728
- center: [0.5, 0.5],
729
- top: [0.5, 0],
730
- bottom: [0.5, 1],
731
- left: [0, 0.5],
732
- right: [1, 0.5],
733
- "top-left": [0, 0],
734
- "top-right": [1, 0],
735
- "bottom-left": [0, 1],
736
- "bottom-right": [1, 1]
737
- };
738
- var COLOR_RE = /^#[0-9a-f]{3,8}$|^(rgb|rgba|hsl|hsla)\([\d.,%/\sdeg]+\)$|^[a-z]{3,20}$/i;
739
- function clamp(n, min, max) {
740
- return Math.min(max, Math.max(min, n));
741
- }
742
- function applyDesignVars(host, style) {
743
- if (!style || typeof style !== "object") return;
744
- const s = style;
745
- if (typeof s.accentColor === "string" && COLOR_RE.test(s.accentColor.trim())) {
746
- host.style.setProperty("--veo-primary", s.accentColor.trim());
874
+ // src/utils/uuid.ts
875
+ function uuidv7() {
876
+ const timestamp = Date.now();
877
+ const timeHex = timestamp.toString(16).padStart(12, "0");
878
+ const random = new Uint8Array(10);
879
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
880
+ crypto.getRandomValues(random);
881
+ } else {
882
+ for (let i = 0; i < random.length; i++) {
883
+ random[i] = Math.floor(Math.random() * 256);
884
+ }
747
885
  }
748
- if (typeof s.fontFamily === "string" && s.fontFamily.length <= 200 && !/[<>{}]/.test(s.fontFamily)) {
749
- host.style.setProperty("--veo-font", s.fontFamily.trim());
886
+ random[0] = (random[0] ?? 0) & 15 | 112;
887
+ random[2] = (random[2] ?? 0) & 63 | 128;
888
+ const hex = Array.from(random, (b) => b.toString(16).padStart(2, "0")).join("");
889
+ return [
890
+ timeHex.slice(0, 8),
891
+ timeHex.slice(8, 12),
892
+ hex.slice(0, 4),
893
+ hex.slice(4, 8),
894
+ hex.slice(8, 20)
895
+ ].join("-");
896
+ }
897
+
898
+ // src/plugins/guides/renderers/custom-frame.ts
899
+ function mountCustomFrame(doc, step, context, opts) {
900
+ const iframe = doc.createElement("iframe");
901
+ iframe.className = "veo-custom-frame";
902
+ iframe.setAttribute("sandbox", "allow-scripts");
903
+ iframe.setAttribute("title", context.guide.guideName || "OnUser");
904
+ iframe.style.width = opts.width;
905
+ const hasFixedHeight = opts.fixedHeight !== void 0 && opts.fixedHeight !== null;
906
+ if (hasFixedHeight) iframe.style.height = toCssSize(opts.fixedHeight);
907
+ const token = uuidv7();
908
+ const wantsTailwind = step.style?.tailwind === true;
909
+ iframe.srcdoc = buildSrcdoc(step.html ?? "", step.css ?? "", step.js ?? "", token, {
910
+ hostCss: collectHostStyles(),
911
+ htmlAttrs: hostHtmlAttrs(),
912
+ tailwind: wantsTailwind
913
+ });
914
+ const close = () => context.onClose();
915
+ const onMessage = (event) => {
916
+ if (event.source !== iframe.contentWindow) return;
917
+ const data = event.data;
918
+ if (!data || typeof data !== "object" || data.__veo !== token) return;
919
+ switch (data.type) {
920
+ case "dismiss":
921
+ context.onInteraction({
922
+ guideId: context.guide.guideId,
923
+ stepIndex: 0,
924
+ action: "dismissed"
925
+ });
926
+ close();
927
+ break;
928
+ case "cta":
929
+ context.onInteraction({
930
+ guideId: context.guide.guideId,
931
+ stepIndex: 0,
932
+ action: "cta_clicked"
933
+ });
934
+ if (typeof data.url === "string" && isSafeUrl(data.url)) {
935
+ window.open(data.url, "_blank", "noopener,noreferrer");
936
+ }
937
+ if (data.close !== false) close();
938
+ break;
939
+ case "navigate":
940
+ if (typeof data.url === "string" && isSafeUrl(data.url)) {
941
+ window.location.assign(data.url);
942
+ }
943
+ break;
944
+ case "track":
945
+ if (typeof data.name === "string" && context.onTrack) {
946
+ context.onTrack(data.name, isRecord(data.props) ? data.props : void 0);
947
+ }
948
+ break;
949
+ case "resize":
950
+ if (!hasFixedHeight && typeof data.height === "number" && data.height > 0) {
951
+ iframe.style.height = `${Math.ceil(data.height)}px`;
952
+ }
953
+ break;
954
+ }
955
+ };
956
+ window.addEventListener("message", onMessage);
957
+ return { iframe, cleanup: () => window.removeEventListener("message", onMessage) };
958
+ }
959
+ function toCssSize(value) {
960
+ return typeof value === "number" ? `${value}px` : value;
961
+ }
962
+ var MAX_HOST_CSS = 800 * 1024;
963
+ function collectHostStyles() {
964
+ if (typeof document === "undefined") return "";
965
+ let out = "";
966
+ try {
967
+ for (const sheet of Array.from(document.styleSheets)) {
968
+ let rules = null;
969
+ try {
970
+ rules = sheet.cssRules;
971
+ } catch {
972
+ continue;
973
+ }
974
+ if (!rules) continue;
975
+ for (const rule of Array.from(rules)) {
976
+ out += `${rule.cssText}
977
+ `;
978
+ if (out.length > MAX_HOST_CSS) return out;
979
+ }
980
+ }
981
+ } catch {
982
+ return out;
750
983
  }
751
- if (s.theme === "dark") {
752
- for (const [key, value] of Object.entries(DARK_THEME)) {
753
- host.style.setProperty(key, value);
984
+ return out;
985
+ }
986
+ function hostHtmlAttrs() {
987
+ if (typeof document === "undefined") return "";
988
+ try {
989
+ const el2 = document.documentElement;
990
+ const attrs = [];
991
+ for (const name of ["class", "style", "data-theme", "data-mode", "data-color-mode"]) {
992
+ const v = el2.getAttribute(name);
993
+ if (v && v.length <= 1e3 && !/["<>]/.test(v)) attrs.push(`${name}="${v}"`);
754
994
  }
995
+ return attrs.join(" ");
996
+ } catch {
997
+ return "";
755
998
  }
756
- if (typeof s.background === "string" && COLOR_RE.test(s.background.trim())) {
757
- host.style.setProperty("--veo-bg", s.background.trim());
999
+ }
1000
+ function isRecord(value) {
1001
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1002
+ }
1003
+ function buildSrcdoc(html, css, js, token, opts) {
1004
+ const bridge = `(function(){var T=${JSON.stringify(token)};function P(m){m.__veo=T;parent.postMessage(m,'*');}window.veo={dismiss:function(){P({type:'dismiss'});},cta:function(u){P({type:'cta',url:u});},track:function(n,p){P({type:'track',name:n,props:p||{}});},navigate:function(u){P({type:'navigate',url:u});}};document.addEventListener('click',function(e){var a=e.target&&e.target.closest?e.target.closest('a[href]'):null;if(!a)return;var h=a.getAttribute('href');if(!h||h.charAt(0)==='#'||/^(javascript|mailto|tel):/i.test(h))return;e.preventDefault();if(a.target==='_blank'){P({type:'cta',url:a.href,close:false});}else{P({type:'navigate',url:h});}},true);function H(){var r=document.getElementById('__veo_root');var h=r?r.getBoundingClientRect().height:document.documentElement.scrollHeight;P({type:'resize',height:Math.ceil(h)});}window.addEventListener('load',H);try{if(typeof ResizeObserver!=='undefined'){var _r=document.getElementById('__veo_root');new ResizeObserver(H).observe(_r||document.documentElement);}}catch(e){}})();`;
1005
+ const hostStyle = opts.hostCss ? `<style>${escapeClosing(opts.hostCss, "style")}</style>` : "";
1006
+ const tailwind = opts.tailwind ? '<script src="https://cdn.tailwindcss.com"></script>' : "";
1007
+ return (
1008
+ // Orden: base → CSS del host (utilidades+clases propias+:root) → reset
1009
+ // flotante (fondo transparente, sin márgenes) → Tailwind → CSS del autor (gana).
1010
+ `<!DOCTYPE html><html ${opts.htmlAttrs}><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;}body{font-family:system-ui,-apple-system,sans-serif;}</style>` + hostStyle + // Reset flotante: el CSS de la app trae reglas de PÁGINA (body{min-height:100vh},
1011
+ // html{height:100%}, fondo del layout…) que en un iframe embebido lo estirarían a
1012
+ // toda la pantalla (caja negra) en vez de ajustarse al contenido. Neutralizamos
1013
+ // tamaño/fondo/overflow (mantenemos color y fuente heredados de la app).
1014
+ // `color-scheme:normal` es CLAVE para la transparencia: si el CSS de la app
1015
+ // declara `:root{color-scheme:dark}` y no coincide con el del elemento iframe
1016
+ // del host (normal), el navegador pinta el canvas OPACO (negro) en vez de
1017
+ // transparente — igualamos ambos lados para conservar el fondo transparente.
1018
+ "<style>html,body{margin:0!important;padding:0!important;background:transparent!important;height:auto!important;min-height:0!important;max-height:none!important;width:auto!important;min-width:0!important;overflow:visible!important;color-scheme:normal!important;}</style>" + tailwind + // Inline en <body>: gana incluso sobre reglas `!important` de la app (p. ej.
1019
+ // `.dark body{background:#000!important}`) porque el estilo inline tiene la
1020
+ // mayor especificidad dentro de la capa important-author.
1021
+ `<style>${escapeClosing(css, "style")}</style></head><body style="margin:0!important;background:transparent!important;min-height:0!important;height:auto!important;"><div id="__veo_root">${html}</div><script>${bridge}</script>` + (js ? `<script>${escapeClosing(js, "script")}</script>` : "") + "</body></html>"
1022
+ );
1023
+ }
1024
+ function escapeClosing(code, tag) {
1025
+ const rx = tag === "script" ? /<\/script/gi : /<\/style/gi;
1026
+ return code.replace(rx, `<\\/${tag}`);
1027
+ }
1028
+
1029
+ // src/plugins/guides/code-block.ts
1030
+ var RAW_BLOCK_ATTR = "data-veo-raw-block";
1031
+ function insertRawNodes(anchor, nodes, pos) {
1032
+ const parent = anchor.parentNode;
1033
+ if (pos === "prepend") {
1034
+ const first = anchor.firstChild;
1035
+ for (const n of nodes) anchor.insertBefore(n, first);
1036
+ } else if (pos === "append") {
1037
+ for (const n of nodes) anchor.appendChild(n);
1038
+ } else if (pos === "before") {
1039
+ for (const n of nodes) parent?.insertBefore(n, anchor);
1040
+ } else {
1041
+ let ref = anchor;
1042
+ for (const n of nodes) {
1043
+ parent?.insertBefore(n, ref.nextSibling);
1044
+ ref = n;
1045
+ }
758
1046
  }
759
- if (typeof s.textColor === "string" && COLOR_RE.test(s.textColor.trim())) {
1047
+ }
1048
+ var codeSlotSeq = 0;
1049
+ function mountCodeBlock(block, doc, context, host, rawInline) {
1050
+ if (block.sandboxed) {
1051
+ const step = { html: block.html ?? "", css: block.css, js: block.js };
1052
+ const { iframe, cleanup } = mountCustomFrame(doc, step, context, { width: "100%" });
1053
+ const wrapper = doc.createElement("div");
1054
+ wrapper.className = "veo-code-sandbox";
1055
+ wrapper.appendChild(iframe);
1056
+ return { node: wrapper, cleanup };
1057
+ }
1058
+ const hostDoc = doc;
1059
+ const injected = [];
1060
+ if (typeof block.css === "string" && block.css.trim()) {
1061
+ const style = hostDoc.createElement("style");
1062
+ style.textContent = block.css;
1063
+ hostDoc.head.appendChild(style);
1064
+ injected.push(style);
1065
+ }
1066
+ let node = null;
1067
+ if (typeof block.html === "string" && block.html.trim()) {
1068
+ const holder = doc.createElement("div");
1069
+ holder.className = "veo-code-html";
1070
+ holder.innerHTML = block.html;
1071
+ for (const old of Array.from(holder.querySelectorAll("script"))) {
1072
+ injected.push(runScript(old, hostDoc));
1073
+ old.remove();
1074
+ }
1075
+ if (rawInline) {
1076
+ const rawNodes = Array.from(holder.childNodes);
1077
+ if (rawInline.tagBlockId) {
1078
+ for (const n of rawNodes) {
1079
+ if (n instanceof Element) n.setAttribute(RAW_BLOCK_ATTR, rawInline.tagBlockId);
1080
+ }
1081
+ }
1082
+ insertRawNodes(rawInline.anchor, rawNodes, rawInline.pos);
1083
+ injected.push(...rawNodes);
1084
+ } else if (host) {
1085
+ const slotName = `veo-code-${++codeSlotSeq}`;
1086
+ holder.slot = slotName;
1087
+ host.appendChild(holder);
1088
+ injected.push(holder);
1089
+ const slot = doc.createElement("slot");
1090
+ slot.setAttribute("name", slotName);
1091
+ const wrapper = doc.createElement("div");
1092
+ wrapper.className = "veo-code-host";
1093
+ wrapper.appendChild(slot);
1094
+ node = wrapper;
1095
+ } else {
1096
+ node = holder;
1097
+ }
1098
+ }
1099
+ let cancelled = false;
1100
+ if (typeof block.js === "string" && block.js.trim()) {
1101
+ const js = block.js;
1102
+ const run = () => {
1103
+ if (cancelled) return;
1104
+ const s = hostDoc.createElement("script");
1105
+ s.textContent = wrapUserScript(js);
1106
+ hostDoc.body.appendChild(s);
1107
+ injected.push(s);
1108
+ };
1109
+ if (host && !host.isConnected) queueMicrotask(run);
1110
+ else run();
1111
+ }
1112
+ return {
1113
+ node,
1114
+ cleanup: () => {
1115
+ cancelled = true;
1116
+ for (const n of injected) n.parentNode?.removeChild(n);
1117
+ }
1118
+ };
1119
+ }
1120
+ function wrapUserScript(js) {
1121
+ return `(function(){try{
1122
+ ${js}
1123
+ }catch(e){console.error("[OnUser] Error en el bloque de c\xF3digo:",e)}})();`;
1124
+ }
1125
+ function runScript(old, hostDoc) {
1126
+ const s = hostDoc.createElement("script");
1127
+ for (const attr of Array.from(old.attributes)) s.setAttribute(attr.name, attr.value);
1128
+ if (!old.src) s.textContent = old.textContent;
1129
+ hostDoc.body.appendChild(s);
1130
+ return s;
1131
+ }
1132
+
1133
+ // src/plugins/guides/guide-design.ts
1134
+ var DARK_THEME = {
1135
+ "--veo-bg": "#1f2937",
1136
+ "--veo-text": "#f9fafb",
1137
+ "--veo-text-secondary": "#d1d5db",
1138
+ "--veo-shadow": "0 20px 60px rgba(0, 0, 0, 0.55)"
1139
+ };
1140
+ var ALIGN_JUSTIFY = {
1141
+ left: "flex-start",
1142
+ center: "center",
1143
+ right: "flex-end"
1144
+ };
1145
+ var ALIGN_SELF = {
1146
+ left: "flex-start",
1147
+ center: "center",
1148
+ right: "flex-end"
1149
+ };
1150
+ var ELEMENT_ALIGN_MODE = {
1151
+ title: "text",
1152
+ text: "text",
1153
+ image: "self"
1154
+ };
1155
+ var SHADOW = {
1156
+ none: "none",
1157
+ soft: "0 4px 14px rgba(0, 0, 0, 0.10)",
1158
+ medium: "0 8px 30px rgba(0, 0, 0, 0.18)",
1159
+ strong: "0 24px 60px rgba(0, 0, 0, 0.32)"
1160
+ };
1161
+ var PRESET_POS = {
1162
+ center: [0.5, 0.5],
1163
+ top: [0.5, 0],
1164
+ bottom: [0.5, 1],
1165
+ left: [0, 0.5],
1166
+ right: [1, 0.5],
1167
+ "top-left": [0, 0],
1168
+ "top-right": [1, 0],
1169
+ "bottom-left": [0, 1],
1170
+ "bottom-right": [1, 1]
1171
+ };
1172
+ var COLOR_RE = /^#[0-9a-f]{3,8}$|^(rgb|rgba|hsl|hsla)\([\d.,%/\sdeg]+\)$|^[a-z]{3,20}$/i;
1173
+ function clamp(n, min, max) {
1174
+ return Math.min(max, Math.max(min, n));
1175
+ }
1176
+ function applyDesignVars(host, style) {
1177
+ if (!style || typeof style !== "object") return;
1178
+ const s = style;
1179
+ if (typeof s.accentColor === "string" && COLOR_RE.test(s.accentColor.trim())) {
1180
+ host.style.setProperty("--veo-primary", s.accentColor.trim());
1181
+ }
1182
+ if (typeof s.fontFamily === "string" && s.fontFamily.length <= 200 && !/[<>{}]/.test(s.fontFamily)) {
1183
+ host.style.setProperty("--veo-font", s.fontFamily.trim());
1184
+ }
1185
+ if (s.theme === "dark") {
1186
+ for (const [key, value] of Object.entries(DARK_THEME)) {
1187
+ host.style.setProperty(key, value);
1188
+ }
1189
+ }
1190
+ if (typeof s.background === "string" && COLOR_RE.test(s.background.trim())) {
1191
+ host.style.setProperty("--veo-bg", s.background.trim());
1192
+ }
1193
+ if (typeof s.textColor === "string" && COLOR_RE.test(s.textColor.trim())) {
760
1194
  host.style.setProperty("--veo-text", s.textColor.trim());
761
1195
  }
762
1196
  if (typeof s.textSecondaryColor === "string" && COLOR_RE.test(s.textSecondaryColor.trim())) {
@@ -1037,7 +1471,161 @@ var GUIDE_STYLES = `
1037
1471
  }
1038
1472
  /* Card gen\xE9rica del modelo unificado. El dise\xF1o (bg/borde/padding/\u2026) lo aplica
1039
1473
  el renderer como estilos inline desde step.design; ac\xE1 van los defaults. */
1474
+ /* Edici\xF3n (builder): sin animaci\xF3n de entrada al repintar la preview. */
1475
+ :host(.veo-no-anim) * { animation: none !important; }
1476
+
1477
+ /* Bloque video: caja con relaci\xF3n de aspecto; el iframe la llena. */
1478
+ .veo-guide-video { position: relative; overflow: hidden; border-radius: 8px; background: #000; }
1479
+ .veo-guide-video iframe, .veo-guide-video video {
1480
+ position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block;
1481
+ }
1482
+
1483
+ /* \u2500\u2500 Layout por filas/columnas (composite) \u2500\u2500
1484
+ El espacio tiene UNA fuente por capa: el aire con el borde lo da el padding
1485
+ de la card; la separaci\xF3n entre filas/bloques la da el margen inferior
1486
+ (12px, ah\xED vive la l\xEDnea \xAB+\xBB del editor); los bloques NO llevan padding
1487
+ propio (solo el que el autor les ponga inline). El \xFAltimo elemento no deja
1488
+ margen colgando. */
1489
+ .veo-card > [data-veo-block], .veo-card > .veo-row, .veo-card > .veo-guide-actions,
1490
+ .veo-col > [data-veo-block], .veo-col > .veo-guide-actions { margin-bottom: 12px; }
1491
+ /* La card y cada columna son columnas flex: cada bloque mide SU contenido (como
1492
+ el bot\xF3n) y se ubica con su alineaci\xF3n (align-self); filas, columnas, formularios
1493
+ y filas de botones ocupan todo el ancho. */
1494
+ .veo-card:not(.veo-card--empty), .veo-col {
1495
+ display: flex; flex-direction: column; align-items: flex-start;
1496
+ }
1497
+ .veo-card > [data-veo-block], .veo-col > [data-veo-block] { max-width: 100%; }
1498
+ /* L\xEDnea horizontal: ocupa todo el ancho; el padding del bloque es solo vertical
1499
+ (zona clickeable en el editor) y la l\xEDnea es el borde superior del hr. */
1500
+ .veo-card > .veo-guide-divider, .veo-col > .veo-guide-divider { padding: 6px 0; align-self: stretch; }
1501
+ .veo-guide-divider { box-sizing: border-box; }
1502
+ /* Bloque de c\xF3digo (modo host): ocupa todo el ancho, como un formulario. */
1503
+ .veo-code-host { display: block; min-width: 0; }
1504
+ .veo-card > .veo-code-host, .veo-col > .veo-code-host { align-self: stretch; }
1505
+ .veo-guide-divider hr { margin: 0; border: 0; height: 0; }
1506
+ .veo-card > .veo-row, .veo-card > .veo-guide-actions, .veo-card > .veo-form,
1507
+ .veo-col > .veo-guide-actions, .veo-col > .veo-form { align-self: stretch; }
1508
+ .veo-row { display: flex; gap: 12px; align-items: flex-start; box-sizing: border-box; }
1509
+ .veo-col { flex: 1 1 0; min-width: 0; }
1510
+ .veo-col > :last-child { margin-bottom: 0; }
1511
+ /* Fila con alineaci\xF3n horizontal (editor de fila): las columnas miden su contenido. */
1512
+ .veo-row--aligned > .veo-col { flex: 0 1 auto; }
1513
+
1514
+ /* \u2500\u2500 Editor in-app: bloque seleccionado + barra de acciones + placeholders \u2500\u2500 */
1515
+ .veo-block-placeholder {
1516
+ border: 1px dashed #b0a9a5; border-radius: 6px; padding: 12px; margin-bottom: 12px;
1517
+ color: #8a8582; font-size: 13px; text-align: center;
1518
+ }
1519
+ /* El bloque seleccionado no lleva contorno propio: lo rodea .veo-block-frame
1520
+ (marco aparte, con aire y alto m\xEDnimo, que no altera el layout). */
1521
+ .veo-block--selected { position: relative; z-index: 1; }
1522
+ .veo-block-frame {
1523
+ position: absolute; z-index: 4; pointer-events: none; box-sizing: border-box;
1524
+ border: 2px solid #3b82f6; border-radius: 10px; background: rgba(59, 130, 246, 0.04);
1525
+ }
1526
+ /* Barra del bloque (mock): p\xEDldora blanca con sombra, solo \xEDconos; la papelera
1527
+ en rojo, separada por una l\xEDnea. */
1528
+ .veo-block-toolbar {
1529
+ position: absolute; z-index: 5; display: inline-flex; align-items: center; gap: 2px; padding: 4px 6px;
1530
+ background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 12px;
1531
+ box-shadow: 0 6px 20px rgba(0,0,0,0.16);
1532
+ }
1533
+ .veo-block-toolbar button {
1534
+ border: 0; background: transparent; color: #4b4642; cursor: pointer; padding: 6px; border-radius: 8px;
1535
+ display: inline-flex; align-items: center; justify-content: center; line-height: 1;
1536
+ }
1537
+ .veo-block-toolbar button:hover { background: #f5efeb; }
1538
+ .veo-block-toolbar button svg { width: 20px; height: 20px; }
1539
+ .veo-block-toolbar .veo-block-toolbar-remove { color: #e5484d; }
1540
+ /* Separadores: antes de la papelera y antes de las flechas de mover. */
1541
+ .veo-block-toolbar .veo-block-toolbar-remove, .veo-block-toolbar .veo-block-toolbar-move-left {
1542
+ margin-left: 6px; position: relative;
1543
+ }
1544
+ .veo-block-toolbar .veo-block-toolbar-remove::before, .veo-block-toolbar .veo-block-toolbar-move-left::before {
1545
+ content: ""; position: absolute; left: -4px; top: 6px; bottom: 6px; width: 1px; background: #e7e2de;
1546
+ }
1547
+ .veo-block-toolbar button:disabled { opacity: 0.3; cursor: default; background: transparent; }
1548
+ /* Contenedor (hover): contorno azul de la tarjeta y dos pesta\xF1as centradas
1549
+ sobre su borde superior: \xABEditar contenedor\xBB (outline) y \xABEditar filas\xBB. */
1550
+ .veo-card-frame {
1551
+ position: absolute; z-index: 3; inset: 0; pointer-events: none; box-sizing: border-box;
1552
+ border: 2px solid #3b82f6; border-radius: inherit;
1553
+ }
1554
+ .veo-card-toolbar {
1555
+ position: absolute; z-index: 6; left: 50%; transform: translateX(-50%);
1556
+ display: inline-flex; align-items: flex-end; gap: 8px; white-space: nowrap;
1557
+ }
1558
+ .veo-card-toolbar button {
1559
+ cursor: pointer; padding: 8px 14px; border-radius: 8px; border: 2px solid #3b82f6;
1560
+ font: 600 14px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
1561
+ background: #fff; color: #3b82f6;
1562
+ }
1563
+ .veo-card-toolbar button:hover { background: #eff6ff; }
1564
+ /* Con fondo = la vista que el panel muestra ahora (contenedor o estructura). */
1565
+ .veo-card-toolbar button.is-active { background: #3b82f6; color: #fff; }
1566
+ .veo-card-toolbar button.is-active:hover { background: #2563eb; border-color: #2563eb; }
1567
+ /* Editor de fila: marco al pasar el mouse por una fila (o la fila del bloque
1568
+ seleccionado) y barra \xABEditar fila \xB7 duplicar\xBB bajo su esquina derecha. */
1569
+ .veo-row-frame {
1570
+ position: absolute; z-index: 3; pointer-events: none; box-sizing: border-box;
1571
+ border: 2px solid #3b82f6; border-radius: 10px;
1572
+ }
1573
+ .veo-row-toolbar {
1574
+ position: absolute; z-index: 5; display: inline-flex; align-items: center; gap: 2px; padding: 4px 6px;
1575
+ background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 12px;
1576
+ box-shadow: 0 6px 20px rgba(0,0,0,0.16); white-space: nowrap;
1577
+ }
1578
+ .veo-row-toolbar button {
1579
+ border: 0; background: transparent; color: #4b4642; cursor: pointer; padding: 6px 8px; border-radius: 8px;
1580
+ display: inline-flex; align-items: center; gap: 8px; line-height: 1;
1581
+ font: 500 14px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
1582
+ }
1583
+ .veo-row-toolbar button:hover { background: #f5efeb; }
1584
+ .veo-row-toolbar button svg { width: 20px; height: 20px; }
1585
+ .veo-row-toolbar .veo-row-toolbar-duplicate { margin-left: 6px; position: relative; }
1586
+ .veo-row-toolbar .veo-row-toolbar-duplicate::before {
1587
+ content: ""; position: absolute; left: -4px; top: 6px; bottom: 6px; width: 1px; background: #e7e2de;
1588
+ }
1589
+ .veo-row-toolbar .veo-row-toolbar-remove { color: #e5484d; }
1590
+ /* Huecos \xAB+\xBB (mock): zona invisible entre elementos; al pasar el mouse aparece
1591
+ la l\xEDnea azul (horizontal = fila / bloque apilado, vertical = columna) con
1592
+ un \xAB+\xBB circular al centro. */
1593
+ .veo-gap { position: absolute; z-index: 4; }
1594
+ .veo-gap i { position: absolute; background: #3b82f6; opacity: 0; transition: opacity 100ms; pointer-events: none; }
1595
+ .veo-gap--h i { left: 0; right: 0; top: 50%; height: 2px; margin-top: -1px; }
1596
+ .veo-gap--v i { top: 0; bottom: 0; left: 50%; width: 2px; margin-left: -1px; }
1597
+ .veo-gap button {
1598
+ position: absolute; left: 50%; top: 50%; margin: -11px 0 0 -11px; box-sizing: border-box;
1599
+ opacity: 0; transition: opacity 100ms; width: 22px; height: 22px; border-radius: 50%; padding: 0;
1600
+ border: 2px solid #fff; background: #3b82f6; color: #fff; cursor: pointer;
1601
+ font: 600 15px/18px system-ui, -apple-system, "Segoe UI", sans-serif; text-align: center;
1602
+ box-shadow: 0 2px 8px rgba(0,0,0,0.25);
1603
+ }
1604
+ .veo-gap:hover i, .veo-gap:hover button, .veo-gap:focus-within button { opacity: 1; }
1605
+ .veo-gap button:hover { background: #2563eb; }
1606
+
1607
+ /* Tarjeta sin bloques (paso reci\xE9n creado). La tarjeta NO impone tama\xF1o ni
1608
+ padding propios (as\xED el ancho/padding del dise\xF1o se ven tal cual); el tama\xF1o
1609
+ m\xEDnimo lo pone la zona punteada azul de la preview. */
1610
+ .veo-card--empty { display: flex; align-items: stretch; }
1611
+ .veo-card-empty-zone {
1612
+ flex: 1; min-width: 0; min-height: 160px; margin-top: 14px;
1613
+ display: flex; align-items: center; justify-content: center; gap: 6px; text-align: center;
1614
+ border: 2px dashed #3b82f6; border-radius: 8px; background: #eff6ff;
1615
+ color: #2563eb; font-size: 16px; font-weight: 500; user-select: none; pointer-events: none;
1616
+ padding: 0 12px; box-sizing: border-box; overflow: hidden;
1617
+ }
1618
+ .veo-card-empty-zone b { font-weight: 600; font-size: 22px; line-height: 1; }
1619
+ /* Sin ancho configurado, la zona da un tama\xF1o c\xF3modo por defecto. */
1620
+ .veo-card--empty:not([style*="width"]) .veo-card-empty-zone { min-width: 360px; }
1621
+ /* Tarjeta angosta (la mide el renderer): solo el \xAB+\xBB, sin texto, y m\xE1s baja. */
1622
+ .veo-card--narrow .veo-card-empty-zone span { display: none; }
1623
+ .veo-card--narrow .veo-card-empty-zone { min-height: 120px; }
1040
1624
  .veo-card {
1625
+ /* Contenedor posicionado SIEMPRE: el \u2715 (absolute) se ubica respecto a la
1626
+ tarjeta tambi\xE9n en pasos embebidos (inline), no respecto a la app. Los
1627
+ flotantes lo pisan con position:fixed inline. */
1628
+ position: relative;
1041
1629
  pointer-events: auto;
1042
1630
  background: var(--veo-bg, #ffffff);
1043
1631
  color: var(--veo-text, #1f2937);
@@ -1130,6 +1718,13 @@ var GUIDE_STYLES = `
1130
1718
  transition: opacity 120ms ease;
1131
1719
  }
1132
1720
  .veo-guide-cta:hover { opacity: 0.9; }
1721
+ /* Presets del boton (editor): secundario = contorno; enlace = solo texto. */
1722
+ .veo-guide-cta--secondary {
1723
+ background: transparent; color: var(--veo-primary);
1724
+ border: 1px solid var(--veo-primary);
1725
+ }
1726
+ .veo-guide-cta--link { background: transparent; color: var(--veo-primary); padding: 0; }
1727
+ .veo-guide-cta--link:hover { opacity: 1; text-decoration: underline; }
1133
1728
  .veo-guide-cta:active { transform: translateY(1px); }
1134
1729
  .veo-guide-close {
1135
1730
  position: absolute; top: -6px; right: -6px;
@@ -1160,6 +1755,10 @@ var GUIDE_STYLES = `
1160
1755
 
1161
1756
  .veo-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
1162
1757
  .veo-form-field { display: flex; flex-direction: column; gap: 6px; }
1758
+ /* Pregunta de la encuesta (rich text del bloque form). */
1759
+ .veo-form-question { font-size: 16px; font-weight: 600; line-height: 1.35; color: var(--veo-text); }
1760
+ .veo-form-question p { margin: 0; }
1761
+ .veo-form > .veo-guide-actions { margin: 0; }
1163
1762
  .veo-form-label { font-size: 13px; font-weight: 500; color: var(--veo-text); }
1164
1763
  .veo-form-input {
1165
1764
  font-size: 14px; color: var(--veo-text);
@@ -1168,26 +1767,71 @@ var GUIDE_STYLES = `
1168
1767
  padding: 8px 10px; width: 100%;
1169
1768
  }
1170
1769
  .veo-form-input:focus { outline: 2px solid var(--veo-primary); outline-offset: -1px; border-color: transparent; }
1171
- .veo-form-options { display: flex; flex-direction: column; gap: 6px; }
1770
+ /* Lista de tareas (mock): \xEDtems con anillo de progreso, texto + subt\xEDtulo,
1771
+ y al pie \xABX de N completadas\xBB + porcentaje + barra. */
1772
+ .veo-checklist {
1773
+ display: flex; flex-direction: column; gap: 22px; align-self: stretch; width: 100%;
1774
+ min-width: 360px; box-sizing: border-box;
1775
+ }
1776
+ .veo-checklist-title { font-size: 24px; font-weight: 700; line-height: 1.25; color: var(--veo-cl-title); }
1777
+ .veo-checklist-title p { margin: 0; }
1778
+ .veo-checklist-items { display: flex; flex-direction: column; gap: 22px; }
1779
+ .veo-checklist-item {
1780
+ display: flex; align-items: center; gap: 18px; width: 100%; text-align: left;
1781
+ background: none; border: 0; padding: 0; cursor: pointer; font: inherit; color: inherit;
1782
+ }
1783
+ .veo-checklist-item:hover .veo-checklist-label { text-decoration: underline; }
1784
+ .veo-checklist-ring { width: 44px; height: 44px; flex: 0 0 auto; }
1785
+ .veo-checklist-texts { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
1786
+ .veo-checklist-label {
1787
+ font-size: calc(var(--veo-cl-font-size, 14px) + 4px); font-weight: var(--veo-cl-font-weight, 600);
1788
+ line-height: 1.3; color: var(--veo-cl-title);
1789
+ }
1790
+ .veo-checklist-sub { font-size: calc(var(--veo-cl-font-size, 14px) + 1px); color: var(--veo-cl-subtitle); }
1791
+ .veo-checklist-footer { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }
1792
+ .veo-checklist-summary {
1793
+ display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
1794
+ font-size: calc(var(--veo-cl-font-size, 14px) + 1px);
1795
+ }
1796
+ .veo-checklist-count { color: var(--veo-cl-subtitle); }
1797
+ .veo-checklist-percent { font-weight: 700; font-size: calc(var(--veo-cl-font-size, 14px) + 3px); color: var(--veo-cl-bar); }
1798
+ .veo-checklist-bar { height: var(--veo-cl-bar-h, 8px); background: #e7e2de; border-radius: 999px; overflow: hidden; }
1799
+ .veo-checklist--dark .veo-checklist-bar { background: #3c3734; }
1800
+ .veo-checklist-bar-fill { height: 100%; background: var(--veo-cl-bar); border-radius: 999px; transition: width 300ms ease; }
1801
+ .veo-checklist-empty { margin: 0; font-size: 13px; color: var(--veo-cl-subtitle); }
1802
+ /* Opci\xF3n m\xFAltiple (mock): c\xEDrculos/casillas grandes con su texto al lado. */
1803
+ .veo-form-options { display: flex; flex-direction: column; gap: 14px; }
1172
1804
  .veo-form-option {
1173
- display: flex; align-items: center; gap: 8px;
1174
- font-size: 14px; color: var(--veo-text); cursor: pointer;
1175
- }
1176
- .veo-form-option input { accent-color: var(--veo-primary); margin: 0; cursor: pointer; }
1177
- .veo-form-scale { display: flex; gap: 4px; flex-wrap: wrap; }
1805
+ display: flex; align-items: center; gap: 14px;
1806
+ font-size: 16px; color: var(--veo-text); cursor: pointer;
1807
+ }
1808
+ .veo-form-option input {
1809
+ accent-color: var(--veo-primary); margin: 0; cursor: pointer;
1810
+ width: 26px; height: 26px; flex: 0 0 auto;
1811
+ }
1812
+ /* Escala num\xE9rica (mock): casillas cuadradas; con etiquetas, cada casilla
1813
+ lleva la suya debajo (la primera cuelga a la izquierda, la \xFAltima a la derecha). */
1814
+ .veo-form-scale { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }
1815
+ .veo-scale-item { display: inline-flex; flex-direction: column; align-items: center; gap: 8px; }
1816
+ .veo-scale-item--first { align-items: flex-start; }
1817
+ .veo-scale-item--last { align-items: flex-end; }
1818
+ .veo-scale-label { font-size: 12px; line-height: 1.2; color: #8a8582; white-space: nowrap; }
1178
1819
  .veo-nps-btn {
1179
- min-width: 30px; height: 30px; padding: 0 4px;
1180
- font-size: 13px; font-weight: 500; color: var(--veo-text);
1181
- background: var(--veo-bg); border: 1px solid #d1d5db; border-radius: 6px;
1820
+ min-width: var(--veo-scale-size, 44px); height: var(--veo-scale-size, 44px); padding: 0 6px;
1821
+ font-size: 16px; font-weight: 600; color: var(--veo-text);
1822
+ background: var(--veo-bg); border: 1px solid #d5cec9; border-radius: 8px;
1182
1823
  cursor: pointer; transition: all 100ms ease;
1183
1824
  }
1184
- .veo-nps-btn:hover { border-color: var(--veo-primary); }
1825
+ .veo-nps-btn:hover { border-color: var(--veo-scale-color, var(--veo-primary)); }
1185
1826
  .veo-rating-btn {
1186
1827
  background: none; border: none; padding: 0 2px;
1187
1828
  font-size: 26px; line-height: 1; color: #d1d5db;
1188
1829
  cursor: pointer; transition: color 100ms ease;
1189
1830
  }
1190
- .veo-scale-active.veo-nps-btn { background: var(--veo-primary); color: #fff; border-color: var(--veo-primary); }
1831
+ .veo-scale-active.veo-nps-btn {
1832
+ background: var(--veo-scale-color, var(--veo-primary)); color: #fff;
1833
+ border-color: var(--veo-scale-color, var(--veo-primary));
1834
+ }
1191
1835
  .veo-scale-active.veo-rating-btn { color: #f59e0b; }
1192
1836
  .veo-form-error { font-size: 13px; color: #dc2626; margin: 0; }
1193
1837
  .veo-form-thanks {
@@ -1856,241 +2500,208 @@ function bannerClassOf(position, embedded) {
1856
2500
  return embedded ? "veo-banner veo-banner-embedded" : `veo-banner veo-banner-${position}`;
1857
2501
  }
1858
2502
 
1859
- // src/utils/uuid.ts
1860
- function uuidv7() {
1861
- const timestamp = Date.now();
1862
- const timeHex = timestamp.toString(16).padStart(12, "0");
1863
- const random = new Uint8Array(10);
1864
- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1865
- crypto.getRandomValues(random);
1866
- } else {
1867
- for (let i = 0; i < random.length; i++) {
1868
- random[i] = Math.floor(Math.random() * 256);
1869
- }
1870
- }
1871
- random[0] = (random[0] ?? 0) & 15 | 112;
1872
- random[2] = (random[2] ?? 0) & 63 | 128;
1873
- const hex = Array.from(random, (b) => b.toString(16).padStart(2, "0")).join("");
1874
- return [
1875
- timeHex.slice(0, 8),
1876
- timeHex.slice(8, 12),
1877
- hex.slice(0, 4),
1878
- hex.slice(4, 8),
1879
- hex.slice(8, 20)
1880
- ].join("-");
1881
- }
1882
-
1883
- // src/plugins/guides/renderers/custom-frame.ts
1884
- function mountCustomFrame(doc, step, context, opts) {
1885
- const iframe = doc.createElement("iframe");
1886
- iframe.className = "veo-custom-frame";
1887
- iframe.setAttribute("sandbox", "allow-scripts");
1888
- iframe.setAttribute("title", context.guide.guideName || "OnUser");
1889
- iframe.style.width = opts.width;
1890
- const hasFixedHeight = opts.fixedHeight !== void 0 && opts.fixedHeight !== null;
1891
- if (hasFixedHeight) iframe.style.height = toCssSize(opts.fixedHeight);
1892
- const token = uuidv7();
1893
- const wantsTailwind = step.style?.tailwind === true;
1894
- iframe.srcdoc = buildSrcdoc(step.html ?? "", step.css ?? "", step.js ?? "", token, {
1895
- hostCss: collectHostStyles(),
1896
- htmlAttrs: hostHtmlAttrs(),
1897
- tailwind: wantsTailwind
1898
- });
1899
- const close = () => context.onClose();
1900
- const onMessage = (event) => {
1901
- if (event.source !== iframe.contentWindow) return;
1902
- const data = event.data;
1903
- if (!data || typeof data !== "object" || data.__veo !== token) return;
1904
- switch (data.type) {
1905
- case "dismiss":
1906
- context.onInteraction({
1907
- guideId: context.guide.guideId,
1908
- stepIndex: 0,
1909
- action: "dismissed"
1910
- });
1911
- close();
1912
- break;
1913
- case "cta":
1914
- context.onInteraction({
1915
- guideId: context.guide.guideId,
1916
- stepIndex: 0,
1917
- action: "cta_clicked"
1918
- });
1919
- if (typeof data.url === "string" && isSafeUrl(data.url)) {
1920
- window.open(data.url, "_blank", "noopener,noreferrer");
1921
- }
1922
- if (data.close !== false) close();
1923
- break;
1924
- case "navigate":
1925
- if (typeof data.url === "string" && isSafeUrl(data.url)) {
1926
- window.location.assign(data.url);
1927
- }
1928
- break;
1929
- case "track":
1930
- if (typeof data.name === "string" && context.onTrack) {
1931
- context.onTrack(data.name, isRecord(data.props) ? data.props : void 0);
1932
- }
1933
- break;
1934
- case "resize":
1935
- if (!hasFixedHeight && typeof data.height === "number" && data.height > 0) {
1936
- iframe.style.height = `${Math.ceil(data.height)}px`;
1937
- }
1938
- break;
1939
- }
2503
+ // src/plugins/guides/checklist-block.ts
2504
+ var CHECKLIST_DEFAULTS = {
2505
+ iconEmptyColor: "#e7e2de",
2506
+ iconFillColor: "#406a31",
2507
+ barColor: "#406a31",
2508
+ barHeight: 8,
2509
+ fontSize: 14,
2510
+ fontWeight: 600,
2511
+ titleColor: "#2c2825",
2512
+ subtitleColor: "#8a8582",
2513
+ subtitleTemplate: "Completado <%= currentStep %> de <%= totalSteps %> pasos"
2514
+ };
2515
+ var WEIGHTS = { regular: 400, medium: 500, semibold: 600, bold: 700 };
2516
+ function checklistStyle(style) {
2517
+ const s = style ?? {};
2518
+ const str = (k, fb) => typeof s[k] === "string" && s[k].trim() ? s[k] : fb;
2519
+ const num2 = (k, fb, min, max) => typeof s[k] === "number" && Number.isFinite(s[k]) ? Math.min(max, Math.max(min, s[k])) : fb;
2520
+ const weightRaw = s.fontWeight;
2521
+ const fontWeight = typeof weightRaw === "number" ? Math.min(900, Math.max(100, weightRaw)) : typeof weightRaw === "string" && WEIGHTS[weightRaw.toLowerCase()] ? WEIGHTS[weightRaw.toLowerCase()] : CHECKLIST_DEFAULTS.fontWeight;
2522
+ const dark = s.colorMode === "dark";
2523
+ return {
2524
+ colorMode: dark ? "dark" : "light",
2525
+ iconEmptyColor: str("iconEmptyColor", dark ? "#3c3734" : CHECKLIST_DEFAULTS.iconEmptyColor),
2526
+ iconFillColor: str("iconFillColor", CHECKLIST_DEFAULTS.iconFillColor),
2527
+ barColor: str("barColor", CHECKLIST_DEFAULTS.barColor),
2528
+ barHeight: num2("barHeight", CHECKLIST_DEFAULTS.barHeight, 2, 40),
2529
+ fontSize: num2("fontSize", CHECKLIST_DEFAULTS.fontSize, 10, 40),
2530
+ fontWeight,
2531
+ titleColor: str("titleColor", dark ? "#f5efeb" : CHECKLIST_DEFAULTS.titleColor),
2532
+ subtitleColor: str("subtitleColor", dark ? "#b0a9a5" : CHECKLIST_DEFAULTS.subtitleColor),
2533
+ subtitleTemplate: str("subtitleTemplate", CHECKLIST_DEFAULTS.subtitleTemplate)
1940
2534
  };
1941
- window.addEventListener("message", onMessage);
1942
- return { iframe, cleanup: () => window.removeEventListener("message", onMessage) };
1943
- }
1944
- function toCssSize(value) {
1945
- return typeof value === "number" ? `${value}px` : value;
1946
- }
1947
- var MAX_HOST_CSS = 800 * 1024;
1948
- function collectHostStyles() {
1949
- if (typeof document === "undefined") return "";
1950
- let out = "";
1951
- try {
1952
- for (const sheet of Array.from(document.styleSheets)) {
1953
- let rules = null;
1954
- try {
1955
- rules = sheet.cssRules;
1956
- } catch {
1957
- continue;
1958
- }
1959
- if (!rules) continue;
1960
- for (const rule of Array.from(rules)) {
1961
- out += `${rule.cssText}
1962
- `;
1963
- if (out.length > MAX_HOST_CSS) return out;
1964
- }
1965
- }
1966
- } catch {
1967
- return out;
1968
- }
1969
- return out;
1970
- }
1971
- function hostHtmlAttrs() {
1972
- if (typeof document === "undefined") return "";
1973
- try {
1974
- const el2 = document.documentElement;
1975
- const attrs = [];
1976
- for (const name of ["class", "style", "data-theme", "data-mode", "data-color-mode"]) {
1977
- const v = el2.getAttribute(name);
1978
- if (v && v.length <= 1e3 && !/["<>]/.test(v)) attrs.push(`${name}="${v}"`);
1979
- }
1980
- return attrs.join(" ");
1981
- } catch {
1982
- return "";
1983
- }
1984
- }
1985
- function isRecord(value) {
1986
- return typeof value === "object" && value !== null && !Array.isArray(value);
1987
- }
1988
- function buildSrcdoc(html, css, js, token, opts) {
1989
- const bridge = `(function(){var T=${JSON.stringify(token)};function P(m){m.__veo=T;parent.postMessage(m,'*');}window.veo={dismiss:function(){P({type:'dismiss'});},cta:function(u){P({type:'cta',url:u});},track:function(n,p){P({type:'track',name:n,props:p||{}});},navigate:function(u){P({type:'navigate',url:u});}};document.addEventListener('click',function(e){var a=e.target&&e.target.closest?e.target.closest('a[href]'):null;if(!a)return;var h=a.getAttribute('href');if(!h||h.charAt(0)==='#'||/^(javascript|mailto|tel):/i.test(h))return;e.preventDefault();if(a.target==='_blank'){P({type:'cta',url:a.href,close:false});}else{P({type:'navigate',url:h});}},true);function H(){var r=document.getElementById('__veo_root');var h=r?r.getBoundingClientRect().height:document.documentElement.scrollHeight;P({type:'resize',height:Math.ceil(h)});}window.addEventListener('load',H);try{if(typeof ResizeObserver!=='undefined'){var _r=document.getElementById('__veo_root');new ResizeObserver(H).observe(_r||document.documentElement);}}catch(e){}})();`;
1990
- const hostStyle = opts.hostCss ? `<style>${escapeClosing(opts.hostCss, "style")}</style>` : "";
1991
- const tailwind = opts.tailwind ? '<script src="https://cdn.tailwindcss.com"></script>' : "";
1992
- return (
1993
- // Orden: base → CSS del host (utilidades+clases propias+:root) → reset
1994
- // flotante (fondo transparente, sin márgenes) → Tailwind → CSS del autor (gana).
1995
- `<!DOCTYPE html><html ${opts.htmlAttrs}><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;}body{font-family:system-ui,-apple-system,sans-serif;}</style>` + hostStyle + // Reset flotante: el CSS de la app trae reglas de PÁGINA (body{min-height:100vh},
1996
- // html{height:100%}, fondo del layout…) que en un iframe embebido lo estirarían a
1997
- // toda la pantalla (caja negra) en vez de ajustarse al contenido. Neutralizamos
1998
- // tamaño/fondo/overflow (mantenemos color y fuente heredados de la app).
1999
- // `color-scheme:normal` es CLAVE para la transparencia: si el CSS de la app
2000
- // declara `:root{color-scheme:dark}` y no coincide con el del elemento iframe
2001
- // del host (normal), el navegador pinta el canvas OPACO (negro) en vez de
2002
- // transparente — igualamos ambos lados para conservar el fondo transparente.
2003
- "<style>html,body{margin:0!important;padding:0!important;background:transparent!important;height:auto!important;min-height:0!important;max-height:none!important;width:auto!important;min-width:0!important;overflow:visible!important;color-scheme:normal!important;}</style>" + tailwind + // Inline en <body>: gana incluso sobre reglas `!important` de la app (p. ej.
2004
- // `.dark body{background:#000!important}`) porque el estilo inline tiene la
2005
- // mayor especificidad dentro de la capa important-author.
2006
- `<style>${escapeClosing(css, "style")}</style></head><body style="margin:0!important;background:transparent!important;min-height:0!important;height:auto!important;"><div id="__veo_root">${html}</div><script>${bridge}</script>` + (js ? `<script>${escapeClosing(js, "script")}</script>` : "") + "</body></html>"
2007
- );
2008
2535
  }
2009
- function escapeClosing(code, tag) {
2010
- const rx = tag === "script" ? /<\/script/gi : /<\/style/gi;
2011
- return code.replace(rx, `<\\/${tag}`);
2536
+ function renderSubtitle(template, p) {
2537
+ return template.replace(/<%=\s*currentStep\s*%>/g, String(p.currentStep)).replace(/<%=\s*totalSteps\s*%>/g, String(p.totalSteps));
2012
2538
  }
2013
-
2014
- // src/plugins/guides/code-block.ts
2015
- function insertRawNodes(anchor, nodes, pos) {
2016
- const parent = anchor.parentNode;
2017
- if (pos === "prepend") {
2018
- const first = anchor.firstChild;
2019
- for (const n of nodes) anchor.insertBefore(n, first);
2020
- } else if (pos === "append") {
2021
- for (const n of nodes) anchor.appendChild(n);
2022
- } else if (pos === "before") {
2023
- for (const n of nodes) parent?.insertBefore(n, anchor);
2024
- } else {
2025
- let ref = anchor;
2026
- for (const n of nodes) {
2027
- parent?.insertBefore(n, ref.nextSibling);
2028
- ref = n;
2029
- }
2539
+ var RING_R = 10;
2540
+ var RING_C = 2 * Math.PI * RING_R;
2541
+ function buildRing(doc, fraction, completed, st) {
2542
+ const ns = "http://www.w3.org/2000/svg";
2543
+ const svg2 = doc.createElementNS(ns, "svg");
2544
+ svg2.setAttribute("viewBox", "0 0 28 28");
2545
+ svg2.setAttribute("class", "veo-checklist-ring");
2546
+ svg2.setAttribute("aria-hidden", "true");
2547
+ const track = doc.createElementNS(ns, "circle");
2548
+ track.setAttribute("cx", "14");
2549
+ track.setAttribute("cy", "14");
2550
+ track.setAttribute("r", String(RING_R));
2551
+ track.setAttribute("fill", completed ? st.iconFillColor : "none");
2552
+ track.setAttribute("stroke", st.iconEmptyColor);
2553
+ track.setAttribute("stroke-width", "4.5");
2554
+ svg2.appendChild(track);
2555
+ if (completed) {
2556
+ const check = doc.createElementNS(ns, "path");
2557
+ check.setAttribute("d", "M9 14.5l3.2 3.2L19 11");
2558
+ check.setAttribute("fill", "none");
2559
+ check.setAttribute("stroke", "#fff");
2560
+ check.setAttribute("stroke-width", "2.4");
2561
+ check.setAttribute("stroke-linecap", "round");
2562
+ check.setAttribute("stroke-linejoin", "round");
2563
+ svg2.appendChild(check);
2564
+ return svg2;
2565
+ }
2566
+ const f = Math.max(0, Math.min(1, fraction));
2567
+ if (f > 0) {
2568
+ const arc = doc.createElementNS(ns, "circle");
2569
+ arc.setAttribute("cx", "14");
2570
+ arc.setAttribute("cy", "14");
2571
+ arc.setAttribute("r", String(RING_R));
2572
+ arc.setAttribute("fill", "none");
2573
+ arc.setAttribute("stroke", st.iconFillColor);
2574
+ arc.setAttribute("stroke-width", "4.5");
2575
+ arc.setAttribute("stroke-linecap", "butt");
2576
+ arc.setAttribute("stroke-dasharray", `${(RING_C * f).toFixed(2)} ${RING_C.toFixed(2)}`);
2577
+ arc.setAttribute("transform", "rotate(-90 14 14)");
2578
+ svg2.appendChild(arc);
2030
2579
  }
2580
+ return svg2;
2031
2581
  }
2032
- var codeSlotSeq = 0;
2033
- function mountCodeBlock(block, doc, context, host, rawInline) {
2034
- if (block.sandboxed) {
2035
- const step = { html: block.html ?? "", css: block.css, js: block.js };
2036
- const { iframe, cleanup } = mountCustomFrame(doc, step, context, { width: "100%" });
2037
- const wrapper = doc.createElement("div");
2038
- wrapper.className = "veo-code-sandbox";
2039
- wrapper.appendChild(iframe);
2040
- return { node: wrapper, cleanup };
2041
- }
2042
- const hostDoc = doc;
2043
- const injected = [];
2044
- if (typeof block.css === "string" && block.css.trim()) {
2045
- const style = hostDoc.createElement("style");
2046
- style.textContent = block.css;
2047
- hostDoc.head.appendChild(style);
2048
- injected.push(style);
2049
- }
2050
- let node = null;
2051
- if (typeof block.html === "string" && block.html.trim()) {
2052
- const holder = doc.createElement("div");
2053
- holder.className = "veo-code-html";
2054
- holder.innerHTML = block.html;
2055
- for (const old of Array.from(holder.querySelectorAll("script"))) {
2056
- injected.push(runScript(old, hostDoc));
2057
- old.remove();
2058
- }
2059
- if (rawInline) {
2060
- const rawNodes = Array.from(holder.childNodes);
2061
- insertRawNodes(rawInline.anchor, rawNodes, rawInline.pos);
2062
- injected.push(...rawNodes);
2063
- } else if (host) {
2064
- const slotName = `veo-code-${++codeSlotSeq}`;
2065
- holder.slot = slotName;
2066
- host.appendChild(holder);
2067
- injected.push(holder);
2068
- const slot = doc.createElement("slot");
2069
- slot.setAttribute("name", slotName);
2070
- node = slot;
2071
- } else {
2072
- node = holder;
2073
- }
2074
- }
2075
- if (typeof block.js === "string" && block.js.trim()) {
2076
- const s = hostDoc.createElement("script");
2077
- s.textContent = block.js;
2078
- hostDoc.body.appendChild(s);
2079
- injected.push(s);
2080
- }
2081
- return {
2082
- node,
2083
- cleanup: () => {
2084
- for (const n of injected) n.parentNode?.removeChild(n);
2582
+ function buildChecklistBlock(block, doc, context) {
2583
+ const st = checklistStyle(block.style);
2584
+ const items = (block.items ?? []).filter((i) => i && typeof i.guideId === "string" && i.guideId);
2585
+ const root = doc.createElement("div");
2586
+ root.className = `veo-checklist${st.colorMode === "dark" ? " veo-checklist--dark" : ""}`;
2587
+ root.style.setProperty("--veo-cl-font-size", `${st.fontSize}px`);
2588
+ root.style.setProperty("--veo-cl-font-weight", String(st.fontWeight));
2589
+ root.style.setProperty("--veo-cl-title", st.titleColor);
2590
+ root.style.setProperty("--veo-cl-subtitle", st.subtitleColor);
2591
+ root.style.setProperty("--veo-cl-bar", st.barColor);
2592
+ root.style.setProperty("--veo-cl-bar-h", `${st.barHeight}px`);
2593
+ if (typeof block.html === "string" && block.html || typeof block.text === "string" && block.text) {
2594
+ const title = doc.createElement("div");
2595
+ title.className = "veo-checklist-title";
2596
+ if (typeof block.html === "string" && block.html)
2597
+ title.appendChild(renderRichText(block.html, doc));
2598
+ else title.textContent = block.text;
2599
+ root.appendChild(title);
2600
+ }
2601
+ const list = doc.createElement("div");
2602
+ list.className = "veo-checklist-items";
2603
+ list.setAttribute("role", "list");
2604
+ root.appendChild(list);
2605
+ const footer = doc.createElement("div");
2606
+ footer.className = "veo-checklist-footer";
2607
+ const summary = doc.createElement("div");
2608
+ summary.className = "veo-checklist-summary";
2609
+ const count = doc.createElement("span");
2610
+ count.className = "veo-checklist-count";
2611
+ const percent = doc.createElement("span");
2612
+ percent.className = "veo-checklist-percent";
2613
+ summary.append(count, percent);
2614
+ const bar = doc.createElement("div");
2615
+ bar.className = "veo-checklist-bar";
2616
+ const fill = doc.createElement("div");
2617
+ fill.className = "veo-checklist-bar-fill";
2618
+ bar.appendChild(fill);
2619
+ footer.append(summary, bar);
2620
+ root.appendChild(footer);
2621
+ const paint = (progress) => {
2622
+ list.textContent = "";
2623
+ const visible = progress ? items.filter((i) => progress.has(i.guideId)) : items;
2624
+ let done = 0;
2625
+ for (const item of visible) {
2626
+ const p = progress?.get(item.guideId) ?? null;
2627
+ if (p?.completed) done++;
2628
+ const row = doc.createElement("button");
2629
+ row.type = "button";
2630
+ row.className = `veo-checklist-item${p?.completed ? " veo-checklist-item--done" : ""}`;
2631
+ row.setAttribute("role", "listitem");
2632
+ const fraction = p && p.totalSteps > 0 ? p.currentStep / p.totalSteps : 0;
2633
+ row.appendChild(buildRing(doc, fraction, p?.completed === true, st));
2634
+ const texts = doc.createElement("span");
2635
+ texts.className = "veo-checklist-texts";
2636
+ const label = doc.createElement("span");
2637
+ label.className = "veo-checklist-label";
2638
+ label.textContent = (item.text ?? "").trim() || p?.guideName || "Gu\xEDa";
2639
+ if (p && p.published === false) row.title = "Gu\xEDa no publicada: los usuarios no la ver\xE1n";
2640
+ texts.appendChild(label);
2641
+ if (p) {
2642
+ const sub = doc.createElement("span");
2643
+ sub.className = "veo-checklist-sub";
2644
+ sub.textContent = renderSubtitle(st.subtitleTemplate, p);
2645
+ texts.appendChild(sub);
2646
+ }
2647
+ row.appendChild(texts);
2648
+ row.addEventListener("click", (e) => {
2649
+ e.preventDefault();
2650
+ e.stopPropagation();
2651
+ if (context.isPreview) return;
2652
+ const start = p && !p.completed ? Math.min(p.currentStep, p.totalSteps - 1) : 0;
2653
+ if (context.onOpenGuideAt) context.onOpenGuideAt(item.guideId, start);
2654
+ else context.onOpenGuide?.(item.guideId);
2655
+ });
2656
+ list.appendChild(row);
2085
2657
  }
2658
+ const total = visible.length;
2659
+ const pct = total > 0 ? Math.round(done / total * 100) : 0;
2660
+ count.textContent = `${done} de ${total} ${total === 1 ? "completada" : "completadas"}`;
2661
+ percent.textContent = `${pct}%`;
2662
+ fill.style.width = `${pct}%`;
2663
+ footer.hidden = total === 0;
2086
2664
  };
2087
- }
2088
- function runScript(old, hostDoc) {
2089
- const s = hostDoc.createElement("script");
2090
- for (const attr of Array.from(old.attributes)) s.setAttribute(attr.name, attr.value);
2091
- if (!old.src) s.textContent = old.textContent;
2092
- hostDoc.body.appendChild(s);
2093
- return s;
2665
+ if (context.isPreview && items.length === 0) {
2666
+ const sample = /* @__PURE__ */ new Map([
2667
+ [
2668
+ "__sample_1",
2669
+ {
2670
+ guideId: "__sample_1",
2671
+ guideName: "Configura tu primer env\xEDo",
2672
+ totalSteps: 2,
2673
+ currentStep: 1,
2674
+ completed: false
2675
+ }
2676
+ ],
2677
+ [
2678
+ "__sample_2",
2679
+ {
2680
+ guideId: "__sample_2",
2681
+ guideName: "Conecta tu tienda",
2682
+ totalSteps: 2,
2683
+ currentStep: 2,
2684
+ completed: true
2685
+ }
2686
+ ]
2687
+ ]);
2688
+ items.push(
2689
+ { guideId: "__sample_1", text: "Configura tu primer env\xEDo" },
2690
+ { guideId: "__sample_2", text: "Conecta tu tienda" }
2691
+ );
2692
+ paint(sample);
2693
+ const hint = doc.createElement("p");
2694
+ hint.className = "veo-checklist-empty";
2695
+ hint.textContent = "Ejemplo: agrega gu\xEDas a la lista desde Opciones.";
2696
+ root.appendChild(hint);
2697
+ return root;
2698
+ }
2699
+ paint(null);
2700
+ if (context.fetchProgress && items.length > 0) {
2701
+ void context.fetchProgress(items.map((i) => i.guideId)).then((rows) => paint(new Map(rows.map((r) => [r.guideId, r])))).catch(() => {
2702
+ });
2703
+ }
2704
+ return root;
2094
2705
  }
2095
2706
 
2096
2707
  // src/plugins/guides/form-content.ts
@@ -2319,8 +2930,14 @@ function buildField(field, doc) {
2319
2930
  wrapper.appendChild(group);
2320
2931
  return { wrapper, read: () => inputs.find((i) => i.input.checked)?.value };
2321
2932
  }
2322
- case "nps":
2323
- return buildScale(wrapper, doc, 0, 10, "veo-nps-btn");
2933
+ case "nps": {
2934
+ const min = typeof field.min === "number" ? field.min : 0;
2935
+ const max = typeof field.max === "number" && field.max > min ? field.max : 10;
2936
+ return buildScale(wrapper, doc, min, max, "veo-nps-btn", void 0, {
2937
+ labels: field.scaleLabels ?? [],
2938
+ showLabels: field.showLabels !== false
2939
+ });
2940
+ }
2324
2941
  case "rating":
2325
2942
  return buildScale(wrapper, doc, 1, 5, "veo-rating-btn", "\u2605");
2326
2943
  default: {
@@ -2333,17 +2950,21 @@ function buildField(field, doc) {
2333
2950
  }
2334
2951
  }
2335
2952
  }
2336
- function buildScale(wrapper, doc, min, max, btnClass, symbol) {
2953
+ function buildScale(wrapper, doc, min, max, btnClass, symbol, opts = { labels: [], showLabels: false }) {
2337
2954
  const row = doc.createElement("div");
2338
2955
  row.className = "veo-form-scale";
2339
2956
  let selected;
2340
2957
  const buttons = [];
2958
+ const labelOf = (n) => opts.labels[n - min]?.trim() ?? "";
2959
+ const anyLabel = opts.showLabels && Array.from({ length: max - min + 1 }, (_, i) => min + i).some((n) => labelOf(n));
2341
2960
  for (let n = min; n <= max; n++) {
2342
2961
  const btn = doc.createElement("button");
2343
2962
  btn.type = "button";
2344
2963
  btn.className = btnClass;
2345
2964
  btn.textContent = symbol ?? String(n);
2346
- btn.setAttribute("aria-label", String(n));
2965
+ const lbl = labelOf(n);
2966
+ btn.setAttribute("aria-label", lbl ? `${n} \xB7 ${lbl}` : String(n));
2967
+ if (lbl) btn.title = lbl;
2347
2968
  btn.addEventListener("click", () => {
2348
2969
  selected = n;
2349
2970
  buttons.forEach((b, i) => {
@@ -2352,11 +2973,113 @@ function buildScale(wrapper, doc, min, max, btnClass, symbol) {
2352
2973
  });
2353
2974
  });
2354
2975
  buttons.push(btn);
2355
- row.appendChild(btn);
2976
+ if (!anyLabel) {
2977
+ row.appendChild(btn);
2978
+ continue;
2979
+ }
2980
+ const item = doc.createElement("div");
2981
+ item.className = `veo-scale-item${n === min ? " veo-scale-item--first" : n === max ? " veo-scale-item--last" : ""}`;
2982
+ const text = doc.createElement("span");
2983
+ text.className = "veo-scale-label";
2984
+ text.textContent = lbl;
2985
+ item.append(btn, text);
2986
+ row.appendChild(item);
2356
2987
  }
2357
2988
  wrapper.appendChild(row);
2358
2989
  return { wrapper, read: () => selected };
2359
2990
  }
2991
+
2992
+ // src/plugins/guides/layout.ts
2993
+ function groupLayout(blocks) {
2994
+ const rows = /* @__PURE__ */ new Map();
2995
+ const order = [];
2996
+ let lastRow = -1;
2997
+ blocks.forEach((block, index) => {
2998
+ if (!block) return;
2999
+ let row;
3000
+ if (typeof block.row === "number" && Number.isFinite(block.row)) {
3001
+ row = block.row;
3002
+ lastRow = row;
3003
+ } else {
3004
+ row = lastRow + 0.5;
3005
+ lastRow = row;
3006
+ }
3007
+ const col = typeof block.col === "number" && Number.isFinite(block.col) ? block.col : 0;
3008
+ let r = rows.get(row);
3009
+ if (!r) {
3010
+ r = { row, columns: [] };
3011
+ rows.set(row, r);
3012
+ order.push(row);
3013
+ }
3014
+ let c = r.columns.find((x) => x.col === col);
3015
+ if (!c) {
3016
+ c = { col, blocks: [] };
3017
+ r.columns.push(c);
3018
+ }
3019
+ c.blocks.push({ block, index });
3020
+ });
3021
+ return order.sort((a, b) => a - b).map((k) => {
3022
+ const r = rows.get(k);
3023
+ r.columns.sort((a, b) => a.col - b.col);
3024
+ return r;
3025
+ });
3026
+ }
3027
+
3028
+ // src/plugins/guides/scroll-lock.ts
3029
+ var states = /* @__PURE__ */ new WeakMap();
3030
+ function lockScroll(doc = document) {
3031
+ const html = doc.documentElement;
3032
+ const body = doc.body;
3033
+ if (!html || !body) return () => {
3034
+ };
3035
+ let state = states.get(doc);
3036
+ if (!state) {
3037
+ state = {
3038
+ count: 0,
3039
+ html: { overflow: html.style.overflow, paddingRight: html.style.paddingRight },
3040
+ body: { overflow: body.style.overflow, paddingRight: body.style.paddingRight }
3041
+ };
3042
+ states.set(doc, state);
3043
+ const win = doc.defaultView;
3044
+ const gap = win ? Math.max(0, win.innerWidth - html.clientWidth) : 0;
3045
+ if (gap > 0) body.style.paddingRight = `${gap}px`;
3046
+ html.style.overflow = "hidden";
3047
+ body.style.overflow = "hidden";
3048
+ }
3049
+ state.count += 1;
3050
+ let released = false;
3051
+ return () => {
3052
+ if (released) return;
3053
+ released = true;
3054
+ const s = states.get(doc);
3055
+ if (!s) return;
3056
+ s.count -= 1;
3057
+ if (s.count > 0) return;
3058
+ states.delete(doc);
3059
+ html.style.overflow = s.html.overflow;
3060
+ html.style.paddingRight = s.html.paddingRight;
3061
+ body.style.overflow = s.body.overflow;
3062
+ body.style.paddingRight = s.body.paddingRight;
3063
+ };
3064
+ }
3065
+
3066
+ // src/plugins/guides/snooze.ts
3067
+ var KEY = (guideId) => `veo:snooze:${guideId}`;
3068
+ var UNIT_MS = {
3069
+ hour: 36e5,
3070
+ day: 864e5,
3071
+ week: 6048e5
3072
+ };
3073
+ function snoozeDurationMs(every, unit) {
3074
+ const n = Number.isFinite(every) && every > 0 ? Math.floor(every) : 1;
3075
+ return n * (UNIT_MS[unit] ?? UNIT_MS.day);
3076
+ }
3077
+ function snoozeGuide(guideId, every, unit, now = Date.now()) {
3078
+ try {
3079
+ localStorage.setItem(KEY(guideId), String(now + snoozeDurationMs(every, unit)));
3080
+ } catch {
3081
+ }
3082
+ }
2360
3083
  var INFO_GLYPH = "M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021";
2361
3084
  var QUESTION_GLYPH = "M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75";
2362
3085
  var STAR_GLYPH = "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z";
@@ -2546,6 +3269,32 @@ var ROW_ALIGN = {
2546
3269
  center: "center",
2547
3270
  right: "flex-end"
2548
3271
  };
3272
+ var ROW_JUSTIFY = { ...ROW_ALIGN, distribute: "space-between" };
3273
+ var ROW_VALIGN = {
3274
+ top: "flex-start",
3275
+ center: "center",
3276
+ bottom: "flex-end"
3277
+ };
3278
+ function applyRowStyle(rowEl, style) {
3279
+ const s = rowEl.style;
3280
+ if (typeof style.background === "string" && style.background) s.background = style.background;
3281
+ for (const side of ["Top", "Right", "Bottom", "Left"]) {
3282
+ const p = style[`padding${side}`];
3283
+ if (typeof p === "number" && Number.isFinite(p))
3284
+ s.setProperty(`padding-${side.toLowerCase()}`, `${p}px`);
3285
+ const m = style[`margin${side}`];
3286
+ if (typeof m === "number" && Number.isFinite(m))
3287
+ s.setProperty(`margin-${side.toLowerCase()}`, `${m}px`);
3288
+ }
3289
+ if (typeof style.minHeight === "number" && Number.isFinite(style.minHeight) && style.minHeight > 0) {
3290
+ s.minHeight = `${style.minHeight}px`;
3291
+ }
3292
+ if (style.align && ROW_JUSTIFY[style.align]) {
3293
+ s.justifyContent = ROW_JUSTIFY[style.align];
3294
+ rowEl.classList.add("veo-row--aligned");
3295
+ }
3296
+ if (style.valign && ROW_VALIGN[style.valign]) s.alignItems = ROW_VALIGN[style.valign];
3297
+ }
2549
3298
  var CompositeRenderer = class extends BaseRenderer {
2550
3299
  async render(context) {
2551
3300
  const idx = context.nav?.stepIndex ?? 0;
@@ -2564,7 +3313,8 @@ var CompositeRenderer = class extends BaseRenderer {
2564
3313
  const rawDoc = anchor.ownerDocument ?? document;
2565
3314
  const { cleanup } = mountCodeBlock(soleCode, rawDoc, context, void 0, {
2566
3315
  anchor,
2567
- pos: inlinePosOf(step.inlinePosition)
3316
+ pos: inlinePosOf(step.inlinePosition),
3317
+ ...context.isPreview && soleCode.editorId ? { tagBlockId: soleCode.editorId } : {}
2568
3318
  });
2569
3319
  this.registerCleanup(cleanup);
2570
3320
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: idx, action: "shown" });
@@ -2577,6 +3327,10 @@ var CompositeRenderer = class extends BaseRenderer {
2577
3327
  const card = doc.createElement("div");
2578
3328
  card.className = "veo-card";
2579
3329
  applyCompositeDesign(card, design);
3330
+ if (typeof design.ariaLabel === "string" && design.ariaLabel.trim()) {
3331
+ card.setAttribute("role", "dialog");
3332
+ card.setAttribute("aria-label", design.ariaLabel.trim());
3333
+ }
2580
3334
  const dismiss = (action, meta) => {
2581
3335
  context.onInteraction({
2582
3336
  guideId: context.guide.guideId,
@@ -2587,6 +3341,28 @@ var CompositeRenderer = class extends BaseRenderer {
2587
3341
  };
2588
3342
  const readers = [];
2589
3343
  this.buildContent(card, step.blocks ?? [], doc, context, idx, readers, host);
3344
+ if (blocks.length === 0) {
3345
+ card.classList.add("veo-card--empty");
3346
+ if (context.isPreview) {
3347
+ const zone = doc.createElement("div");
3348
+ zone.className = "veo-card-empty-zone";
3349
+ const plus = doc.createElement("b");
3350
+ plus.textContent = "+";
3351
+ const text = doc.createElement("span");
3352
+ text.textContent = "Clic para agregar contenido";
3353
+ zone.append(plus, text);
3354
+ card.appendChild(zone);
3355
+ const applyNarrow = () => {
3356
+ card.classList.toggle("veo-card--narrow", card.clientWidth < NARROW_CARD_WIDTH);
3357
+ };
3358
+ if (typeof ResizeObserver !== "undefined") {
3359
+ const ro = new ResizeObserver(applyNarrow);
3360
+ ro.observe(card);
3361
+ this.registerCleanup(() => ro.disconnect());
3362
+ }
3363
+ applyNarrow();
3364
+ }
3365
+ }
2590
3366
  const lastBlock = card.lastElementChild;
2591
3367
  if (lastBlock && !lastBlock.style.marginBottom) lastBlock.style.marginBottom = "0";
2592
3368
  let closeTooltip = null;
@@ -2604,6 +3380,14 @@ var CompositeRenderer = class extends BaseRenderer {
2604
3380
  if (closeColor) closeBtn.style.color = closeColor;
2605
3381
  const closePos = typeof design.closePosition === "string" ? design.closePosition : "top-right";
2606
3382
  applyClosePosition(closeBtn, closePos);
3383
+ for (const side of ["Top", "Right", "Bottom", "Left"]) {
3384
+ const p = design[`closePadding${side}`];
3385
+ if (typeof p === "number")
3386
+ closeBtn.style.setProperty(`padding-${side.toLowerCase()}`, `${p}px`);
3387
+ const m = design[`closeMargin${side}`];
3388
+ if (typeof m === "number")
3389
+ closeBtn.style.setProperty(`margin-${side.toLowerCase()}`, `${m}px`);
3390
+ }
2607
3391
  if (hoverColor) {
2608
3392
  closeBtn.addEventListener("mouseenter", () => {
2609
3393
  closeBtn.style.color = hoverColor;
@@ -2613,6 +3397,7 @@ var CompositeRenderer = class extends BaseRenderer {
2613
3397
  });
2614
3398
  }
2615
3399
  card.appendChild(closeBtn);
3400
+ reserveCloseSpace(card, blocks, closePos, design);
2616
3401
  }
2617
3402
  if (badgeMode && anchor) {
2618
3403
  closeTooltip = this.renderBadgeMode(
@@ -2643,6 +3428,7 @@ var CompositeRenderer = class extends BaseRenderer {
2643
3428
  const bz = typeof design.backdropZIndex === "number" ? design.backdropZIndex : 2e5;
2644
3429
  bg.style.zIndex = String(bz);
2645
3430
  root.appendChild(bg);
3431
+ this.registerCleanup(lockScroll(doc));
2646
3432
  if (design.targetAbove === true) this.registerCleanup(liftTarget(anchor, bz + 1));
2647
3433
  }
2648
3434
  const wrapper = doc.createElement("div");
@@ -2669,6 +3455,7 @@ var CompositeRenderer = class extends BaseRenderer {
2669
3455
  this.closeOrSkip(context);
2670
3456
  }
2671
3457
  });
3458
+ this.registerCleanup(lockScroll(doc));
2672
3459
  }
2673
3460
  applyViewportPosition(card, step.position, design);
2674
3461
  overlay.appendChild(card);
@@ -2686,83 +3473,217 @@ var CompositeRenderer = class extends BaseRenderer {
2686
3473
  ...block.id ? { metadata: { buttonId: block.id } } : {}
2687
3474
  });
2688
3475
  };
2689
- let i = 0;
2690
- while (i < blocks.length) {
2691
- const b = blocks[i];
2692
- if (!b) {
2693
- i++;
2694
- continue;
2695
- }
2696
- if (b.type === "button") {
2697
- const row = doc.createElement("div");
2698
- row.className = "veo-guide-actions";
2699
- let rowAlign;
2700
- while (i < blocks.length && blocks[i]?.type === "button") {
2701
- const bb = blocks[i];
2702
- if (!rowAlign) {
2703
- const a = bb.style?.align;
2704
- if (a === "left" || a === "center" || a === "right") rowAlign = a;
3476
+ const tag = (node, block, index) => {
3477
+ node.dataset.veoBlock = String(index);
3478
+ if (block.editorId) node.dataset.veoBlockId = block.editorId;
3479
+ return node;
3480
+ };
3481
+ const placeholder = (block, label) => {
3482
+ if (!context.isPreview || !block.editorId) return null;
3483
+ const el2 = doc.createElement("div");
3484
+ el2.className = "veo-block-placeholder";
3485
+ el2.textContent = label;
3486
+ return el2;
3487
+ };
3488
+ const buildSequence = (container, cell) => {
3489
+ let i = 0;
3490
+ while (i < cell.length) {
3491
+ const { block: b, index } = cell[i];
3492
+ if (b.type === "button") {
3493
+ const row = doc.createElement("div");
3494
+ row.className = "veo-guide-actions";
3495
+ let rowAlign;
3496
+ while (i < cell.length && cell[i]?.block.type === "button") {
3497
+ const { block: bb, index: bi } = cell[i];
3498
+ if (!rowAlign) {
3499
+ const a = bb.style?.align;
3500
+ if (a === "left" || a === "center" || a === "right") rowAlign = a;
3501
+ }
3502
+ row.appendChild(tag(this.buildButton(bb, doc, context, idx, readers, emit), bb, bi));
3503
+ i++;
2705
3504
  }
2706
- row.appendChild(this.buildButton(bb, doc, context, idx, readers, emit));
3505
+ if (rowAlign) row.style.justifyContent = ROW_ALIGN[rowAlign] ?? "flex-end";
3506
+ container.appendChild(row);
3507
+ continue;
3508
+ }
3509
+ if (b.type === "code") {
3510
+ const { node: node2, cleanup } = mountCodeBlock(b, doc, context, host);
3511
+ const el2 = node2 ?? placeholder(b, "Bloque de c\xF3digo");
3512
+ if (el2) container.appendChild(tag(el2, b, index));
3513
+ this.registerCleanup(cleanup);
2707
3514
  i++;
3515
+ continue;
2708
3516
  }
2709
- if (rowAlign) row.style.justifyContent = ROW_ALIGN[rowAlign] ?? "flex-end";
2710
- card.appendChild(row);
2711
- continue;
2712
- }
2713
- if (b.type === "code") {
2714
- const { node: node2, cleanup } = mountCodeBlock(b, doc, context, host);
2715
- if (node2) card.appendChild(node2);
2716
- this.registerCleanup(cleanup);
2717
- i++;
2718
- continue;
2719
- }
2720
- if (b.type === "form") {
2721
- const form = doc.createElement("div");
2722
- form.className = "veo-form";
2723
- for (const field of b.fields ?? []) {
2724
- const { wrapper, read } = buildField(field, doc);
2725
- readers.push({ field, read });
2726
- form.appendChild(wrapper);
3517
+ if (b.type === "form") {
3518
+ container.appendChild(tag(this.buildForm(b, doc, context, idx, readers, emit), b, index));
3519
+ i++;
3520
+ continue;
3521
+ }
3522
+ if (b.type === "checklist") {
3523
+ const el2 = buildChecklistBlock(b, doc, context);
3524
+ applyBlockStyle(el2, b.style, "text");
3525
+ el2.style.alignSelf = "stretch";
3526
+ el2.style.width = "100%";
3527
+ container.appendChild(tag(el2, b, index));
3528
+ i++;
3529
+ continue;
2727
3530
  }
2728
- card.appendChild(form);
3531
+ const node = buildContentBlock(b, doc, { editing: context.isPreview === true }) ?? placeholder(
3532
+ b,
3533
+ b.type === "image" ? "Imagen (sin URL)" : b.type === "video" ? "Video (elige proveedor y pega la URL)" : "Bloque vac\xEDo"
3534
+ );
3535
+ if (node) container.appendChild(tag(node, b, index));
2729
3536
  i++;
3537
+ }
3538
+ };
3539
+ const rowStyles = context.guide.guideSteps[idx]?.rows;
3540
+ for (const row of groupLayout(blocks)) {
3541
+ const rowStyle = Number.isInteger(row.row) ? rowStyles?.[String(row.row)] : void 0;
3542
+ if (row.columns.length === 1 && !rowStyle) {
3543
+ buildSequence(card, row.columns[0].blocks);
2730
3544
  continue;
2731
3545
  }
2732
- const node = buildContentBlock(b, doc);
2733
- if (node) card.appendChild(node);
2734
- i++;
3546
+ const rowEl = doc.createElement("div");
3547
+ rowEl.className = "veo-row";
3548
+ if (Number.isInteger(row.row)) rowEl.dataset.veoRow = String(row.row);
3549
+ for (const column of row.columns) {
3550
+ const colEl = doc.createElement("div");
3551
+ colEl.className = "veo-col";
3552
+ buildSequence(colEl, column.blocks);
3553
+ rowEl.appendChild(colEl);
3554
+ }
3555
+ if (rowStyle) applyRowStyle(rowEl, rowStyle);
3556
+ card.appendChild(rowEl);
2735
3557
  }
2736
3558
  }
2737
3559
  /** Botón con enrutado de acción (navegación / cierre / url / guía / envío de forms). */
2738
- buildButton(block, doc, context, idx, readers, emit) {
3560
+ /**
3561
+ * Bloque `form` (encuesta): pregunta (rich text del bloque) + campos + botón
3562
+ * de envío propio (`submit`). Con pregunta, el label del campo se omite (sería
3563
+ * la misma pregunta dos veces) y pasa a `aria-label` del input. El estilo del
3564
+ * bloque viste la pregunta (preset/alineación/color); los márgenes van al
3565
+ * contenedor del bloque entero.
3566
+ */
3567
+ buildForm(b, doc, context, idx, readers, emit) {
3568
+ const form = doc.createElement("div");
3569
+ form.className = "veo-form";
3570
+ const style = b.style ?? {};
3571
+ const { marginTop, marginBottom, ...questionStyle } = style;
3572
+ for (const [side, v] of [
3573
+ ["marginTop", marginTop],
3574
+ ["marginBottom", marginBottom]
3575
+ ]) {
3576
+ if (typeof v === "number" && Number.isFinite(v)) form.style[side] = `${v}px`;
3577
+ }
3578
+ let question = null;
3579
+ if (typeof b.html === "string" && b.html || typeof b.text === "string" && b.text) {
3580
+ question = doc.createElement("div");
3581
+ question.className = "veo-form-question";
3582
+ if (typeof b.html === "string" && b.html) question.appendChild(renderRichText(b.html, doc));
3583
+ else question.textContent = b.text;
3584
+ applyBlockStyle(question, questionStyle, "text");
3585
+ form.appendChild(question);
3586
+ }
3587
+ const questionText = question?.textContent?.trim() ?? "";
3588
+ const own = [];
3589
+ for (const field of b.fields ?? []) {
3590
+ const { wrapper, read } = buildField(field, doc);
3591
+ const st = b.style ?? {};
3592
+ if (typeof st.scaleColor === "string" && st.scaleColor) {
3593
+ wrapper.style.setProperty("--veo-scale-color", st.scaleColor);
3594
+ }
3595
+ if (typeof st.scaleSize === "number" && st.scaleSize >= 24 && st.scaleSize <= 96) {
3596
+ wrapper.style.setProperty("--veo-scale-size", `${st.scaleSize}px`);
3597
+ }
3598
+ if (question) {
3599
+ wrapper.querySelector(".veo-form-label")?.remove();
3600
+ if (questionText) {
3601
+ for (const inp of wrapper.querySelectorAll("input, textarea, select")) {
3602
+ inp.setAttribute("aria-label", questionText);
3603
+ }
3604
+ }
3605
+ }
3606
+ const reader = { field, read, blockId: b.id ?? null };
3607
+ readers.push(reader);
3608
+ own.push(reader);
3609
+ form.appendChild(wrapper);
3610
+ }
3611
+ if (b.submit) {
3612
+ const actions = doc.createElement("div");
3613
+ actions.className = "veo-guide-actions";
3614
+ const submit = { ...b.submit, type: "button" };
3615
+ const sa = submit.style?.align;
3616
+ if (sa === "left" || sa === "center" || sa === "right") {
3617
+ actions.style.justifyContent = ROW_ALIGN[sa] ?? "flex-end";
3618
+ }
3619
+ actions.appendChild(this.buildButton(submit, doc, context, idx, readers, emit, own));
3620
+ form.appendChild(actions);
3621
+ }
3622
+ return form;
3623
+ }
3624
+ buildButton(block, doc, context, idx, readers, emit, submitFirst) {
2739
3625
  const btn = doc.createElement("button");
2740
3626
  btn.type = "button";
2741
3627
  btn.className = "veo-guide-cta";
2742
3628
  btn.textContent = block.text?.trim() || "OK";
3629
+ const st = block.style ?? {};
3630
+ if (st.preset === "secondary" || st.preset === "link") {
3631
+ btn.classList.add(`veo-guide-cta--${st.preset}`);
3632
+ }
3633
+ if (typeof st.ariaLabel === "string" && st.ariaLabel.trim()) {
3634
+ btn.setAttribute("aria-label", st.ariaLabel.trim());
3635
+ }
2743
3636
  applyBlockStyle(btn, block.style, "button");
2744
3637
  const nav = context.nav?.callbacks;
2745
- btn.addEventListener("click", () => {
2746
- switch (block.action) {
3638
+ const runSimple = (spec) => {
3639
+ switch (spec.action) {
2747
3640
  case "next":
2748
- emit(block);
2749
3641
  if (nav) nav.onNext();
2750
3642
  else context.onClose();
2751
3643
  break;
2752
3644
  case "prev":
2753
- emit(block);
2754
3645
  nav?.onBack();
2755
3646
  break;
3647
+ case "goto":
3648
+ if (nav?.onGoto && typeof spec.targetStep === "number") nav.onGoto(spec.targetStep);
3649
+ else context.onClose();
3650
+ break;
3651
+ case "snooze":
3652
+ snoozeGuide(
3653
+ context.guide.guideId,
3654
+ typeof spec.snoozeEvery === "number" ? spec.snoozeEvery : 1,
3655
+ spec.snoozeUnit ?? "day"
3656
+ );
3657
+ context.onClose();
3658
+ break;
2756
3659
  case "url":
2757
- emit(block);
2758
- if (typeof block.url === "string" && isSafeUrl(block.url)) {
2759
- window.open(block.url, "_blank", "noopener,noreferrer");
3660
+ if (typeof spec.url === "string" && isSafeUrl(spec.url)) {
3661
+ if (spec.newTab === false) window.location.assign(spec.url);
3662
+ else window.open(spec.url, "_blank", "noopener,noreferrer");
2760
3663
  }
2761
3664
  break;
2762
3665
  case "guide":
2763
- emit(block);
2764
- if (block.targetGuideId) context.onOpenGuide?.(block.targetGuideId);
3666
+ if (spec.targetGuideId) context.onOpenGuide?.(spec.targetGuideId);
3667
+ this.closeOrSkip(context);
3668
+ break;
3669
+ default:
2765
3670
  this.closeOrSkip(context);
3671
+ }
3672
+ };
3673
+ const runSecond = () => {
3674
+ if (block.secondAction?.action) runSimple(block.secondAction);
3675
+ };
3676
+ const runActions = () => {
3677
+ switch (block.action) {
3678
+ case "next":
3679
+ case "prev":
3680
+ case "goto":
3681
+ case "snooze":
3682
+ case "url":
3683
+ case "guide":
3684
+ emit(block);
3685
+ runSimple(block);
3686
+ runSecond();
2766
3687
  break;
2767
3688
  case "submit-forms":
2768
3689
  case "submit-forms-advance":
@@ -2781,7 +3702,12 @@ var CompositeRenderer = class extends BaseRenderer {
2781
3702
  default:
2782
3703
  emit(block);
2783
3704
  this.closeOrSkip(context);
3705
+ runSecond();
2784
3706
  }
3707
+ };
3708
+ btn.addEventListener("click", () => {
3709
+ if (submitFirst) this.submitForms(submitFirst, context, idx, btn, runActions);
3710
+ else runActions();
2785
3711
  });
2786
3712
  return btn;
2787
3713
  }
@@ -2800,17 +3726,27 @@ var CompositeRenderer = class extends BaseRenderer {
2800
3726
  }
2801
3727
  answers[field.key] = value;
2802
3728
  }
2803
- if (readers.length > 0 && Object.keys(answers).length === 0) return;
3729
+ if (Object.keys(answers).length === 0) {
3730
+ onDone();
3731
+ return;
3732
+ }
2804
3733
  btn.disabled = true;
2805
3734
  const submitFn = context.onFormSubmit ?? (() => Promise.resolve(true));
2806
- void submitFn(answers).then((ok) => {
3735
+ const blockIds = new Set(
3736
+ readers.filter((r) => answers[r.field.key] !== void 0).map((r) => r.blockId ?? "")
3737
+ );
3738
+ const pollId = blockIds.size === 1 ? [...blockIds][0] || void 0 : void 0;
3739
+ void submitFn(answers, { ...pollId ? { pollId } : {}, stepIndex: idx }).then((ok) => {
2807
3740
  btn.disabled = false;
2808
3741
  if (!ok) return;
2809
- context.onInteraction({
2810
- guideId: context.guide.guideId,
2811
- stepIndex: idx,
2812
- action: "completed"
2813
- });
3742
+ const lastStep = !context.nav || idx >= context.nav.totalSteps - 1;
3743
+ if (lastStep) {
3744
+ context.onInteraction({
3745
+ guideId: context.guide.guideId,
3746
+ stepIndex: idx,
3747
+ action: "completed"
3748
+ });
3749
+ }
2814
3750
  onDone();
2815
3751
  });
2816
3752
  }
@@ -2924,21 +3860,38 @@ function inlinePosOf(p) {
2924
3860
  function applyViewportPosition(card, position, design) {
2925
3861
  const s = card.style;
2926
3862
  s.position = "fixed";
2927
- const M = "16px";
3863
+ const BASE = 16;
3864
+ const m = (k) => typeof design[k] === "number" && Number.isFinite(design[k]) ? design[k] : 0;
3865
+ s.margin = "0";
2928
3866
  const hAlign = design.hAlign === "left" ? "left" : design.hAlign === "right" ? "right" : null;
2929
3867
  const v = position.includes("top") ? "top" : position.includes("bottom") ? "bottom" : "middle";
2930
3868
  const h = hAlign ?? (position.includes("left") ? "left" : position.includes("right") ? "right" : "middle");
2931
- if (v === "top") s.top = M;
2932
- else if (v === "bottom") s.bottom = M;
3869
+ if (v === "top") s.top = `${BASE + m("marginTop")}px`;
3870
+ else if (v === "bottom") s.bottom = `${BASE + m("marginBottom")}px`;
2933
3871
  else s.top = "50%";
2934
- if (h === "left") s.left = M;
2935
- else if (h === "right") s.right = M;
3872
+ if (h === "left") s.left = `${BASE + m("marginLeft")}px`;
3873
+ else if (h === "right") s.right = `${BASE + m("marginRight")}px`;
2936
3874
  else s.left = "50%";
2937
- const tx = h === "middle" ? "-50%" : "0";
2938
- const ty = v === "middle" ? "-50%" : "0";
3875
+ const dx = m("marginLeft") - m("marginRight");
3876
+ const dy = m("marginTop") - m("marginBottom");
3877
+ const tx = h === "middle" ? `calc(-50% + ${dx}px)` : "0";
3878
+ const ty = v === "middle" ? `calc(-50% + ${dy}px)` : "0";
2939
3879
  s.transform = tx === "0" && ty === "0" ? "" : `translate(${tx}, ${ty})`;
2940
3880
  }
2941
3881
  var CLOSE_INSET = "10px";
3882
+ var CLOSE_RESERVE_PX = 42;
3883
+ var CARD_DEFAULT_PAD_PX = 20;
3884
+ function reserveCloseSpace(card, blocks, position, design) {
3885
+ if (blocks.length === 0) return;
3886
+ const side = position.includes("bottom") ? "Bottom" : "Top";
3887
+ const edgeBlock = side === "Top" ? blocks[0] : blocks[blocks.length - 1];
3888
+ if (edgeBlock?.type === "image") return;
3889
+ const margin = design[`closeMargin${side}`];
3890
+ const need = CLOSE_RESERVE_PX + (typeof margin === "number" ? margin : 0);
3891
+ const current = design[`padding${side}`];
3892
+ const have = typeof current === "number" ? current : CARD_DEFAULT_PAD_PX;
3893
+ if (have < need) card.style.setProperty(`padding-${side.toLowerCase()}`, `${need}px`);
3894
+ }
2942
3895
  function applyClosePosition(btn, position) {
2943
3896
  const s = btn.style;
2944
3897
  s.top = "auto";
@@ -3003,10 +3956,17 @@ async function anchorPosition(anchor, wrapper, caret, preferred = "bottom") {
3003
3956
  window.removeEventListener("resize", reposition);
3004
3957
  };
3005
3958
  }
3959
+ var MIN_CARD_WIDTH = 120;
3960
+ var NARROW_CARD_WIDTH = 260;
3006
3961
  function applyCompositeDesign(card, design) {
3007
3962
  const s = card.style;
3008
3963
  const px = (v) => typeof v === "number" && Number.isFinite(v) ? `${v}px` : null;
3009
3964
  const strv = (v) => typeof v === "string" && v ? v : null;
3965
+ if (design.colorMode === "dark") {
3966
+ s.background = "#1f1d1b";
3967
+ s.color = "#f5f0eb";
3968
+ card.classList.add("veo-card--dark");
3969
+ }
3010
3970
  if (strv(design.background)) s.background = design.background;
3011
3971
  const textColor = strv(design.color) ?? strv(design.textColor);
3012
3972
  if (textColor) s.color = textColor;
@@ -3028,13 +3988,14 @@ function applyCompositeDesign(card, design) {
3028
3988
  const n = (k) => typeof design[k] === "number" ? design[k] : 0;
3029
3989
  s.boxShadow = `${n("shadowX")}px ${n("shadowY")}px ${n("shadowBlur")}px ${n("shadowSpread")}px ${strv(design.shadowColor) ?? "rgba(0,0,0,0.2)"}`;
3030
3990
  }
3991
+ const width = typeof design.width === "number" && Number.isFinite(design.width) ? `${Math.max(MIN_CARD_WIDTH, design.width)}px` : null;
3031
3992
  if (design.fullWidth === true) {
3032
3993
  s.width = "100%";
3033
3994
  } else if (design.widthType === "fluid") {
3034
- s.width = "auto";
3035
- if (px(design.width)) s.maxWidth = px(design.width);
3036
- } else if (px(design.width)) {
3037
- s.width = px(design.width);
3995
+ s.width = "100%";
3996
+ if (width) s.maxWidth = width;
3997
+ } else if (width) {
3998
+ s.width = width;
3038
3999
  }
3039
4000
  for (const side of ["Top", "Right", "Bottom", "Left"]) {
3040
4001
  const p = px(design[`padding${side}`]);
@@ -3138,6 +4099,7 @@ var FormRenderer = class extends BaseRenderer {
3138
4099
  mountFormContent(card, context, doc);
3139
4100
  overlay.appendChild(card);
3140
4101
  root.appendChild(overlay);
4102
+ this.registerCleanup(lockScroll(doc));
3141
4103
  const dismiss = () => {
3142
4104
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "dismissed" });
3143
4105
  context.onClose();
@@ -3265,6 +4227,18 @@ var ModalRenderer = class extends BaseRenderer {
3265
4227
  constructor() {
3266
4228
  super(...arguments);
3267
4229
  this.overlay = null;
4230
+ /** Suelta el bloqueo de scroll del backdrop (null = sin backdrop). */
4231
+ this.releaseScroll = null;
4232
+ }
4233
+ /** Con backdrop la app de atrás queda congelada; sin él, scrollea normal. */
4234
+ syncScrollLock(step) {
4235
+ const wantLock = step.style?.backdrop !== false;
4236
+ if (wantLock && !this.releaseScroll) {
4237
+ this.releaseScroll = lockScroll(this.host?.ownerDocument ?? document);
4238
+ } else if (!wantLock && this.releaseScroll) {
4239
+ this.releaseScroll();
4240
+ this.releaseScroll = null;
4241
+ }
3268
4242
  }
3269
4243
  render(context) {
3270
4244
  const idx = context.nav?.stepIndex ?? 0;
@@ -3276,6 +4250,7 @@ var ModalRenderer = class extends BaseRenderer {
3276
4250
  const overlay = ownerDocument.createElement("div");
3277
4251
  overlay.className = overlayClassOf(step);
3278
4252
  this.overlay = overlay;
4253
+ this.syncScrollLock(step);
3279
4254
  const card = ownerDocument.createElement("div");
3280
4255
  card.className = "veo-modal-card";
3281
4256
  const dismiss = (action, ctaUrl, meta) => {
@@ -3333,9 +4308,12 @@ var ModalRenderer = class extends BaseRenderer {
3333
4308
  }
3334
4309
  onLiveUpdate(step) {
3335
4310
  if (this.overlay) this.overlay.className = overlayClassOf(step);
4311
+ this.syncScrollLock(step);
3336
4312
  }
3337
4313
  destroy() {
3338
4314
  this.overlay = null;
4315
+ this.releaseScroll?.();
4316
+ this.releaseScroll = null;
3339
4317
  super.destroy();
3340
4318
  }
3341
4319
  };
@@ -3561,6 +4539,7 @@ var GuidePreviewController = class {
3561
4539
  // Preview de forms: por defecto simula el envío SIN escribir props;
3562
4540
  // el caller puede pasar `formSubmit` real (link de preview).
3563
4541
  onFormSubmit: this.input?.formSubmit ?? (() => Promise.resolve(true)),
4542
+ ...this.input?.fetchProgress ? { fetchProgress: this.input.fetchProgress } : {},
3564
4543
  isPreview: true,
3565
4544
  ...this.input?.formSubmit && this.input.formSubmitNote !== void 0 ? { previewNote: this.input.formSubmitNote } : {}
3566
4545
  });
@@ -3738,9 +4717,35 @@ function matchesUrl(matcher, pageUrl) {
3738
4717
  }
3739
4718
  }
3740
4719
  function stepMatchesPage(loc, pageUrl) {
3741
- if (!loc || loc.scope === "sitewide" || !loc.pattern) return true;
4720
+ if (!loc || loc.scope === "sitewide") return true;
4721
+ if (loc.scope === "pages") {
4722
+ const rules = loc.rules ?? [];
4723
+ return rules.some((r) => pageRuleMatches(r, pageUrl));
4724
+ }
4725
+ if (!loc.pattern) return true;
3742
4726
  return matchesUrl({ type: loc.matchType ?? "prefix", pattern: loc.pattern }, pageUrl);
3743
4727
  }
4728
+ function pageRuleMatches(rule, pageUrl) {
4729
+ const pattern = rule.pattern.toLowerCase();
4730
+ const candidates = urlCandidates(pageUrl).map((c) => c.toLowerCase());
4731
+ switch (rule.type) {
4732
+ case "exact":
4733
+ return candidates.some((c) => c === pattern);
4734
+ case "prefix":
4735
+ return candidates.some((c) => c.startsWith(pattern));
4736
+ case "contains":
4737
+ return candidates.some((c) => c.includes(pattern));
4738
+ case "regex":
4739
+ try {
4740
+ const re = new RegExp(rule.pattern, "i");
4741
+ return candidates.some((c) => re.test(c));
4742
+ } catch {
4743
+ return false;
4744
+ }
4745
+ default:
4746
+ return false;
4747
+ }
4748
+ }
3744
4749
 
3745
4750
  // src/plugins/builder/bridge-protocol.ts
3746
4751
  var VEO_BUILDER_SOURCE = "veo-builder";
@@ -3757,6 +4762,9 @@ var ICONS = {
3757
4762
  chevronDown: '<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/>',
3758
4763
  chevronUp: '<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5"/>',
3759
4764
  plus: '<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>',
4765
+ pencil: '<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/>',
4766
+ copy: '<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v8.25A2.25 2.25 0 0 0 6 16.5h2.25m8.25-8.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-7.5A2.25 2.25 0 0 1 8.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 0 0-2.25 2.25v6"/>',
4767
+ trash: '<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/>',
3760
4768
  cursor: '<path stroke-linecap="round" stroke-linejoin="round" d="M15.042 21.672 13.684 16.6m0 0-2.51 2.225.569-9.47 5.227 7.917-3.286-.672ZM12 2.25V4.5m5.834.166-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243-1.59-1.59"/>',
3761
4769
  spinner: '<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182"/>'
3762
4770
  };
@@ -3942,8 +4950,14 @@ function injectBuilderChrome(opts) {
3942
4950
  stepsList.append(activation);
3943
4951
  s.steps.forEach((st, i) => {
3944
4952
  const thumb = st.presentation === "inline" ? '<div class="veo-thumb veo-thumb-inline"><i></i><i></i><i></i></div>' : '<div class="veo-thumb"><i></i><i></i><b></b></div>';
3945
- const card = stepCard(thumb, st.label, s.section === "step" && s.activeStep === i);
3946
- card.addEventListener("click", () => act({ action: "select-step", index: i }));
4953
+ const isStepView = s.section === "step" || s.section === "block" || s.section === "add-block" || s.section === "row" || s.section === "structure";
4954
+ const card = stepCard(thumb, st.label, isStepView && s.activeStep === i, {
4955
+ // Se pueden borrar TODOS los pasos (el panel vuelve a Activación).
4956
+ canRemove: true,
4957
+ onEdit: () => act({ action: "select-step", index: i }),
4958
+ onDuplicate: () => act({ action: "duplicate-step", index: i }),
4959
+ onRemove: () => act({ action: "remove-step", index: i })
4960
+ });
3947
4961
  stepsList.append(card);
3948
4962
  });
3949
4963
  const add = el("button", "veo-step-add", `${svg("plus", 18)}<span>Agregar paso</span>`);
@@ -3972,16 +4986,40 @@ function injectBuilderChrome(opts) {
3972
4986
  }
3973
4987
  };
3974
4988
  }
3975
- function stepCard(inner, label, active) {
3976
- const b = el(
3977
- "button",
3978
- `veo-step${active ? " is-active" : ""}`,
3979
- `<div class="veo-step-box">${inner}</div><span class="veo-step-label"></span>`
4989
+ function stepCard(inner, label, active, actions) {
4990
+ const cls = `veo-step${active ? " is-active" : ""}`;
4991
+ if (!actions) {
4992
+ const b = el(
4993
+ "button",
4994
+ cls,
4995
+ `<div class="veo-step-box">${inner}</div><span class="veo-step-label"></span>`
4996
+ );
4997
+ b.type = "button";
4998
+ b.querySelector(".veo-step-label").textContent = label;
4999
+ b.setAttribute("aria-pressed", active ? "true" : "false");
5000
+ return b;
5001
+ }
5002
+ const card = el(
5003
+ "div",
5004
+ cls,
5005
+ `<div class="veo-step-box">${inner}<div class="veo-step-hover"><div class="veo-step-hover-top"><button type="button" class="veo-step-icon veo-step-remove" title="Eliminar paso" aria-label="Eliminar paso">${svg("trash", 14)}</button><button type="button" class="veo-step-icon veo-step-duplicate" title="Duplicar paso" aria-label="Duplicar paso">${svg("copy", 14)}</button></div><div class="veo-step-hover-main"><button type="button" class="veo-step-edit">${svg("pencil", 14)}<span>Editar</span></button></div></div></div><span class="veo-step-label"></span>`
3980
5006
  );
3981
- b.type = "button";
3982
- b.querySelector(".veo-step-label").textContent = label;
3983
- b.setAttribute("aria-pressed", active ? "true" : "false");
3984
- return b;
5007
+ card.querySelector(".veo-step-label").textContent = label;
5008
+ card.setAttribute("aria-current", active ? "step" : "false");
5009
+ const remove = card.querySelector(".veo-step-remove");
5010
+ remove.disabled = !actions.canRemove;
5011
+ if (!actions.canRemove) remove.title = "La gu\xEDa necesita al menos un paso";
5012
+ const on = (sel, fn) => {
5013
+ card.querySelector(sel)?.addEventListener("click", (e) => {
5014
+ e.stopPropagation();
5015
+ fn();
5016
+ });
5017
+ };
5018
+ on(".veo-step-edit", actions.onEdit);
5019
+ on(".veo-step-duplicate", actions.onDuplicate);
5020
+ on(".veo-step-remove", actions.onRemove);
5021
+ card.addEventListener("click", actions.onEdit);
5022
+ return card;
3985
5023
  }
3986
5024
  var CHROME_CSS = `
3987
5025
  :host { all: initial; }
@@ -4066,10 +5104,31 @@ var CHROME_CSS = `
4066
5104
  .veo-steps-list { display: flex; align-items: flex-start; gap: 20px; }
4067
5105
  .veo-step { border: 0; background: transparent; padding: 0; cursor: pointer; font: inherit; display: flex; flex-direction: column; align-items: center; gap: 8px; flex: 0 0 auto; }
4068
5106
  .veo-step-box {
5107
+ position: relative; overflow: hidden;
4069
5108
  width: 116px; height: 64px; border-radius: 10px; background: #fff; border: 2px solid transparent;
4070
5109
  display: flex; align-items: center; justify-content: center; color: #2b2623;
4071
5110
  box-shadow: 0 1px 2px rgba(0,0,0,0.06);
4072
5111
  }
5112
+ /* Capa de acciones del paso: aparece al pasar el mouse o al enfocar un bot\xF3n. */
5113
+ .veo-step-hover {
5114
+ position: absolute; inset: 0; background: #2b2623; border-radius: 8px;
5115
+ display: flex; flex-direction: column; padding: 3px 4px 6px; box-sizing: border-box;
5116
+ opacity: 0; transition: opacity 120ms ease-out;
5117
+ }
5118
+ .veo-step:hover .veo-step-hover, .veo-step:focus-within .veo-step-hover { opacity: 1; }
5119
+ .veo-step-hover-top { display: flex; justify-content: space-between; align-items: center; }
5120
+ .veo-step-hover-main { flex: 1; display: flex; align-items: center; justify-content: center; }
5121
+ .veo-step-icon {
5122
+ border: 0; background: transparent; color: #fff; cursor: pointer; padding: 0;
5123
+ width: 20px; height: 20px; border-radius: 5px; display: inline-flex; align-items: center; justify-content: center;
5124
+ }
5125
+ .veo-step-icon:hover { background: rgba(255,255,255,0.14); }
5126
+ .veo-step-icon:disabled { opacity: 0.35; cursor: default; background: transparent; }
5127
+ .veo-step-edit {
5128
+ width: 100%; margin: 0 6px; border: 0; background: #dbeefc; color: #1d6fb8; font: inherit; font-size: 13px; font-weight: 600;
5129
+ border-radius: 6px; padding: 5px 10px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
5130
+ }
5131
+ .veo-step-edit:hover { background: #cbe4f9; }
4073
5132
  .veo-step.is-active .veo-step-box { border-color: #b74d00; }
4074
5133
  .veo-step-label { font-size: 13px; color: #6b635d; }
4075
5134
  .veo-step.is-active .veo-step-label { color: #2b2623; font-weight: 600; }
@@ -4112,6 +5171,8 @@ function injectBuilderPanel(opts) {
4112
5171
  },
4113
5172
  isVisible: () => false,
4114
5173
  setTopOffset: () => {
5174
+ },
5175
+ setSize: () => {
4115
5176
  }
4116
5177
  };
4117
5178
  }
@@ -4164,6 +5225,21 @@ function injectBuilderPanel(opts) {
4164
5225
  card.style.display = next && !(popped && !popped.closed) ? "" : "none";
4165
5226
  };
4166
5227
  minimize.addEventListener("click", () => setVisible(false));
5228
+ let compactRestore = null;
5229
+ const setSize = (size) => {
5230
+ if (maximized || popped && !popped.closed) return;
5231
+ if (size) {
5232
+ if (!compactRestore) compactRestore = { w: card.offsetWidth, h: card.offsetHeight };
5233
+ card.style.minHeight = "0";
5234
+ card.style.width = `${clamp3(Math.round(size.width), MIN_W, window.innerWidth - MARGIN * 2)}px`;
5235
+ card.style.height = `${clamp3(Math.round(size.height) + HEADER_H, HEADER_H + 80, window.innerHeight - top - MARGIN)}px`;
5236
+ } else if (compactRestore) {
5237
+ card.style.minHeight = "";
5238
+ card.style.width = `${compactRestore.w}px`;
5239
+ card.style.height = `${compactRestore.h}px`;
5240
+ compactRestore = null;
5241
+ }
5242
+ };
4167
5243
  maximize.addEventListener("click", () => {
4168
5244
  maximized = !maximized;
4169
5245
  if (maximized) {
@@ -4263,6 +5339,7 @@ function injectBuilderPanel(opts) {
4263
5339
  title.textContent = t;
4264
5340
  },
4265
5341
  setVisible,
5342
+ setSize,
4266
5343
  isVisible: () => visible,
4267
5344
  setTopOffset: (px) => {
4268
5345
  topOffset = px;
@@ -4909,6 +5986,9 @@ function initBuilderMode() {
4909
5986
  let staticTarget = null;
4910
5987
  let manipulator = null;
4911
5988
  let dropTarget = null;
5989
+ let cardClick = null;
5990
+ let rawBlock = null;
5991
+ let selectedBlockId = null;
4912
5992
  let dragGhost = null;
4913
5993
  let previewStepIndex = 0;
4914
5994
  let pendingPreview = null;
@@ -4929,13 +6009,742 @@ function initBuilderMode() {
4929
6009
  };
4930
6010
  const previewMatchesPage = (cmd) => stepMatchesPage(previewStep(cmd)?.pageLocation, window.location.href);
4931
6011
  const postPreviewStatus = (payload) => post({ type: "preview-status", payload });
4932
- const withholdPreview = () => {
6012
+ const detachPreviewTools = () => {
4933
6013
  manipulator?.detach();
4934
6014
  manipulator = null;
4935
6015
  dropTarget?.detach();
4936
6016
  dropTarget = null;
6017
+ cardClick?.();
6018
+ cardClick = null;
6019
+ rowHover?.();
6020
+ rowHover = null;
6021
+ hoverRowId = null;
6022
+ hoverCard = false;
6023
+ rawBlock?.detach();
6024
+ rawBlock = null;
6025
+ shownPreview = null;
6026
+ };
6027
+ const withholdPreview = () => {
6028
+ detachPreviewTools();
6029
+ shownPreview = null;
4937
6030
  closeGuidePreview();
4938
6031
  };
6032
+ const FRAME_PAD = 4;
6033
+ const FRAME_MIN_H = 0;
6034
+ const TOOLBAR_H = 30;
6035
+ const ICON = {
6036
+ pencil: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"/></svg>',
6037
+ copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16.5 8.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v8.25A2.25 2.25 0 0 0 6 16.5h2.25m8.25-8.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-7.5A2.25 2.25 0 0 1 8.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 0 0-2.25 2.25v6"/></svg>',
6038
+ trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/></svg>'
6039
+ };
6040
+ const ICON_MOVE = {
6041
+ left: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M19.5 12h-15m0 0 6.75 6.75M4.5 12l6.75-6.75"/></svg>',
6042
+ right: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 12h15m0 0-6.75-6.75M19.5 12l-6.75 6.75"/></svg>'
6043
+ };
6044
+ const moveAbility = (blocks, blockId) => {
6045
+ for (const row of groupLayout(blocks)) {
6046
+ for (let c = 0; c < row.columns.length; c++) {
6047
+ const col = row.columns[c];
6048
+ if (!col.blocks.some((b) => b.block.editorId === blockId)) continue;
6049
+ return { left: c > 0, right: c < row.columns.length - 1 };
6050
+ }
6051
+ }
6052
+ return { left: false, right: false };
6053
+ };
6054
+ let previewBlocks = [];
6055
+ let hoverRowId = null;
6056
+ let selectedRowId = null;
6057
+ let rowHover = null;
6058
+ let hoverCard = false;
6059
+ let panelSection = null;
6060
+ let shownPreview = null;
6061
+ const structureOf = (step) => {
6062
+ if (!step) return "";
6063
+ const { blocks, ...rest } = step;
6064
+ return JSON.stringify({
6065
+ rest,
6066
+ blocks: (blocks ?? []).map((b) => {
6067
+ const { text: _t, html: _h, style: _s, ...keep } = b;
6068
+ return keep;
6069
+ })
6070
+ });
6071
+ };
6072
+ const tryPatchPreview = (cmd) => {
6073
+ const prev = shownPreview;
6074
+ if (!prev || cmd.guide.guideType !== "composite" || prev.guide.guideType !== "composite") {
6075
+ return false;
6076
+ }
6077
+ const idx = cmd.guide.startStepIndex ?? 0;
6078
+ if ((prev.guide.startStepIndex ?? 0) !== idx) return false;
6079
+ const guideSig = (g) => JSON.stringify({ ...g, guideSteps: void 0 });
6080
+ if (guideSig(prev.guide) !== guideSig(cmd.guide) || prev.editable !== cmd.editable)
6081
+ return false;
6082
+ const host = document.querySelector("[data-veo-guide]");
6083
+ const card = host ? previewCard(host) : null;
6084
+ if (!host || !card) return false;
6085
+ const prevStep = prev.guide.guideSteps[idx];
6086
+ const nextStep = cmd.guide.guideSteps[idx];
6087
+ if (!prevStep || !nextStep) return false;
6088
+ if (structureOf(prevStep) !== structureOf(nextStep)) return false;
6089
+ const doc = card.ownerDocument;
6090
+ const prevBlocks = prevStep.blocks ?? [];
6091
+ const nextBlocks = nextStep.blocks ?? [];
6092
+ for (let i = 0; i < nextBlocks.length; i++) {
6093
+ const a = prevBlocks[i];
6094
+ const b = nextBlocks[i];
6095
+ if (a.text === b.text && a.html === b.html && JSON.stringify(a.style) === JSON.stringify(b.style)) {
6096
+ continue;
6097
+ }
6098
+ if (!b.editorId) return false;
6099
+ const node = card.querySelector(`[data-veo-block-id="${b.editorId}"]`);
6100
+ if (!node) return false;
6101
+ if (b.type === "button") {
6102
+ node.textContent = b.text?.trim() || "OK";
6103
+ node.removeAttribute("style");
6104
+ node.classList.remove("veo-guide-cta--secondary", "veo-guide-cta--link");
6105
+ const st = b.style ?? {};
6106
+ if (st.preset === "secondary" || st.preset === "link") {
6107
+ node.classList.add(`veo-guide-cta--${st.preset}`);
6108
+ }
6109
+ if (typeof st.ariaLabel === "string" && st.ariaLabel.trim()) {
6110
+ node.setAttribute("aria-label", st.ariaLabel.trim());
6111
+ } else node.removeAttribute("aria-label");
6112
+ applyBlockStyle(node, b.style, "button");
6113
+ continue;
6114
+ }
6115
+ if (b.type === "code" || b.type === "form") return false;
6116
+ const fresh = buildContentBlock(b, doc, { editing: true }) ?? (() => {
6117
+ const el2 = doc.createElement("div");
6118
+ el2.className = "veo-block-placeholder";
6119
+ el2.textContent = b.type === "image" ? "Imagen (sin URL)" : b.type === "video" ? "Video (elige proveedor y pega la URL)" : "Bloque vac\xEDo";
6120
+ return el2;
6121
+ })();
6122
+ fresh.dataset.veoBlock = node.dataset.veoBlock ?? String(i);
6123
+ fresh.dataset.veoBlockId = b.editorId;
6124
+ node.replaceWith(fresh);
6125
+ }
6126
+ shownPreview = cmd;
6127
+ previewBlocks = nextBlocks;
6128
+ applyBlockHighlight(host, idx);
6129
+ renderGapZones(host, idx, nextBlocks);
6130
+ return true;
6131
+ };
6132
+ const previewCard = (host) => host.shadowRoot?.querySelector(".veo-card") ?? null;
6133
+ const clearBlockHighlight = (host) => {
6134
+ const card = previewCard(host);
6135
+ if (!card) return;
6136
+ for (const el2 of card.querySelectorAll(".veo-block--selected")) {
6137
+ el2.classList.remove("veo-block--selected");
6138
+ }
6139
+ card.querySelector(".veo-block-toolbar")?.remove();
6140
+ card.querySelector(".veo-block-frame")?.remove();
6141
+ };
6142
+ const applyBlockHighlight = (host, stepIndex) => {
6143
+ clearBlockHighlight(host);
6144
+ const card = previewCard(host);
6145
+ if (!card) return;
6146
+ const el2 = selectedBlockId ? card.querySelector(`[data-veo-block-id="${selectedBlockId}"]`) : null;
6147
+ if (!el2 || !selectedBlockId) {
6148
+ renderRowChrome(host, stepIndex);
6149
+ return;
6150
+ }
6151
+ el2.classList.add("veo-block--selected");
6152
+ const doc = card.ownerDocument;
6153
+ const cs = getComputedStyle(card);
6154
+ const padL = Number.parseFloat(cs.paddingLeft) || 0;
6155
+ const padT = Number.parseFloat(cs.paddingTop) || 0;
6156
+ const padR = Number.parseFloat(cs.paddingRight) || 0;
6157
+ const padB = Number.parseFloat(cs.paddingBottom) || 0;
6158
+ const parent = el2.parentElement;
6159
+ const inCol = parent?.classList.contains("veo-col") ? parent : null;
6160
+ const boxL = inCol ? inCol.offsetLeft : padL;
6161
+ const boxR = inCol ? inCol.offsetLeft + inCol.offsetWidth : card.clientWidth - padR;
6162
+ const boxT = padT;
6163
+ const boxB = card.clientHeight - padB;
6164
+ const fx = Math.max(boxL, el2.offsetLeft - FRAME_PAD);
6165
+ const fr = Math.min(boxR, el2.offsetLeft + el2.offsetWidth + FRAME_PAD);
6166
+ const fw = Math.max(0, fr - fx);
6167
+ const fy = Math.max(boxT, el2.offsetTop - FRAME_PAD);
6168
+ const fb = Math.min(boxB, el2.offsetTop + el2.offsetHeight + FRAME_PAD);
6169
+ const fh = Math.max(FRAME_MIN_H, fb - fy);
6170
+ const frame = doc.createElement("div");
6171
+ frame.className = "veo-block-frame";
6172
+ frame.setAttribute("data-veo-builder-chrome", "");
6173
+ frame.style.left = `${fx}px`;
6174
+ frame.style.top = `${fy}px`;
6175
+ frame.style.width = `${fw}px`;
6176
+ frame.style.height = `${fh}px`;
6177
+ card.appendChild(frame);
6178
+ const bar = doc.createElement("div");
6179
+ bar.className = "veo-block-toolbar";
6180
+ bar.setAttribute("data-veo-builder-chrome", "");
6181
+ const blockId = selectedBlockId;
6182
+ const mk = (action, label, icon, enabled = true) => {
6183
+ const b = doc.createElement("button");
6184
+ b.type = "button";
6185
+ b.title = label;
6186
+ b.setAttribute("aria-label", label);
6187
+ b.className = `veo-block-toolbar-${action}`;
6188
+ b.innerHTML = icon;
6189
+ b.disabled = !enabled;
6190
+ b.addEventListener("click", (e) => {
6191
+ e.preventDefault();
6192
+ e.stopPropagation();
6193
+ post({ type: "block-action", payload: { stepIndex, blockId, action } });
6194
+ });
6195
+ bar.appendChild(b);
6196
+ };
6197
+ const can = moveAbility(previewBlocks, blockId);
6198
+ mk("edit", "Editar", ICON.pencil);
6199
+ mk("duplicate", "Duplicar", ICON.copy);
6200
+ mk("remove", "Eliminar", ICON.trash);
6201
+ mk("move-left", "Mover a la izquierda", ICON_MOVE.left, can.left);
6202
+ mk("move-right", "Mover a la derecha", ICON_MOVE.right, can.right);
6203
+ bar.style.right = `${Math.max(0, card.clientWidth - fr)}px`;
6204
+ card.appendChild(bar);
6205
+ const barH = bar.offsetHeight || TOOLBAR_H;
6206
+ bar.dataset.veoTopAbove = String(fy - barH - 6);
6207
+ bar.dataset.veoTopBelow = String(fb + 6);
6208
+ bar.style.top = bar.dataset.veoTopAbove;
6209
+ renderRowChrome(host, stepIndex);
6210
+ };
6211
+ const chromeRect = (el2) => ({
6212
+ left: el2.offsetLeft,
6213
+ top: el2.offsetTop,
6214
+ right: el2.offsetLeft + el2.offsetWidth,
6215
+ bottom: el2.offsetTop + el2.offsetHeight
6216
+ });
6217
+ const intersects = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
6218
+ const resolveToolbarOverlaps = (card) => {
6219
+ const cardBar = card.querySelector(".veo-card-toolbar");
6220
+ const blockBar = card.querySelector(".veo-block-toolbar");
6221
+ const rowBar = card.querySelector(".veo-row-toolbar");
6222
+ if (blockBar?.dataset.veoTopAbove) {
6223
+ blockBar.style.top = `${blockBar.dataset.veoTopAbove}px`;
6224
+ if (cardBar && intersects(chromeRect(blockBar), chromeRect(cardBar))) {
6225
+ blockBar.style.top = `${blockBar.dataset.veoTopBelow}px`;
6226
+ }
6227
+ }
6228
+ if (rowBar?.dataset.veoRight) {
6229
+ rowBar.style.left = "";
6230
+ rowBar.style.right = `${rowBar.dataset.veoRight}px`;
6231
+ rowBar.style.top = `${rowBar.dataset.veoTopBelow}px`;
6232
+ const others = [cardBar, blockBar].filter((b) => !!b);
6233
+ const clashes = () => others.some((o) => intersects(chromeRect(rowBar), chromeRect(o)));
6234
+ if (clashes()) {
6235
+ rowBar.style.right = "";
6236
+ rowBar.style.left = `${rowBar.dataset.veoLeft}px`;
6237
+ }
6238
+ if (clashes() && blockBar) {
6239
+ rowBar.style.left = "";
6240
+ rowBar.style.right = `${rowBar.dataset.veoRight}px`;
6241
+ rowBar.style.top = `${chromeRect(blockBar).bottom + 6}px`;
6242
+ }
6243
+ }
6244
+ };
6245
+ const rectOf = (el2) => ({
6246
+ left: el2.offsetLeft,
6247
+ top: el2.offsetTop,
6248
+ right: el2.offsetLeft + el2.offsetWidth,
6249
+ bottom: el2.offsetTop + el2.offsetHeight
6250
+ });
6251
+ const union = (rs) => rs.length === 0 ? null : rs.reduce((acc, r) => ({
6252
+ left: Math.min(acc.left, r.left),
6253
+ top: Math.min(acc.top, r.top),
6254
+ right: Math.max(acc.right, r.right),
6255
+ bottom: Math.max(acc.bottom, r.bottom)
6256
+ }));
6257
+ const layoutRows = (card, blocks) => {
6258
+ const elOf = (b) => b.editorId ? card.querySelector(`[data-veo-block-id="${b.editorId}"]`) : null;
6259
+ const cs = getComputedStyle(card);
6260
+ const padL = Number.parseFloat(cs.paddingLeft) || 0;
6261
+ const padR = Number.parseFloat(cs.paddingRight) || 0;
6262
+ const contentLeft = padL;
6263
+ const contentRight = card.clientWidth - padR;
6264
+ const rows = groupLayout(blocks).map((row) => {
6265
+ const columns = row.columns.map((col) => {
6266
+ const items = col.blocks.map(({ block }) => ({ block, el: elOf(block) })).filter(
6267
+ (x) => !!x.el && !!x.block.editorId
6268
+ );
6269
+ const blocksRect2 = union(items.map((x) => rectOf(x.el)));
6270
+ if (!blocksRect2) return null;
6271
+ const colEl = items[0]?.el.parentElement;
6272
+ const rect2 = colEl?.classList.contains("veo-col") ? rectOf(colEl) : blocksRect2;
6273
+ return { items, rect: rect2 };
6274
+ }).filter((c) => c !== null);
6275
+ const blocksRect = union(columns.map((c) => c.rect));
6276
+ if (!blocksRect) return null;
6277
+ const rowEl = columns[0]?.items[0]?.el.parentElement?.parentElement;
6278
+ const rect = rowEl?.classList.contains("veo-row") ? rectOf(rowEl) : { ...blocksRect, left: contentLeft, right: contentRight };
6279
+ const id = columns[0]?.items[0]?.block.editorId;
6280
+ return { id, row: row.row, columns, rect };
6281
+ }).filter((r) => r !== null);
6282
+ return { rows, content: { left: contentLeft, right: contentRight } };
6283
+ };
6284
+ const clearRowChrome = (card) => {
6285
+ card.querySelector(".veo-row-frame")?.remove();
6286
+ card.querySelector(".veo-row-toolbar")?.remove();
6287
+ };
6288
+ const renderRowChrome = (host, stepIndex) => {
6289
+ const card = previewCard(host);
6290
+ if (!card) return;
6291
+ clearRowChrome(card);
6292
+ resolveToolbarOverlaps(card);
6293
+ const { rows } = layoutRows(card, previewBlocks);
6294
+ const rowOfSelected = selectedBlockId ? rows.find(
6295
+ (r) => r.columns.some((c) => c.items.some((i) => i.block.editorId === selectedBlockId))
6296
+ ) : void 0;
6297
+ const target = (selectedRowId ? rows.find((r) => r.id === selectedRowId) : void 0) ?? rowOfSelected ?? (hoverRowId ? rows.find((r) => r.id === hoverRowId) : void 0);
6298
+ if (!target) return;
6299
+ const doc = card.ownerDocument;
6300
+ const cs = getComputedStyle(card);
6301
+ const padT = Number.parseFloat(cs.paddingTop) || 0;
6302
+ const padB = Number.parseFloat(cs.paddingBottom) || 0;
6303
+ const PAD = 6;
6304
+ const fx = Math.max(0, target.rect.left - PAD);
6305
+ const fr = Math.min(card.clientWidth, target.rect.right + PAD);
6306
+ const fy = Math.max(Math.min(padT, target.rect.top), target.rect.top - PAD);
6307
+ const fb = Math.min(
6308
+ Math.max(card.clientHeight - padB, target.rect.bottom),
6309
+ target.rect.bottom + PAD
6310
+ );
6311
+ const frame = doc.createElement("div");
6312
+ frame.className = "veo-row-frame";
6313
+ frame.setAttribute("data-veo-builder-chrome", "");
6314
+ frame.style.left = `${fx}px`;
6315
+ frame.style.top = `${fy}px`;
6316
+ frame.style.width = `${Math.max(0, fr - fx)}px`;
6317
+ frame.style.height = `${Math.max(0, fb - fy)}px`;
6318
+ card.appendChild(frame);
6319
+ const bar = doc.createElement("div");
6320
+ bar.className = "veo-row-toolbar";
6321
+ bar.setAttribute("data-veo-builder-chrome", "");
6322
+ const blockId = target.id;
6323
+ const mk = (action, label, icon, text) => {
6324
+ const b = doc.createElement("button");
6325
+ b.type = "button";
6326
+ b.title = label;
6327
+ b.setAttribute("aria-label", label);
6328
+ b.className = `veo-row-toolbar-${action}`;
6329
+ b.innerHTML = icon;
6330
+ if (text) b.append(text);
6331
+ b.addEventListener("click", (e) => {
6332
+ e.preventDefault();
6333
+ e.stopPropagation();
6334
+ post({ type: "row-action", payload: { stepIndex, blockId, action } });
6335
+ });
6336
+ bar.appendChild(b);
6337
+ };
6338
+ mk("edit", "Editar fila", ICON.pencil, "Editar fila");
6339
+ mk("duplicate", "Duplicar fila", ICON.copy);
6340
+ mk("remove", "Eliminar fila", ICON.trash);
6341
+ card.appendChild(bar);
6342
+ bar.dataset.veoRight = String(Math.max(0, card.clientWidth - fr));
6343
+ bar.dataset.veoLeft = String(fx);
6344
+ bar.dataset.veoTopBelow = String(fb + 6);
6345
+ resolveToolbarOverlaps(card);
6346
+ };
6347
+ const renderCardChrome = (host, stepIndex) => {
6348
+ const card = previewCard(host);
6349
+ if (!card) return;
6350
+ card.querySelector(".veo-card-frame")?.remove();
6351
+ card.querySelector(".veo-card-toolbar")?.remove();
6352
+ if (!hoverCard) {
6353
+ resolveToolbarOverlaps(card);
6354
+ return;
6355
+ }
6356
+ const doc = card.ownerDocument;
6357
+ const frame = doc.createElement("div");
6358
+ frame.className = "veo-card-frame";
6359
+ frame.setAttribute("data-veo-builder-chrome", "");
6360
+ card.appendChild(frame);
6361
+ const bar = doc.createElement("div");
6362
+ bar.className = "veo-card-toolbar";
6363
+ bar.setAttribute("data-veo-builder-chrome", "");
6364
+ const mk = (action, label, active) => {
6365
+ const b = doc.createElement("button");
6366
+ b.type = "button";
6367
+ b.className = `veo-card-toolbar-${action}${active ? " is-active" : ""}`;
6368
+ b.setAttribute("aria-pressed", active ? "true" : "false");
6369
+ b.textContent = label;
6370
+ b.addEventListener("click", (e) => {
6371
+ e.preventDefault();
6372
+ e.stopPropagation();
6373
+ post({ type: "card-action", payload: { stepIndex, action } });
6374
+ });
6375
+ bar.appendChild(b);
6376
+ };
6377
+ mk("edit-container", "Editar contenedor", panelSection === "step");
6378
+ mk("edit-rows", "Editar filas", panelSection === "structure");
6379
+ card.appendChild(bar);
6380
+ bar.style.top = `${-Math.round((bar.offsetHeight || 34) / 2)}px`;
6381
+ resolveToolbarOverlaps(card);
6382
+ };
6383
+ const attachRowHover = (host, stepIndex) => {
6384
+ const onMove = (e) => {
6385
+ const card = previewCard(host);
6386
+ if (!card) return;
6387
+ const overCard = e.composedPath().some(
6388
+ (n) => n instanceof Element && (n.classList.contains("veo-card") || n.classList.contains("veo-card-toolbar"))
6389
+ );
6390
+ if (overCard !== hoverCard) {
6391
+ hoverCard = overCard;
6392
+ renderCardChrome(host, stepIndex);
6393
+ }
6394
+ if (e.composedPath().some((n) => n instanceof Element && n.classList.contains("veo-row-toolbar"))) {
6395
+ return;
6396
+ }
6397
+ const rect = card.getBoundingClientRect();
6398
+ const x = e.clientX - rect.left - card.clientLeft;
6399
+ const y = e.clientY - rect.top - card.clientTop;
6400
+ const bar = card.querySelector(".veo-row-toolbar");
6401
+ if (bar) {
6402
+ const AIR = 10;
6403
+ if (x >= bar.offsetLeft - AIR && x <= bar.offsetLeft + bar.offsetWidth + AIR && y >= bar.offsetTop - 14 && y <= bar.offsetTop + bar.offsetHeight + AIR) {
6404
+ return;
6405
+ }
6406
+ }
6407
+ const TOL = 6;
6408
+ const hit = layoutRows(card, previewBlocks).rows.find(
6409
+ (r) => x >= r.rect.left - TOL && x <= r.rect.right + TOL && y >= r.rect.top - TOL && y <= r.rect.bottom + TOL
6410
+ );
6411
+ const next = hit?.id ?? null;
6412
+ if (next === hoverRowId) return;
6413
+ hoverRowId = next;
6414
+ renderRowChrome(host, stepIndex);
6415
+ };
6416
+ const onLeave = () => {
6417
+ if (hoverCard) {
6418
+ hoverCard = false;
6419
+ renderCardChrome(host, stepIndex);
6420
+ }
6421
+ if (hoverRowId === null) return;
6422
+ hoverRowId = null;
6423
+ renderRowChrome(host, stepIndex);
6424
+ };
6425
+ host.addEventListener("mousemove", onMove);
6426
+ host.addEventListener("mouseleave", onLeave);
6427
+ return () => {
6428
+ host.removeEventListener("mousemove", onMove);
6429
+ host.removeEventListener("mouseleave", onLeave);
6430
+ };
6431
+ };
6432
+ const renderGapZones = (host, stepIndex, blocks) => {
6433
+ const card = previewCard(host);
6434
+ if (!card) return;
6435
+ for (const z of card.querySelectorAll(".veo-gap")) z.remove();
6436
+ const doc = card.ownerDocument;
6437
+ const GAP = 18;
6438
+ const EDGE2 = 4;
6439
+ const zone = (kind, x, y, w, h, label, blockId, action) => {
6440
+ const z = doc.createElement("div");
6441
+ z.className = `veo-gap veo-gap--${kind}`;
6442
+ z.setAttribute("data-veo-builder-chrome", "");
6443
+ z.dataset.veoGapAction = action;
6444
+ z.style.left = `${x}px`;
6445
+ z.style.top = `${y}px`;
6446
+ z.style.width = `${Math.max(w, GAP)}px`;
6447
+ z.style.height = `${Math.max(h, GAP)}px`;
6448
+ const line = doc.createElement("i");
6449
+ const btn = doc.createElement("button");
6450
+ btn.type = "button";
6451
+ btn.title = label;
6452
+ btn.setAttribute("aria-label", label);
6453
+ btn.textContent = "+";
6454
+ btn.addEventListener("click", (e) => {
6455
+ e.preventDefault();
6456
+ e.stopPropagation();
6457
+ post({ type: "block-layout", payload: { stepIndex, blockId, action } });
6458
+ });
6459
+ z.append(line, btn);
6460
+ card.appendChild(z);
6461
+ };
6462
+ const { rows, content } = layoutRows(card, blocks);
6463
+ if (rows.length === 0) return;
6464
+ const cw = content.right - content.left;
6465
+ rows.forEach((row, k) => {
6466
+ const first = row.columns[0]?.items[0]?.block.editorId;
6467
+ if (k === 0) {
6468
+ zone(
6469
+ "h",
6470
+ content.left,
6471
+ row.rect.top - EDGE2 - GAP,
6472
+ cw,
6473
+ GAP,
6474
+ "Agregar fila",
6475
+ first,
6476
+ "row-before"
6477
+ );
6478
+ } else {
6479
+ const prev = rows[k - 1];
6480
+ const y = (prev.rect.bottom + row.rect.top) / 2 - GAP / 2;
6481
+ zone("h", content.left, y, cw, GAP, "Agregar fila", first, "row-before");
6482
+ }
6483
+ if (k === rows.length - 1) {
6484
+ zone(
6485
+ "h",
6486
+ content.left,
6487
+ row.rect.bottom + EDGE2,
6488
+ cw,
6489
+ GAP,
6490
+ "Agregar fila",
6491
+ first,
6492
+ "row-after"
6493
+ );
6494
+ }
6495
+ });
6496
+ for (const row of rows) {
6497
+ const rh = row.rect.bottom - row.rect.top;
6498
+ row.columns.forEach((col, j) => {
6499
+ const id = col.items[0]?.block.editorId;
6500
+ if (j === 0) {
6501
+ zone(
6502
+ "v",
6503
+ row.rect.left - EDGE2 - GAP,
6504
+ row.rect.top,
6505
+ GAP,
6506
+ rh,
6507
+ "Agregar columna",
6508
+ id,
6509
+ "col-before"
6510
+ );
6511
+ } else {
6512
+ const prev = row.columns[j - 1];
6513
+ const x = (prev.rect.right + col.rect.left) / 2 - GAP / 2;
6514
+ zone("v", x, row.rect.top, GAP, rh, "Agregar columna", id, "col-before");
6515
+ }
6516
+ if (j === row.columns.length - 1) {
6517
+ zone(
6518
+ "v",
6519
+ row.rect.right + EDGE2,
6520
+ row.rect.top,
6521
+ GAP,
6522
+ rh,
6523
+ "Agregar columna",
6524
+ id,
6525
+ "col-after"
6526
+ );
6527
+ }
6528
+ });
6529
+ }
6530
+ for (const row of rows) {
6531
+ for (const col of row.columns) {
6532
+ const w = col.rect.right - col.rect.left;
6533
+ col.items.forEach((it, i) => {
6534
+ const next = col.items[i + 1];
6535
+ if (next) {
6536
+ const a = rectOf(it.el);
6537
+ const b = rectOf(next.el);
6538
+ const y = (a.bottom + b.top) / 2 - GAP / 2;
6539
+ zone(
6540
+ "h",
6541
+ col.rect.left,
6542
+ y,
6543
+ w,
6544
+ GAP,
6545
+ "Agregar bloque",
6546
+ it.block.editorId,
6547
+ "stack-after"
6548
+ );
6549
+ } else if (row.columns.length > 1 && col.rect.bottom < row.rect.bottom - GAP) {
6550
+ zone(
6551
+ "h",
6552
+ col.rect.left,
6553
+ col.rect.bottom + EDGE2,
6554
+ w,
6555
+ GAP,
6556
+ "Agregar bloque",
6557
+ it.block.editorId,
6558
+ "stack-after"
6559
+ );
6560
+ }
6561
+ });
6562
+ }
6563
+ }
6564
+ };
6565
+ const attachCardClick = (host, stepIndex) => {
6566
+ let downAt = null;
6567
+ const onDown = (e) => {
6568
+ downAt = { x: e.clientX, y: e.clientY };
6569
+ };
6570
+ const onClick = (e) => {
6571
+ const path = e.composedPath();
6572
+ if (path.some(
6573
+ (n) => n instanceof Element && (n.classList.contains("veo-block-toolbar") || n.classList.contains("veo-row-toolbar") || n.classList.contains("veo-card-toolbar") || n.classList.contains("veo-gap"))
6574
+ )) {
6575
+ return;
6576
+ }
6577
+ const inCard = path.some((n) => n instanceof Element && n.classList.contains("veo-card"));
6578
+ if (!inCard) return;
6579
+ if (downAt && Math.hypot(e.clientX - downAt.x, e.clientY - downAt.y) > 4) return;
6580
+ const blockEl = path.find(
6581
+ (n) => n instanceof HTMLElement && !!n.dataset.veoBlockId
6582
+ );
6583
+ if (blockEl) {
6584
+ if (path.some((n) => n instanceof Element && n.matches("button, a"))) {
6585
+ e.preventDefault();
6586
+ e.stopPropagation();
6587
+ }
6588
+ selectedBlockId = blockEl.dataset.veoBlockId ?? null;
6589
+ applyBlockHighlight(host, stepIndex);
6590
+ if (selectedBlockId) {
6591
+ post({ type: "block-clicked", payload: { stepIndex, blockId: selectedBlockId } });
6592
+ }
6593
+ return;
6594
+ }
6595
+ const interactive = path.some(
6596
+ (n) => n instanceof Element && n.matches("button, a, input, textarea, select, label, [contenteditable]")
6597
+ );
6598
+ if (interactive) return;
6599
+ selectedBlockId = null;
6600
+ clearBlockHighlight(host);
6601
+ renderRowChrome(host, stepIndex);
6602
+ post({ type: "card-clicked", payload: { stepIndex } });
6603
+ };
6604
+ host.addEventListener("mousedown", onDown, true);
6605
+ host.addEventListener("click", onClick, true);
6606
+ return () => {
6607
+ host.removeEventListener("mousedown", onDown, true);
6608
+ host.removeEventListener("click", onClick, true);
6609
+ };
6610
+ };
6611
+ const attachRawBlock = (stepIndex, blockId) => {
6612
+ const nodes = Array.from(
6613
+ document.querySelectorAll(`[${RAW_BLOCK_ATTR}="${blockId}"]`)
6614
+ );
6615
+ if (nodes.length === 0) return null;
6616
+ const doc = document;
6617
+ const box = doc.createElement("div");
6618
+ box.setAttribute("data-veo-builder-chrome", "");
6619
+ box.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;z-index:2147483646;";
6620
+ box.style.display = "none";
6621
+ const shadow = box.attachShadow({ mode: "open" });
6622
+ const style = doc.createElement("style");
6623
+ style.textContent = GUIDE_STYLES;
6624
+ shadow.appendChild(style);
6625
+ const frame = doc.createElement("div");
6626
+ frame.className = "veo-block-frame";
6627
+ frame.style.position = "fixed";
6628
+ const bar = doc.createElement("div");
6629
+ bar.className = "veo-block-toolbar";
6630
+ bar.style.position = "fixed";
6631
+ const mk = (action, label, icon, enabled = true) => {
6632
+ const b = doc.createElement("button");
6633
+ b.type = "button";
6634
+ b.title = label;
6635
+ b.setAttribute("aria-label", label);
6636
+ b.className = `veo-block-toolbar-${action}`;
6637
+ b.innerHTML = icon;
6638
+ b.disabled = !enabled;
6639
+ b.addEventListener("click", (e) => {
6640
+ e.preventDefault();
6641
+ e.stopPropagation();
6642
+ post({ type: "block-action", payload: { stepIndex, blockId, action } });
6643
+ });
6644
+ bar.appendChild(b);
6645
+ };
6646
+ mk("edit", "Editar", ICON.pencil);
6647
+ mk("duplicate", "Duplicar", ICON.copy, false);
6648
+ mk("remove", "Eliminar", ICON.trash);
6649
+ mk("move-left", "Mover a la izquierda", ICON_MOVE.left, false);
6650
+ mk("move-right", "Mover a la derecha", ICON_MOVE.right, false);
6651
+ frame.style.display = "none";
6652
+ bar.style.display = "none";
6653
+ const GAP = 18;
6654
+ const EDGE2 = 4;
6655
+ const gap = (kind, label, action) => {
6656
+ const z = doc.createElement("div");
6657
+ z.className = `veo-gap veo-gap--${kind}`;
6658
+ z.style.position = "fixed";
6659
+ z.dataset.veoGapAction = action;
6660
+ const line = doc.createElement("i");
6661
+ const btn = doc.createElement("button");
6662
+ btn.type = "button";
6663
+ btn.title = label;
6664
+ btn.setAttribute("aria-label", label);
6665
+ btn.textContent = "+";
6666
+ btn.addEventListener("click", (e) => {
6667
+ e.preventDefault();
6668
+ e.stopPropagation();
6669
+ post({ type: "block-layout", payload: { stepIndex, blockId, action } });
6670
+ });
6671
+ z.append(line, btn);
6672
+ return z;
6673
+ };
6674
+ const gaps = {
6675
+ top: gap("h", "Agregar fila", "row-before"),
6676
+ bottom: gap("h", "Agregar fila", "row-after"),
6677
+ left: gap("v", "Agregar columna", "col-before"),
6678
+ right: gap("v", "Agregar columna", "col-after")
6679
+ };
6680
+ shadow.append(frame, bar, gaps.top, gaps.bottom, gaps.left, gaps.right);
6681
+ doc.body.appendChild(box);
6682
+ box.style.display = "";
6683
+ let visible = false;
6684
+ const place = (el2, x, y, w, h) => {
6685
+ el2.style.left = `${x}px`;
6686
+ el2.style.top = `${y}px`;
6687
+ el2.style.width = `${w}px`;
6688
+ el2.style.height = `${h}px`;
6689
+ };
6690
+ const reposition = () => {
6691
+ const live = nodes.filter((n) => n.isConnected);
6692
+ if (live.length === 0) return;
6693
+ let l = Number.POSITIVE_INFINITY;
6694
+ let t = Number.POSITIVE_INFINITY;
6695
+ let r = Number.NEGATIVE_INFINITY;
6696
+ let b = Number.NEGATIVE_INFINITY;
6697
+ for (const n of live) {
6698
+ const rc = n.getBoundingClientRect();
6699
+ if (rc.width === 0 && rc.height === 0) continue;
6700
+ l = Math.min(l, rc.left);
6701
+ t = Math.min(t, rc.top);
6702
+ r = Math.max(r, rc.right);
6703
+ b = Math.max(b, rc.bottom);
6704
+ }
6705
+ if (!Number.isFinite(l)) return;
6706
+ place(gaps.top, l, t - EDGE2 - GAP, r - l, GAP);
6707
+ place(gaps.bottom, l, b + EDGE2, r - l, GAP);
6708
+ place(gaps.left, l - EDGE2 - GAP, t, GAP, b - t);
6709
+ place(gaps.right, r + EDGE2, t, GAP, b - t);
6710
+ if (!visible) return;
6711
+ place(frame, l, t, r - l, b - t);
6712
+ const bh = bar.offsetHeight || TOOLBAR_H;
6713
+ const bw = bar.offsetWidth;
6714
+ bar.style.left = `${Math.max(0, r - bw)}px`;
6715
+ bar.style.top = `${Math.max(0, t - bh - 6)}px`;
6716
+ };
6717
+ const select = (id) => {
6718
+ visible = id === blockId;
6719
+ frame.style.display = visible ? "" : "none";
6720
+ bar.style.display = visible ? "" : "none";
6721
+ reposition();
6722
+ };
6723
+ reposition();
6724
+ const onClick = (e) => {
6725
+ const path = e.composedPath();
6726
+ if (!nodes.some((n) => path.includes(n))) return;
6727
+ if (path.some((n) => n instanceof Element && n.matches("button, a"))) {
6728
+ e.preventDefault();
6729
+ e.stopPropagation();
6730
+ }
6731
+ selectedBlockId = blockId;
6732
+ select(blockId);
6733
+ post({ type: "block-clicked", payload: { stepIndex, blockId } });
6734
+ };
6735
+ doc.addEventListener("click", onClick, true);
6736
+ window.addEventListener("scroll", reposition, true);
6737
+ window.addEventListener("resize", reposition);
6738
+ return {
6739
+ select,
6740
+ detach: () => {
6741
+ doc.removeEventListener("click", onClick, true);
6742
+ window.removeEventListener("scroll", reposition, true);
6743
+ window.removeEventListener("resize", reposition);
6744
+ box.remove();
6745
+ }
6746
+ };
6747
+ };
4939
6748
  const applyPreviewGated = (cmd) => {
4940
6749
  const showNow = !cmd.honorTrigger;
4941
6750
  if (showNow) {
@@ -4955,7 +6764,7 @@ function initBuilderMode() {
4955
6764
  return;
4956
6765
  }
4957
6766
  const devices = cmd.guide.activationRules?.devices;
4958
- if (!showNow && !previewMode && devices && !isDeviceAllowed(devices)) {
6767
+ if (!showNow && previewMode && devices && !isDeviceAllowed(devices)) {
4959
6768
  cancelPreviewDelay();
4960
6769
  withholdPreview();
4961
6770
  postPreviewStatus({ state: "device-mismatch", devices });
@@ -5007,15 +6816,16 @@ function initBuilderMode() {
5007
6816
  postPreviewStatus({ state: "waiting-delay", delayMs });
5008
6817
  };
5009
6818
  const applyPreview = (cmd) => {
5010
- manipulator?.detach();
5011
- manipulator = null;
5012
- dropTarget?.detach();
5013
- dropTarget = null;
6819
+ if (cmd.editable && !previewMode && tryPatchPreview(cmd)) {
6820
+ previewStepIndex = cmd.guide.startStepIndex ?? 0;
6821
+ return;
6822
+ }
6823
+ detachPreviewTools();
5014
6824
  previewStepIndex = cmd.guide.startStepIndex ?? 0;
5015
6825
  const rules = cmd.guide.activationRules;
5016
6826
  const trig = rules?.trigger;
5017
6827
  const sel = rules?.selector?.trim();
5018
- const gate = cmd.honorTrigger && !previewFired && !previewMode;
6828
+ const gate = cmd.honorTrigger && !previewFired && previewMode;
5019
6829
  const waitsForTarget = (trig === "on_click" || trig === "on_hover") && (!isAutomaticActivation(rules) || previewDismissed);
5020
6830
  if (gate && waitsForTarget && sel) {
5021
6831
  cancelPreviewDelay();
@@ -5032,13 +6842,13 @@ function initBuilderMode() {
5032
6842
  }
5033
6843
  disarmPreviewTrigger();
5034
6844
  cancelPreviewDelay();
6845
+ const fetchProgress = previewProgressFetcher();
5035
6846
  const handle = previewGuide({
5036
6847
  ...cmd.guide,
6848
+ ...fetchProgress ? { fetchProgress } : {},
5037
6849
  onClosed: () => {
5038
- manipulator?.detach();
5039
- manipulator = null;
5040
- dropTarget?.detach();
5041
- dropTarget = null;
6850
+ detachPreviewTools();
6851
+ shownPreview = null;
5042
6852
  previewFired = false;
5043
6853
  previewDismissed = true;
5044
6854
  if (previewMode) {
@@ -5046,9 +6856,7 @@ function initBuilderMode() {
5046
6856
  const t = r?.trigger;
5047
6857
  const selector = r?.selector?.trim();
5048
6858
  if ((t === "on_click" || t === "on_hover") && selector) armPreviewTrigger(selector, t);
5049
- return;
5050
6859
  }
5051
- if (lastPreview) applyPreviewGated(lastPreview);
5052
6860
  }
5053
6861
  });
5054
6862
  if (cmd.honorTrigger) postPreviewStatus({ state: "shown" });
@@ -5056,7 +6864,23 @@ function initBuilderMode() {
5056
6864
  void handle.ready.then((result) => {
5057
6865
  if (!result.rendered) return;
5058
6866
  const host = handle.host();
5059
- if (!host) return;
6867
+ if (!host) {
6868
+ const idx = cmd.guide.startStepIndex ?? 0;
6869
+ const only = cmd.guide.guideSteps[idx]?.blocks?.[0];
6870
+ if (only?.type === "code" && only.editorId) {
6871
+ rawBlock = attachRawBlock(idx, only.editorId);
6872
+ rawBlock?.select(selectedBlockId);
6873
+ }
6874
+ return;
6875
+ }
6876
+ host.classList.add("veo-no-anim");
6877
+ shownPreview = cmd;
6878
+ cardClick = attachCardClick(host, cmd.guide.startStepIndex ?? 0);
6879
+ const stepBlocks = cmd.guide.guideSteps[cmd.guide.startStepIndex ?? 0]?.blocks ?? [];
6880
+ previewBlocks = stepBlocks;
6881
+ applyBlockHighlight(host, cmd.guide.startStepIndex ?? 0);
6882
+ renderGapZones(host, cmd.guide.startStepIndex ?? 0, stepBlocks);
6883
+ rowHover = attachRowHover(host, cmd.guide.startStepIndex ?? 0);
5060
6884
  manipulator = attachDesignManipulator({
5061
6885
  host,
5062
6886
  guideType: cmd.guide.guideType,
@@ -5124,13 +6948,21 @@ function initBuilderMode() {
5124
6948
  break;
5125
6949
  case "editor-state":
5126
6950
  chrome?.setState(cmd.state);
6951
+ if (panelSection !== cmd.state.section) {
6952
+ panelSection = cmd.state.section;
6953
+ const h = document.querySelector("[data-veo-guide]");
6954
+ if (h && hoverCard && !previewMode) renderCardChrome(h, previewStepIndex);
6955
+ }
5127
6956
  panel?.setTitle(
5128
- cmd.state.section === "activation" ? "Ajustes de activaci\xF3n" : cmd.state.section === "badge" ? "Ajustes de badge" : cmd.state.section === "target" ? "Ajustes del elemento objetivo" : cmd.state.steps[cmd.state.activeStep]?.label ?? "Paso"
6957
+ cmd.state.section === "activation" ? "Ajustes de activaci\xF3n" : cmd.state.section === "badge" ? "Ajustes de badge" : cmd.state.section === "target" ? "Ajustes del elemento objetivo" : cmd.state.section === "row" ? "Editar fila" : cmd.state.blockLabel ? `Editor \xB7 ${cmd.state.blockLabel}` : cmd.state.steps[cmd.state.activeStep]?.label ?? "Paso"
5129
6958
  );
5130
6959
  break;
5131
6960
  case "panel-visibility":
5132
6961
  panel?.setVisible(cmd.visible);
5133
6962
  break;
6963
+ case "panel-size":
6964
+ panel?.setSize(cmd.size);
6965
+ break;
5134
6966
  case "start-picker": {
5135
6967
  picker?.stop();
5136
6968
  const panelWasShown = panel?.isVisible() ?? false;
@@ -5138,10 +6970,7 @@ function initBuilderMode() {
5138
6970
  chrome?.setHidden(true);
5139
6971
  const previewToRestore = lastPreview;
5140
6972
  if (previewToRestore) {
5141
- manipulator?.detach();
5142
- manipulator = null;
5143
- dropTarget?.detach();
5144
- dropTarget = null;
6973
+ detachPreviewTools();
5145
6974
  closeGuidePreview();
5146
6975
  }
5147
6976
  const restorePanel = () => {
@@ -5180,11 +7009,21 @@ function initBuilderMode() {
5180
7009
  case "block-drag":
5181
7010
  handleBlockDrag(cmd);
5182
7011
  break;
7012
+ case "select-block": {
7013
+ selectedBlockId = cmd.blockId;
7014
+ const h = document.querySelector("[data-veo-guide]");
7015
+ if (h && !previewMode) applyBlockHighlight(h, previewStepIndex);
7016
+ if (!previewMode) rawBlock?.select(cmd.blockId);
7017
+ break;
7018
+ }
7019
+ case "select-row": {
7020
+ selectedRowId = cmd.blockId;
7021
+ const h = document.querySelector("[data-veo-guide]");
7022
+ if (h && !previewMode) renderRowChrome(h, previewStepIndex);
7023
+ break;
7024
+ }
5183
7025
  case "close-preview":
5184
- manipulator?.detach();
5185
- manipulator = null;
5186
- dropTarget?.detach();
5187
- dropTarget = null;
7026
+ detachPreviewTools();
5188
7027
  hideDragGhost();
5189
7028
  pendingPreview = null;
5190
7029
  lastPreview = null;
@@ -5237,10 +7076,7 @@ function initBuilderMode() {
5237
7076
  removeNavWatcher();
5238
7077
  picker?.stop();
5239
7078
  picker = null;
5240
- manipulator?.detach();
5241
- manipulator = null;
5242
- dropTarget?.detach();
5243
- dropTarget = null;
7079
+ detachPreviewTools();
5244
7080
  hideDragGhost();
5245
7081
  pendingPreview = null;
5246
7082
  disarmPreviewTrigger();
@@ -5268,6 +7104,8 @@ function initBuilderMode() {
5268
7104
  case "preview":
5269
7105
  if (previewMode) return;
5270
7106
  previewMode = true;
7107
+ previewFired = false;
7108
+ previewDismissed = false;
5271
7109
  panelWasVisible = panel?.isVisible() ?? true;
5272
7110
  panel?.setVisible(false);
5273
7111
  chrome?.setPreview(true);
@@ -5276,6 +7114,8 @@ function initBuilderMode() {
5276
7114
  case "exit-preview":
5277
7115
  if (!previewMode) return;
5278
7116
  previewMode = false;
7117
+ disarmPreviewTrigger();
7118
+ cancelPreviewDelay();
5279
7119
  chrome?.setPreview(false);
5280
7120
  panel?.setVisible(panelWasVisible);
5281
7121
  if (lastPreview) applyPreviewGated(lastPreview);
@@ -5357,6 +7197,25 @@ function resolveDashboardOrigin(originHint) {
5357
7197
  }
5358
7198
  return null;
5359
7199
  }
7200
+ function previewProgressFetcher() {
7201
+ const cfg = window.__veoBuilder;
7202
+ if (!cfg?.apiKey || !cfg?.apiUrl) return void 0;
7203
+ const apiUrl = cfg.apiUrl.replace(/\/+$/, "");
7204
+ const apiKey = cfg.apiKey;
7205
+ return async (guideIds) => {
7206
+ const params = new URLSearchParams({
7207
+ endUserId: "veo_preview",
7208
+ guideIds: guideIds.join(","),
7209
+ includeUnpublished: "1"
7210
+ });
7211
+ const res = await fetch(`${apiUrl}/v1/guides/progress?${params}`, {
7212
+ headers: { "X-Api-Key": apiKey }
7213
+ });
7214
+ if (!res.ok) return [];
7215
+ const data = await res.json();
7216
+ return data.items ?? [];
7217
+ };
7218
+ }
5360
7219
 
5361
7220
  // src/plugins/builder/builder-session.ts
5362
7221
  var DEFAULT_FEATURES = "popup,width=1280,height=860";