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/index.cjs CHANGED
@@ -571,6 +571,129 @@ var init_rich_text = __esm({
571
571
  }
572
572
  });
573
573
 
574
+ // src/plugins/guides/video-block.ts
575
+ function videoEmbedUrl(provider, url) {
576
+ let u;
577
+ try {
578
+ u = new URL(url);
579
+ } catch {
580
+ return null;
581
+ }
582
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
583
+ const host = u.hostname.replace(/^www\./, "").replace(/^m\./, "");
584
+ const id = (re, from) => {
585
+ const m = from.match(re);
586
+ return m?.[1] ?? null;
587
+ };
588
+ switch (provider) {
589
+ case "youtube": {
590
+ let v = null;
591
+ if (host === "youtube.com" || host === "youtube-nocookie.com") {
592
+ v = u.searchParams.get("v") ?? id(/^\/(?:embed|shorts|live)\/([A-Za-z0-9_-]{6,})/, u.pathname);
593
+ } else if (host === "youtu.be") {
594
+ v = id(/^\/([A-Za-z0-9_-]{6,})/, u.pathname);
595
+ }
596
+ if (!v || !/^[A-Za-z0-9_-]{6,}$/.test(v)) return null;
597
+ return `https://www.youtube-nocookie.com/embed/${v}`;
598
+ }
599
+ case "vimeo": {
600
+ if (host !== "vimeo.com" && host !== "player.vimeo.com") return null;
601
+ const v = id(/(\d{6,})/, u.pathname);
602
+ return v ? `https://player.vimeo.com/video/${v}` : null;
603
+ }
604
+ case "loom": {
605
+ if (host !== "loom.com") return null;
606
+ const v = id(/^\/(?:share|embed)\/([A-Za-z0-9]{16,})/, u.pathname);
607
+ return v ? `https://www.loom.com/embed/${v}` : null;
608
+ }
609
+ case "wistia": {
610
+ if (!under(host, "wistia.com") && !under(host, "wistia.net")) return null;
611
+ const v = id(/\/(?:medias|embed\/iframe)\/([A-Za-z0-9]{6,})/, u.pathname);
612
+ return v ? `https://fast.wistia.net/embed/iframe/${v}` : null;
613
+ }
614
+ case "msstream": {
615
+ if (!under(host, "microsoftstream.com")) return null;
616
+ const v = id(/\/(?:embed\/)?video\/([0-9a-f-]{36})/i, u.pathname);
617
+ return v ? `https://web.microsoftstream.com/embed/video/${v}?autoplay=false&showinfo=true` : null;
618
+ }
619
+ case "vidyard": {
620
+ if (!under(host, "vidyard.com")) return null;
621
+ const v = id(/\/watch\/([A-Za-z0-9_-]{8,})/, u.pathname) ?? (host === "play.vidyard.com" ? id(/^\/([A-Za-z0-9_-]{8,})/, u.pathname) : null);
622
+ return v ? `https://play.vidyard.com/${v}` : null;
623
+ }
624
+ case "sharepoint":
625
+ return u.protocol === "https:" && under(host, "sharepoint.com") ? u.href : null;
626
+ case "brightcove":
627
+ return u.protocol === "https:" && host === "players.brightcove.net" && u.searchParams.get("videoId") ? u.href : null;
628
+ case "kaltura":
629
+ return u.protocol === "https:" && under(host, "kaltura.com") && u.pathname.startsWith("/p/") ? u.href : null;
630
+ default:
631
+ return null;
632
+ }
633
+ }
634
+ function under(host, domain) {
635
+ return host === domain || host.endsWith(`.${domain}`);
636
+ }
637
+ function fileVideoUrl(url) {
638
+ try {
639
+ const u = new URL(url);
640
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
641
+ return /\.(mp4|webm|mov)$/i.test(u.pathname) ? u.href : null;
642
+ } catch {
643
+ return null;
644
+ }
645
+ }
646
+ function buildVideoBlock(block, doc, opts = {}) {
647
+ const isFile = block.provider === "file";
648
+ const src = typeof block.url === "string" ? isFile ? fileVideoUrl(block.url) : videoEmbedUrl(block.provider, block.url) : null;
649
+ if (!src) return null;
650
+ const s = block.style ?? {};
651
+ const wrap = doc.createElement("div");
652
+ wrap.className = "veo-guide-video";
653
+ const ratio = s.aspect === "4:3" ? "4 / 3" : "16 / 9";
654
+ wrap.style.aspectRatio = ratio;
655
+ if (s.widthMode === "fill") {
656
+ wrap.style.width = "100%";
657
+ } else {
658
+ const w = typeof s.width === "number" && Number.isFinite(s.width) ? s.width : 400;
659
+ wrap.style.width = `${Math.max(120, Math.min(1600, w))}px`;
660
+ wrap.style.maxWidth = "100%";
661
+ }
662
+ const align = s.align === "center" ? "center" : s.align === "right" ? "flex-end" : "flex-start";
663
+ wrap.style.alignSelf = align;
664
+ if (isFile) {
665
+ const video = doc.createElement("video");
666
+ video.src = src;
667
+ video.controls = true;
668
+ video.setAttribute("playsinline", "");
669
+ video.preload = "metadata";
670
+ video.setAttribute(
671
+ "aria-label",
672
+ typeof block.alt === "string" && block.alt.trim() ? block.alt.trim() : "Video"
673
+ );
674
+ if (opts.editing) video.style.pointerEvents = "none";
675
+ wrap.appendChild(video);
676
+ return wrap;
677
+ }
678
+ const frame = doc.createElement("iframe");
679
+ frame.src = src;
680
+ frame.title = typeof block.alt === "string" && block.alt.trim() ? block.alt.trim() : "Video";
681
+ frame.setAttribute("loading", "lazy");
682
+ frame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
683
+ frame.setAttribute(
684
+ "allow",
685
+ `accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture${opts.editing ? "" : "; fullscreen"}`
686
+ );
687
+ if (!opts.editing) frame.setAttribute("allowfullscreen", "");
688
+ if (opts.editing) frame.style.pointerEvents = "none";
689
+ wrap.appendChild(frame);
690
+ return wrap;
691
+ }
692
+ var init_video_block = __esm({
693
+ "src/plugins/guides/video-block.ts"() {
694
+ }
695
+ });
696
+
574
697
  // src/plugins/guides/block-builder.ts
575
698
  function buildStepContent(step, doc, callbacks) {
576
699
  const container = doc.createElement("div");
@@ -695,8 +818,12 @@ function renderBlocks(container, blocks, doc, callbacks) {
695
818
  i++;
696
819
  }
697
820
  }
698
- function buildContentBlock(block, doc) {
821
+ function buildContentBlock(block, doc, opts = {}) {
699
822
  switch (block.type) {
823
+ case "video":
824
+ return buildVideoBlock(block, doc, opts);
825
+ case "divider":
826
+ return buildDividerBlock(block, doc);
700
827
  case "title": {
701
828
  const el2 = doc.createElement("h2");
702
829
  el2.className = "veo-guide-title";
@@ -720,7 +847,7 @@ function buildContentBlock(block, doc) {
720
847
  const el2 = doc.createElement("img");
721
848
  el2.className = "veo-guide-image";
722
849
  el2.src = block.url;
723
- el2.alt = "";
850
+ el2.alt = typeof block.alt === "string" ? block.alt : "";
724
851
  applyBlockStyle(el2, block.style, "image");
725
852
  return el2;
726
853
  }
@@ -728,6 +855,31 @@ function buildContentBlock(block, doc) {
728
855
  return null;
729
856
  }
730
857
  }
858
+ function buildDividerBlock(block, doc) {
859
+ const s = block.style ?? {};
860
+ const wrap = doc.createElement("div");
861
+ wrap.className = "veo-guide-divider";
862
+ wrap.setAttribute("role", "separator");
863
+ const hr = doc.createElement("hr");
864
+ const custom = s.preset === "custom";
865
+ const color = custom && typeof s.color === "string" && s.color ? s.color : s.colorMode === "dark" ? "rgba(255, 255, 255, 0.35)" : "#e5e7eb";
866
+ const thickness = custom && typeof s.thickness === "number" && Number.isFinite(s.thickness) ? clampNum(Math.round(s.thickness), 1, 12) : 1;
867
+ const lineStyle = typeof s.lineStyle === "string" && DIVIDER_LINE_STYLES.has(s.lineStyle) ? s.lineStyle : "solid";
868
+ hr.style.borderTop = `${thickness}px ${lineStyle} ${color}`;
869
+ wrap.appendChild(hr);
870
+ for (const [k, prop] of [
871
+ ["marginTop", "marginTop"],
872
+ ["marginRight", "marginRight"],
873
+ ["marginBottom", "marginBottom"],
874
+ ["marginLeft", "marginLeft"]
875
+ ]) {
876
+ const v = s[k];
877
+ if (typeof v === "number" && Number.isFinite(v)) {
878
+ wrap.style[prop] = `${clampNum(v, 0, 64)}px`;
879
+ }
880
+ }
881
+ return wrap;
882
+ }
731
883
  function buildButtonBlock(block, doc, callbacks) {
732
884
  const btn = doc.createElement("button");
733
885
  btn.type = "button";
@@ -758,14 +910,34 @@ function applyBlockStyle(el2, style, kind) {
758
910
  }
759
911
  const align = typeof s.align === "string" ? s.align : null;
760
912
  if (align && (align === "left" || align === "center" || align === "right")) {
761
- if (kind === "text") el2.style.textAlign = align;
762
- else if (kind === "image") el2.style.alignSelf = BLOCK_ALIGN_SELF[align];
913
+ if (kind === "text") {
914
+ el2.style.textAlign = align;
915
+ el2.style.alignSelf = BLOCK_ALIGN_SELF[align];
916
+ } else if (kind === "image") el2.style.alignSelf = BLOCK_ALIGN_SELF[align];
917
+ }
918
+ if (kind === "text" && typeof s.preset === "string" && s.preset in TEXT_PRESETS) {
919
+ const preset = TEXT_PRESETS[s.preset];
920
+ if (preset) {
921
+ el2.style.fontSize = `${preset.size}px`;
922
+ el2.style.fontWeight = String(preset.weight);
923
+ el2.style.lineHeight = String(preset.lineHeight);
924
+ }
925
+ }
926
+ if (kind !== "image" && s.colorMode === "dark") {
927
+ el2.style.background = "#1f1d1b";
928
+ el2.style.color = "#f5f0eb";
929
+ el2.style.borderRadius = el2.style.borderRadius || "8px";
763
930
  }
764
931
  if (kind !== "image" && typeof s.color === "string") el2.style.color = s.color;
765
932
  if (kind === "text" && typeof s.fontSize === "number" && Number.isFinite(s.fontSize)) {
766
933
  el2.style.fontSize = `${clampNum(s.fontSize, 8, 72)}px`;
767
934
  }
768
- if (kind === "image" && typeof s.width === "number" && Number.isFinite(s.width)) {
935
+ if (kind === "image" && s.naturalSize === true) {
936
+ el2.style.width = "auto";
937
+ el2.style.maxWidth = "100%";
938
+ el2.style.maxHeight = "none";
939
+ el2.style.objectFit = "contain";
940
+ } else if (kind === "image" && typeof s.width === "number" && Number.isFinite(s.width)) {
769
941
  el2.style.width = `${clampNum(s.width, 10, 100)}%`;
770
942
  }
771
943
  if (kind === "image" && typeof s.radius === "number" && Number.isFinite(s.radius)) {
@@ -844,16 +1016,24 @@ function readCtaUrl(step) {
844
1016
  if (typeof candidate !== "string") return void 0;
845
1017
  return isSafeUrl(candidate) ? candidate : void 0;
846
1018
  }
847
- var BLOCK_ALIGN_SELF;
1019
+ var DIVIDER_LINE_STYLES, BLOCK_ALIGN_SELF, TEXT_PRESETS;
848
1020
  var init_block_builder = __esm({
849
1021
  "src/plugins/guides/block-builder.ts"() {
850
1022
  init_constants2();
851
1023
  init_rich_text();
1024
+ init_video_block();
1025
+ DIVIDER_LINE_STYLES = /* @__PURE__ */ new Set(["solid", "dotted", "dashed"]);
852
1026
  BLOCK_ALIGN_SELF = {
853
1027
  left: "flex-start",
854
1028
  center: "center",
855
1029
  right: "flex-end"
856
1030
  };
1031
+ TEXT_PRESETS = {
1032
+ title: { size: 18, weight: 600, lineHeight: 1.3 },
1033
+ subtitle: { size: 16, weight: 600, lineHeight: 1.35 },
1034
+ paragraph: { size: 14, weight: 400, lineHeight: 1.5 },
1035
+ small: { size: 12, weight: 400, lineHeight: 1.5 }
1036
+ };
857
1037
  }
858
1038
  });
859
1039
 
@@ -1216,7 +1396,161 @@ var init_styles = __esm({
1216
1396
  }
1217
1397
  /* Card gen\xE9rica del modelo unificado. El dise\xF1o (bg/borde/padding/\u2026) lo aplica
1218
1398
  el renderer como estilos inline desde step.design; ac\xE1 van los defaults. */
1399
+ /* Edici\xF3n (builder): sin animaci\xF3n de entrada al repintar la preview. */
1400
+ :host(.veo-no-anim) * { animation: none !important; }
1401
+
1402
+ /* Bloque video: caja con relaci\xF3n de aspecto; el iframe la llena. */
1403
+ .veo-guide-video { position: relative; overflow: hidden; border-radius: 8px; background: #000; }
1404
+ .veo-guide-video iframe, .veo-guide-video video {
1405
+ position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block;
1406
+ }
1407
+
1408
+ /* \u2500\u2500 Layout por filas/columnas (composite) \u2500\u2500
1409
+ El espacio tiene UNA fuente por capa: el aire con el borde lo da el padding
1410
+ de la card; la separaci\xF3n entre filas/bloques la da el margen inferior
1411
+ (12px, ah\xED vive la l\xEDnea \xAB+\xBB del editor); los bloques NO llevan padding
1412
+ propio (solo el que el autor les ponga inline). El \xFAltimo elemento no deja
1413
+ margen colgando. */
1414
+ .veo-card > [data-veo-block], .veo-card > .veo-row, .veo-card > .veo-guide-actions,
1415
+ .veo-col > [data-veo-block], .veo-col > .veo-guide-actions { margin-bottom: 12px; }
1416
+ /* La card y cada columna son columnas flex: cada bloque mide SU contenido (como
1417
+ el bot\xF3n) y se ubica con su alineaci\xF3n (align-self); filas, columnas, formularios
1418
+ y filas de botones ocupan todo el ancho. */
1419
+ .veo-card:not(.veo-card--empty), .veo-col {
1420
+ display: flex; flex-direction: column; align-items: flex-start;
1421
+ }
1422
+ .veo-card > [data-veo-block], .veo-col > [data-veo-block] { max-width: 100%; }
1423
+ /* L\xEDnea horizontal: ocupa todo el ancho; el padding del bloque es solo vertical
1424
+ (zona clickeable en el editor) y la l\xEDnea es el borde superior del hr. */
1425
+ .veo-card > .veo-guide-divider, .veo-col > .veo-guide-divider { padding: 6px 0; align-self: stretch; }
1426
+ .veo-guide-divider { box-sizing: border-box; }
1427
+ /* Bloque de c\xF3digo (modo host): ocupa todo el ancho, como un formulario. */
1428
+ .veo-code-host { display: block; min-width: 0; }
1429
+ .veo-card > .veo-code-host, .veo-col > .veo-code-host { align-self: stretch; }
1430
+ .veo-guide-divider hr { margin: 0; border: 0; height: 0; }
1431
+ .veo-card > .veo-row, .veo-card > .veo-guide-actions, .veo-card > .veo-form,
1432
+ .veo-col > .veo-guide-actions, .veo-col > .veo-form { align-self: stretch; }
1433
+ .veo-row { display: flex; gap: 12px; align-items: flex-start; box-sizing: border-box; }
1434
+ .veo-col { flex: 1 1 0; min-width: 0; }
1435
+ .veo-col > :last-child { margin-bottom: 0; }
1436
+ /* Fila con alineaci\xF3n horizontal (editor de fila): las columnas miden su contenido. */
1437
+ .veo-row--aligned > .veo-col { flex: 0 1 auto; }
1438
+
1439
+ /* \u2500\u2500 Editor in-app: bloque seleccionado + barra de acciones + placeholders \u2500\u2500 */
1440
+ .veo-block-placeholder {
1441
+ border: 1px dashed #b0a9a5; border-radius: 6px; padding: 12px; margin-bottom: 12px;
1442
+ color: #8a8582; font-size: 13px; text-align: center;
1443
+ }
1444
+ /* El bloque seleccionado no lleva contorno propio: lo rodea .veo-block-frame
1445
+ (marco aparte, con aire y alto m\xEDnimo, que no altera el layout). */
1446
+ .veo-block--selected { position: relative; z-index: 1; }
1447
+ .veo-block-frame {
1448
+ position: absolute; z-index: 4; pointer-events: none; box-sizing: border-box;
1449
+ border: 2px solid #3b82f6; border-radius: 10px; background: rgba(59, 130, 246, 0.04);
1450
+ }
1451
+ /* Barra del bloque (mock): p\xEDldora blanca con sombra, solo \xEDconos; la papelera
1452
+ en rojo, separada por una l\xEDnea. */
1453
+ .veo-block-toolbar {
1454
+ position: absolute; z-index: 5; display: inline-flex; align-items: center; gap: 2px; padding: 4px 6px;
1455
+ background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 12px;
1456
+ box-shadow: 0 6px 20px rgba(0,0,0,0.16);
1457
+ }
1458
+ .veo-block-toolbar button {
1459
+ border: 0; background: transparent; color: #4b4642; cursor: pointer; padding: 6px; border-radius: 8px;
1460
+ display: inline-flex; align-items: center; justify-content: center; line-height: 1;
1461
+ }
1462
+ .veo-block-toolbar button:hover { background: #f5efeb; }
1463
+ .veo-block-toolbar button svg { width: 20px; height: 20px; }
1464
+ .veo-block-toolbar .veo-block-toolbar-remove { color: #e5484d; }
1465
+ /* Separadores: antes de la papelera y antes de las flechas de mover. */
1466
+ .veo-block-toolbar .veo-block-toolbar-remove, .veo-block-toolbar .veo-block-toolbar-move-left {
1467
+ margin-left: 6px; position: relative;
1468
+ }
1469
+ .veo-block-toolbar .veo-block-toolbar-remove::before, .veo-block-toolbar .veo-block-toolbar-move-left::before {
1470
+ content: ""; position: absolute; left: -4px; top: 6px; bottom: 6px; width: 1px; background: #e7e2de;
1471
+ }
1472
+ .veo-block-toolbar button:disabled { opacity: 0.3; cursor: default; background: transparent; }
1473
+ /* Contenedor (hover): contorno azul de la tarjeta y dos pesta\xF1as centradas
1474
+ sobre su borde superior: \xABEditar contenedor\xBB (outline) y \xABEditar filas\xBB. */
1475
+ .veo-card-frame {
1476
+ position: absolute; z-index: 3; inset: 0; pointer-events: none; box-sizing: border-box;
1477
+ border: 2px solid #3b82f6; border-radius: inherit;
1478
+ }
1479
+ .veo-card-toolbar {
1480
+ position: absolute; z-index: 6; left: 50%; transform: translateX(-50%);
1481
+ display: inline-flex; align-items: flex-end; gap: 8px; white-space: nowrap;
1482
+ }
1483
+ .veo-card-toolbar button {
1484
+ cursor: pointer; padding: 8px 14px; border-radius: 8px; border: 2px solid #3b82f6;
1485
+ font: 600 14px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
1486
+ background: #fff; color: #3b82f6;
1487
+ }
1488
+ .veo-card-toolbar button:hover { background: #eff6ff; }
1489
+ /* Con fondo = la vista que el panel muestra ahora (contenedor o estructura). */
1490
+ .veo-card-toolbar button.is-active { background: #3b82f6; color: #fff; }
1491
+ .veo-card-toolbar button.is-active:hover { background: #2563eb; border-color: #2563eb; }
1492
+ /* Editor de fila: marco al pasar el mouse por una fila (o la fila del bloque
1493
+ seleccionado) y barra \xABEditar fila \xB7 duplicar\xBB bajo su esquina derecha. */
1494
+ .veo-row-frame {
1495
+ position: absolute; z-index: 3; pointer-events: none; box-sizing: border-box;
1496
+ border: 2px solid #3b82f6; border-radius: 10px;
1497
+ }
1498
+ .veo-row-toolbar {
1499
+ position: absolute; z-index: 5; display: inline-flex; align-items: center; gap: 2px; padding: 4px 6px;
1500
+ background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 12px;
1501
+ box-shadow: 0 6px 20px rgba(0,0,0,0.16); white-space: nowrap;
1502
+ }
1503
+ .veo-row-toolbar button {
1504
+ border: 0; background: transparent; color: #4b4642; cursor: pointer; padding: 6px 8px; border-radius: 8px;
1505
+ display: inline-flex; align-items: center; gap: 8px; line-height: 1;
1506
+ font: 500 14px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
1507
+ }
1508
+ .veo-row-toolbar button:hover { background: #f5efeb; }
1509
+ .veo-row-toolbar button svg { width: 20px; height: 20px; }
1510
+ .veo-row-toolbar .veo-row-toolbar-duplicate { margin-left: 6px; position: relative; }
1511
+ .veo-row-toolbar .veo-row-toolbar-duplicate::before {
1512
+ content: ""; position: absolute; left: -4px; top: 6px; bottom: 6px; width: 1px; background: #e7e2de;
1513
+ }
1514
+ .veo-row-toolbar .veo-row-toolbar-remove { color: #e5484d; }
1515
+ /* Huecos \xAB+\xBB (mock): zona invisible entre elementos; al pasar el mouse aparece
1516
+ la l\xEDnea azul (horizontal = fila / bloque apilado, vertical = columna) con
1517
+ un \xAB+\xBB circular al centro. */
1518
+ .veo-gap { position: absolute; z-index: 4; }
1519
+ .veo-gap i { position: absolute; background: #3b82f6; opacity: 0; transition: opacity 100ms; pointer-events: none; }
1520
+ .veo-gap--h i { left: 0; right: 0; top: 50%; height: 2px; margin-top: -1px; }
1521
+ .veo-gap--v i { top: 0; bottom: 0; left: 50%; width: 2px; margin-left: -1px; }
1522
+ .veo-gap button {
1523
+ position: absolute; left: 50%; top: 50%; margin: -11px 0 0 -11px; box-sizing: border-box;
1524
+ opacity: 0; transition: opacity 100ms; width: 22px; height: 22px; border-radius: 50%; padding: 0;
1525
+ border: 2px solid #fff; background: #3b82f6; color: #fff; cursor: pointer;
1526
+ font: 600 15px/18px system-ui, -apple-system, "Segoe UI", sans-serif; text-align: center;
1527
+ box-shadow: 0 2px 8px rgba(0,0,0,0.25);
1528
+ }
1529
+ .veo-gap:hover i, .veo-gap:hover button, .veo-gap:focus-within button { opacity: 1; }
1530
+ .veo-gap button:hover { background: #2563eb; }
1531
+
1532
+ /* Tarjeta sin bloques (paso reci\xE9n creado). La tarjeta NO impone tama\xF1o ni
1533
+ padding propios (as\xED el ancho/padding del dise\xF1o se ven tal cual); el tama\xF1o
1534
+ m\xEDnimo lo pone la zona punteada azul de la preview. */
1535
+ .veo-card--empty { display: flex; align-items: stretch; }
1536
+ .veo-card-empty-zone {
1537
+ flex: 1; min-width: 0; min-height: 160px; margin-top: 14px;
1538
+ display: flex; align-items: center; justify-content: center; gap: 6px; text-align: center;
1539
+ border: 2px dashed #3b82f6; border-radius: 8px; background: #eff6ff;
1540
+ color: #2563eb; font-size: 16px; font-weight: 500; user-select: none; pointer-events: none;
1541
+ padding: 0 12px; box-sizing: border-box; overflow: hidden;
1542
+ }
1543
+ .veo-card-empty-zone b { font-weight: 600; font-size: 22px; line-height: 1; }
1544
+ /* Sin ancho configurado, la zona da un tama\xF1o c\xF3modo por defecto. */
1545
+ .veo-card--empty:not([style*="width"]) .veo-card-empty-zone { min-width: 360px; }
1546
+ /* Tarjeta angosta (la mide el renderer): solo el \xAB+\xBB, sin texto, y m\xE1s baja. */
1547
+ .veo-card--narrow .veo-card-empty-zone span { display: none; }
1548
+ .veo-card--narrow .veo-card-empty-zone { min-height: 120px; }
1219
1549
  .veo-card {
1550
+ /* Contenedor posicionado SIEMPRE: el \u2715 (absolute) se ubica respecto a la
1551
+ tarjeta tambi\xE9n en pasos embebidos (inline), no respecto a la app. Los
1552
+ flotantes lo pisan con position:fixed inline. */
1553
+ position: relative;
1220
1554
  pointer-events: auto;
1221
1555
  background: var(--veo-bg, #ffffff);
1222
1556
  color: var(--veo-text, #1f2937);
@@ -1309,6 +1643,13 @@ var init_styles = __esm({
1309
1643
  transition: opacity 120ms ease;
1310
1644
  }
1311
1645
  .veo-guide-cta:hover { opacity: 0.9; }
1646
+ /* Presets del boton (editor): secundario = contorno; enlace = solo texto. */
1647
+ .veo-guide-cta--secondary {
1648
+ background: transparent; color: var(--veo-primary);
1649
+ border: 1px solid var(--veo-primary);
1650
+ }
1651
+ .veo-guide-cta--link { background: transparent; color: var(--veo-primary); padding: 0; }
1652
+ .veo-guide-cta--link:hover { opacity: 1; text-decoration: underline; }
1312
1653
  .veo-guide-cta:active { transform: translateY(1px); }
1313
1654
  .veo-guide-close {
1314
1655
  position: absolute; top: -6px; right: -6px;
@@ -1339,6 +1680,10 @@ var init_styles = __esm({
1339
1680
 
1340
1681
  .veo-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
1341
1682
  .veo-form-field { display: flex; flex-direction: column; gap: 6px; }
1683
+ /* Pregunta de la encuesta (rich text del bloque form). */
1684
+ .veo-form-question { font-size: 16px; font-weight: 600; line-height: 1.35; color: var(--veo-text); }
1685
+ .veo-form-question p { margin: 0; }
1686
+ .veo-form > .veo-guide-actions { margin: 0; }
1342
1687
  .veo-form-label { font-size: 13px; font-weight: 500; color: var(--veo-text); }
1343
1688
  .veo-form-input {
1344
1689
  font-size: 14px; color: var(--veo-text);
@@ -1347,26 +1692,71 @@ var init_styles = __esm({
1347
1692
  padding: 8px 10px; width: 100%;
1348
1693
  }
1349
1694
  .veo-form-input:focus { outline: 2px solid var(--veo-primary); outline-offset: -1px; border-color: transparent; }
1350
- .veo-form-options { display: flex; flex-direction: column; gap: 6px; }
1695
+ /* Lista de tareas (mock): \xEDtems con anillo de progreso, texto + subt\xEDtulo,
1696
+ y al pie \xABX de N completadas\xBB + porcentaje + barra. */
1697
+ .veo-checklist {
1698
+ display: flex; flex-direction: column; gap: 22px; align-self: stretch; width: 100%;
1699
+ min-width: 360px; box-sizing: border-box;
1700
+ }
1701
+ .veo-checklist-title { font-size: 24px; font-weight: 700; line-height: 1.25; color: var(--veo-cl-title); }
1702
+ .veo-checklist-title p { margin: 0; }
1703
+ .veo-checklist-items { display: flex; flex-direction: column; gap: 22px; }
1704
+ .veo-checklist-item {
1705
+ display: flex; align-items: center; gap: 18px; width: 100%; text-align: left;
1706
+ background: none; border: 0; padding: 0; cursor: pointer; font: inherit; color: inherit;
1707
+ }
1708
+ .veo-checklist-item:hover .veo-checklist-label { text-decoration: underline; }
1709
+ .veo-checklist-ring { width: 44px; height: 44px; flex: 0 0 auto; }
1710
+ .veo-checklist-texts { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
1711
+ .veo-checklist-label {
1712
+ font-size: calc(var(--veo-cl-font-size, 14px) + 4px); font-weight: var(--veo-cl-font-weight, 600);
1713
+ line-height: 1.3; color: var(--veo-cl-title);
1714
+ }
1715
+ .veo-checklist-sub { font-size: calc(var(--veo-cl-font-size, 14px) + 1px); color: var(--veo-cl-subtitle); }
1716
+ .veo-checklist-footer { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }
1717
+ .veo-checklist-summary {
1718
+ display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
1719
+ font-size: calc(var(--veo-cl-font-size, 14px) + 1px);
1720
+ }
1721
+ .veo-checklist-count { color: var(--veo-cl-subtitle); }
1722
+ .veo-checklist-percent { font-weight: 700; font-size: calc(var(--veo-cl-font-size, 14px) + 3px); color: var(--veo-cl-bar); }
1723
+ .veo-checklist-bar { height: var(--veo-cl-bar-h, 8px); background: #e7e2de; border-radius: 999px; overflow: hidden; }
1724
+ .veo-checklist--dark .veo-checklist-bar { background: #3c3734; }
1725
+ .veo-checklist-bar-fill { height: 100%; background: var(--veo-cl-bar); border-radius: 999px; transition: width 300ms ease; }
1726
+ .veo-checklist-empty { margin: 0; font-size: 13px; color: var(--veo-cl-subtitle); }
1727
+ /* Opci\xF3n m\xFAltiple (mock): c\xEDrculos/casillas grandes con su texto al lado. */
1728
+ .veo-form-options { display: flex; flex-direction: column; gap: 14px; }
1351
1729
  .veo-form-option {
1352
- display: flex; align-items: center; gap: 8px;
1353
- font-size: 14px; color: var(--veo-text); cursor: pointer;
1354
- }
1355
- .veo-form-option input { accent-color: var(--veo-primary); margin: 0; cursor: pointer; }
1356
- .veo-form-scale { display: flex; gap: 4px; flex-wrap: wrap; }
1730
+ display: flex; align-items: center; gap: 14px;
1731
+ font-size: 16px; color: var(--veo-text); cursor: pointer;
1732
+ }
1733
+ .veo-form-option input {
1734
+ accent-color: var(--veo-primary); margin: 0; cursor: pointer;
1735
+ width: 26px; height: 26px; flex: 0 0 auto;
1736
+ }
1737
+ /* Escala num\xE9rica (mock): casillas cuadradas; con etiquetas, cada casilla
1738
+ lleva la suya debajo (la primera cuelga a la izquierda, la \xFAltima a la derecha). */
1739
+ .veo-form-scale { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }
1740
+ .veo-scale-item { display: inline-flex; flex-direction: column; align-items: center; gap: 8px; }
1741
+ .veo-scale-item--first { align-items: flex-start; }
1742
+ .veo-scale-item--last { align-items: flex-end; }
1743
+ .veo-scale-label { font-size: 12px; line-height: 1.2; color: #8a8582; white-space: nowrap; }
1357
1744
  .veo-nps-btn {
1358
- min-width: 30px; height: 30px; padding: 0 4px;
1359
- font-size: 13px; font-weight: 500; color: var(--veo-text);
1360
- background: var(--veo-bg); border: 1px solid #d1d5db; border-radius: 6px;
1745
+ min-width: var(--veo-scale-size, 44px); height: var(--veo-scale-size, 44px); padding: 0 6px;
1746
+ font-size: 16px; font-weight: 600; color: var(--veo-text);
1747
+ background: var(--veo-bg); border: 1px solid #d5cec9; border-radius: 8px;
1361
1748
  cursor: pointer; transition: all 100ms ease;
1362
1749
  }
1363
- .veo-nps-btn:hover { border-color: var(--veo-primary); }
1750
+ .veo-nps-btn:hover { border-color: var(--veo-scale-color, var(--veo-primary)); }
1364
1751
  .veo-rating-btn {
1365
1752
  background: none; border: none; padding: 0 2px;
1366
1753
  font-size: 26px; line-height: 1; color: #d1d5db;
1367
1754
  cursor: pointer; transition: color 100ms ease;
1368
1755
  }
1369
- .veo-scale-active.veo-nps-btn { background: var(--veo-primary); color: #fff; border-color: var(--veo-primary); }
1756
+ .veo-scale-active.veo-nps-btn {
1757
+ background: var(--veo-scale-color, var(--veo-primary)); color: #fff;
1758
+ border-color: var(--veo-scale-color, var(--veo-primary));
1759
+ }
1370
1760
  .veo-scale-active.veo-rating-btn { color: #f59e0b; }
1371
1761
  .veo-form-error { font-size: 13px; color: #dc2626; margin: 0; }
1372
1762
  .veo-form-thanks {
@@ -2074,6 +2464,217 @@ var init_banner_renderer = __esm({
2074
2464
  }
2075
2465
  });
2076
2466
 
2467
+ // src/plugins/guides/checklist-block.ts
2468
+ function checklistStyle(style) {
2469
+ const s = style ?? {};
2470
+ const str = (k, fb) => typeof s[k] === "string" && s[k].trim() ? s[k] : fb;
2471
+ const num2 = (k, fb, min, max) => typeof s[k] === "number" && Number.isFinite(s[k]) ? Math.min(max, Math.max(min, s[k])) : fb;
2472
+ const weightRaw = s.fontWeight;
2473
+ const fontWeight = typeof weightRaw === "number" ? Math.min(900, Math.max(100, weightRaw)) : typeof weightRaw === "string" && WEIGHTS[weightRaw.toLowerCase()] ? WEIGHTS[weightRaw.toLowerCase()] : CHECKLIST_DEFAULTS.fontWeight;
2474
+ const dark = s.colorMode === "dark";
2475
+ return {
2476
+ colorMode: dark ? "dark" : "light",
2477
+ iconEmptyColor: str("iconEmptyColor", dark ? "#3c3734" : CHECKLIST_DEFAULTS.iconEmptyColor),
2478
+ iconFillColor: str("iconFillColor", CHECKLIST_DEFAULTS.iconFillColor),
2479
+ barColor: str("barColor", CHECKLIST_DEFAULTS.barColor),
2480
+ barHeight: num2("barHeight", CHECKLIST_DEFAULTS.barHeight, 2, 40),
2481
+ fontSize: num2("fontSize", CHECKLIST_DEFAULTS.fontSize, 10, 40),
2482
+ fontWeight,
2483
+ titleColor: str("titleColor", dark ? "#f5efeb" : CHECKLIST_DEFAULTS.titleColor),
2484
+ subtitleColor: str("subtitleColor", dark ? "#b0a9a5" : CHECKLIST_DEFAULTS.subtitleColor),
2485
+ subtitleTemplate: str("subtitleTemplate", CHECKLIST_DEFAULTS.subtitleTemplate)
2486
+ };
2487
+ }
2488
+ function renderSubtitle(template, p) {
2489
+ return template.replace(/<%=\s*currentStep\s*%>/g, String(p.currentStep)).replace(/<%=\s*totalSteps\s*%>/g, String(p.totalSteps));
2490
+ }
2491
+ function buildRing(doc, fraction, completed, st) {
2492
+ const ns = "http://www.w3.org/2000/svg";
2493
+ const svg2 = doc.createElementNS(ns, "svg");
2494
+ svg2.setAttribute("viewBox", "0 0 28 28");
2495
+ svg2.setAttribute("class", "veo-checklist-ring");
2496
+ svg2.setAttribute("aria-hidden", "true");
2497
+ const track = doc.createElementNS(ns, "circle");
2498
+ track.setAttribute("cx", "14");
2499
+ track.setAttribute("cy", "14");
2500
+ track.setAttribute("r", String(RING_R));
2501
+ track.setAttribute("fill", completed ? st.iconFillColor : "none");
2502
+ track.setAttribute("stroke", st.iconEmptyColor);
2503
+ track.setAttribute("stroke-width", "4.5");
2504
+ svg2.appendChild(track);
2505
+ if (completed) {
2506
+ const check = doc.createElementNS(ns, "path");
2507
+ check.setAttribute("d", "M9 14.5l3.2 3.2L19 11");
2508
+ check.setAttribute("fill", "none");
2509
+ check.setAttribute("stroke", "#fff");
2510
+ check.setAttribute("stroke-width", "2.4");
2511
+ check.setAttribute("stroke-linecap", "round");
2512
+ check.setAttribute("stroke-linejoin", "round");
2513
+ svg2.appendChild(check);
2514
+ return svg2;
2515
+ }
2516
+ const f = Math.max(0, Math.min(1, fraction));
2517
+ if (f > 0) {
2518
+ const arc = doc.createElementNS(ns, "circle");
2519
+ arc.setAttribute("cx", "14");
2520
+ arc.setAttribute("cy", "14");
2521
+ arc.setAttribute("r", String(RING_R));
2522
+ arc.setAttribute("fill", "none");
2523
+ arc.setAttribute("stroke", st.iconFillColor);
2524
+ arc.setAttribute("stroke-width", "4.5");
2525
+ arc.setAttribute("stroke-linecap", "butt");
2526
+ arc.setAttribute("stroke-dasharray", `${(RING_C * f).toFixed(2)} ${RING_C.toFixed(2)}`);
2527
+ arc.setAttribute("transform", "rotate(-90 14 14)");
2528
+ svg2.appendChild(arc);
2529
+ }
2530
+ return svg2;
2531
+ }
2532
+ function buildChecklistBlock(block, doc, context) {
2533
+ const st = checklistStyle(block.style);
2534
+ const items = (block.items ?? []).filter((i) => i && typeof i.guideId === "string" && i.guideId);
2535
+ const root = doc.createElement("div");
2536
+ root.className = `veo-checklist${st.colorMode === "dark" ? " veo-checklist--dark" : ""}`;
2537
+ root.style.setProperty("--veo-cl-font-size", `${st.fontSize}px`);
2538
+ root.style.setProperty("--veo-cl-font-weight", String(st.fontWeight));
2539
+ root.style.setProperty("--veo-cl-title", st.titleColor);
2540
+ root.style.setProperty("--veo-cl-subtitle", st.subtitleColor);
2541
+ root.style.setProperty("--veo-cl-bar", st.barColor);
2542
+ root.style.setProperty("--veo-cl-bar-h", `${st.barHeight}px`);
2543
+ if (typeof block.html === "string" && block.html || typeof block.text === "string" && block.text) {
2544
+ const title = doc.createElement("div");
2545
+ title.className = "veo-checklist-title";
2546
+ if (typeof block.html === "string" && block.html)
2547
+ title.appendChild(renderRichText(block.html, doc));
2548
+ else title.textContent = block.text;
2549
+ root.appendChild(title);
2550
+ }
2551
+ const list = doc.createElement("div");
2552
+ list.className = "veo-checklist-items";
2553
+ list.setAttribute("role", "list");
2554
+ root.appendChild(list);
2555
+ const footer = doc.createElement("div");
2556
+ footer.className = "veo-checklist-footer";
2557
+ const summary = doc.createElement("div");
2558
+ summary.className = "veo-checklist-summary";
2559
+ const count = doc.createElement("span");
2560
+ count.className = "veo-checklist-count";
2561
+ const percent = doc.createElement("span");
2562
+ percent.className = "veo-checklist-percent";
2563
+ summary.append(count, percent);
2564
+ const bar = doc.createElement("div");
2565
+ bar.className = "veo-checklist-bar";
2566
+ const fill = doc.createElement("div");
2567
+ fill.className = "veo-checklist-bar-fill";
2568
+ bar.appendChild(fill);
2569
+ footer.append(summary, bar);
2570
+ root.appendChild(footer);
2571
+ const paint = (progress) => {
2572
+ list.textContent = "";
2573
+ const visible = progress ? items.filter((i) => progress.has(i.guideId)) : items;
2574
+ let done = 0;
2575
+ for (const item of visible) {
2576
+ const p = progress?.get(item.guideId) ?? null;
2577
+ if (p?.completed) done++;
2578
+ const row = doc.createElement("button");
2579
+ row.type = "button";
2580
+ row.className = `veo-checklist-item${p?.completed ? " veo-checklist-item--done" : ""}`;
2581
+ row.setAttribute("role", "listitem");
2582
+ const fraction = p && p.totalSteps > 0 ? p.currentStep / p.totalSteps : 0;
2583
+ row.appendChild(buildRing(doc, fraction, p?.completed === true, st));
2584
+ const texts = doc.createElement("span");
2585
+ texts.className = "veo-checklist-texts";
2586
+ const label = doc.createElement("span");
2587
+ label.className = "veo-checklist-label";
2588
+ label.textContent = (item.text ?? "").trim() || p?.guideName || "Gu\xEDa";
2589
+ if (p && p.published === false) row.title = "Gu\xEDa no publicada: los usuarios no la ver\xE1n";
2590
+ texts.appendChild(label);
2591
+ if (p) {
2592
+ const sub = doc.createElement("span");
2593
+ sub.className = "veo-checklist-sub";
2594
+ sub.textContent = renderSubtitle(st.subtitleTemplate, p);
2595
+ texts.appendChild(sub);
2596
+ }
2597
+ row.appendChild(texts);
2598
+ row.addEventListener("click", (e) => {
2599
+ e.preventDefault();
2600
+ e.stopPropagation();
2601
+ if (context.isPreview) return;
2602
+ const start = p && !p.completed ? Math.min(p.currentStep, p.totalSteps - 1) : 0;
2603
+ if (context.onOpenGuideAt) context.onOpenGuideAt(item.guideId, start);
2604
+ else context.onOpenGuide?.(item.guideId);
2605
+ });
2606
+ list.appendChild(row);
2607
+ }
2608
+ const total = visible.length;
2609
+ const pct = total > 0 ? Math.round(done / total * 100) : 0;
2610
+ count.textContent = `${done} de ${total} ${total === 1 ? "completada" : "completadas"}`;
2611
+ percent.textContent = `${pct}%`;
2612
+ fill.style.width = `${pct}%`;
2613
+ footer.hidden = total === 0;
2614
+ };
2615
+ if (context.isPreview && items.length === 0) {
2616
+ const sample = /* @__PURE__ */ new Map([
2617
+ [
2618
+ "__sample_1",
2619
+ {
2620
+ guideId: "__sample_1",
2621
+ guideName: "Configura tu primer env\xEDo",
2622
+ totalSteps: 2,
2623
+ currentStep: 1,
2624
+ completed: false
2625
+ }
2626
+ ],
2627
+ [
2628
+ "__sample_2",
2629
+ {
2630
+ guideId: "__sample_2",
2631
+ guideName: "Conecta tu tienda",
2632
+ totalSteps: 2,
2633
+ currentStep: 2,
2634
+ completed: true
2635
+ }
2636
+ ]
2637
+ ]);
2638
+ items.push(
2639
+ { guideId: "__sample_1", text: "Configura tu primer env\xEDo" },
2640
+ { guideId: "__sample_2", text: "Conecta tu tienda" }
2641
+ );
2642
+ paint(sample);
2643
+ const hint = doc.createElement("p");
2644
+ hint.className = "veo-checklist-empty";
2645
+ hint.textContent = "Ejemplo: agrega gu\xEDas a la lista desde Opciones.";
2646
+ root.appendChild(hint);
2647
+ return root;
2648
+ }
2649
+ paint(null);
2650
+ if (context.fetchProgress && items.length > 0) {
2651
+ void context.fetchProgress(items.map((i) => i.guideId)).then((rows) => paint(new Map(rows.map((r) => [r.guideId, r])))).catch(() => {
2652
+ });
2653
+ }
2654
+ return root;
2655
+ }
2656
+ var CHECKLIST_DEFAULTS, WEIGHTS, RING_R, RING_C;
2657
+ var init_checklist_block = __esm({
2658
+ "src/plugins/guides/checklist-block.ts"() {
2659
+ init_rich_text();
2660
+ CHECKLIST_DEFAULTS = {
2661
+ colorMode: "light",
2662
+ iconEmptyColor: "#e7e2de",
2663
+ iconFillColor: "#406a31",
2664
+ barColor: "#406a31",
2665
+ barHeight: 8,
2666
+ fontSize: 14,
2667
+ fontWeight: 600,
2668
+ titleColor: "#2c2825",
2669
+ subtitleColor: "#8a8582",
2670
+ subtitleTemplate: "Completado <%= currentStep %> de <%= totalSteps %> pasos"
2671
+ };
2672
+ WEIGHTS = { regular: 400, medium: 500, semibold: 600, bold: 700 };
2673
+ RING_R = 10;
2674
+ RING_C = 2 * Math.PI * RING_R;
2675
+ }
2676
+ });
2677
+
2077
2678
  // src/plugins/guides/renderers/custom-frame.ts
2078
2679
  function mountCustomFrame(doc, step, context, opts) {
2079
2680
  const iframe = doc.createElement("iframe");
@@ -2258,6 +2859,11 @@ function mountCodeBlock(block, doc, context, host, rawInline) {
2258
2859
  }
2259
2860
  if (rawInline) {
2260
2861
  const rawNodes = Array.from(holder.childNodes);
2862
+ if (rawInline.tagBlockId) {
2863
+ for (const n of rawNodes) {
2864
+ if (n instanceof Element) n.setAttribute(RAW_BLOCK_ATTR, rawInline.tagBlockId);
2865
+ }
2866
+ }
2261
2867
  insertRawNodes(rawInline.anchor, rawNodes, rawInline.pos);
2262
2868
  injected.push(...rawNodes);
2263
2869
  } else if (host) {
@@ -2267,24 +2873,40 @@ function mountCodeBlock(block, doc, context, host, rawInline) {
2267
2873
  injected.push(holder);
2268
2874
  const slot = doc.createElement("slot");
2269
2875
  slot.setAttribute("name", slotName);
2270
- node = slot;
2876
+ const wrapper = doc.createElement("div");
2877
+ wrapper.className = "veo-code-host";
2878
+ wrapper.appendChild(slot);
2879
+ node = wrapper;
2271
2880
  } else {
2272
2881
  node = holder;
2273
2882
  }
2274
2883
  }
2884
+ let cancelled = false;
2275
2885
  if (typeof block.js === "string" && block.js.trim()) {
2276
- const s = hostDoc.createElement("script");
2277
- s.textContent = block.js;
2278
- hostDoc.body.appendChild(s);
2279
- injected.push(s);
2886
+ const js = block.js;
2887
+ const run = () => {
2888
+ if (cancelled) return;
2889
+ const s = hostDoc.createElement("script");
2890
+ s.textContent = wrapUserScript(js);
2891
+ hostDoc.body.appendChild(s);
2892
+ injected.push(s);
2893
+ };
2894
+ if (host && !host.isConnected) queueMicrotask(run);
2895
+ else run();
2280
2896
  }
2281
2897
  return {
2282
2898
  node,
2283
2899
  cleanup: () => {
2900
+ cancelled = true;
2284
2901
  for (const n of injected) n.parentNode?.removeChild(n);
2285
2902
  }
2286
2903
  };
2287
2904
  }
2905
+ function wrapUserScript(js) {
2906
+ return `(function(){try{
2907
+ ${js}
2908
+ }catch(e){console.error("[OnUser] Error en el bloque de c\xF3digo:",e)}})();`;
2909
+ }
2288
2910
  function runScript(old, hostDoc) {
2289
2911
  const s = hostDoc.createElement("script");
2290
2912
  for (const attr of Array.from(old.attributes)) s.setAttribute(attr.name, attr.value);
@@ -2292,10 +2914,11 @@ function runScript(old, hostDoc) {
2292
2914
  hostDoc.body.appendChild(s);
2293
2915
  return s;
2294
2916
  }
2295
- var codeSlotSeq;
2917
+ var RAW_BLOCK_ATTR, codeSlotSeq;
2296
2918
  var init_code_block = __esm({
2297
2919
  "src/plugins/guides/code-block.ts"() {
2298
2920
  init_custom_frame();
2921
+ RAW_BLOCK_ATTR = "data-veo-raw-block";
2299
2922
  codeSlotSeq = 0;
2300
2923
  }
2301
2924
  });
@@ -2526,8 +3149,14 @@ function buildField(field, doc) {
2526
3149
  wrapper.appendChild(group);
2527
3150
  return { wrapper, read: () => inputs.find((i) => i.input.checked)?.value };
2528
3151
  }
2529
- case "nps":
2530
- return buildScale(wrapper, doc, 0, 10, "veo-nps-btn");
3152
+ case "nps": {
3153
+ const min = typeof field.min === "number" ? field.min : 0;
3154
+ const max = typeof field.max === "number" && field.max > min ? field.max : 10;
3155
+ return buildScale(wrapper, doc, min, max, "veo-nps-btn", void 0, {
3156
+ labels: field.scaleLabels ?? [],
3157
+ showLabels: field.showLabels !== false
3158
+ });
3159
+ }
2531
3160
  case "rating":
2532
3161
  return buildScale(wrapper, doc, 1, 5, "veo-rating-btn", "\u2605");
2533
3162
  default: {
@@ -2540,17 +3169,21 @@ function buildField(field, doc) {
2540
3169
  }
2541
3170
  }
2542
3171
  }
2543
- function buildScale(wrapper, doc, min, max, btnClass, symbol) {
3172
+ function buildScale(wrapper, doc, min, max, btnClass, symbol, opts = { labels: [], showLabels: false }) {
2544
3173
  const row = doc.createElement("div");
2545
3174
  row.className = "veo-form-scale";
2546
3175
  let selected;
2547
3176
  const buttons = [];
3177
+ const labelOf = (n) => opts.labels[n - min]?.trim() ?? "";
3178
+ const anyLabel = opts.showLabels && Array.from({ length: max - min + 1 }, (_, i) => min + i).some((n) => labelOf(n));
2548
3179
  for (let n = min; n <= max; n++) {
2549
3180
  const btn = doc.createElement("button");
2550
3181
  btn.type = "button";
2551
3182
  btn.className = btnClass;
2552
3183
  btn.textContent = symbol ?? String(n);
2553
- btn.setAttribute("aria-label", String(n));
3184
+ const lbl = labelOf(n);
3185
+ btn.setAttribute("aria-label", lbl ? `${n} \xB7 ${lbl}` : String(n));
3186
+ if (lbl) btn.title = lbl;
2554
3187
  btn.addEventListener("click", () => {
2555
3188
  selected = n;
2556
3189
  buttons.forEach((b, i) => {
@@ -2559,7 +3192,17 @@ function buildScale(wrapper, doc, min, max, btnClass, symbol) {
2559
3192
  });
2560
3193
  });
2561
3194
  buttons.push(btn);
2562
- row.appendChild(btn);
3195
+ if (!anyLabel) {
3196
+ row.appendChild(btn);
3197
+ continue;
3198
+ }
3199
+ const item = doc.createElement("div");
3200
+ item.className = `veo-scale-item${n === min ? " veo-scale-item--first" : n === max ? " veo-scale-item--last" : ""}`;
3201
+ const text = doc.createElement("span");
3202
+ text.className = "veo-scale-label";
3203
+ text.textContent = lbl;
3204
+ item.append(btn, text);
3205
+ row.appendChild(item);
2563
3206
  }
2564
3207
  wrapper.appendChild(row);
2565
3208
  return { wrapper, read: () => selected };
@@ -2569,6 +3212,126 @@ var init_form_content = __esm({
2569
3212
  init_block_builder();
2570
3213
  }
2571
3214
  });
3215
+
3216
+ // src/plugins/guides/layout.ts
3217
+ function groupLayout(blocks) {
3218
+ const rows = /* @__PURE__ */ new Map();
3219
+ const order = [];
3220
+ let lastRow = -1;
3221
+ blocks.forEach((block, index) => {
3222
+ if (!block) return;
3223
+ let row;
3224
+ if (typeof block.row === "number" && Number.isFinite(block.row)) {
3225
+ row = block.row;
3226
+ lastRow = row;
3227
+ } else {
3228
+ row = lastRow + 0.5;
3229
+ lastRow = row;
3230
+ }
3231
+ const col = typeof block.col === "number" && Number.isFinite(block.col) ? block.col : 0;
3232
+ let r = rows.get(row);
3233
+ if (!r) {
3234
+ r = { row, columns: [] };
3235
+ rows.set(row, r);
3236
+ order.push(row);
3237
+ }
3238
+ let c = r.columns.find((x) => x.col === col);
3239
+ if (!c) {
3240
+ c = { col, blocks: [] };
3241
+ r.columns.push(c);
3242
+ }
3243
+ c.blocks.push({ block, index });
3244
+ });
3245
+ return order.sort((a, b) => a - b).map((k) => {
3246
+ const r = rows.get(k);
3247
+ r.columns.sort((a, b) => a.col - b.col);
3248
+ return r;
3249
+ });
3250
+ }
3251
+ var init_layout = __esm({
3252
+ "src/plugins/guides/layout.ts"() {
3253
+ }
3254
+ });
3255
+
3256
+ // src/plugins/guides/scroll-lock.ts
3257
+ function lockScroll(doc = document) {
3258
+ const html = doc.documentElement;
3259
+ const body = doc.body;
3260
+ if (!html || !body) return () => {
3261
+ };
3262
+ let state = states.get(doc);
3263
+ if (!state) {
3264
+ state = {
3265
+ count: 0,
3266
+ html: { overflow: html.style.overflow, paddingRight: html.style.paddingRight },
3267
+ body: { overflow: body.style.overflow, paddingRight: body.style.paddingRight }
3268
+ };
3269
+ states.set(doc, state);
3270
+ const win = doc.defaultView;
3271
+ const gap = win ? Math.max(0, win.innerWidth - html.clientWidth) : 0;
3272
+ if (gap > 0) body.style.paddingRight = `${gap}px`;
3273
+ html.style.overflow = "hidden";
3274
+ body.style.overflow = "hidden";
3275
+ }
3276
+ state.count += 1;
3277
+ let released = false;
3278
+ return () => {
3279
+ if (released) return;
3280
+ released = true;
3281
+ const s = states.get(doc);
3282
+ if (!s) return;
3283
+ s.count -= 1;
3284
+ if (s.count > 0) return;
3285
+ states.delete(doc);
3286
+ html.style.overflow = s.html.overflow;
3287
+ html.style.paddingRight = s.html.paddingRight;
3288
+ body.style.overflow = s.body.overflow;
3289
+ body.style.paddingRight = s.body.paddingRight;
3290
+ };
3291
+ }
3292
+ var states;
3293
+ var init_scroll_lock = __esm({
3294
+ "src/plugins/guides/scroll-lock.ts"() {
3295
+ states = /* @__PURE__ */ new WeakMap();
3296
+ }
3297
+ });
3298
+
3299
+ // src/plugins/guides/snooze.ts
3300
+ function snoozeDurationMs(every, unit) {
3301
+ const n = Number.isFinite(every) && every > 0 ? Math.floor(every) : 1;
3302
+ return n * (UNIT_MS[unit] ?? UNIT_MS.day);
3303
+ }
3304
+ function snoozeGuide(guideId, every, unit, now = Date.now()) {
3305
+ try {
3306
+ localStorage.setItem(KEY(guideId), String(now + snoozeDurationMs(every, unit)));
3307
+ } catch {
3308
+ }
3309
+ }
3310
+ function isSnoozed(guideId, now = Date.now()) {
3311
+ try {
3312
+ const raw = localStorage.getItem(KEY(guideId));
3313
+ if (raw === null) return false;
3314
+ const until = Number(raw);
3315
+ if (!Number.isFinite(until) || until <= now) {
3316
+ localStorage.removeItem(KEY(guideId));
3317
+ return false;
3318
+ }
3319
+ return true;
3320
+ } catch {
3321
+ return false;
3322
+ }
3323
+ }
3324
+ var KEY, UNIT_MS;
3325
+ var init_snooze = __esm({
3326
+ "src/plugins/guides/snooze.ts"() {
3327
+ KEY = (guideId) => `veo:snooze:${guideId}`;
3328
+ UNIT_MS = {
3329
+ hour: 36e5,
3330
+ day: 864e5,
3331
+ week: 6048e5
3332
+ };
3333
+ }
3334
+ });
2572
3335
  function num(v, fallback) {
2573
3336
  return typeof v === "number" && Number.isFinite(v) ? v : fallback;
2574
3337
  }
@@ -2756,26 +3519,61 @@ var init_badge_marker = __esm({
2756
3519
  SIDE_GAP = 8;
2757
3520
  }
2758
3521
  });
3522
+ function applyRowStyle(rowEl, style) {
3523
+ const s = rowEl.style;
3524
+ if (typeof style.background === "string" && style.background) s.background = style.background;
3525
+ for (const side of ["Top", "Right", "Bottom", "Left"]) {
3526
+ const p = style[`padding${side}`];
3527
+ if (typeof p === "number" && Number.isFinite(p))
3528
+ s.setProperty(`padding-${side.toLowerCase()}`, `${p}px`);
3529
+ const m = style[`margin${side}`];
3530
+ if (typeof m === "number" && Number.isFinite(m))
3531
+ s.setProperty(`margin-${side.toLowerCase()}`, `${m}px`);
3532
+ }
3533
+ if (typeof style.minHeight === "number" && Number.isFinite(style.minHeight) && style.minHeight > 0) {
3534
+ s.minHeight = `${style.minHeight}px`;
3535
+ }
3536
+ if (style.align && ROW_JUSTIFY[style.align]) {
3537
+ s.justifyContent = ROW_JUSTIFY[style.align];
3538
+ rowEl.classList.add("veo-row--aligned");
3539
+ }
3540
+ if (style.valign && ROW_VALIGN[style.valign]) s.alignItems = ROW_VALIGN[style.valign];
3541
+ }
2759
3542
  function inlinePosOf(p) {
2760
3543
  return p === "before" || p === "prepend" || p === "append" ? p : "after";
2761
3544
  }
2762
3545
  function applyViewportPosition(card, position, design) {
2763
3546
  const s = card.style;
2764
3547
  s.position = "fixed";
2765
- const M = "16px";
3548
+ const BASE = 16;
3549
+ const m = (k) => typeof design[k] === "number" && Number.isFinite(design[k]) ? design[k] : 0;
3550
+ s.margin = "0";
2766
3551
  const hAlign = design.hAlign === "left" ? "left" : design.hAlign === "right" ? "right" : null;
2767
3552
  const v = position.includes("top") ? "top" : position.includes("bottom") ? "bottom" : "middle";
2768
3553
  const h = hAlign ?? (position.includes("left") ? "left" : position.includes("right") ? "right" : "middle");
2769
- if (v === "top") s.top = M;
2770
- else if (v === "bottom") s.bottom = M;
3554
+ if (v === "top") s.top = `${BASE + m("marginTop")}px`;
3555
+ else if (v === "bottom") s.bottom = `${BASE + m("marginBottom")}px`;
2771
3556
  else s.top = "50%";
2772
- if (h === "left") s.left = M;
2773
- else if (h === "right") s.right = M;
3557
+ if (h === "left") s.left = `${BASE + m("marginLeft")}px`;
3558
+ else if (h === "right") s.right = `${BASE + m("marginRight")}px`;
2774
3559
  else s.left = "50%";
2775
- const tx = h === "middle" ? "-50%" : "0";
2776
- const ty = v === "middle" ? "-50%" : "0";
3560
+ const dx = m("marginLeft") - m("marginRight");
3561
+ const dy = m("marginTop") - m("marginBottom");
3562
+ const tx = h === "middle" ? `calc(-50% + ${dx}px)` : "0";
3563
+ const ty = v === "middle" ? `calc(-50% + ${dy}px)` : "0";
2777
3564
  s.transform = tx === "0" && ty === "0" ? "" : `translate(${tx}, ${ty})`;
2778
3565
  }
3566
+ function reserveCloseSpace(card, blocks, position, design) {
3567
+ if (blocks.length === 0) return;
3568
+ const side = position.includes("bottom") ? "Bottom" : "Top";
3569
+ const edgeBlock = side === "Top" ? blocks[0] : blocks[blocks.length - 1];
3570
+ if (edgeBlock?.type === "image") return;
3571
+ const margin = design[`closeMargin${side}`];
3572
+ const need = CLOSE_RESERVE_PX + (typeof margin === "number" ? margin : 0);
3573
+ const current = design[`padding${side}`];
3574
+ const have = typeof current === "number" ? current : CARD_DEFAULT_PAD_PX;
3575
+ if (have < need) card.style.setProperty(`padding-${side.toLowerCase()}`, `${need}px`);
3576
+ }
2779
3577
  function applyClosePosition(btn, position) {
2780
3578
  const s = btn.style;
2781
3579
  s.top = "auto";
@@ -2844,6 +3642,11 @@ function applyCompositeDesign(card, design) {
2844
3642
  const s = card.style;
2845
3643
  const px = (v) => typeof v === "number" && Number.isFinite(v) ? `${v}px` : null;
2846
3644
  const strv = (v) => typeof v === "string" && v ? v : null;
3645
+ if (design.colorMode === "dark") {
3646
+ s.background = "#1f1d1b";
3647
+ s.color = "#f5f0eb";
3648
+ card.classList.add("veo-card--dark");
3649
+ }
2847
3650
  if (strv(design.background)) s.background = design.background;
2848
3651
  const textColor = strv(design.color) ?? strv(design.textColor);
2849
3652
  if (textColor) s.color = textColor;
@@ -2865,13 +3668,14 @@ function applyCompositeDesign(card, design) {
2865
3668
  const n = (k) => typeof design[k] === "number" ? design[k] : 0;
2866
3669
  s.boxShadow = `${n("shadowX")}px ${n("shadowY")}px ${n("shadowBlur")}px ${n("shadowSpread")}px ${strv(design.shadowColor) ?? "rgba(0,0,0,0.2)"}`;
2867
3670
  }
3671
+ const width = typeof design.width === "number" && Number.isFinite(design.width) ? `${Math.max(MIN_CARD_WIDTH, design.width)}px` : null;
2868
3672
  if (design.fullWidth === true) {
2869
3673
  s.width = "100%";
2870
3674
  } else if (design.widthType === "fluid") {
2871
- s.width = "auto";
2872
- if (px(design.width)) s.maxWidth = px(design.width);
2873
- } else if (px(design.width)) {
2874
- s.width = px(design.width);
3675
+ s.width = "100%";
3676
+ if (width) s.maxWidth = width;
3677
+ } else if (width) {
3678
+ s.width = width;
2875
3679
  }
2876
3680
  for (const side of ["Top", "Right", "Bottom", "Left"]) {
2877
3681
  const p = px(design[`padding${side}`]);
@@ -2880,13 +3684,18 @@ function applyCompositeDesign(card, design) {
2880
3684
  if (m) s.setProperty(`margin-${side.toLowerCase()}`, m);
2881
3685
  }
2882
3686
  }
2883
- var ROW_ALIGN, CompositeRenderer, CLOSE_INSET;
3687
+ var ROW_ALIGN, ROW_JUSTIFY, ROW_VALIGN, CompositeRenderer, CLOSE_INSET, CLOSE_RESERVE_PX, CARD_DEFAULT_PAD_PX, MIN_CARD_WIDTH, NARROW_CARD_WIDTH;
2884
3688
  var init_composite_renderer = __esm({
2885
3689
  "src/plugins/guides/renderers/composite-renderer.ts"() {
2886
3690
  init_block_builder();
3691
+ init_checklist_block();
2887
3692
  init_code_block();
2888
3693
  init_form_content();
2889
3694
  init_inline_host();
3695
+ init_layout();
3696
+ init_rich_text();
3697
+ init_scroll_lock();
3698
+ init_snooze();
2890
3699
  init_wait_for_element();
2891
3700
  init_badge_marker();
2892
3701
  init_base_renderer();
@@ -2896,6 +3705,12 @@ var init_composite_renderer = __esm({
2896
3705
  center: "center",
2897
3706
  right: "flex-end"
2898
3707
  };
3708
+ ROW_JUSTIFY = { ...ROW_ALIGN, distribute: "space-between" };
3709
+ ROW_VALIGN = {
3710
+ top: "flex-start",
3711
+ center: "center",
3712
+ bottom: "flex-end"
3713
+ };
2899
3714
  CompositeRenderer = class extends BaseRenderer {
2900
3715
  async render(context) {
2901
3716
  const idx = context.nav?.stepIndex ?? 0;
@@ -2914,7 +3729,8 @@ var init_composite_renderer = __esm({
2914
3729
  const rawDoc = anchor.ownerDocument ?? document;
2915
3730
  const { cleanup } = mountCodeBlock(soleCode, rawDoc, context, void 0, {
2916
3731
  anchor,
2917
- pos: inlinePosOf(step.inlinePosition)
3732
+ pos: inlinePosOf(step.inlinePosition),
3733
+ ...context.isPreview && soleCode.editorId ? { tagBlockId: soleCode.editorId } : {}
2918
3734
  });
2919
3735
  this.registerCleanup(cleanup);
2920
3736
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: idx, action: "shown" });
@@ -2927,6 +3743,10 @@ var init_composite_renderer = __esm({
2927
3743
  const card = doc.createElement("div");
2928
3744
  card.className = "veo-card";
2929
3745
  applyCompositeDesign(card, design);
3746
+ if (typeof design.ariaLabel === "string" && design.ariaLabel.trim()) {
3747
+ card.setAttribute("role", "dialog");
3748
+ card.setAttribute("aria-label", design.ariaLabel.trim());
3749
+ }
2930
3750
  const dismiss = (action, meta) => {
2931
3751
  context.onInteraction({
2932
3752
  guideId: context.guide.guideId,
@@ -2937,6 +3757,28 @@ var init_composite_renderer = __esm({
2937
3757
  };
2938
3758
  const readers = [];
2939
3759
  this.buildContent(card, step.blocks ?? [], doc, context, idx, readers, host);
3760
+ if (blocks.length === 0) {
3761
+ card.classList.add("veo-card--empty");
3762
+ if (context.isPreview) {
3763
+ const zone = doc.createElement("div");
3764
+ zone.className = "veo-card-empty-zone";
3765
+ const plus = doc.createElement("b");
3766
+ plus.textContent = "+";
3767
+ const text = doc.createElement("span");
3768
+ text.textContent = "Clic para agregar contenido";
3769
+ zone.append(plus, text);
3770
+ card.appendChild(zone);
3771
+ const applyNarrow = () => {
3772
+ card.classList.toggle("veo-card--narrow", card.clientWidth < NARROW_CARD_WIDTH);
3773
+ };
3774
+ if (typeof ResizeObserver !== "undefined") {
3775
+ const ro = new ResizeObserver(applyNarrow);
3776
+ ro.observe(card);
3777
+ this.registerCleanup(() => ro.disconnect());
3778
+ }
3779
+ applyNarrow();
3780
+ }
3781
+ }
2940
3782
  const lastBlock = card.lastElementChild;
2941
3783
  if (lastBlock && !lastBlock.style.marginBottom) lastBlock.style.marginBottom = "0";
2942
3784
  let closeTooltip = null;
@@ -2954,6 +3796,14 @@ var init_composite_renderer = __esm({
2954
3796
  if (closeColor) closeBtn.style.color = closeColor;
2955
3797
  const closePos = typeof design.closePosition === "string" ? design.closePosition : "top-right";
2956
3798
  applyClosePosition(closeBtn, closePos);
3799
+ for (const side of ["Top", "Right", "Bottom", "Left"]) {
3800
+ const p = design[`closePadding${side}`];
3801
+ if (typeof p === "number")
3802
+ closeBtn.style.setProperty(`padding-${side.toLowerCase()}`, `${p}px`);
3803
+ const m = design[`closeMargin${side}`];
3804
+ if (typeof m === "number")
3805
+ closeBtn.style.setProperty(`margin-${side.toLowerCase()}`, `${m}px`);
3806
+ }
2957
3807
  if (hoverColor) {
2958
3808
  closeBtn.addEventListener("mouseenter", () => {
2959
3809
  closeBtn.style.color = hoverColor;
@@ -2963,6 +3813,7 @@ var init_composite_renderer = __esm({
2963
3813
  });
2964
3814
  }
2965
3815
  card.appendChild(closeBtn);
3816
+ reserveCloseSpace(card, blocks, closePos, design);
2966
3817
  }
2967
3818
  if (badgeMode && anchor) {
2968
3819
  closeTooltip = this.renderBadgeMode(
@@ -2993,6 +3844,7 @@ var init_composite_renderer = __esm({
2993
3844
  const bz = typeof design.backdropZIndex === "number" ? design.backdropZIndex : 2e5;
2994
3845
  bg.style.zIndex = String(bz);
2995
3846
  root.appendChild(bg);
3847
+ this.registerCleanup(lockScroll(doc));
2996
3848
  if (design.targetAbove === true) this.registerCleanup(liftTarget(anchor, bz + 1));
2997
3849
  }
2998
3850
  const wrapper = doc.createElement("div");
@@ -3019,6 +3871,7 @@ var init_composite_renderer = __esm({
3019
3871
  this.closeOrSkip(context);
3020
3872
  }
3021
3873
  });
3874
+ this.registerCleanup(lockScroll(doc));
3022
3875
  }
3023
3876
  applyViewportPosition(card, step.position, design);
3024
3877
  overlay.appendChild(card);
@@ -3036,84 +3889,218 @@ var init_composite_renderer = __esm({
3036
3889
  ...block.id ? { metadata: { buttonId: block.id } } : {}
3037
3890
  });
3038
3891
  };
3039
- let i = 0;
3040
- while (i < blocks.length) {
3041
- const b = blocks[i];
3042
- if (!b) {
3043
- i++;
3044
- continue;
3045
- }
3046
- if (b.type === "button") {
3047
- const row = doc.createElement("div");
3048
- row.className = "veo-guide-actions";
3049
- let rowAlign;
3050
- while (i < blocks.length && blocks[i]?.type === "button") {
3051
- const bb = blocks[i];
3052
- if (!rowAlign) {
3053
- const a = bb.style?.align;
3054
- if (a === "left" || a === "center" || a === "right") rowAlign = a;
3892
+ const tag = (node, block, index) => {
3893
+ node.dataset.veoBlock = String(index);
3894
+ if (block.editorId) node.dataset.veoBlockId = block.editorId;
3895
+ return node;
3896
+ };
3897
+ const placeholder = (block, label) => {
3898
+ if (!context.isPreview || !block.editorId) return null;
3899
+ const el2 = doc.createElement("div");
3900
+ el2.className = "veo-block-placeholder";
3901
+ el2.textContent = label;
3902
+ return el2;
3903
+ };
3904
+ const buildSequence = (container, cell) => {
3905
+ let i = 0;
3906
+ while (i < cell.length) {
3907
+ const { block: b, index } = cell[i];
3908
+ if (b.type === "button") {
3909
+ const row = doc.createElement("div");
3910
+ row.className = "veo-guide-actions";
3911
+ let rowAlign;
3912
+ while (i < cell.length && cell[i]?.block.type === "button") {
3913
+ const { block: bb, index: bi } = cell[i];
3914
+ if (!rowAlign) {
3915
+ const a = bb.style?.align;
3916
+ if (a === "left" || a === "center" || a === "right") rowAlign = a;
3917
+ }
3918
+ row.appendChild(tag(this.buildButton(bb, doc, context, idx, readers, emit), bb, bi));
3919
+ i++;
3055
3920
  }
3056
- row.appendChild(this.buildButton(bb, doc, context, idx, readers, emit));
3921
+ if (rowAlign) row.style.justifyContent = ROW_ALIGN[rowAlign] ?? "flex-end";
3922
+ container.appendChild(row);
3923
+ continue;
3924
+ }
3925
+ if (b.type === "code") {
3926
+ const { node: node2, cleanup } = mountCodeBlock(b, doc, context, host);
3927
+ const el2 = node2 ?? placeholder(b, "Bloque de c\xF3digo");
3928
+ if (el2) container.appendChild(tag(el2, b, index));
3929
+ this.registerCleanup(cleanup);
3057
3930
  i++;
3931
+ continue;
3058
3932
  }
3059
- if (rowAlign) row.style.justifyContent = ROW_ALIGN[rowAlign] ?? "flex-end";
3060
- card.appendChild(row);
3061
- continue;
3062
- }
3063
- if (b.type === "code") {
3064
- const { node: node2, cleanup } = mountCodeBlock(b, doc, context, host);
3065
- if (node2) card.appendChild(node2);
3066
- this.registerCleanup(cleanup);
3067
- i++;
3068
- continue;
3069
- }
3070
- if (b.type === "form") {
3071
- const form = doc.createElement("div");
3072
- form.className = "veo-form";
3073
- for (const field of b.fields ?? []) {
3074
- const { wrapper, read } = buildField(field, doc);
3075
- readers.push({ field, read });
3076
- form.appendChild(wrapper);
3933
+ if (b.type === "form") {
3934
+ container.appendChild(tag(this.buildForm(b, doc, context, idx, readers, emit), b, index));
3935
+ i++;
3936
+ continue;
3077
3937
  }
3078
- card.appendChild(form);
3938
+ if (b.type === "checklist") {
3939
+ const el2 = buildChecklistBlock(b, doc, context);
3940
+ applyBlockStyle(el2, b.style, "text");
3941
+ el2.style.alignSelf = "stretch";
3942
+ el2.style.width = "100%";
3943
+ container.appendChild(tag(el2, b, index));
3944
+ i++;
3945
+ continue;
3946
+ }
3947
+ const node = buildContentBlock(b, doc, { editing: context.isPreview === true }) ?? placeholder(
3948
+ b,
3949
+ b.type === "image" ? "Imagen (sin URL)" : b.type === "video" ? "Video (elige proveedor y pega la URL)" : "Bloque vac\xEDo"
3950
+ );
3951
+ if (node) container.appendChild(tag(node, b, index));
3079
3952
  i++;
3953
+ }
3954
+ };
3955
+ const rowStyles = context.guide.guideSteps[idx]?.rows;
3956
+ for (const row of groupLayout(blocks)) {
3957
+ const rowStyle = Number.isInteger(row.row) ? rowStyles?.[String(row.row)] : void 0;
3958
+ if (row.columns.length === 1 && !rowStyle) {
3959
+ buildSequence(card, row.columns[0].blocks);
3080
3960
  continue;
3081
3961
  }
3082
- const node = buildContentBlock(b, doc);
3083
- if (node) card.appendChild(node);
3084
- i++;
3962
+ const rowEl = doc.createElement("div");
3963
+ rowEl.className = "veo-row";
3964
+ if (Number.isInteger(row.row)) rowEl.dataset.veoRow = String(row.row);
3965
+ for (const column of row.columns) {
3966
+ const colEl = doc.createElement("div");
3967
+ colEl.className = "veo-col";
3968
+ buildSequence(colEl, column.blocks);
3969
+ rowEl.appendChild(colEl);
3970
+ }
3971
+ if (rowStyle) applyRowStyle(rowEl, rowStyle);
3972
+ card.appendChild(rowEl);
3085
3973
  }
3086
3974
  }
3087
3975
  /** Botón con enrutado de acción (navegación / cierre / url / guía / envío de forms). */
3088
- buildButton(block, doc, context, idx, readers, emit) {
3976
+ /**
3977
+ * Bloque `form` (encuesta): pregunta (rich text del bloque) + campos + botón
3978
+ * de envío propio (`submit`). Con pregunta, el label del campo se omite (sería
3979
+ * la misma pregunta dos veces) y pasa a `aria-label` del input. El estilo del
3980
+ * bloque viste la pregunta (preset/alineación/color); los márgenes van al
3981
+ * contenedor del bloque entero.
3982
+ */
3983
+ buildForm(b, doc, context, idx, readers, emit) {
3984
+ const form = doc.createElement("div");
3985
+ form.className = "veo-form";
3986
+ const style = b.style ?? {};
3987
+ const { marginTop, marginBottom, ...questionStyle } = style;
3988
+ for (const [side, v] of [
3989
+ ["marginTop", marginTop],
3990
+ ["marginBottom", marginBottom]
3991
+ ]) {
3992
+ if (typeof v === "number" && Number.isFinite(v)) form.style[side] = `${v}px`;
3993
+ }
3994
+ let question = null;
3995
+ if (typeof b.html === "string" && b.html || typeof b.text === "string" && b.text) {
3996
+ question = doc.createElement("div");
3997
+ question.className = "veo-form-question";
3998
+ if (typeof b.html === "string" && b.html) question.appendChild(renderRichText(b.html, doc));
3999
+ else question.textContent = b.text;
4000
+ applyBlockStyle(question, questionStyle, "text");
4001
+ form.appendChild(question);
4002
+ }
4003
+ const questionText = question?.textContent?.trim() ?? "";
4004
+ const own = [];
4005
+ for (const field of b.fields ?? []) {
4006
+ const { wrapper, read } = buildField(field, doc);
4007
+ const st = b.style ?? {};
4008
+ if (typeof st.scaleColor === "string" && st.scaleColor) {
4009
+ wrapper.style.setProperty("--veo-scale-color", st.scaleColor);
4010
+ }
4011
+ if (typeof st.scaleSize === "number" && st.scaleSize >= 24 && st.scaleSize <= 96) {
4012
+ wrapper.style.setProperty("--veo-scale-size", `${st.scaleSize}px`);
4013
+ }
4014
+ if (question) {
4015
+ wrapper.querySelector(".veo-form-label")?.remove();
4016
+ if (questionText) {
4017
+ for (const inp of wrapper.querySelectorAll("input, textarea, select")) {
4018
+ inp.setAttribute("aria-label", questionText);
4019
+ }
4020
+ }
4021
+ }
4022
+ const reader = { field, read, blockId: b.id ?? null };
4023
+ readers.push(reader);
4024
+ own.push(reader);
4025
+ form.appendChild(wrapper);
4026
+ }
4027
+ if (b.submit) {
4028
+ const actions = doc.createElement("div");
4029
+ actions.className = "veo-guide-actions";
4030
+ const submit = { ...b.submit, type: "button" };
4031
+ const sa = submit.style?.align;
4032
+ if (sa === "left" || sa === "center" || sa === "right") {
4033
+ actions.style.justifyContent = ROW_ALIGN[sa] ?? "flex-end";
4034
+ }
4035
+ actions.appendChild(this.buildButton(submit, doc, context, idx, readers, emit, own));
4036
+ form.appendChild(actions);
4037
+ }
4038
+ return form;
4039
+ }
4040
+ buildButton(block, doc, context, idx, readers, emit, submitFirst) {
3089
4041
  const btn = doc.createElement("button");
3090
4042
  btn.type = "button";
3091
4043
  btn.className = "veo-guide-cta";
3092
4044
  btn.textContent = block.text?.trim() || "OK";
4045
+ const st = block.style ?? {};
4046
+ if (st.preset === "secondary" || st.preset === "link") {
4047
+ btn.classList.add(`veo-guide-cta--${st.preset}`);
4048
+ }
4049
+ if (typeof st.ariaLabel === "string" && st.ariaLabel.trim()) {
4050
+ btn.setAttribute("aria-label", st.ariaLabel.trim());
4051
+ }
3093
4052
  applyBlockStyle(btn, block.style, "button");
3094
4053
  const nav = context.nav?.callbacks;
3095
- btn.addEventListener("click", () => {
3096
- switch (block.action) {
4054
+ const runSimple = (spec) => {
4055
+ switch (spec.action) {
3097
4056
  case "next":
3098
- emit(block);
3099
4057
  if (nav) nav.onNext();
3100
4058
  else context.onClose();
3101
4059
  break;
3102
4060
  case "prev":
3103
- emit(block);
3104
4061
  nav?.onBack();
3105
4062
  break;
4063
+ case "goto":
4064
+ if (nav?.onGoto && typeof spec.targetStep === "number") nav.onGoto(spec.targetStep);
4065
+ else context.onClose();
4066
+ break;
4067
+ case "snooze":
4068
+ snoozeGuide(
4069
+ context.guide.guideId,
4070
+ typeof spec.snoozeEvery === "number" ? spec.snoozeEvery : 1,
4071
+ spec.snoozeUnit ?? "day"
4072
+ );
4073
+ context.onClose();
4074
+ break;
3106
4075
  case "url":
3107
- emit(block);
3108
- if (typeof block.url === "string" && isSafeUrl(block.url)) {
3109
- window.open(block.url, "_blank", "noopener,noreferrer");
4076
+ if (typeof spec.url === "string" && isSafeUrl(spec.url)) {
4077
+ if (spec.newTab === false) window.location.assign(spec.url);
4078
+ else window.open(spec.url, "_blank", "noopener,noreferrer");
3110
4079
  }
3111
4080
  break;
3112
4081
  case "guide":
3113
- emit(block);
3114
- if (block.targetGuideId) context.onOpenGuide?.(block.targetGuideId);
4082
+ if (spec.targetGuideId) context.onOpenGuide?.(spec.targetGuideId);
3115
4083
  this.closeOrSkip(context);
3116
4084
  break;
4085
+ default:
4086
+ this.closeOrSkip(context);
4087
+ }
4088
+ };
4089
+ const runSecond = () => {
4090
+ if (block.secondAction?.action) runSimple(block.secondAction);
4091
+ };
4092
+ const runActions = () => {
4093
+ switch (block.action) {
4094
+ case "next":
4095
+ case "prev":
4096
+ case "goto":
4097
+ case "snooze":
4098
+ case "url":
4099
+ case "guide":
4100
+ emit(block);
4101
+ runSimple(block);
4102
+ runSecond();
4103
+ break;
3117
4104
  case "submit-forms":
3118
4105
  case "submit-forms-advance":
3119
4106
  case "submit-forms-goto":
@@ -3131,7 +4118,12 @@ var init_composite_renderer = __esm({
3131
4118
  default:
3132
4119
  emit(block);
3133
4120
  this.closeOrSkip(context);
4121
+ runSecond();
3134
4122
  }
4123
+ };
4124
+ btn.addEventListener("click", () => {
4125
+ if (submitFirst) this.submitForms(submitFirst, context, idx, btn, runActions);
4126
+ else runActions();
3135
4127
  });
3136
4128
  return btn;
3137
4129
  }
@@ -3150,17 +4142,27 @@ var init_composite_renderer = __esm({
3150
4142
  }
3151
4143
  answers[field.key] = value;
3152
4144
  }
3153
- if (readers.length > 0 && Object.keys(answers).length === 0) return;
4145
+ if (Object.keys(answers).length === 0) {
4146
+ onDone();
4147
+ return;
4148
+ }
3154
4149
  btn.disabled = true;
3155
4150
  const submitFn = context.onFormSubmit ?? (() => Promise.resolve(true));
3156
- void submitFn(answers).then((ok) => {
4151
+ const blockIds = new Set(
4152
+ readers.filter((r) => answers[r.field.key] !== void 0).map((r) => r.blockId ?? "")
4153
+ );
4154
+ const pollId = blockIds.size === 1 ? [...blockIds][0] || void 0 : void 0;
4155
+ void submitFn(answers, { ...pollId ? { pollId } : {}, stepIndex: idx }).then((ok) => {
3157
4156
  btn.disabled = false;
3158
4157
  if (!ok) return;
3159
- context.onInteraction({
3160
- guideId: context.guide.guideId,
3161
- stepIndex: idx,
3162
- action: "completed"
3163
- });
4158
+ const lastStep = !context.nav || idx >= context.nav.totalSteps - 1;
4159
+ if (lastStep) {
4160
+ context.onInteraction({
4161
+ guideId: context.guide.guideId,
4162
+ stepIndex: idx,
4163
+ action: "completed"
4164
+ });
4165
+ }
3164
4166
  onDone();
3165
4167
  });
3166
4168
  }
@@ -3269,6 +4271,10 @@ var init_composite_renderer = __esm({
3269
4271
  }
3270
4272
  };
3271
4273
  CLOSE_INSET = "10px";
4274
+ CLOSE_RESERVE_PX = 42;
4275
+ CARD_DEFAULT_PAD_PX = 20;
4276
+ MIN_CARD_WIDTH = 120;
4277
+ NARROW_CARD_WIDTH = 260;
3272
4278
  }
3273
4279
  });
3274
4280
  function applyFloatingPosition(wrapper, placement) {
@@ -3364,6 +4370,7 @@ var FormRenderer;
3364
4370
  var init_form_renderer = __esm({
3365
4371
  "src/plugins/guides/renderers/form-renderer.ts"() {
3366
4372
  init_form_content();
4373
+ init_scroll_lock();
3367
4374
  init_base_renderer();
3368
4375
  FormRenderer = class extends BaseRenderer {
3369
4376
  render(context) {
@@ -3379,6 +4386,7 @@ var init_form_renderer = __esm({
3379
4386
  mountFormContent(card, context, doc);
3380
4387
  overlay.appendChild(card);
3381
4388
  root.appendChild(overlay);
4389
+ this.registerCleanup(lockScroll(doc));
3382
4390
  const dismiss = () => {
3383
4391
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "dismissed" });
3384
4392
  context.onClose();
@@ -3535,12 +4543,25 @@ var ModalRenderer;
3535
4543
  var init_modal_renderer = __esm({
3536
4544
  "src/plugins/guides/renderers/modal-renderer.ts"() {
3537
4545
  init_block_builder();
4546
+ init_scroll_lock();
3538
4547
  init_walkthrough_block_builder();
3539
4548
  init_base_renderer();
3540
4549
  ModalRenderer = class extends BaseRenderer {
3541
4550
  constructor() {
3542
4551
  super(...arguments);
3543
4552
  this.overlay = null;
4553
+ /** Suelta el bloqueo de scroll del backdrop (null = sin backdrop). */
4554
+ this.releaseScroll = null;
4555
+ }
4556
+ /** Con backdrop la app de atrás queda congelada; sin él, scrollea normal. */
4557
+ syncScrollLock(step) {
4558
+ const wantLock = step.style?.backdrop !== false;
4559
+ if (wantLock && !this.releaseScroll) {
4560
+ this.releaseScroll = lockScroll(this.host?.ownerDocument ?? document);
4561
+ } else if (!wantLock && this.releaseScroll) {
4562
+ this.releaseScroll();
4563
+ this.releaseScroll = null;
4564
+ }
3544
4565
  }
3545
4566
  render(context) {
3546
4567
  const idx = context.nav?.stepIndex ?? 0;
@@ -3552,6 +4573,7 @@ var init_modal_renderer = __esm({
3552
4573
  const overlay = ownerDocument.createElement("div");
3553
4574
  overlay.className = overlayClassOf(step);
3554
4575
  this.overlay = overlay;
4576
+ this.syncScrollLock(step);
3555
4577
  const card = ownerDocument.createElement("div");
3556
4578
  card.className = "veo-modal-card";
3557
4579
  const dismiss = (action, ctaUrl, meta) => {
@@ -3609,9 +4631,12 @@ var init_modal_renderer = __esm({
3609
4631
  }
3610
4632
  onLiveUpdate(step) {
3611
4633
  if (this.overlay) this.overlay.className = overlayClassOf(step);
4634
+ this.syncScrollLock(step);
3612
4635
  }
3613
4636
  destroy() {
3614
4637
  this.overlay = null;
4638
+ this.releaseScroll?.();
4639
+ this.releaseScroll = null;
3615
4640
  super.destroy();
3616
4641
  }
3617
4642
  };
@@ -3802,9 +4827,35 @@ function matchesUrl(matcher, pageUrl) {
3802
4827
  }
3803
4828
  }
3804
4829
  function stepMatchesPage(loc, pageUrl) {
3805
- if (!loc || loc.scope === "sitewide" || !loc.pattern) return true;
4830
+ if (!loc || loc.scope === "sitewide") return true;
4831
+ if (loc.scope === "pages") {
4832
+ const rules = loc.rules ?? [];
4833
+ return rules.some((r) => pageRuleMatches(r, pageUrl));
4834
+ }
4835
+ if (!loc.pattern) return true;
3806
4836
  return matchesUrl({ type: loc.matchType ?? "prefix", pattern: loc.pattern }, pageUrl);
3807
4837
  }
4838
+ function pageRuleMatches(rule, pageUrl) {
4839
+ const pattern = rule.pattern.toLowerCase();
4840
+ const candidates = urlCandidates(pageUrl).map((c) => c.toLowerCase());
4841
+ switch (rule.type) {
4842
+ case "exact":
4843
+ return candidates.some((c) => c === pattern);
4844
+ case "prefix":
4845
+ return candidates.some((c) => c.startsWith(pattern));
4846
+ case "contains":
4847
+ return candidates.some((c) => c.includes(pattern));
4848
+ case "regex":
4849
+ try {
4850
+ const re = new RegExp(rule.pattern, "i");
4851
+ return candidates.some((c) => re.test(c));
4852
+ } catch {
4853
+ return false;
4854
+ }
4855
+ default:
4856
+ return false;
4857
+ }
4858
+ }
3808
4859
  var init_url_match = __esm({
3809
4860
  "src/plugins/guides/url-match.ts"() {
3810
4861
  }
@@ -3973,6 +5024,7 @@ var init_guide_preview = __esm({
3973
5024
  // Preview de forms: por defecto simula el envío SIN escribir props;
3974
5025
  // el caller puede pasar `formSubmit` real (link de preview).
3975
5026
  onFormSubmit: this.input?.formSubmit ?? (() => Promise.resolve(true)),
5027
+ ...this.input?.fetchProgress ? { fetchProgress: this.input.fetchProgress } : {},
3976
5028
  isPreview: true,
3977
5029
  ...this.input?.formSubmit && this.input.formSubmitNote !== void 0 ? { previewNote: this.input.formSubmitNote } : {}
3978
5030
  });
@@ -4365,8 +5417,14 @@ function injectBuilderChrome(opts) {
4365
5417
  stepsList.append(activation);
4366
5418
  s.steps.forEach((st, i) => {
4367
5419
  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>';
4368
- const card = stepCard(thumb, st.label, s.section === "step" && s.activeStep === i);
4369
- card.addEventListener("click", () => act({ action: "select-step", index: i }));
5420
+ const isStepView = s.section === "step" || s.section === "block" || s.section === "add-block" || s.section === "row" || s.section === "structure";
5421
+ const card = stepCard(thumb, st.label, isStepView && s.activeStep === i, {
5422
+ // Se pueden borrar TODOS los pasos (el panel vuelve a Activación).
5423
+ canRemove: true,
5424
+ onEdit: () => act({ action: "select-step", index: i }),
5425
+ onDuplicate: () => act({ action: "duplicate-step", index: i }),
5426
+ onRemove: () => act({ action: "remove-step", index: i })
5427
+ });
4370
5428
  stepsList.append(card);
4371
5429
  });
4372
5430
  const add = el("button", "veo-step-add", `${svg("plus", 18)}<span>Agregar paso</span>`);
@@ -4395,16 +5453,40 @@ function injectBuilderChrome(opts) {
4395
5453
  }
4396
5454
  };
4397
5455
  }
4398
- function stepCard(inner, label, active2) {
4399
- const b = el(
4400
- "button",
4401
- `veo-step${active2 ? " is-active" : ""}`,
4402
- `<div class="veo-step-box">${inner}</div><span class="veo-step-label"></span>`
5456
+ function stepCard(inner, label, active2, actions) {
5457
+ const cls = `veo-step${active2 ? " is-active" : ""}`;
5458
+ if (!actions) {
5459
+ const b = el(
5460
+ "button",
5461
+ cls,
5462
+ `<div class="veo-step-box">${inner}</div><span class="veo-step-label"></span>`
5463
+ );
5464
+ b.type = "button";
5465
+ b.querySelector(".veo-step-label").textContent = label;
5466
+ b.setAttribute("aria-pressed", active2 ? "true" : "false");
5467
+ return b;
5468
+ }
5469
+ const card = el(
5470
+ "div",
5471
+ cls,
5472
+ `<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>`
4403
5473
  );
4404
- b.type = "button";
4405
- b.querySelector(".veo-step-label").textContent = label;
4406
- b.setAttribute("aria-pressed", active2 ? "true" : "false");
4407
- return b;
5474
+ card.querySelector(".veo-step-label").textContent = label;
5475
+ card.setAttribute("aria-current", active2 ? "step" : "false");
5476
+ const remove = card.querySelector(".veo-step-remove");
5477
+ remove.disabled = !actions.canRemove;
5478
+ if (!actions.canRemove) remove.title = "La gu\xEDa necesita al menos un paso";
5479
+ const on = (sel, fn) => {
5480
+ card.querySelector(sel)?.addEventListener("click", (e) => {
5481
+ e.stopPropagation();
5482
+ fn();
5483
+ });
5484
+ };
5485
+ on(".veo-step-edit", actions.onEdit);
5486
+ on(".veo-step-duplicate", actions.onDuplicate);
5487
+ on(".veo-step-remove", actions.onRemove);
5488
+ card.addEventListener("click", actions.onEdit);
5489
+ return card;
4408
5490
  }
4409
5491
  var CHROME_TOPBAR_H, CHROME_STEPS_H, ICONS, CHROME_CSS;
4410
5492
  var init_builder_chrome = __esm({
@@ -4421,6 +5503,9 @@ var init_builder_chrome = __esm({
4421
5503
  chevronDown: '<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/>',
4422
5504
  chevronUp: '<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5"/>',
4423
5505
  plus: '<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>',
5506
+ 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"/>',
5507
+ 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"/>',
5508
+ 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"/>',
4424
5509
  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"/>',
4425
5510
  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"/>'
4426
5511
  };
@@ -4507,10 +5592,31 @@ var init_builder_chrome = __esm({
4507
5592
  .veo-steps-list { display: flex; align-items: flex-start; gap: 20px; }
4508
5593
  .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; }
4509
5594
  .veo-step-box {
5595
+ position: relative; overflow: hidden;
4510
5596
  width: 116px; height: 64px; border-radius: 10px; background: #fff; border: 2px solid transparent;
4511
5597
  display: flex; align-items: center; justify-content: center; color: #2b2623;
4512
5598
  box-shadow: 0 1px 2px rgba(0,0,0,0.06);
4513
5599
  }
5600
+ /* Capa de acciones del paso: aparece al pasar el mouse o al enfocar un bot\xF3n. */
5601
+ .veo-step-hover {
5602
+ position: absolute; inset: 0; background: #2b2623; border-radius: 8px;
5603
+ display: flex; flex-direction: column; padding: 3px 4px 6px; box-sizing: border-box;
5604
+ opacity: 0; transition: opacity 120ms ease-out;
5605
+ }
5606
+ .veo-step:hover .veo-step-hover, .veo-step:focus-within .veo-step-hover { opacity: 1; }
5607
+ .veo-step-hover-top { display: flex; justify-content: space-between; align-items: center; }
5608
+ .veo-step-hover-main { flex: 1; display: flex; align-items: center; justify-content: center; }
5609
+ .veo-step-icon {
5610
+ border: 0; background: transparent; color: #fff; cursor: pointer; padding: 0;
5611
+ width: 20px; height: 20px; border-radius: 5px; display: inline-flex; align-items: center; justify-content: center;
5612
+ }
5613
+ .veo-step-icon:hover { background: rgba(255,255,255,0.14); }
5614
+ .veo-step-icon:disabled { opacity: 0.35; cursor: default; background: transparent; }
5615
+ .veo-step-edit {
5616
+ width: 100%; margin: 0 6px; border: 0; background: #dbeefc; color: #1d6fb8; font: inherit; font-size: 13px; font-weight: 600;
5617
+ border-radius: 6px; padding: 5px 10px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
5618
+ }
5619
+ .veo-step-edit:hover { background: #cbe4f9; }
4514
5620
  .veo-step.is-active .veo-step-box { border-color: #b74d00; }
4515
5621
  .veo-step-label { font-size: 13px; color: #6b635d; }
4516
5622
  .veo-step.is-active .veo-step-label { color: #2b2623; font-weight: 600; }
@@ -4548,6 +5654,8 @@ function injectBuilderPanel(opts) {
4548
5654
  },
4549
5655
  isVisible: () => false,
4550
5656
  setTopOffset: () => {
5657
+ },
5658
+ setSize: () => {
4551
5659
  }
4552
5660
  };
4553
5661
  }
@@ -4600,6 +5708,21 @@ function injectBuilderPanel(opts) {
4600
5708
  card.style.display = next && !(popped && !popped.closed) ? "" : "none";
4601
5709
  };
4602
5710
  minimize.addEventListener("click", () => setVisible(false));
5711
+ let compactRestore = null;
5712
+ const setSize = (size) => {
5713
+ if (maximized || popped && !popped.closed) return;
5714
+ if (size) {
5715
+ if (!compactRestore) compactRestore = { w: card.offsetWidth, h: card.offsetHeight };
5716
+ card.style.minHeight = "0";
5717
+ card.style.width = `${clamp3(Math.round(size.width), MIN_W, window.innerWidth - MARGIN * 2)}px`;
5718
+ card.style.height = `${clamp3(Math.round(size.height) + HEADER_H, HEADER_H + 80, window.innerHeight - top - MARGIN)}px`;
5719
+ } else if (compactRestore) {
5720
+ card.style.minHeight = "";
5721
+ card.style.width = `${compactRestore.w}px`;
5722
+ card.style.height = `${compactRestore.h}px`;
5723
+ compactRestore = null;
5724
+ }
5725
+ };
4603
5726
  maximize.addEventListener("click", () => {
4604
5727
  maximized = !maximized;
4605
5728
  if (maximized) {
@@ -4699,6 +5822,7 @@ function injectBuilderPanel(opts) {
4699
5822
  title.textContent = t;
4700
5823
  },
4701
5824
  setVisible,
5825
+ setSize,
4702
5826
  isVisible: () => visible,
4703
5827
  setTopOffset: (px) => {
4704
5828
  topOffset = px;
@@ -5284,6 +6408,9 @@ function initBuilderMode() {
5284
6408
  let staticTarget = null;
5285
6409
  let manipulator = null;
5286
6410
  let dropTarget = null;
6411
+ let cardClick = null;
6412
+ let rawBlock = null;
6413
+ let selectedBlockId = null;
5287
6414
  let dragGhost = null;
5288
6415
  let previewStepIndex = 0;
5289
6416
  let pendingPreview = null;
@@ -5304,13 +6431,742 @@ function initBuilderMode() {
5304
6431
  };
5305
6432
  const previewMatchesPage = (cmd) => stepMatchesPage(previewStep(cmd)?.pageLocation, window.location.href);
5306
6433
  const postPreviewStatus = (payload) => post({ type: "preview-status", payload });
5307
- const withholdPreview = () => {
6434
+ const detachPreviewTools = () => {
5308
6435
  manipulator?.detach();
5309
6436
  manipulator = null;
5310
6437
  dropTarget?.detach();
5311
6438
  dropTarget = null;
6439
+ cardClick?.();
6440
+ cardClick = null;
6441
+ rowHover?.();
6442
+ rowHover = null;
6443
+ hoverRowId = null;
6444
+ hoverCard = false;
6445
+ rawBlock?.detach();
6446
+ rawBlock = null;
6447
+ shownPreview = null;
6448
+ };
6449
+ const withholdPreview = () => {
6450
+ detachPreviewTools();
6451
+ shownPreview = null;
5312
6452
  closeGuidePreview();
5313
6453
  };
6454
+ const FRAME_PAD = 4;
6455
+ const FRAME_MIN_H = 0;
6456
+ const TOOLBAR_H = 30;
6457
+ const ICON = {
6458
+ 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>',
6459
+ 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>',
6460
+ 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>'
6461
+ };
6462
+ const ICON_MOVE = {
6463
+ 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>',
6464
+ 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>'
6465
+ };
6466
+ const moveAbility = (blocks, blockId) => {
6467
+ for (const row of groupLayout(blocks)) {
6468
+ for (let c = 0; c < row.columns.length; c++) {
6469
+ const col = row.columns[c];
6470
+ if (!col.blocks.some((b) => b.block.editorId === blockId)) continue;
6471
+ return { left: c > 0, right: c < row.columns.length - 1 };
6472
+ }
6473
+ }
6474
+ return { left: false, right: false };
6475
+ };
6476
+ let previewBlocks = [];
6477
+ let hoverRowId = null;
6478
+ let selectedRowId = null;
6479
+ let rowHover = null;
6480
+ let hoverCard = false;
6481
+ let panelSection = null;
6482
+ let shownPreview = null;
6483
+ const structureOf = (step) => {
6484
+ if (!step) return "";
6485
+ const { blocks, ...rest } = step;
6486
+ return JSON.stringify({
6487
+ rest,
6488
+ blocks: (blocks ?? []).map((b) => {
6489
+ const { text: _t, html: _h, style: _s, ...keep } = b;
6490
+ return keep;
6491
+ })
6492
+ });
6493
+ };
6494
+ const tryPatchPreview = (cmd) => {
6495
+ const prev = shownPreview;
6496
+ if (!prev || cmd.guide.guideType !== "composite" || prev.guide.guideType !== "composite") {
6497
+ return false;
6498
+ }
6499
+ const idx = cmd.guide.startStepIndex ?? 0;
6500
+ if ((prev.guide.startStepIndex ?? 0) !== idx) return false;
6501
+ const guideSig = (g) => JSON.stringify({ ...g, guideSteps: void 0 });
6502
+ if (guideSig(prev.guide) !== guideSig(cmd.guide) || prev.editable !== cmd.editable)
6503
+ return false;
6504
+ const host = document.querySelector("[data-veo-guide]");
6505
+ const card = host ? previewCard(host) : null;
6506
+ if (!host || !card) return false;
6507
+ const prevStep = prev.guide.guideSteps[idx];
6508
+ const nextStep = cmd.guide.guideSteps[idx];
6509
+ if (!prevStep || !nextStep) return false;
6510
+ if (structureOf(prevStep) !== structureOf(nextStep)) return false;
6511
+ const doc = card.ownerDocument;
6512
+ const prevBlocks = prevStep.blocks ?? [];
6513
+ const nextBlocks = nextStep.blocks ?? [];
6514
+ for (let i = 0; i < nextBlocks.length; i++) {
6515
+ const a = prevBlocks[i];
6516
+ const b = nextBlocks[i];
6517
+ if (a.text === b.text && a.html === b.html && JSON.stringify(a.style) === JSON.stringify(b.style)) {
6518
+ continue;
6519
+ }
6520
+ if (!b.editorId) return false;
6521
+ const node = card.querySelector(`[data-veo-block-id="${b.editorId}"]`);
6522
+ if (!node) return false;
6523
+ if (b.type === "button") {
6524
+ node.textContent = b.text?.trim() || "OK";
6525
+ node.removeAttribute("style");
6526
+ node.classList.remove("veo-guide-cta--secondary", "veo-guide-cta--link");
6527
+ const st = b.style ?? {};
6528
+ if (st.preset === "secondary" || st.preset === "link") {
6529
+ node.classList.add(`veo-guide-cta--${st.preset}`);
6530
+ }
6531
+ if (typeof st.ariaLabel === "string" && st.ariaLabel.trim()) {
6532
+ node.setAttribute("aria-label", st.ariaLabel.trim());
6533
+ } else node.removeAttribute("aria-label");
6534
+ applyBlockStyle(node, b.style, "button");
6535
+ continue;
6536
+ }
6537
+ if (b.type === "code" || b.type === "form") return false;
6538
+ const fresh = buildContentBlock(b, doc, { editing: true }) ?? (() => {
6539
+ const el2 = doc.createElement("div");
6540
+ el2.className = "veo-block-placeholder";
6541
+ el2.textContent = b.type === "image" ? "Imagen (sin URL)" : b.type === "video" ? "Video (elige proveedor y pega la URL)" : "Bloque vac\xEDo";
6542
+ return el2;
6543
+ })();
6544
+ fresh.dataset.veoBlock = node.dataset.veoBlock ?? String(i);
6545
+ fresh.dataset.veoBlockId = b.editorId;
6546
+ node.replaceWith(fresh);
6547
+ }
6548
+ shownPreview = cmd;
6549
+ previewBlocks = nextBlocks;
6550
+ applyBlockHighlight(host, idx);
6551
+ renderGapZones(host, idx, nextBlocks);
6552
+ return true;
6553
+ };
6554
+ const previewCard = (host) => host.shadowRoot?.querySelector(".veo-card") ?? null;
6555
+ const clearBlockHighlight = (host) => {
6556
+ const card = previewCard(host);
6557
+ if (!card) return;
6558
+ for (const el2 of card.querySelectorAll(".veo-block--selected")) {
6559
+ el2.classList.remove("veo-block--selected");
6560
+ }
6561
+ card.querySelector(".veo-block-toolbar")?.remove();
6562
+ card.querySelector(".veo-block-frame")?.remove();
6563
+ };
6564
+ const applyBlockHighlight = (host, stepIndex) => {
6565
+ clearBlockHighlight(host);
6566
+ const card = previewCard(host);
6567
+ if (!card) return;
6568
+ const el2 = selectedBlockId ? card.querySelector(`[data-veo-block-id="${selectedBlockId}"]`) : null;
6569
+ if (!el2 || !selectedBlockId) {
6570
+ renderRowChrome(host, stepIndex);
6571
+ return;
6572
+ }
6573
+ el2.classList.add("veo-block--selected");
6574
+ const doc = card.ownerDocument;
6575
+ const cs = getComputedStyle(card);
6576
+ const padL = Number.parseFloat(cs.paddingLeft) || 0;
6577
+ const padT = Number.parseFloat(cs.paddingTop) || 0;
6578
+ const padR = Number.parseFloat(cs.paddingRight) || 0;
6579
+ const padB = Number.parseFloat(cs.paddingBottom) || 0;
6580
+ const parent = el2.parentElement;
6581
+ const inCol = parent?.classList.contains("veo-col") ? parent : null;
6582
+ const boxL = inCol ? inCol.offsetLeft : padL;
6583
+ const boxR = inCol ? inCol.offsetLeft + inCol.offsetWidth : card.clientWidth - padR;
6584
+ const boxT = padT;
6585
+ const boxB = card.clientHeight - padB;
6586
+ const fx = Math.max(boxL, el2.offsetLeft - FRAME_PAD);
6587
+ const fr = Math.min(boxR, el2.offsetLeft + el2.offsetWidth + FRAME_PAD);
6588
+ const fw = Math.max(0, fr - fx);
6589
+ const fy = Math.max(boxT, el2.offsetTop - FRAME_PAD);
6590
+ const fb = Math.min(boxB, el2.offsetTop + el2.offsetHeight + FRAME_PAD);
6591
+ const fh = Math.max(FRAME_MIN_H, fb - fy);
6592
+ const frame = doc.createElement("div");
6593
+ frame.className = "veo-block-frame";
6594
+ frame.setAttribute("data-veo-builder-chrome", "");
6595
+ frame.style.left = `${fx}px`;
6596
+ frame.style.top = `${fy}px`;
6597
+ frame.style.width = `${fw}px`;
6598
+ frame.style.height = `${fh}px`;
6599
+ card.appendChild(frame);
6600
+ const bar = doc.createElement("div");
6601
+ bar.className = "veo-block-toolbar";
6602
+ bar.setAttribute("data-veo-builder-chrome", "");
6603
+ const blockId = selectedBlockId;
6604
+ const mk = (action, label, icon, enabled = true) => {
6605
+ const b = doc.createElement("button");
6606
+ b.type = "button";
6607
+ b.title = label;
6608
+ b.setAttribute("aria-label", label);
6609
+ b.className = `veo-block-toolbar-${action}`;
6610
+ b.innerHTML = icon;
6611
+ b.disabled = !enabled;
6612
+ b.addEventListener("click", (e) => {
6613
+ e.preventDefault();
6614
+ e.stopPropagation();
6615
+ post({ type: "block-action", payload: { stepIndex, blockId, action } });
6616
+ });
6617
+ bar.appendChild(b);
6618
+ };
6619
+ const can = moveAbility(previewBlocks, blockId);
6620
+ mk("edit", "Editar", ICON.pencil);
6621
+ mk("duplicate", "Duplicar", ICON.copy);
6622
+ mk("remove", "Eliminar", ICON.trash);
6623
+ mk("move-left", "Mover a la izquierda", ICON_MOVE.left, can.left);
6624
+ mk("move-right", "Mover a la derecha", ICON_MOVE.right, can.right);
6625
+ bar.style.right = `${Math.max(0, card.clientWidth - fr)}px`;
6626
+ card.appendChild(bar);
6627
+ const barH = bar.offsetHeight || TOOLBAR_H;
6628
+ bar.dataset.veoTopAbove = String(fy - barH - 6);
6629
+ bar.dataset.veoTopBelow = String(fb + 6);
6630
+ bar.style.top = bar.dataset.veoTopAbove;
6631
+ renderRowChrome(host, stepIndex);
6632
+ };
6633
+ const chromeRect = (el2) => ({
6634
+ left: el2.offsetLeft,
6635
+ top: el2.offsetTop,
6636
+ right: el2.offsetLeft + el2.offsetWidth,
6637
+ bottom: el2.offsetTop + el2.offsetHeight
6638
+ });
6639
+ const intersects = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
6640
+ const resolveToolbarOverlaps = (card) => {
6641
+ const cardBar = card.querySelector(".veo-card-toolbar");
6642
+ const blockBar = card.querySelector(".veo-block-toolbar");
6643
+ const rowBar = card.querySelector(".veo-row-toolbar");
6644
+ if (blockBar?.dataset.veoTopAbove) {
6645
+ blockBar.style.top = `${blockBar.dataset.veoTopAbove}px`;
6646
+ if (cardBar && intersects(chromeRect(blockBar), chromeRect(cardBar))) {
6647
+ blockBar.style.top = `${blockBar.dataset.veoTopBelow}px`;
6648
+ }
6649
+ }
6650
+ if (rowBar?.dataset.veoRight) {
6651
+ rowBar.style.left = "";
6652
+ rowBar.style.right = `${rowBar.dataset.veoRight}px`;
6653
+ rowBar.style.top = `${rowBar.dataset.veoTopBelow}px`;
6654
+ const others = [cardBar, blockBar].filter((b) => !!b);
6655
+ const clashes = () => others.some((o) => intersects(chromeRect(rowBar), chromeRect(o)));
6656
+ if (clashes()) {
6657
+ rowBar.style.right = "";
6658
+ rowBar.style.left = `${rowBar.dataset.veoLeft}px`;
6659
+ }
6660
+ if (clashes() && blockBar) {
6661
+ rowBar.style.left = "";
6662
+ rowBar.style.right = `${rowBar.dataset.veoRight}px`;
6663
+ rowBar.style.top = `${chromeRect(blockBar).bottom + 6}px`;
6664
+ }
6665
+ }
6666
+ };
6667
+ const rectOf = (el2) => ({
6668
+ left: el2.offsetLeft,
6669
+ top: el2.offsetTop,
6670
+ right: el2.offsetLeft + el2.offsetWidth,
6671
+ bottom: el2.offsetTop + el2.offsetHeight
6672
+ });
6673
+ const union = (rs) => rs.length === 0 ? null : rs.reduce((acc, r) => ({
6674
+ left: Math.min(acc.left, r.left),
6675
+ top: Math.min(acc.top, r.top),
6676
+ right: Math.max(acc.right, r.right),
6677
+ bottom: Math.max(acc.bottom, r.bottom)
6678
+ }));
6679
+ const layoutRows = (card, blocks) => {
6680
+ const elOf = (b) => b.editorId ? card.querySelector(`[data-veo-block-id="${b.editorId}"]`) : null;
6681
+ const cs = getComputedStyle(card);
6682
+ const padL = Number.parseFloat(cs.paddingLeft) || 0;
6683
+ const padR = Number.parseFloat(cs.paddingRight) || 0;
6684
+ const contentLeft = padL;
6685
+ const contentRight = card.clientWidth - padR;
6686
+ const rows = groupLayout(blocks).map((row) => {
6687
+ const columns = row.columns.map((col) => {
6688
+ const items = col.blocks.map(({ block }) => ({ block, el: elOf(block) })).filter(
6689
+ (x) => !!x.el && !!x.block.editorId
6690
+ );
6691
+ const blocksRect2 = union(items.map((x) => rectOf(x.el)));
6692
+ if (!blocksRect2) return null;
6693
+ const colEl = items[0]?.el.parentElement;
6694
+ const rect2 = colEl?.classList.contains("veo-col") ? rectOf(colEl) : blocksRect2;
6695
+ return { items, rect: rect2 };
6696
+ }).filter((c) => c !== null);
6697
+ const blocksRect = union(columns.map((c) => c.rect));
6698
+ if (!blocksRect) return null;
6699
+ const rowEl = columns[0]?.items[0]?.el.parentElement?.parentElement;
6700
+ const rect = rowEl?.classList.contains("veo-row") ? rectOf(rowEl) : { ...blocksRect, left: contentLeft, right: contentRight };
6701
+ const id = columns[0]?.items[0]?.block.editorId;
6702
+ return { id, row: row.row, columns, rect };
6703
+ }).filter((r) => r !== null);
6704
+ return { rows, content: { left: contentLeft, right: contentRight } };
6705
+ };
6706
+ const clearRowChrome = (card) => {
6707
+ card.querySelector(".veo-row-frame")?.remove();
6708
+ card.querySelector(".veo-row-toolbar")?.remove();
6709
+ };
6710
+ const renderRowChrome = (host, stepIndex) => {
6711
+ const card = previewCard(host);
6712
+ if (!card) return;
6713
+ clearRowChrome(card);
6714
+ resolveToolbarOverlaps(card);
6715
+ const { rows } = layoutRows(card, previewBlocks);
6716
+ const rowOfSelected = selectedBlockId ? rows.find(
6717
+ (r) => r.columns.some((c) => c.items.some((i) => i.block.editorId === selectedBlockId))
6718
+ ) : void 0;
6719
+ const target = (selectedRowId ? rows.find((r) => r.id === selectedRowId) : void 0) ?? rowOfSelected ?? (hoverRowId ? rows.find((r) => r.id === hoverRowId) : void 0);
6720
+ if (!target) return;
6721
+ const doc = card.ownerDocument;
6722
+ const cs = getComputedStyle(card);
6723
+ const padT = Number.parseFloat(cs.paddingTop) || 0;
6724
+ const padB = Number.parseFloat(cs.paddingBottom) || 0;
6725
+ const PAD = 6;
6726
+ const fx = Math.max(0, target.rect.left - PAD);
6727
+ const fr = Math.min(card.clientWidth, target.rect.right + PAD);
6728
+ const fy = Math.max(Math.min(padT, target.rect.top), target.rect.top - PAD);
6729
+ const fb = Math.min(
6730
+ Math.max(card.clientHeight - padB, target.rect.bottom),
6731
+ target.rect.bottom + PAD
6732
+ );
6733
+ const frame = doc.createElement("div");
6734
+ frame.className = "veo-row-frame";
6735
+ frame.setAttribute("data-veo-builder-chrome", "");
6736
+ frame.style.left = `${fx}px`;
6737
+ frame.style.top = `${fy}px`;
6738
+ frame.style.width = `${Math.max(0, fr - fx)}px`;
6739
+ frame.style.height = `${Math.max(0, fb - fy)}px`;
6740
+ card.appendChild(frame);
6741
+ const bar = doc.createElement("div");
6742
+ bar.className = "veo-row-toolbar";
6743
+ bar.setAttribute("data-veo-builder-chrome", "");
6744
+ const blockId = target.id;
6745
+ const mk = (action, label, icon, text) => {
6746
+ const b = doc.createElement("button");
6747
+ b.type = "button";
6748
+ b.title = label;
6749
+ b.setAttribute("aria-label", label);
6750
+ b.className = `veo-row-toolbar-${action}`;
6751
+ b.innerHTML = icon;
6752
+ if (text) b.append(text);
6753
+ b.addEventListener("click", (e) => {
6754
+ e.preventDefault();
6755
+ e.stopPropagation();
6756
+ post({ type: "row-action", payload: { stepIndex, blockId, action } });
6757
+ });
6758
+ bar.appendChild(b);
6759
+ };
6760
+ mk("edit", "Editar fila", ICON.pencil, "Editar fila");
6761
+ mk("duplicate", "Duplicar fila", ICON.copy);
6762
+ mk("remove", "Eliminar fila", ICON.trash);
6763
+ card.appendChild(bar);
6764
+ bar.dataset.veoRight = String(Math.max(0, card.clientWidth - fr));
6765
+ bar.dataset.veoLeft = String(fx);
6766
+ bar.dataset.veoTopBelow = String(fb + 6);
6767
+ resolveToolbarOverlaps(card);
6768
+ };
6769
+ const renderCardChrome = (host, stepIndex) => {
6770
+ const card = previewCard(host);
6771
+ if (!card) return;
6772
+ card.querySelector(".veo-card-frame")?.remove();
6773
+ card.querySelector(".veo-card-toolbar")?.remove();
6774
+ if (!hoverCard) {
6775
+ resolveToolbarOverlaps(card);
6776
+ return;
6777
+ }
6778
+ const doc = card.ownerDocument;
6779
+ const frame = doc.createElement("div");
6780
+ frame.className = "veo-card-frame";
6781
+ frame.setAttribute("data-veo-builder-chrome", "");
6782
+ card.appendChild(frame);
6783
+ const bar = doc.createElement("div");
6784
+ bar.className = "veo-card-toolbar";
6785
+ bar.setAttribute("data-veo-builder-chrome", "");
6786
+ const mk = (action, label, active2) => {
6787
+ const b = doc.createElement("button");
6788
+ b.type = "button";
6789
+ b.className = `veo-card-toolbar-${action}${active2 ? " is-active" : ""}`;
6790
+ b.setAttribute("aria-pressed", active2 ? "true" : "false");
6791
+ b.textContent = label;
6792
+ b.addEventListener("click", (e) => {
6793
+ e.preventDefault();
6794
+ e.stopPropagation();
6795
+ post({ type: "card-action", payload: { stepIndex, action } });
6796
+ });
6797
+ bar.appendChild(b);
6798
+ };
6799
+ mk("edit-container", "Editar contenedor", panelSection === "step");
6800
+ mk("edit-rows", "Editar filas", panelSection === "structure");
6801
+ card.appendChild(bar);
6802
+ bar.style.top = `${-Math.round((bar.offsetHeight || 34) / 2)}px`;
6803
+ resolveToolbarOverlaps(card);
6804
+ };
6805
+ const attachRowHover = (host, stepIndex) => {
6806
+ const onMove = (e) => {
6807
+ const card = previewCard(host);
6808
+ if (!card) return;
6809
+ const overCard = e.composedPath().some(
6810
+ (n) => n instanceof Element && (n.classList.contains("veo-card") || n.classList.contains("veo-card-toolbar"))
6811
+ );
6812
+ if (overCard !== hoverCard) {
6813
+ hoverCard = overCard;
6814
+ renderCardChrome(host, stepIndex);
6815
+ }
6816
+ if (e.composedPath().some((n) => n instanceof Element && n.classList.contains("veo-row-toolbar"))) {
6817
+ return;
6818
+ }
6819
+ const rect = card.getBoundingClientRect();
6820
+ const x = e.clientX - rect.left - card.clientLeft;
6821
+ const y = e.clientY - rect.top - card.clientTop;
6822
+ const bar = card.querySelector(".veo-row-toolbar");
6823
+ if (bar) {
6824
+ const AIR = 10;
6825
+ if (x >= bar.offsetLeft - AIR && x <= bar.offsetLeft + bar.offsetWidth + AIR && y >= bar.offsetTop - 14 && y <= bar.offsetTop + bar.offsetHeight + AIR) {
6826
+ return;
6827
+ }
6828
+ }
6829
+ const TOL = 6;
6830
+ const hit = layoutRows(card, previewBlocks).rows.find(
6831
+ (r) => x >= r.rect.left - TOL && x <= r.rect.right + TOL && y >= r.rect.top - TOL && y <= r.rect.bottom + TOL
6832
+ );
6833
+ const next = hit?.id ?? null;
6834
+ if (next === hoverRowId) return;
6835
+ hoverRowId = next;
6836
+ renderRowChrome(host, stepIndex);
6837
+ };
6838
+ const onLeave = () => {
6839
+ if (hoverCard) {
6840
+ hoverCard = false;
6841
+ renderCardChrome(host, stepIndex);
6842
+ }
6843
+ if (hoverRowId === null) return;
6844
+ hoverRowId = null;
6845
+ renderRowChrome(host, stepIndex);
6846
+ };
6847
+ host.addEventListener("mousemove", onMove);
6848
+ host.addEventListener("mouseleave", onLeave);
6849
+ return () => {
6850
+ host.removeEventListener("mousemove", onMove);
6851
+ host.removeEventListener("mouseleave", onLeave);
6852
+ };
6853
+ };
6854
+ const renderGapZones = (host, stepIndex, blocks) => {
6855
+ const card = previewCard(host);
6856
+ if (!card) return;
6857
+ for (const z of card.querySelectorAll(".veo-gap")) z.remove();
6858
+ const doc = card.ownerDocument;
6859
+ const GAP = 18;
6860
+ const EDGE2 = 4;
6861
+ const zone = (kind, x, y, w, h, label, blockId, action) => {
6862
+ const z = doc.createElement("div");
6863
+ z.className = `veo-gap veo-gap--${kind}`;
6864
+ z.setAttribute("data-veo-builder-chrome", "");
6865
+ z.dataset.veoGapAction = action;
6866
+ z.style.left = `${x}px`;
6867
+ z.style.top = `${y}px`;
6868
+ z.style.width = `${Math.max(w, GAP)}px`;
6869
+ z.style.height = `${Math.max(h, GAP)}px`;
6870
+ const line = doc.createElement("i");
6871
+ const btn = doc.createElement("button");
6872
+ btn.type = "button";
6873
+ btn.title = label;
6874
+ btn.setAttribute("aria-label", label);
6875
+ btn.textContent = "+";
6876
+ btn.addEventListener("click", (e) => {
6877
+ e.preventDefault();
6878
+ e.stopPropagation();
6879
+ post({ type: "block-layout", payload: { stepIndex, blockId, action } });
6880
+ });
6881
+ z.append(line, btn);
6882
+ card.appendChild(z);
6883
+ };
6884
+ const { rows, content } = layoutRows(card, blocks);
6885
+ if (rows.length === 0) return;
6886
+ const cw = content.right - content.left;
6887
+ rows.forEach((row, k) => {
6888
+ const first = row.columns[0]?.items[0]?.block.editorId;
6889
+ if (k === 0) {
6890
+ zone(
6891
+ "h",
6892
+ content.left,
6893
+ row.rect.top - EDGE2 - GAP,
6894
+ cw,
6895
+ GAP,
6896
+ "Agregar fila",
6897
+ first,
6898
+ "row-before"
6899
+ );
6900
+ } else {
6901
+ const prev = rows[k - 1];
6902
+ const y = (prev.rect.bottom + row.rect.top) / 2 - GAP / 2;
6903
+ zone("h", content.left, y, cw, GAP, "Agregar fila", first, "row-before");
6904
+ }
6905
+ if (k === rows.length - 1) {
6906
+ zone(
6907
+ "h",
6908
+ content.left,
6909
+ row.rect.bottom + EDGE2,
6910
+ cw,
6911
+ GAP,
6912
+ "Agregar fila",
6913
+ first,
6914
+ "row-after"
6915
+ );
6916
+ }
6917
+ });
6918
+ for (const row of rows) {
6919
+ const rh = row.rect.bottom - row.rect.top;
6920
+ row.columns.forEach((col, j) => {
6921
+ const id = col.items[0]?.block.editorId;
6922
+ if (j === 0) {
6923
+ zone(
6924
+ "v",
6925
+ row.rect.left - EDGE2 - GAP,
6926
+ row.rect.top,
6927
+ GAP,
6928
+ rh,
6929
+ "Agregar columna",
6930
+ id,
6931
+ "col-before"
6932
+ );
6933
+ } else {
6934
+ const prev = row.columns[j - 1];
6935
+ const x = (prev.rect.right + col.rect.left) / 2 - GAP / 2;
6936
+ zone("v", x, row.rect.top, GAP, rh, "Agregar columna", id, "col-before");
6937
+ }
6938
+ if (j === row.columns.length - 1) {
6939
+ zone(
6940
+ "v",
6941
+ row.rect.right + EDGE2,
6942
+ row.rect.top,
6943
+ GAP,
6944
+ rh,
6945
+ "Agregar columna",
6946
+ id,
6947
+ "col-after"
6948
+ );
6949
+ }
6950
+ });
6951
+ }
6952
+ for (const row of rows) {
6953
+ for (const col of row.columns) {
6954
+ const w = col.rect.right - col.rect.left;
6955
+ col.items.forEach((it, i) => {
6956
+ const next = col.items[i + 1];
6957
+ if (next) {
6958
+ const a = rectOf(it.el);
6959
+ const b = rectOf(next.el);
6960
+ const y = (a.bottom + b.top) / 2 - GAP / 2;
6961
+ zone(
6962
+ "h",
6963
+ col.rect.left,
6964
+ y,
6965
+ w,
6966
+ GAP,
6967
+ "Agregar bloque",
6968
+ it.block.editorId,
6969
+ "stack-after"
6970
+ );
6971
+ } else if (row.columns.length > 1 && col.rect.bottom < row.rect.bottom - GAP) {
6972
+ zone(
6973
+ "h",
6974
+ col.rect.left,
6975
+ col.rect.bottom + EDGE2,
6976
+ w,
6977
+ GAP,
6978
+ "Agregar bloque",
6979
+ it.block.editorId,
6980
+ "stack-after"
6981
+ );
6982
+ }
6983
+ });
6984
+ }
6985
+ }
6986
+ };
6987
+ const attachCardClick = (host, stepIndex) => {
6988
+ let downAt = null;
6989
+ const onDown = (e) => {
6990
+ downAt = { x: e.clientX, y: e.clientY };
6991
+ };
6992
+ const onClick = (e) => {
6993
+ const path = e.composedPath();
6994
+ if (path.some(
6995
+ (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"))
6996
+ )) {
6997
+ return;
6998
+ }
6999
+ const inCard = path.some((n) => n instanceof Element && n.classList.contains("veo-card"));
7000
+ if (!inCard) return;
7001
+ if (downAt && Math.hypot(e.clientX - downAt.x, e.clientY - downAt.y) > 4) return;
7002
+ const blockEl = path.find(
7003
+ (n) => n instanceof HTMLElement && !!n.dataset.veoBlockId
7004
+ );
7005
+ if (blockEl) {
7006
+ if (path.some((n) => n instanceof Element && n.matches("button, a"))) {
7007
+ e.preventDefault();
7008
+ e.stopPropagation();
7009
+ }
7010
+ selectedBlockId = blockEl.dataset.veoBlockId ?? null;
7011
+ applyBlockHighlight(host, stepIndex);
7012
+ if (selectedBlockId) {
7013
+ post({ type: "block-clicked", payload: { stepIndex, blockId: selectedBlockId } });
7014
+ }
7015
+ return;
7016
+ }
7017
+ const interactive = path.some(
7018
+ (n) => n instanceof Element && n.matches("button, a, input, textarea, select, label, [contenteditable]")
7019
+ );
7020
+ if (interactive) return;
7021
+ selectedBlockId = null;
7022
+ clearBlockHighlight(host);
7023
+ renderRowChrome(host, stepIndex);
7024
+ post({ type: "card-clicked", payload: { stepIndex } });
7025
+ };
7026
+ host.addEventListener("mousedown", onDown, true);
7027
+ host.addEventListener("click", onClick, true);
7028
+ return () => {
7029
+ host.removeEventListener("mousedown", onDown, true);
7030
+ host.removeEventListener("click", onClick, true);
7031
+ };
7032
+ };
7033
+ const attachRawBlock = (stepIndex, blockId) => {
7034
+ const nodes = Array.from(
7035
+ document.querySelectorAll(`[${RAW_BLOCK_ATTR}="${blockId}"]`)
7036
+ );
7037
+ if (nodes.length === 0) return null;
7038
+ const doc = document;
7039
+ const box = doc.createElement("div");
7040
+ box.setAttribute("data-veo-builder-chrome", "");
7041
+ box.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;z-index:2147483646;";
7042
+ box.style.display = "none";
7043
+ const shadow = box.attachShadow({ mode: "open" });
7044
+ const style = doc.createElement("style");
7045
+ style.textContent = GUIDE_STYLES;
7046
+ shadow.appendChild(style);
7047
+ const frame = doc.createElement("div");
7048
+ frame.className = "veo-block-frame";
7049
+ frame.style.position = "fixed";
7050
+ const bar = doc.createElement("div");
7051
+ bar.className = "veo-block-toolbar";
7052
+ bar.style.position = "fixed";
7053
+ const mk = (action, label, icon, enabled = true) => {
7054
+ const b = doc.createElement("button");
7055
+ b.type = "button";
7056
+ b.title = label;
7057
+ b.setAttribute("aria-label", label);
7058
+ b.className = `veo-block-toolbar-${action}`;
7059
+ b.innerHTML = icon;
7060
+ b.disabled = !enabled;
7061
+ b.addEventListener("click", (e) => {
7062
+ e.preventDefault();
7063
+ e.stopPropagation();
7064
+ post({ type: "block-action", payload: { stepIndex, blockId, action } });
7065
+ });
7066
+ bar.appendChild(b);
7067
+ };
7068
+ mk("edit", "Editar", ICON.pencil);
7069
+ mk("duplicate", "Duplicar", ICON.copy, false);
7070
+ mk("remove", "Eliminar", ICON.trash);
7071
+ mk("move-left", "Mover a la izquierda", ICON_MOVE.left, false);
7072
+ mk("move-right", "Mover a la derecha", ICON_MOVE.right, false);
7073
+ frame.style.display = "none";
7074
+ bar.style.display = "none";
7075
+ const GAP = 18;
7076
+ const EDGE2 = 4;
7077
+ const gap = (kind, label, action) => {
7078
+ const z = doc.createElement("div");
7079
+ z.className = `veo-gap veo-gap--${kind}`;
7080
+ z.style.position = "fixed";
7081
+ z.dataset.veoGapAction = action;
7082
+ const line = doc.createElement("i");
7083
+ const btn = doc.createElement("button");
7084
+ btn.type = "button";
7085
+ btn.title = label;
7086
+ btn.setAttribute("aria-label", label);
7087
+ btn.textContent = "+";
7088
+ btn.addEventListener("click", (e) => {
7089
+ e.preventDefault();
7090
+ e.stopPropagation();
7091
+ post({ type: "block-layout", payload: { stepIndex, blockId, action } });
7092
+ });
7093
+ z.append(line, btn);
7094
+ return z;
7095
+ };
7096
+ const gaps = {
7097
+ top: gap("h", "Agregar fila", "row-before"),
7098
+ bottom: gap("h", "Agregar fila", "row-after"),
7099
+ left: gap("v", "Agregar columna", "col-before"),
7100
+ right: gap("v", "Agregar columna", "col-after")
7101
+ };
7102
+ shadow.append(frame, bar, gaps.top, gaps.bottom, gaps.left, gaps.right);
7103
+ doc.body.appendChild(box);
7104
+ box.style.display = "";
7105
+ let visible = false;
7106
+ const place = (el2, x, y, w, h) => {
7107
+ el2.style.left = `${x}px`;
7108
+ el2.style.top = `${y}px`;
7109
+ el2.style.width = `${w}px`;
7110
+ el2.style.height = `${h}px`;
7111
+ };
7112
+ const reposition = () => {
7113
+ const live = nodes.filter((n) => n.isConnected);
7114
+ if (live.length === 0) return;
7115
+ let l = Number.POSITIVE_INFINITY;
7116
+ let t = Number.POSITIVE_INFINITY;
7117
+ let r = Number.NEGATIVE_INFINITY;
7118
+ let b = Number.NEGATIVE_INFINITY;
7119
+ for (const n of live) {
7120
+ const rc = n.getBoundingClientRect();
7121
+ if (rc.width === 0 && rc.height === 0) continue;
7122
+ l = Math.min(l, rc.left);
7123
+ t = Math.min(t, rc.top);
7124
+ r = Math.max(r, rc.right);
7125
+ b = Math.max(b, rc.bottom);
7126
+ }
7127
+ if (!Number.isFinite(l)) return;
7128
+ place(gaps.top, l, t - EDGE2 - GAP, r - l, GAP);
7129
+ place(gaps.bottom, l, b + EDGE2, r - l, GAP);
7130
+ place(gaps.left, l - EDGE2 - GAP, t, GAP, b - t);
7131
+ place(gaps.right, r + EDGE2, t, GAP, b - t);
7132
+ if (!visible) return;
7133
+ place(frame, l, t, r - l, b - t);
7134
+ const bh = bar.offsetHeight || TOOLBAR_H;
7135
+ const bw = bar.offsetWidth;
7136
+ bar.style.left = `${Math.max(0, r - bw)}px`;
7137
+ bar.style.top = `${Math.max(0, t - bh - 6)}px`;
7138
+ };
7139
+ const select = (id) => {
7140
+ visible = id === blockId;
7141
+ frame.style.display = visible ? "" : "none";
7142
+ bar.style.display = visible ? "" : "none";
7143
+ reposition();
7144
+ };
7145
+ reposition();
7146
+ const onClick = (e) => {
7147
+ const path = e.composedPath();
7148
+ if (!nodes.some((n) => path.includes(n))) return;
7149
+ if (path.some((n) => n instanceof Element && n.matches("button, a"))) {
7150
+ e.preventDefault();
7151
+ e.stopPropagation();
7152
+ }
7153
+ selectedBlockId = blockId;
7154
+ select(blockId);
7155
+ post({ type: "block-clicked", payload: { stepIndex, blockId } });
7156
+ };
7157
+ doc.addEventListener("click", onClick, true);
7158
+ window.addEventListener("scroll", reposition, true);
7159
+ window.addEventListener("resize", reposition);
7160
+ return {
7161
+ select,
7162
+ detach: () => {
7163
+ doc.removeEventListener("click", onClick, true);
7164
+ window.removeEventListener("scroll", reposition, true);
7165
+ window.removeEventListener("resize", reposition);
7166
+ box.remove();
7167
+ }
7168
+ };
7169
+ };
5314
7170
  const applyPreviewGated = (cmd) => {
5315
7171
  const showNow = !cmd.honorTrigger;
5316
7172
  if (showNow) {
@@ -5330,7 +7186,7 @@ function initBuilderMode() {
5330
7186
  return;
5331
7187
  }
5332
7188
  const devices = cmd.guide.activationRules?.devices;
5333
- if (!showNow && !previewMode && devices && !isDeviceAllowed(devices)) {
7189
+ if (!showNow && previewMode && devices && !isDeviceAllowed(devices)) {
5334
7190
  cancelPreviewDelay();
5335
7191
  withholdPreview();
5336
7192
  postPreviewStatus({ state: "device-mismatch", devices });
@@ -5382,15 +7238,16 @@ function initBuilderMode() {
5382
7238
  postPreviewStatus({ state: "waiting-delay", delayMs });
5383
7239
  };
5384
7240
  const applyPreview = (cmd) => {
5385
- manipulator?.detach();
5386
- manipulator = null;
5387
- dropTarget?.detach();
5388
- dropTarget = null;
7241
+ if (cmd.editable && !previewMode && tryPatchPreview(cmd)) {
7242
+ previewStepIndex = cmd.guide.startStepIndex ?? 0;
7243
+ return;
7244
+ }
7245
+ detachPreviewTools();
5389
7246
  previewStepIndex = cmd.guide.startStepIndex ?? 0;
5390
7247
  const rules = cmd.guide.activationRules;
5391
7248
  const trig = rules?.trigger;
5392
7249
  const sel = rules?.selector?.trim();
5393
- const gate = cmd.honorTrigger && !previewFired && !previewMode;
7250
+ const gate = cmd.honorTrigger && !previewFired && previewMode;
5394
7251
  const waitsForTarget = (trig === "on_click" || trig === "on_hover") && (!isAutomaticActivation(rules) || previewDismissed);
5395
7252
  if (gate && waitsForTarget && sel) {
5396
7253
  cancelPreviewDelay();
@@ -5407,13 +7264,13 @@ function initBuilderMode() {
5407
7264
  }
5408
7265
  disarmPreviewTrigger();
5409
7266
  cancelPreviewDelay();
7267
+ const fetchProgress = previewProgressFetcher();
5410
7268
  const handle = previewGuide({
5411
7269
  ...cmd.guide,
7270
+ ...fetchProgress ? { fetchProgress } : {},
5412
7271
  onClosed: () => {
5413
- manipulator?.detach();
5414
- manipulator = null;
5415
- dropTarget?.detach();
5416
- dropTarget = null;
7272
+ detachPreviewTools();
7273
+ shownPreview = null;
5417
7274
  previewFired = false;
5418
7275
  previewDismissed = true;
5419
7276
  if (previewMode) {
@@ -5421,9 +7278,7 @@ function initBuilderMode() {
5421
7278
  const t = r?.trigger;
5422
7279
  const selector = r?.selector?.trim();
5423
7280
  if ((t === "on_click" || t === "on_hover") && selector) armPreviewTrigger(selector, t);
5424
- return;
5425
7281
  }
5426
- if (lastPreview) applyPreviewGated(lastPreview);
5427
7282
  }
5428
7283
  });
5429
7284
  if (cmd.honorTrigger) postPreviewStatus({ state: "shown" });
@@ -5431,7 +7286,23 @@ function initBuilderMode() {
5431
7286
  void handle.ready.then((result) => {
5432
7287
  if (!result.rendered) return;
5433
7288
  const host = handle.host();
5434
- if (!host) return;
7289
+ if (!host) {
7290
+ const idx = cmd.guide.startStepIndex ?? 0;
7291
+ const only = cmd.guide.guideSteps[idx]?.blocks?.[0];
7292
+ if (only?.type === "code" && only.editorId) {
7293
+ rawBlock = attachRawBlock(idx, only.editorId);
7294
+ rawBlock?.select(selectedBlockId);
7295
+ }
7296
+ return;
7297
+ }
7298
+ host.classList.add("veo-no-anim");
7299
+ shownPreview = cmd;
7300
+ cardClick = attachCardClick(host, cmd.guide.startStepIndex ?? 0);
7301
+ const stepBlocks = cmd.guide.guideSteps[cmd.guide.startStepIndex ?? 0]?.blocks ?? [];
7302
+ previewBlocks = stepBlocks;
7303
+ applyBlockHighlight(host, cmd.guide.startStepIndex ?? 0);
7304
+ renderGapZones(host, cmd.guide.startStepIndex ?? 0, stepBlocks);
7305
+ rowHover = attachRowHover(host, cmd.guide.startStepIndex ?? 0);
5435
7306
  manipulator = attachDesignManipulator({
5436
7307
  host,
5437
7308
  guideType: cmd.guide.guideType,
@@ -5499,13 +7370,21 @@ function initBuilderMode() {
5499
7370
  break;
5500
7371
  case "editor-state":
5501
7372
  chrome?.setState(cmd.state);
7373
+ if (panelSection !== cmd.state.section) {
7374
+ panelSection = cmd.state.section;
7375
+ const h = document.querySelector("[data-veo-guide]");
7376
+ if (h && hoverCard && !previewMode) renderCardChrome(h, previewStepIndex);
7377
+ }
5502
7378
  panel?.setTitle(
5503
- 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"
7379
+ 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"
5504
7380
  );
5505
7381
  break;
5506
7382
  case "panel-visibility":
5507
7383
  panel?.setVisible(cmd.visible);
5508
7384
  break;
7385
+ case "panel-size":
7386
+ panel?.setSize(cmd.size);
7387
+ break;
5509
7388
  case "start-picker": {
5510
7389
  picker?.stop();
5511
7390
  const panelWasShown = panel?.isVisible() ?? false;
@@ -5513,10 +7392,7 @@ function initBuilderMode() {
5513
7392
  chrome?.setHidden(true);
5514
7393
  const previewToRestore = lastPreview;
5515
7394
  if (previewToRestore) {
5516
- manipulator?.detach();
5517
- manipulator = null;
5518
- dropTarget?.detach();
5519
- dropTarget = null;
7395
+ detachPreviewTools();
5520
7396
  closeGuidePreview();
5521
7397
  }
5522
7398
  const restorePanel = () => {
@@ -5555,11 +7431,21 @@ function initBuilderMode() {
5555
7431
  case "block-drag":
5556
7432
  handleBlockDrag(cmd);
5557
7433
  break;
7434
+ case "select-block": {
7435
+ selectedBlockId = cmd.blockId;
7436
+ const h = document.querySelector("[data-veo-guide]");
7437
+ if (h && !previewMode) applyBlockHighlight(h, previewStepIndex);
7438
+ if (!previewMode) rawBlock?.select(cmd.blockId);
7439
+ break;
7440
+ }
7441
+ case "select-row": {
7442
+ selectedRowId = cmd.blockId;
7443
+ const h = document.querySelector("[data-veo-guide]");
7444
+ if (h && !previewMode) renderRowChrome(h, previewStepIndex);
7445
+ break;
7446
+ }
5558
7447
  case "close-preview":
5559
- manipulator?.detach();
5560
- manipulator = null;
5561
- dropTarget?.detach();
5562
- dropTarget = null;
7448
+ detachPreviewTools();
5563
7449
  hideDragGhost();
5564
7450
  pendingPreview = null;
5565
7451
  lastPreview = null;
@@ -5612,10 +7498,7 @@ function initBuilderMode() {
5612
7498
  removeNavWatcher();
5613
7499
  picker?.stop();
5614
7500
  picker = null;
5615
- manipulator?.detach();
5616
- manipulator = null;
5617
- dropTarget?.detach();
5618
- dropTarget = null;
7501
+ detachPreviewTools();
5619
7502
  hideDragGhost();
5620
7503
  pendingPreview = null;
5621
7504
  disarmPreviewTrigger();
@@ -5643,6 +7526,8 @@ function initBuilderMode() {
5643
7526
  case "preview":
5644
7527
  if (previewMode) return;
5645
7528
  previewMode = true;
7529
+ previewFired = false;
7530
+ previewDismissed = false;
5646
7531
  panelWasVisible = panel?.isVisible() ?? true;
5647
7532
  panel?.setVisible(false);
5648
7533
  chrome?.setPreview(true);
@@ -5651,6 +7536,8 @@ function initBuilderMode() {
5651
7536
  case "exit-preview":
5652
7537
  if (!previewMode) return;
5653
7538
  previewMode = false;
7539
+ disarmPreviewTrigger();
7540
+ cancelPreviewDelay();
5654
7541
  chrome?.setPreview(false);
5655
7542
  panel?.setVisible(panelWasVisible);
5656
7543
  if (lastPreview) applyPreviewGated(lastPreview);
@@ -5732,13 +7619,36 @@ function resolveDashboardOrigin(originHint) {
5732
7619
  }
5733
7620
  return null;
5734
7621
  }
7622
+ function previewProgressFetcher() {
7623
+ const cfg = window.__veoBuilder;
7624
+ if (!cfg?.apiKey || !cfg?.apiUrl) return void 0;
7625
+ const apiUrl = cfg.apiUrl.replace(/\/+$/, "");
7626
+ const apiKey = cfg.apiKey;
7627
+ return async (guideIds) => {
7628
+ const params = new URLSearchParams({
7629
+ endUserId: "veo_preview",
7630
+ guideIds: guideIds.join(","),
7631
+ includeUnpublished: "1"
7632
+ });
7633
+ const res = await fetch(`${apiUrl}/v1/guides/progress?${params}`, {
7634
+ headers: { "X-Api-Key": apiKey }
7635
+ });
7636
+ if (!res.ok) return [];
7637
+ const data = await res.json();
7638
+ return data.items ?? [];
7639
+ };
7640
+ }
5735
7641
  var init_builder_mode = __esm({
5736
7642
  "src/plugins/builder/builder-mode.ts"() {
5737
7643
  init_builder_token();
5738
7644
  init_safe_env();
5739
7645
  init_activation_devices();
5740
7646
  init_activation_rules();
7647
+ init_block_builder();
7648
+ init_code_block();
5741
7649
  init_guide_preview();
7650
+ init_layout();
7651
+ init_styles();
5742
7652
  init_url_match();
5743
7653
  init_block_drop_target();
5744
7654
  init_bridge_protocol();
@@ -7137,6 +9047,7 @@ init_inline_renderer();
7137
9047
  init_modal_renderer();
7138
9048
  init_tooltip_renderer();
7139
9049
  init_walkthrough_renderer();
9050
+ init_snooze();
7140
9051
  init_url_match();
7141
9052
 
7142
9053
  // src/plugins/guides/walkthrough-state.ts
@@ -7329,6 +9240,12 @@ var GuidesController = class {
7329
9240
  * en cada `checkGuides` con {@link pruneComposite}.
7330
9241
  */
7331
9242
  this.activeComposite = /* @__PURE__ */ new Map();
9243
+ /**
9244
+ * Progreso de esta sesión, por guía: pasos vistos y completado, según las
9245
+ * interacciones que ESTE SDK emitió (aunque el lote aún no haya viajado al
9246
+ * backend). Se mezcla con la respuesta del backend en `fetchProgress`.
9247
+ */
9248
+ this.sessionProgress = /* @__PURE__ */ new Map();
7332
9249
  this.debug = config.debug === true;
7333
9250
  this.apiUrl = config.apiUrl;
7334
9251
  this.apiKey = config.apiKey;
@@ -7415,6 +9332,10 @@ var GuidesController = class {
7415
9332
  if (this.debug) console.log(`[veo] guides: filtered locally guideId=${guide.guideId}`);
7416
9333
  return;
7417
9334
  }
9335
+ if (!opts.byUser && isSnoozed(guide.guideId)) {
9336
+ if (this.debug) console.log(`[veo] guides: snoozed guideId=${guide.guideId}`);
9337
+ return;
9338
+ }
7418
9339
  if (!opts.byUser && this.isHeldByRepeat(guide)) {
7419
9340
  if (this.debug) console.log(`[veo] guides: held by repeat guideId=${guide.guideId}`);
7420
9341
  return;
@@ -7690,6 +9611,17 @@ ${pageUrl}`;
7690
9611
  const goToStep = (target) => {
7691
9612
  cleanup();
7692
9613
  if (target < 0 || target >= steps.length) return;
9614
+ const endUserId = this.client.getEndUserId();
9615
+ if (endUserId) {
9616
+ this.enqueueInteraction(
9617
+ guide.guideId,
9618
+ endUserId,
9619
+ sessionId,
9620
+ pageUrl,
9621
+ target > idx ? "step_advanced" : "step_back",
9622
+ target
9623
+ );
9624
+ }
7693
9625
  const step = steps[target];
7694
9626
  if (step && stepMatchesPage(step.pageLocation, pageUrl)) {
7695
9627
  this.renderCompositeStep(guide, target, sessionId, pageUrl, badgeOpen);
@@ -7714,10 +9646,15 @@ ${pageUrl}`;
7714
9646
  onInteraction: (e) => this.handleInteraction(guide, sessionId, pageUrl, e),
7715
9647
  onClose: cleanup,
7716
9648
  onTrack: (name, props) => this.client.track(name, props),
7717
- onFormSubmit: (answers) => this.submitFormResponse(guide.guideId, answers),
9649
+ onFormSubmit: (answers, meta) => this.submitFormResponse(guide.guideId, answers, meta),
7718
9650
  onOpenGuide: (targetGuideId) => {
7719
9651
  void this.openGuideById(targetGuideId, sessionId, pageUrl);
7720
9652
  },
9653
+ // Lista de tareas: progreso del usuario y abrir una guía en su paso.
9654
+ fetchProgress: (ids) => this.fetchProgress(ids),
9655
+ onOpenGuideAt: (targetGuideId, stepIndex) => {
9656
+ void this.openGuideById(targetGuideId, sessionId, pageUrl, stepIndex);
9657
+ },
7721
9658
  // Badge: cuando el MOTOR muestra la guía (automático, elemento objetivo
7722
9659
  // o encadenada), el contenido se abre solo; el marcador queda para
7723
9660
  // volver a verla. Solo `manual` deja el tooltip cerrado.
@@ -7745,7 +9682,7 @@ ${pageUrl}`;
7745
9682
  const onTrack = (eventName, properties) => {
7746
9683
  this.client.track(eventName, properties);
7747
9684
  };
7748
- const onFormSubmit = (answers) => this.submitFormResponse(guide.guideId, answers);
9685
+ const onFormSubmit = (answers, meta) => this.submitFormResponse(guide.guideId, answers, meta);
7749
9686
  const onOpenGuide = (targetGuideId) => {
7750
9687
  void this.openGuideById(targetGuideId, sessionId, pageUrl);
7751
9688
  };
@@ -7765,7 +9702,7 @@ ${pageUrl}`;
7765
9702
  * un botón); respeta el dedupe de guías activas y el flujo normal de
7766
9703
  * tracking. 404/red → no-op (la guía destino pudo borrarse).
7767
9704
  */
7768
- async openGuideById(guideId, sessionId, pageUrl) {
9705
+ async openGuideById(guideId, sessionId, pageUrl, startStep) {
7769
9706
  if (this.activeByGuideId.has(guideId)) return;
7770
9707
  const guide = await this.resolver.fetchById(guideId);
7771
9708
  if (!guide) {
@@ -7773,6 +9710,15 @@ ${pageUrl}`;
7773
9710
  return;
7774
9711
  }
7775
9712
  const endUserId = this.client.getEndUserId() ?? this.client.getAnonymousId();
9713
+ if (guide.guideType === "composite" && typeof startStep === "number") {
9714
+ const total = guide.guideSteps.length;
9715
+ const idx = Math.max(0, Math.min(total - 1, Math.floor(startStep)));
9716
+ this.enqueueInteractionOnce(guide.guideId, endUserId, sessionId, pageUrl, "shown", 0);
9717
+ if (idx > 0)
9718
+ this.enqueueInteraction(guide.guideId, endUserId, sessionId, pageUrl, "step_advanced", idx);
9719
+ this.renderCompositeStep(guide, idx, sessionId, pageUrl);
9720
+ return;
9721
+ }
7776
9722
  if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
7777
9723
  if (!this.walkthroughState.hasActive()) {
7778
9724
  await this.startWalkthrough(guide, endUserId, sessionId, pageUrl);
@@ -7983,6 +9929,7 @@ ${pageUrl}`;
7983
9929
  console.warn("[veo] guides: interaction without endUserId \u2014 skipping enqueue");
7984
9930
  return;
7985
9931
  }
9932
+ this.noteProgress(guide.guideId, event.action, event.stepIndex);
7986
9933
  this.trackerQueue.enqueue({
7987
9934
  guideId: guide.guideId,
7988
9935
  payload: {
@@ -8002,6 +9949,7 @@ ${pageUrl}`;
8002
9949
  * una navegación distinta) y deduplicarlas perdería datos.
8003
9950
  */
8004
9951
  enqueueInteraction(guideId, endUserId, sessionId, pageUrl, action, stepIndex) {
9952
+ this.noteProgress(guideId, action, stepIndex);
8005
9953
  this.trackerQueue.enqueue({
8006
9954
  guideId,
8007
9955
  payload: {
@@ -8014,6 +9962,45 @@ ${pageUrl}`;
8014
9962
  }
8015
9963
  });
8016
9964
  }
9965
+ noteProgress(guideId, action, stepIndex) {
9966
+ if (action !== "shown" && action !== "step_advanced" && action !== "step_back" && action !== "completed") {
9967
+ return;
9968
+ }
9969
+ const cur = this.sessionProgress.get(guideId) ?? { maxStep: -1, completed: false };
9970
+ this.sessionProgress.set(guideId, {
9971
+ maxStep: Math.max(cur.maxStep, stepIndex),
9972
+ completed: cur.completed || action === "completed"
9973
+ });
9974
+ }
9975
+ /**
9976
+ * Progreso del usuario en varias guías (bloque «Lista de tareas»): lo pide
9977
+ * al backend y lo eleva con lo visto en esta sesión (nunca lo baja).
9978
+ */
9979
+ async fetchProgress(guideIds) {
9980
+ const endUserId = this.client.getEndUserId() ?? this.client.getAnonymousId();
9981
+ let rows = [];
9982
+ try {
9983
+ const params = new URLSearchParams({ endUserId, guideIds: guideIds.join(",") });
9984
+ const res = await fetch(`${this.apiUrl}/v1/guides/progress?${params}`, {
9985
+ headers: { "X-Api-Key": this.apiKey }
9986
+ });
9987
+ if (res.ok) rows = (await res.json()).items ?? [];
9988
+ else if (this.debug) console.warn("[veo] guides/progress respondi\xF3", res.status);
9989
+ } catch (err) {
9990
+ if (this.debug) console.warn("[veo] guides/progress fall\xF3:", err);
9991
+ }
9992
+ return rows.map((r) => {
9993
+ const local2 = this.sessionProgress.get(r.guideId);
9994
+ if (!local2) return r;
9995
+ const seen = Math.min(r.totalSteps, local2.maxStep + 1);
9996
+ const completed = r.completed || local2.completed || seen >= r.totalSteps;
9997
+ return {
9998
+ ...r,
9999
+ currentStep: completed ? r.totalSteps : Math.max(r.currentStep, seen),
10000
+ completed
10001
+ };
10002
+ });
10003
+ }
8017
10004
  /**
8018
10005
  * Encola con dedup por sesión del SDK. Para `shown` del primer step:
8019
10006
  * no queremos re-emitirlo si el usuario navega entre dos páginas que
@@ -8058,13 +10045,19 @@ ${pageUrl}`;
8058
10045
  * propiedades del usuario. Usa el endUserId actual (o el anónimo si aún no
8059
10046
  * hubo identify — el backend pre-crea la fila igual que identify).
8060
10047
  */
8061
- async submitFormResponse(guideId, answers) {
10048
+ async submitFormResponse(guideId, answers, meta) {
8062
10049
  const endUserId = this.client.getEndUserId() ?? this.client.getAnonymousId();
8063
10050
  try {
8064
10051
  const res = await fetch(`${this.apiUrl}/v1/guides/${guideId}/form-response`, {
8065
10052
  method: "POST",
8066
10053
  headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey },
8067
- body: JSON.stringify({ endUserId, answers })
10054
+ body: JSON.stringify({
10055
+ endUserId,
10056
+ answers,
10057
+ sessionId: this.client.getSessionId(),
10058
+ ...meta ? { stepPosition: meta.stepIndex } : {},
10059
+ ...meta?.pollId ? { pollId: meta.pollId } : {}
10060
+ })
8068
10061
  });
8069
10062
  if (!res.ok && this.debug) {
8070
10063
  console.warn("[veo] form-response respondi\xF3", res.status);
@@ -8117,7 +10110,7 @@ async function runPreviewMode(opts) {
8117
10110
  } catch (err) {
8118
10111
  if (opts.debug) console.warn("[veo] preview-resolve fall\xF3:", err);
8119
10112
  }
8120
- const submitToBackend = async (answers) => {
10113
+ const submitToBackend = async (answers, meta) => {
8121
10114
  if (!guide) return false;
8122
10115
  const veo = window.veo;
8123
10116
  const endUserId = veo?.getEndUserId?.() ?? veo?.getAnonymousId?.() ?? null;
@@ -8126,7 +10119,12 @@ async function runPreviewMode(opts) {
8126
10119
  const res = await fetch(`${opts.apiUrl}/v1/guides/${guide.guideId}/form-response`, {
8127
10120
  method: "POST",
8128
10121
  headers: { "Content-Type": "application/json", "X-Api-Key": opts.apiKey },
8129
- body: JSON.stringify({ endUserId, answers })
10122
+ body: JSON.stringify({
10123
+ endUserId,
10124
+ answers,
10125
+ ...meta ? { stepPosition: meta.stepIndex } : {},
10126
+ ...meta?.pollId ? { pollId: meta.pollId } : {}
10127
+ })
8130
10128
  });
8131
10129
  return res.ok;
8132
10130
  } catch {