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.
@@ -329,6 +329,125 @@ function rebuildNode(node, doc, depth, budget) {
329
329
  return rebuilt;
330
330
  }
331
331
 
332
+ // src/plugins/guides/video-block.ts
333
+ function videoEmbedUrl(provider, url) {
334
+ let u;
335
+ try {
336
+ u = new URL(url);
337
+ } catch {
338
+ return null;
339
+ }
340
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
341
+ const host = u.hostname.replace(/^www\./, "").replace(/^m\./, "");
342
+ const id = (re, from) => {
343
+ const m = from.match(re);
344
+ return m?.[1] ?? null;
345
+ };
346
+ switch (provider) {
347
+ case "youtube": {
348
+ let v = null;
349
+ if (host === "youtube.com" || host === "youtube-nocookie.com") {
350
+ v = u.searchParams.get("v") ?? id(/^\/(?:embed|shorts|live)\/([A-Za-z0-9_-]{6,})/, u.pathname);
351
+ } else if (host === "youtu.be") {
352
+ v = id(/^\/([A-Za-z0-9_-]{6,})/, u.pathname);
353
+ }
354
+ if (!v || !/^[A-Za-z0-9_-]{6,}$/.test(v)) return null;
355
+ return `https://www.youtube-nocookie.com/embed/${v}`;
356
+ }
357
+ case "vimeo": {
358
+ if (host !== "vimeo.com" && host !== "player.vimeo.com") return null;
359
+ const v = id(/(\d{6,})/, u.pathname);
360
+ return v ? `https://player.vimeo.com/video/${v}` : null;
361
+ }
362
+ case "loom": {
363
+ if (host !== "loom.com") return null;
364
+ const v = id(/^\/(?:share|embed)\/([A-Za-z0-9]{16,})/, u.pathname);
365
+ return v ? `https://www.loom.com/embed/${v}` : null;
366
+ }
367
+ case "wistia": {
368
+ if (!under(host, "wistia.com") && !under(host, "wistia.net")) return null;
369
+ const v = id(/\/(?:medias|embed\/iframe)\/([A-Za-z0-9]{6,})/, u.pathname);
370
+ return v ? `https://fast.wistia.net/embed/iframe/${v}` : null;
371
+ }
372
+ case "msstream": {
373
+ if (!under(host, "microsoftstream.com")) return null;
374
+ const v = id(/\/(?:embed\/)?video\/([0-9a-f-]{36})/i, u.pathname);
375
+ return v ? `https://web.microsoftstream.com/embed/video/${v}?autoplay=false&showinfo=true` : null;
376
+ }
377
+ case "vidyard": {
378
+ if (!under(host, "vidyard.com")) return null;
379
+ const v = id(/\/watch\/([A-Za-z0-9_-]{8,})/, u.pathname) ?? (host === "play.vidyard.com" ? id(/^\/([A-Za-z0-9_-]{8,})/, u.pathname) : null);
380
+ return v ? `https://play.vidyard.com/${v}` : null;
381
+ }
382
+ case "sharepoint":
383
+ return u.protocol === "https:" && under(host, "sharepoint.com") ? u.href : null;
384
+ case "brightcove":
385
+ return u.protocol === "https:" && host === "players.brightcove.net" && u.searchParams.get("videoId") ? u.href : null;
386
+ case "kaltura":
387
+ return u.protocol === "https:" && under(host, "kaltura.com") && u.pathname.startsWith("/p/") ? u.href : null;
388
+ default:
389
+ return null;
390
+ }
391
+ }
392
+ function under(host, domain) {
393
+ return host === domain || host.endsWith(`.${domain}`);
394
+ }
395
+ function fileVideoUrl(url) {
396
+ try {
397
+ const u = new URL(url);
398
+ if (u.protocol !== "https:" && u.protocol !== "http:") return null;
399
+ return /\.(mp4|webm|mov)$/i.test(u.pathname) ? u.href : null;
400
+ } catch {
401
+ return null;
402
+ }
403
+ }
404
+ function buildVideoBlock(block, doc, opts = {}) {
405
+ const isFile = block.provider === "file";
406
+ const src = typeof block.url === "string" ? isFile ? fileVideoUrl(block.url) : videoEmbedUrl(block.provider, block.url) : null;
407
+ if (!src) return null;
408
+ const s = block.style ?? {};
409
+ const wrap = doc.createElement("div");
410
+ wrap.className = "veo-guide-video";
411
+ const ratio = s.aspect === "4:3" ? "4 / 3" : "16 / 9";
412
+ wrap.style.aspectRatio = ratio;
413
+ if (s.widthMode === "fill") {
414
+ wrap.style.width = "100%";
415
+ } else {
416
+ const w = typeof s.width === "number" && Number.isFinite(s.width) ? s.width : 400;
417
+ wrap.style.width = `${Math.max(120, Math.min(1600, w))}px`;
418
+ wrap.style.maxWidth = "100%";
419
+ }
420
+ const align = s.align === "center" ? "center" : s.align === "right" ? "flex-end" : "flex-start";
421
+ wrap.style.alignSelf = align;
422
+ if (isFile) {
423
+ const video = doc.createElement("video");
424
+ video.src = src;
425
+ video.controls = true;
426
+ video.setAttribute("playsinline", "");
427
+ video.preload = "metadata";
428
+ video.setAttribute(
429
+ "aria-label",
430
+ typeof block.alt === "string" && block.alt.trim() ? block.alt.trim() : "Video"
431
+ );
432
+ if (opts.editing) video.style.pointerEvents = "none";
433
+ wrap.appendChild(video);
434
+ return wrap;
435
+ }
436
+ const frame = doc.createElement("iframe");
437
+ frame.src = src;
438
+ frame.title = typeof block.alt === "string" && block.alt.trim() ? block.alt.trim() : "Video";
439
+ frame.setAttribute("loading", "lazy");
440
+ frame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
441
+ frame.setAttribute(
442
+ "allow",
443
+ `accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture${opts.editing ? "" : "; fullscreen"}`
444
+ );
445
+ if (!opts.editing) frame.setAttribute("allowfullscreen", "");
446
+ if (opts.editing) frame.style.pointerEvents = "none";
447
+ wrap.appendChild(frame);
448
+ return wrap;
449
+ }
450
+
332
451
  // src/plugins/guides/block-builder.ts
333
452
  function buildStepContent(step, doc, callbacks) {
334
453
  const container = doc.createElement("div");
@@ -453,8 +572,12 @@ function renderBlocks(container, blocks, doc, callbacks) {
453
572
  i++;
454
573
  }
455
574
  }
456
- function buildContentBlock(block, doc) {
575
+ function buildContentBlock(block, doc, opts = {}) {
457
576
  switch (block.type) {
577
+ case "video":
578
+ return buildVideoBlock(block, doc, opts);
579
+ case "divider":
580
+ return buildDividerBlock(block, doc);
458
581
  case "title": {
459
582
  const el = doc.createElement("h2");
460
583
  el.className = "veo-guide-title";
@@ -478,7 +601,7 @@ function buildContentBlock(block, doc) {
478
601
  const el = doc.createElement("img");
479
602
  el.className = "veo-guide-image";
480
603
  el.src = block.url;
481
- el.alt = "";
604
+ el.alt = typeof block.alt === "string" ? block.alt : "";
482
605
  applyBlockStyle(el, block.style, "image");
483
606
  return el;
484
607
  }
@@ -486,6 +609,32 @@ function buildContentBlock(block, doc) {
486
609
  return null;
487
610
  }
488
611
  }
612
+ var DIVIDER_LINE_STYLES = /* @__PURE__ */ new Set(["solid", "dotted", "dashed"]);
613
+ function buildDividerBlock(block, doc) {
614
+ const s = block.style ?? {};
615
+ const wrap = doc.createElement("div");
616
+ wrap.className = "veo-guide-divider";
617
+ wrap.setAttribute("role", "separator");
618
+ const hr = doc.createElement("hr");
619
+ const custom = s.preset === "custom";
620
+ const color = custom && typeof s.color === "string" && s.color ? s.color : s.colorMode === "dark" ? "rgba(255, 255, 255, 0.35)" : "#e5e7eb";
621
+ const thickness = custom && typeof s.thickness === "number" && Number.isFinite(s.thickness) ? clampNum(Math.round(s.thickness), 1, 12) : 1;
622
+ const lineStyle = typeof s.lineStyle === "string" && DIVIDER_LINE_STYLES.has(s.lineStyle) ? s.lineStyle : "solid";
623
+ hr.style.borderTop = `${thickness}px ${lineStyle} ${color}`;
624
+ wrap.appendChild(hr);
625
+ for (const [k, prop] of [
626
+ ["marginTop", "marginTop"],
627
+ ["marginRight", "marginRight"],
628
+ ["marginBottom", "marginBottom"],
629
+ ["marginLeft", "marginLeft"]
630
+ ]) {
631
+ const v = s[k];
632
+ if (typeof v === "number" && Number.isFinite(v)) {
633
+ wrap.style[prop] = `${clampNum(v, 0, 64)}px`;
634
+ }
635
+ }
636
+ return wrap;
637
+ }
489
638
  function buildButtonBlock(block, doc, callbacks) {
490
639
  const btn = doc.createElement("button");
491
640
  btn.type = "button";
@@ -512,6 +661,12 @@ var BLOCK_ALIGN_SELF = {
512
661
  function clampNum(n, min, max) {
513
662
  return Math.min(max, Math.max(min, n));
514
663
  }
664
+ var TEXT_PRESETS = {
665
+ title: { size: 18, weight: 600, lineHeight: 1.3 },
666
+ subtitle: { size: 16, weight: 600, lineHeight: 1.35 },
667
+ paragraph: { size: 14, weight: 400, lineHeight: 1.5 },
668
+ small: { size: 12, weight: 400, lineHeight: 1.5 }
669
+ };
515
670
  function applyBlockStyle(el, style, kind) {
516
671
  if (!style || typeof style !== "object") return;
517
672
  const s = style;
@@ -521,14 +676,34 @@ function applyBlockStyle(el, style, kind) {
521
676
  }
522
677
  const align = typeof s.align === "string" ? s.align : null;
523
678
  if (align && (align === "left" || align === "center" || align === "right")) {
524
- if (kind === "text") el.style.textAlign = align;
525
- else if (kind === "image") el.style.alignSelf = BLOCK_ALIGN_SELF[align];
679
+ if (kind === "text") {
680
+ el.style.textAlign = align;
681
+ el.style.alignSelf = BLOCK_ALIGN_SELF[align];
682
+ } else if (kind === "image") el.style.alignSelf = BLOCK_ALIGN_SELF[align];
683
+ }
684
+ if (kind === "text" && typeof s.preset === "string" && s.preset in TEXT_PRESETS) {
685
+ const preset = TEXT_PRESETS[s.preset];
686
+ if (preset) {
687
+ el.style.fontSize = `${preset.size}px`;
688
+ el.style.fontWeight = String(preset.weight);
689
+ el.style.lineHeight = String(preset.lineHeight);
690
+ }
691
+ }
692
+ if (kind !== "image" && s.colorMode === "dark") {
693
+ el.style.background = "#1f1d1b";
694
+ el.style.color = "#f5f0eb";
695
+ el.style.borderRadius = el.style.borderRadius || "8px";
526
696
  }
527
697
  if (kind !== "image" && typeof s.color === "string") el.style.color = s.color;
528
698
  if (kind === "text" && typeof s.fontSize === "number" && Number.isFinite(s.fontSize)) {
529
699
  el.style.fontSize = `${clampNum(s.fontSize, 8, 72)}px`;
530
700
  }
531
- if (kind === "image" && typeof s.width === "number" && Number.isFinite(s.width)) {
701
+ if (kind === "image" && s.naturalSize === true) {
702
+ el.style.width = "auto";
703
+ el.style.maxWidth = "100%";
704
+ el.style.maxHeight = "none";
705
+ el.style.objectFit = "contain";
706
+ } else if (kind === "image" && typeof s.width === "number" && Number.isFinite(s.width)) {
532
707
  el.style.width = `${clampNum(s.width, 10, 100)}%`;
533
708
  }
534
709
  if (kind === "image" && typeof s.radius === "number" && Number.isFinite(s.radius)) {
@@ -949,7 +1124,161 @@ var GUIDE_STYLES = `
949
1124
  }
950
1125
  /* Card gen\xE9rica del modelo unificado. El dise\xF1o (bg/borde/padding/\u2026) lo aplica
951
1126
  el renderer como estilos inline desde step.design; ac\xE1 van los defaults. */
1127
+ /* Edici\xF3n (builder): sin animaci\xF3n de entrada al repintar la preview. */
1128
+ :host(.veo-no-anim) * { animation: none !important; }
1129
+
1130
+ /* Bloque video: caja con relaci\xF3n de aspecto; el iframe la llena. */
1131
+ .veo-guide-video { position: relative; overflow: hidden; border-radius: 8px; background: #000; }
1132
+ .veo-guide-video iframe, .veo-guide-video video {
1133
+ position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block;
1134
+ }
1135
+
1136
+ /* \u2500\u2500 Layout por filas/columnas (composite) \u2500\u2500
1137
+ El espacio tiene UNA fuente por capa: el aire con el borde lo da el padding
1138
+ de la card; la separaci\xF3n entre filas/bloques la da el margen inferior
1139
+ (12px, ah\xED vive la l\xEDnea \xAB+\xBB del editor); los bloques NO llevan padding
1140
+ propio (solo el que el autor les ponga inline). El \xFAltimo elemento no deja
1141
+ margen colgando. */
1142
+ .veo-card > [data-veo-block], .veo-card > .veo-row, .veo-card > .veo-guide-actions,
1143
+ .veo-col > [data-veo-block], .veo-col > .veo-guide-actions { margin-bottom: 12px; }
1144
+ /* La card y cada columna son columnas flex: cada bloque mide SU contenido (como
1145
+ el bot\xF3n) y se ubica con su alineaci\xF3n (align-self); filas, columnas, formularios
1146
+ y filas de botones ocupan todo el ancho. */
1147
+ .veo-card:not(.veo-card--empty), .veo-col {
1148
+ display: flex; flex-direction: column; align-items: flex-start;
1149
+ }
1150
+ .veo-card > [data-veo-block], .veo-col > [data-veo-block] { max-width: 100%; }
1151
+ /* L\xEDnea horizontal: ocupa todo el ancho; el padding del bloque es solo vertical
1152
+ (zona clickeable en el editor) y la l\xEDnea es el borde superior del hr. */
1153
+ .veo-card > .veo-guide-divider, .veo-col > .veo-guide-divider { padding: 6px 0; align-self: stretch; }
1154
+ .veo-guide-divider { box-sizing: border-box; }
1155
+ /* Bloque de c\xF3digo (modo host): ocupa todo el ancho, como un formulario. */
1156
+ .veo-code-host { display: block; min-width: 0; }
1157
+ .veo-card > .veo-code-host, .veo-col > .veo-code-host { align-self: stretch; }
1158
+ .veo-guide-divider hr { margin: 0; border: 0; height: 0; }
1159
+ .veo-card > .veo-row, .veo-card > .veo-guide-actions, .veo-card > .veo-form,
1160
+ .veo-col > .veo-guide-actions, .veo-col > .veo-form { align-self: stretch; }
1161
+ .veo-row { display: flex; gap: 12px; align-items: flex-start; box-sizing: border-box; }
1162
+ .veo-col { flex: 1 1 0; min-width: 0; }
1163
+ .veo-col > :last-child { margin-bottom: 0; }
1164
+ /* Fila con alineaci\xF3n horizontal (editor de fila): las columnas miden su contenido. */
1165
+ .veo-row--aligned > .veo-col { flex: 0 1 auto; }
1166
+
1167
+ /* \u2500\u2500 Editor in-app: bloque seleccionado + barra de acciones + placeholders \u2500\u2500 */
1168
+ .veo-block-placeholder {
1169
+ border: 1px dashed #b0a9a5; border-radius: 6px; padding: 12px; margin-bottom: 12px;
1170
+ color: #8a8582; font-size: 13px; text-align: center;
1171
+ }
1172
+ /* El bloque seleccionado no lleva contorno propio: lo rodea .veo-block-frame
1173
+ (marco aparte, con aire y alto m\xEDnimo, que no altera el layout). */
1174
+ .veo-block--selected { position: relative; z-index: 1; }
1175
+ .veo-block-frame {
1176
+ position: absolute; z-index: 4; pointer-events: none; box-sizing: border-box;
1177
+ border: 2px solid #3b82f6; border-radius: 10px; background: rgba(59, 130, 246, 0.04);
1178
+ }
1179
+ /* Barra del bloque (mock): p\xEDldora blanca con sombra, solo \xEDconos; la papelera
1180
+ en rojo, separada por una l\xEDnea. */
1181
+ .veo-block-toolbar {
1182
+ position: absolute; z-index: 5; display: inline-flex; align-items: center; gap: 2px; padding: 4px 6px;
1183
+ background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 12px;
1184
+ box-shadow: 0 6px 20px rgba(0,0,0,0.16);
1185
+ }
1186
+ .veo-block-toolbar button {
1187
+ border: 0; background: transparent; color: #4b4642; cursor: pointer; padding: 6px; border-radius: 8px;
1188
+ display: inline-flex; align-items: center; justify-content: center; line-height: 1;
1189
+ }
1190
+ .veo-block-toolbar button:hover { background: #f5efeb; }
1191
+ .veo-block-toolbar button svg { width: 20px; height: 20px; }
1192
+ .veo-block-toolbar .veo-block-toolbar-remove { color: #e5484d; }
1193
+ /* Separadores: antes de la papelera y antes de las flechas de mover. */
1194
+ .veo-block-toolbar .veo-block-toolbar-remove, .veo-block-toolbar .veo-block-toolbar-move-left {
1195
+ margin-left: 6px; position: relative;
1196
+ }
1197
+ .veo-block-toolbar .veo-block-toolbar-remove::before, .veo-block-toolbar .veo-block-toolbar-move-left::before {
1198
+ content: ""; position: absolute; left: -4px; top: 6px; bottom: 6px; width: 1px; background: #e7e2de;
1199
+ }
1200
+ .veo-block-toolbar button:disabled { opacity: 0.3; cursor: default; background: transparent; }
1201
+ /* Contenedor (hover): contorno azul de la tarjeta y dos pesta\xF1as centradas
1202
+ sobre su borde superior: \xABEditar contenedor\xBB (outline) y \xABEditar filas\xBB. */
1203
+ .veo-card-frame {
1204
+ position: absolute; z-index: 3; inset: 0; pointer-events: none; box-sizing: border-box;
1205
+ border: 2px solid #3b82f6; border-radius: inherit;
1206
+ }
1207
+ .veo-card-toolbar {
1208
+ position: absolute; z-index: 6; left: 50%; transform: translateX(-50%);
1209
+ display: inline-flex; align-items: flex-end; gap: 8px; white-space: nowrap;
1210
+ }
1211
+ .veo-card-toolbar button {
1212
+ cursor: pointer; padding: 8px 14px; border-radius: 8px; border: 2px solid #3b82f6;
1213
+ font: 600 14px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
1214
+ background: #fff; color: #3b82f6;
1215
+ }
1216
+ .veo-card-toolbar button:hover { background: #eff6ff; }
1217
+ /* Con fondo = la vista que el panel muestra ahora (contenedor o estructura). */
1218
+ .veo-card-toolbar button.is-active { background: #3b82f6; color: #fff; }
1219
+ .veo-card-toolbar button.is-active:hover { background: #2563eb; border-color: #2563eb; }
1220
+ /* Editor de fila: marco al pasar el mouse por una fila (o la fila del bloque
1221
+ seleccionado) y barra \xABEditar fila \xB7 duplicar\xBB bajo su esquina derecha. */
1222
+ .veo-row-frame {
1223
+ position: absolute; z-index: 3; pointer-events: none; box-sizing: border-box;
1224
+ border: 2px solid #3b82f6; border-radius: 10px;
1225
+ }
1226
+ .veo-row-toolbar {
1227
+ position: absolute; z-index: 5; display: inline-flex; align-items: center; gap: 2px; padding: 4px 6px;
1228
+ background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 12px;
1229
+ box-shadow: 0 6px 20px rgba(0,0,0,0.16); white-space: nowrap;
1230
+ }
1231
+ .veo-row-toolbar button {
1232
+ border: 0; background: transparent; color: #4b4642; cursor: pointer; padding: 6px 8px; border-radius: 8px;
1233
+ display: inline-flex; align-items: center; gap: 8px; line-height: 1;
1234
+ font: 500 14px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
1235
+ }
1236
+ .veo-row-toolbar button:hover { background: #f5efeb; }
1237
+ .veo-row-toolbar button svg { width: 20px; height: 20px; }
1238
+ .veo-row-toolbar .veo-row-toolbar-duplicate { margin-left: 6px; position: relative; }
1239
+ .veo-row-toolbar .veo-row-toolbar-duplicate::before {
1240
+ content: ""; position: absolute; left: -4px; top: 6px; bottom: 6px; width: 1px; background: #e7e2de;
1241
+ }
1242
+ .veo-row-toolbar .veo-row-toolbar-remove { color: #e5484d; }
1243
+ /* Huecos \xAB+\xBB (mock): zona invisible entre elementos; al pasar el mouse aparece
1244
+ la l\xEDnea azul (horizontal = fila / bloque apilado, vertical = columna) con
1245
+ un \xAB+\xBB circular al centro. */
1246
+ .veo-gap { position: absolute; z-index: 4; }
1247
+ .veo-gap i { position: absolute; background: #3b82f6; opacity: 0; transition: opacity 100ms; pointer-events: none; }
1248
+ .veo-gap--h i { left: 0; right: 0; top: 50%; height: 2px; margin-top: -1px; }
1249
+ .veo-gap--v i { top: 0; bottom: 0; left: 50%; width: 2px; margin-left: -1px; }
1250
+ .veo-gap button {
1251
+ position: absolute; left: 50%; top: 50%; margin: -11px 0 0 -11px; box-sizing: border-box;
1252
+ opacity: 0; transition: opacity 100ms; width: 22px; height: 22px; border-radius: 50%; padding: 0;
1253
+ border: 2px solid #fff; background: #3b82f6; color: #fff; cursor: pointer;
1254
+ font: 600 15px/18px system-ui, -apple-system, "Segoe UI", sans-serif; text-align: center;
1255
+ box-shadow: 0 2px 8px rgba(0,0,0,0.25);
1256
+ }
1257
+ .veo-gap:hover i, .veo-gap:hover button, .veo-gap:focus-within button { opacity: 1; }
1258
+ .veo-gap button:hover { background: #2563eb; }
1259
+
1260
+ /* Tarjeta sin bloques (paso reci\xE9n creado). La tarjeta NO impone tama\xF1o ni
1261
+ padding propios (as\xED el ancho/padding del dise\xF1o se ven tal cual); el tama\xF1o
1262
+ m\xEDnimo lo pone la zona punteada azul de la preview. */
1263
+ .veo-card--empty { display: flex; align-items: stretch; }
1264
+ .veo-card-empty-zone {
1265
+ flex: 1; min-width: 0; min-height: 160px; margin-top: 14px;
1266
+ display: flex; align-items: center; justify-content: center; gap: 6px; text-align: center;
1267
+ border: 2px dashed #3b82f6; border-radius: 8px; background: #eff6ff;
1268
+ color: #2563eb; font-size: 16px; font-weight: 500; user-select: none; pointer-events: none;
1269
+ padding: 0 12px; box-sizing: border-box; overflow: hidden;
1270
+ }
1271
+ .veo-card-empty-zone b { font-weight: 600; font-size: 22px; line-height: 1; }
1272
+ /* Sin ancho configurado, la zona da un tama\xF1o c\xF3modo por defecto. */
1273
+ .veo-card--empty:not([style*="width"]) .veo-card-empty-zone { min-width: 360px; }
1274
+ /* Tarjeta angosta (la mide el renderer): solo el \xAB+\xBB, sin texto, y m\xE1s baja. */
1275
+ .veo-card--narrow .veo-card-empty-zone span { display: none; }
1276
+ .veo-card--narrow .veo-card-empty-zone { min-height: 120px; }
952
1277
  .veo-card {
1278
+ /* Contenedor posicionado SIEMPRE: el \u2715 (absolute) se ubica respecto a la
1279
+ tarjeta tambi\xE9n en pasos embebidos (inline), no respecto a la app. Los
1280
+ flotantes lo pisan con position:fixed inline. */
1281
+ position: relative;
953
1282
  pointer-events: auto;
954
1283
  background: var(--veo-bg, #ffffff);
955
1284
  color: var(--veo-text, #1f2937);
@@ -1042,6 +1371,13 @@ var GUIDE_STYLES = `
1042
1371
  transition: opacity 120ms ease;
1043
1372
  }
1044
1373
  .veo-guide-cta:hover { opacity: 0.9; }
1374
+ /* Presets del boton (editor): secundario = contorno; enlace = solo texto. */
1375
+ .veo-guide-cta--secondary {
1376
+ background: transparent; color: var(--veo-primary);
1377
+ border: 1px solid var(--veo-primary);
1378
+ }
1379
+ .veo-guide-cta--link { background: transparent; color: var(--veo-primary); padding: 0; }
1380
+ .veo-guide-cta--link:hover { opacity: 1; text-decoration: underline; }
1045
1381
  .veo-guide-cta:active { transform: translateY(1px); }
1046
1382
  .veo-guide-close {
1047
1383
  position: absolute; top: -6px; right: -6px;
@@ -1072,6 +1408,10 @@ var GUIDE_STYLES = `
1072
1408
 
1073
1409
  .veo-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
1074
1410
  .veo-form-field { display: flex; flex-direction: column; gap: 6px; }
1411
+ /* Pregunta de la encuesta (rich text del bloque form). */
1412
+ .veo-form-question { font-size: 16px; font-weight: 600; line-height: 1.35; color: var(--veo-text); }
1413
+ .veo-form-question p { margin: 0; }
1414
+ .veo-form > .veo-guide-actions { margin: 0; }
1075
1415
  .veo-form-label { font-size: 13px; font-weight: 500; color: var(--veo-text); }
1076
1416
  .veo-form-input {
1077
1417
  font-size: 14px; color: var(--veo-text);
@@ -1080,26 +1420,71 @@ var GUIDE_STYLES = `
1080
1420
  padding: 8px 10px; width: 100%;
1081
1421
  }
1082
1422
  .veo-form-input:focus { outline: 2px solid var(--veo-primary); outline-offset: -1px; border-color: transparent; }
1083
- .veo-form-options { display: flex; flex-direction: column; gap: 6px; }
1423
+ /* Lista de tareas (mock): \xEDtems con anillo de progreso, texto + subt\xEDtulo,
1424
+ y al pie \xABX de N completadas\xBB + porcentaje + barra. */
1425
+ .veo-checklist {
1426
+ display: flex; flex-direction: column; gap: 22px; align-self: stretch; width: 100%;
1427
+ min-width: 360px; box-sizing: border-box;
1428
+ }
1429
+ .veo-checklist-title { font-size: 24px; font-weight: 700; line-height: 1.25; color: var(--veo-cl-title); }
1430
+ .veo-checklist-title p { margin: 0; }
1431
+ .veo-checklist-items { display: flex; flex-direction: column; gap: 22px; }
1432
+ .veo-checklist-item {
1433
+ display: flex; align-items: center; gap: 18px; width: 100%; text-align: left;
1434
+ background: none; border: 0; padding: 0; cursor: pointer; font: inherit; color: inherit;
1435
+ }
1436
+ .veo-checklist-item:hover .veo-checklist-label { text-decoration: underline; }
1437
+ .veo-checklist-ring { width: 44px; height: 44px; flex: 0 0 auto; }
1438
+ .veo-checklist-texts { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
1439
+ .veo-checklist-label {
1440
+ font-size: calc(var(--veo-cl-font-size, 14px) + 4px); font-weight: var(--veo-cl-font-weight, 600);
1441
+ line-height: 1.3; color: var(--veo-cl-title);
1442
+ }
1443
+ .veo-checklist-sub { font-size: calc(var(--veo-cl-font-size, 14px) + 1px); color: var(--veo-cl-subtitle); }
1444
+ .veo-checklist-footer { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }
1445
+ .veo-checklist-summary {
1446
+ display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
1447
+ font-size: calc(var(--veo-cl-font-size, 14px) + 1px);
1448
+ }
1449
+ .veo-checklist-count { color: var(--veo-cl-subtitle); }
1450
+ .veo-checklist-percent { font-weight: 700; font-size: calc(var(--veo-cl-font-size, 14px) + 3px); color: var(--veo-cl-bar); }
1451
+ .veo-checklist-bar { height: var(--veo-cl-bar-h, 8px); background: #e7e2de; border-radius: 999px; overflow: hidden; }
1452
+ .veo-checklist--dark .veo-checklist-bar { background: #3c3734; }
1453
+ .veo-checklist-bar-fill { height: 100%; background: var(--veo-cl-bar); border-radius: 999px; transition: width 300ms ease; }
1454
+ .veo-checklist-empty { margin: 0; font-size: 13px; color: var(--veo-cl-subtitle); }
1455
+ /* Opci\xF3n m\xFAltiple (mock): c\xEDrculos/casillas grandes con su texto al lado. */
1456
+ .veo-form-options { display: flex; flex-direction: column; gap: 14px; }
1084
1457
  .veo-form-option {
1085
- display: flex; align-items: center; gap: 8px;
1086
- font-size: 14px; color: var(--veo-text); cursor: pointer;
1087
- }
1088
- .veo-form-option input { accent-color: var(--veo-primary); margin: 0; cursor: pointer; }
1089
- .veo-form-scale { display: flex; gap: 4px; flex-wrap: wrap; }
1458
+ display: flex; align-items: center; gap: 14px;
1459
+ font-size: 16px; color: var(--veo-text); cursor: pointer;
1460
+ }
1461
+ .veo-form-option input {
1462
+ accent-color: var(--veo-primary); margin: 0; cursor: pointer;
1463
+ width: 26px; height: 26px; flex: 0 0 auto;
1464
+ }
1465
+ /* Escala num\xE9rica (mock): casillas cuadradas; con etiquetas, cada casilla
1466
+ lleva la suya debajo (la primera cuelga a la izquierda, la \xFAltima a la derecha). */
1467
+ .veo-form-scale { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }
1468
+ .veo-scale-item { display: inline-flex; flex-direction: column; align-items: center; gap: 8px; }
1469
+ .veo-scale-item--first { align-items: flex-start; }
1470
+ .veo-scale-item--last { align-items: flex-end; }
1471
+ .veo-scale-label { font-size: 12px; line-height: 1.2; color: #8a8582; white-space: nowrap; }
1090
1472
  .veo-nps-btn {
1091
- min-width: 30px; height: 30px; padding: 0 4px;
1092
- font-size: 13px; font-weight: 500; color: var(--veo-text);
1093
- background: var(--veo-bg); border: 1px solid #d1d5db; border-radius: 6px;
1473
+ min-width: var(--veo-scale-size, 44px); height: var(--veo-scale-size, 44px); padding: 0 6px;
1474
+ font-size: 16px; font-weight: 600; color: var(--veo-text);
1475
+ background: var(--veo-bg); border: 1px solid #d5cec9; border-radius: 8px;
1094
1476
  cursor: pointer; transition: all 100ms ease;
1095
1477
  }
1096
- .veo-nps-btn:hover { border-color: var(--veo-primary); }
1478
+ .veo-nps-btn:hover { border-color: var(--veo-scale-color, var(--veo-primary)); }
1097
1479
  .veo-rating-btn {
1098
1480
  background: none; border: none; padding: 0 2px;
1099
1481
  font-size: 26px; line-height: 1; color: #d1d5db;
1100
1482
  cursor: pointer; transition: color 100ms ease;
1101
1483
  }
1102
- .veo-scale-active.veo-nps-btn { background: var(--veo-primary); color: #fff; border-color: var(--veo-primary); }
1484
+ .veo-scale-active.veo-nps-btn {
1485
+ background: var(--veo-scale-color, var(--veo-primary)); color: #fff;
1486
+ border-color: var(--veo-scale-color, var(--veo-primary));
1487
+ }
1103
1488
  .veo-scale-active.veo-rating-btn { color: #f59e0b; }
1104
1489
  .veo-form-error { font-size: 13px; color: #dc2626; margin: 0; }
1105
1490
  .veo-form-thanks {
@@ -1768,6 +2153,210 @@ function bannerClassOf(position, embedded) {
1768
2153
  return embedded ? "veo-banner veo-banner-embedded" : `veo-banner veo-banner-${position}`;
1769
2154
  }
1770
2155
 
2156
+ // src/plugins/guides/checklist-block.ts
2157
+ var CHECKLIST_DEFAULTS = {
2158
+ iconEmptyColor: "#e7e2de",
2159
+ iconFillColor: "#406a31",
2160
+ barColor: "#406a31",
2161
+ barHeight: 8,
2162
+ fontSize: 14,
2163
+ fontWeight: 600,
2164
+ titleColor: "#2c2825",
2165
+ subtitleColor: "#8a8582",
2166
+ subtitleTemplate: "Completado <%= currentStep %> de <%= totalSteps %> pasos"
2167
+ };
2168
+ var WEIGHTS = { regular: 400, medium: 500, semibold: 600, bold: 700 };
2169
+ function checklistStyle(style) {
2170
+ const s = style ?? {};
2171
+ const str = (k, fb) => typeof s[k] === "string" && s[k].trim() ? s[k] : fb;
2172
+ const num2 = (k, fb, min, max) => typeof s[k] === "number" && Number.isFinite(s[k]) ? Math.min(max, Math.max(min, s[k])) : fb;
2173
+ const weightRaw = s.fontWeight;
2174
+ const fontWeight = typeof weightRaw === "number" ? Math.min(900, Math.max(100, weightRaw)) : typeof weightRaw === "string" && WEIGHTS[weightRaw.toLowerCase()] ? WEIGHTS[weightRaw.toLowerCase()] : CHECKLIST_DEFAULTS.fontWeight;
2175
+ const dark = s.colorMode === "dark";
2176
+ return {
2177
+ colorMode: dark ? "dark" : "light",
2178
+ iconEmptyColor: str("iconEmptyColor", dark ? "#3c3734" : CHECKLIST_DEFAULTS.iconEmptyColor),
2179
+ iconFillColor: str("iconFillColor", CHECKLIST_DEFAULTS.iconFillColor),
2180
+ barColor: str("barColor", CHECKLIST_DEFAULTS.barColor),
2181
+ barHeight: num2("barHeight", CHECKLIST_DEFAULTS.barHeight, 2, 40),
2182
+ fontSize: num2("fontSize", CHECKLIST_DEFAULTS.fontSize, 10, 40),
2183
+ fontWeight,
2184
+ titleColor: str("titleColor", dark ? "#f5efeb" : CHECKLIST_DEFAULTS.titleColor),
2185
+ subtitleColor: str("subtitleColor", dark ? "#b0a9a5" : CHECKLIST_DEFAULTS.subtitleColor),
2186
+ subtitleTemplate: str("subtitleTemplate", CHECKLIST_DEFAULTS.subtitleTemplate)
2187
+ };
2188
+ }
2189
+ function renderSubtitle(template, p) {
2190
+ return template.replace(/<%=\s*currentStep\s*%>/g, String(p.currentStep)).replace(/<%=\s*totalSteps\s*%>/g, String(p.totalSteps));
2191
+ }
2192
+ var RING_R = 10;
2193
+ var RING_C = 2 * Math.PI * RING_R;
2194
+ function buildRing(doc, fraction, completed, st) {
2195
+ const ns = "http://www.w3.org/2000/svg";
2196
+ const svg = doc.createElementNS(ns, "svg");
2197
+ svg.setAttribute("viewBox", "0 0 28 28");
2198
+ svg.setAttribute("class", "veo-checklist-ring");
2199
+ svg.setAttribute("aria-hidden", "true");
2200
+ const track = doc.createElementNS(ns, "circle");
2201
+ track.setAttribute("cx", "14");
2202
+ track.setAttribute("cy", "14");
2203
+ track.setAttribute("r", String(RING_R));
2204
+ track.setAttribute("fill", completed ? st.iconFillColor : "none");
2205
+ track.setAttribute("stroke", st.iconEmptyColor);
2206
+ track.setAttribute("stroke-width", "4.5");
2207
+ svg.appendChild(track);
2208
+ if (completed) {
2209
+ const check = doc.createElementNS(ns, "path");
2210
+ check.setAttribute("d", "M9 14.5l3.2 3.2L19 11");
2211
+ check.setAttribute("fill", "none");
2212
+ check.setAttribute("stroke", "#fff");
2213
+ check.setAttribute("stroke-width", "2.4");
2214
+ check.setAttribute("stroke-linecap", "round");
2215
+ check.setAttribute("stroke-linejoin", "round");
2216
+ svg.appendChild(check);
2217
+ return svg;
2218
+ }
2219
+ const f = Math.max(0, Math.min(1, fraction));
2220
+ if (f > 0) {
2221
+ const arc = doc.createElementNS(ns, "circle");
2222
+ arc.setAttribute("cx", "14");
2223
+ arc.setAttribute("cy", "14");
2224
+ arc.setAttribute("r", String(RING_R));
2225
+ arc.setAttribute("fill", "none");
2226
+ arc.setAttribute("stroke", st.iconFillColor);
2227
+ arc.setAttribute("stroke-width", "4.5");
2228
+ arc.setAttribute("stroke-linecap", "butt");
2229
+ arc.setAttribute("stroke-dasharray", `${(RING_C * f).toFixed(2)} ${RING_C.toFixed(2)}`);
2230
+ arc.setAttribute("transform", "rotate(-90 14 14)");
2231
+ svg.appendChild(arc);
2232
+ }
2233
+ return svg;
2234
+ }
2235
+ function buildChecklistBlock(block, doc, context) {
2236
+ const st = checklistStyle(block.style);
2237
+ const items = (block.items ?? []).filter((i) => i && typeof i.guideId === "string" && i.guideId);
2238
+ const root = doc.createElement("div");
2239
+ root.className = `veo-checklist${st.colorMode === "dark" ? " veo-checklist--dark" : ""}`;
2240
+ root.style.setProperty("--veo-cl-font-size", `${st.fontSize}px`);
2241
+ root.style.setProperty("--veo-cl-font-weight", String(st.fontWeight));
2242
+ root.style.setProperty("--veo-cl-title", st.titleColor);
2243
+ root.style.setProperty("--veo-cl-subtitle", st.subtitleColor);
2244
+ root.style.setProperty("--veo-cl-bar", st.barColor);
2245
+ root.style.setProperty("--veo-cl-bar-h", `${st.barHeight}px`);
2246
+ if (typeof block.html === "string" && block.html || typeof block.text === "string" && block.text) {
2247
+ const title = doc.createElement("div");
2248
+ title.className = "veo-checklist-title";
2249
+ if (typeof block.html === "string" && block.html)
2250
+ title.appendChild(renderRichText(block.html, doc));
2251
+ else title.textContent = block.text;
2252
+ root.appendChild(title);
2253
+ }
2254
+ const list = doc.createElement("div");
2255
+ list.className = "veo-checklist-items";
2256
+ list.setAttribute("role", "list");
2257
+ root.appendChild(list);
2258
+ const footer = doc.createElement("div");
2259
+ footer.className = "veo-checklist-footer";
2260
+ const summary = doc.createElement("div");
2261
+ summary.className = "veo-checklist-summary";
2262
+ const count = doc.createElement("span");
2263
+ count.className = "veo-checklist-count";
2264
+ const percent = doc.createElement("span");
2265
+ percent.className = "veo-checklist-percent";
2266
+ summary.append(count, percent);
2267
+ const bar = doc.createElement("div");
2268
+ bar.className = "veo-checklist-bar";
2269
+ const fill = doc.createElement("div");
2270
+ fill.className = "veo-checklist-bar-fill";
2271
+ bar.appendChild(fill);
2272
+ footer.append(summary, bar);
2273
+ root.appendChild(footer);
2274
+ const paint = (progress) => {
2275
+ list.textContent = "";
2276
+ const visible = progress ? items.filter((i) => progress.has(i.guideId)) : items;
2277
+ let done = 0;
2278
+ for (const item of visible) {
2279
+ const p = progress?.get(item.guideId) ?? null;
2280
+ if (p?.completed) done++;
2281
+ const row = doc.createElement("button");
2282
+ row.type = "button";
2283
+ row.className = `veo-checklist-item${p?.completed ? " veo-checklist-item--done" : ""}`;
2284
+ row.setAttribute("role", "listitem");
2285
+ const fraction = p && p.totalSteps > 0 ? p.currentStep / p.totalSteps : 0;
2286
+ row.appendChild(buildRing(doc, fraction, p?.completed === true, st));
2287
+ const texts = doc.createElement("span");
2288
+ texts.className = "veo-checklist-texts";
2289
+ const label = doc.createElement("span");
2290
+ label.className = "veo-checklist-label";
2291
+ label.textContent = (item.text ?? "").trim() || p?.guideName || "Gu\xEDa";
2292
+ if (p && p.published === false) row.title = "Gu\xEDa no publicada: los usuarios no la ver\xE1n";
2293
+ texts.appendChild(label);
2294
+ if (p) {
2295
+ const sub = doc.createElement("span");
2296
+ sub.className = "veo-checklist-sub";
2297
+ sub.textContent = renderSubtitle(st.subtitleTemplate, p);
2298
+ texts.appendChild(sub);
2299
+ }
2300
+ row.appendChild(texts);
2301
+ row.addEventListener("click", (e) => {
2302
+ e.preventDefault();
2303
+ e.stopPropagation();
2304
+ if (context.isPreview) return;
2305
+ const start = p && !p.completed ? Math.min(p.currentStep, p.totalSteps - 1) : 0;
2306
+ if (context.onOpenGuideAt) context.onOpenGuideAt(item.guideId, start);
2307
+ else context.onOpenGuide?.(item.guideId);
2308
+ });
2309
+ list.appendChild(row);
2310
+ }
2311
+ const total = visible.length;
2312
+ const pct = total > 0 ? Math.round(done / total * 100) : 0;
2313
+ count.textContent = `${done} de ${total} ${total === 1 ? "completada" : "completadas"}`;
2314
+ percent.textContent = `${pct}%`;
2315
+ fill.style.width = `${pct}%`;
2316
+ footer.hidden = total === 0;
2317
+ };
2318
+ if (context.isPreview && items.length === 0) {
2319
+ const sample = /* @__PURE__ */ new Map([
2320
+ [
2321
+ "__sample_1",
2322
+ {
2323
+ guideId: "__sample_1",
2324
+ guideName: "Configura tu primer env\xEDo",
2325
+ totalSteps: 2,
2326
+ currentStep: 1,
2327
+ completed: false
2328
+ }
2329
+ ],
2330
+ [
2331
+ "__sample_2",
2332
+ {
2333
+ guideId: "__sample_2",
2334
+ guideName: "Conecta tu tienda",
2335
+ totalSteps: 2,
2336
+ currentStep: 2,
2337
+ completed: true
2338
+ }
2339
+ ]
2340
+ ]);
2341
+ items.push(
2342
+ { guideId: "__sample_1", text: "Configura tu primer env\xEDo" },
2343
+ { guideId: "__sample_2", text: "Conecta tu tienda" }
2344
+ );
2345
+ paint(sample);
2346
+ const hint = doc.createElement("p");
2347
+ hint.className = "veo-checklist-empty";
2348
+ hint.textContent = "Ejemplo: agrega gu\xEDas a la lista desde Opciones.";
2349
+ root.appendChild(hint);
2350
+ return root;
2351
+ }
2352
+ paint(null);
2353
+ if (context.fetchProgress && items.length > 0) {
2354
+ void context.fetchProgress(items.map((i) => i.guideId)).then((rows) => paint(new Map(rows.map((r) => [r.guideId, r])))).catch(() => {
2355
+ });
2356
+ }
2357
+ return root;
2358
+ }
2359
+
1771
2360
  // src/utils/uuid.ts
1772
2361
  function uuidv7() {
1773
2362
  const timestamp = Date.now();
@@ -1924,6 +2513,7 @@ function escapeClosing(code, tag) {
1924
2513
  }
1925
2514
 
1926
2515
  // src/plugins/guides/code-block.ts
2516
+ var RAW_BLOCK_ATTR = "data-veo-raw-block";
1927
2517
  function insertRawNodes(anchor, nodes, pos) {
1928
2518
  const parent = anchor.parentNode;
1929
2519
  if (pos === "prepend") {
@@ -1970,6 +2560,11 @@ function mountCodeBlock(block, doc, context, host, rawInline) {
1970
2560
  }
1971
2561
  if (rawInline) {
1972
2562
  const rawNodes = Array.from(holder.childNodes);
2563
+ if (rawInline.tagBlockId) {
2564
+ for (const n of rawNodes) {
2565
+ if (n instanceof Element) n.setAttribute(RAW_BLOCK_ATTR, rawInline.tagBlockId);
2566
+ }
2567
+ }
1973
2568
  insertRawNodes(rawInline.anchor, rawNodes, rawInline.pos);
1974
2569
  injected.push(...rawNodes);
1975
2570
  } else if (host) {
@@ -1979,24 +2574,40 @@ function mountCodeBlock(block, doc, context, host, rawInline) {
1979
2574
  injected.push(holder);
1980
2575
  const slot = doc.createElement("slot");
1981
2576
  slot.setAttribute("name", slotName);
1982
- node = slot;
2577
+ const wrapper = doc.createElement("div");
2578
+ wrapper.className = "veo-code-host";
2579
+ wrapper.appendChild(slot);
2580
+ node = wrapper;
1983
2581
  } else {
1984
2582
  node = holder;
1985
2583
  }
1986
2584
  }
2585
+ let cancelled = false;
1987
2586
  if (typeof block.js === "string" && block.js.trim()) {
1988
- const s = hostDoc.createElement("script");
1989
- s.textContent = block.js;
1990
- hostDoc.body.appendChild(s);
1991
- injected.push(s);
2587
+ const js = block.js;
2588
+ const run = () => {
2589
+ if (cancelled) return;
2590
+ const s = hostDoc.createElement("script");
2591
+ s.textContent = wrapUserScript(js);
2592
+ hostDoc.body.appendChild(s);
2593
+ injected.push(s);
2594
+ };
2595
+ if (host && !host.isConnected) queueMicrotask(run);
2596
+ else run();
1992
2597
  }
1993
2598
  return {
1994
2599
  node,
1995
2600
  cleanup: () => {
2601
+ cancelled = true;
1996
2602
  for (const n of injected) n.parentNode?.removeChild(n);
1997
2603
  }
1998
2604
  };
1999
2605
  }
2606
+ function wrapUserScript(js) {
2607
+ return `(function(){try{
2608
+ ${js}
2609
+ }catch(e){console.error("[OnUser] Error en el bloque de c\xF3digo:",e)}})();`;
2610
+ }
2000
2611
  function runScript(old, hostDoc) {
2001
2612
  const s = hostDoc.createElement("script");
2002
2613
  for (const attr of Array.from(old.attributes)) s.setAttribute(attr.name, attr.value);
@@ -2231,8 +2842,14 @@ function buildField(field, doc) {
2231
2842
  wrapper.appendChild(group);
2232
2843
  return { wrapper, read: () => inputs.find((i) => i.input.checked)?.value };
2233
2844
  }
2234
- case "nps":
2235
- return buildScale(wrapper, doc, 0, 10, "veo-nps-btn");
2845
+ case "nps": {
2846
+ const min = typeof field.min === "number" ? field.min : 0;
2847
+ const max = typeof field.max === "number" && field.max > min ? field.max : 10;
2848
+ return buildScale(wrapper, doc, min, max, "veo-nps-btn", void 0, {
2849
+ labels: field.scaleLabels ?? [],
2850
+ showLabels: field.showLabels !== false
2851
+ });
2852
+ }
2236
2853
  case "rating":
2237
2854
  return buildScale(wrapper, doc, 1, 5, "veo-rating-btn", "\u2605");
2238
2855
  default: {
@@ -2245,17 +2862,21 @@ function buildField(field, doc) {
2245
2862
  }
2246
2863
  }
2247
2864
  }
2248
- function buildScale(wrapper, doc, min, max, btnClass, symbol) {
2865
+ function buildScale(wrapper, doc, min, max, btnClass, symbol, opts = { labels: [], showLabels: false }) {
2249
2866
  const row = doc.createElement("div");
2250
2867
  row.className = "veo-form-scale";
2251
2868
  let selected;
2252
2869
  const buttons = [];
2870
+ const labelOf = (n) => opts.labels[n - min]?.trim() ?? "";
2871
+ const anyLabel = opts.showLabels && Array.from({ length: max - min + 1 }, (_, i) => min + i).some((n) => labelOf(n));
2253
2872
  for (let n = min; n <= max; n++) {
2254
2873
  const btn = doc.createElement("button");
2255
2874
  btn.type = "button";
2256
2875
  btn.className = btnClass;
2257
2876
  btn.textContent = symbol ?? String(n);
2258
- btn.setAttribute("aria-label", String(n));
2877
+ const lbl = labelOf(n);
2878
+ btn.setAttribute("aria-label", lbl ? `${n} \xB7 ${lbl}` : String(n));
2879
+ if (lbl) btn.title = lbl;
2259
2880
  btn.addEventListener("click", () => {
2260
2881
  selected = n;
2261
2882
  buttons.forEach((b, i) => {
@@ -2264,11 +2885,127 @@ function buildScale(wrapper, doc, min, max, btnClass, symbol) {
2264
2885
  });
2265
2886
  });
2266
2887
  buttons.push(btn);
2267
- row.appendChild(btn);
2888
+ if (!anyLabel) {
2889
+ row.appendChild(btn);
2890
+ continue;
2891
+ }
2892
+ const item = doc.createElement("div");
2893
+ item.className = `veo-scale-item${n === min ? " veo-scale-item--first" : n === max ? " veo-scale-item--last" : ""}`;
2894
+ const text = doc.createElement("span");
2895
+ text.className = "veo-scale-label";
2896
+ text.textContent = lbl;
2897
+ item.append(btn, text);
2898
+ row.appendChild(item);
2268
2899
  }
2269
2900
  wrapper.appendChild(row);
2270
2901
  return { wrapper, read: () => selected };
2271
2902
  }
2903
+
2904
+ // src/plugins/guides/layout.ts
2905
+ function groupLayout(blocks) {
2906
+ const rows = /* @__PURE__ */ new Map();
2907
+ const order = [];
2908
+ let lastRow = -1;
2909
+ blocks.forEach((block, index) => {
2910
+ if (!block) return;
2911
+ let row;
2912
+ if (typeof block.row === "number" && Number.isFinite(block.row)) {
2913
+ row = block.row;
2914
+ lastRow = row;
2915
+ } else {
2916
+ row = lastRow + 0.5;
2917
+ lastRow = row;
2918
+ }
2919
+ const col = typeof block.col === "number" && Number.isFinite(block.col) ? block.col : 0;
2920
+ let r = rows.get(row);
2921
+ if (!r) {
2922
+ r = { row, columns: [] };
2923
+ rows.set(row, r);
2924
+ order.push(row);
2925
+ }
2926
+ let c = r.columns.find((x) => x.col === col);
2927
+ if (!c) {
2928
+ c = { col, blocks: [] };
2929
+ r.columns.push(c);
2930
+ }
2931
+ c.blocks.push({ block, index });
2932
+ });
2933
+ return order.sort((a, b) => a - b).map((k) => {
2934
+ const r = rows.get(k);
2935
+ r.columns.sort((a, b) => a.col - b.col);
2936
+ return r;
2937
+ });
2938
+ }
2939
+
2940
+ // src/plugins/guides/scroll-lock.ts
2941
+ var states = /* @__PURE__ */ new WeakMap();
2942
+ function lockScroll(doc = document) {
2943
+ const html = doc.documentElement;
2944
+ const body = doc.body;
2945
+ if (!html || !body) return () => {
2946
+ };
2947
+ let state = states.get(doc);
2948
+ if (!state) {
2949
+ state = {
2950
+ count: 0,
2951
+ html: { overflow: html.style.overflow, paddingRight: html.style.paddingRight },
2952
+ body: { overflow: body.style.overflow, paddingRight: body.style.paddingRight }
2953
+ };
2954
+ states.set(doc, state);
2955
+ const win = doc.defaultView;
2956
+ const gap = win ? Math.max(0, win.innerWidth - html.clientWidth) : 0;
2957
+ if (gap > 0) body.style.paddingRight = `${gap}px`;
2958
+ html.style.overflow = "hidden";
2959
+ body.style.overflow = "hidden";
2960
+ }
2961
+ state.count += 1;
2962
+ let released = false;
2963
+ return () => {
2964
+ if (released) return;
2965
+ released = true;
2966
+ const s = states.get(doc);
2967
+ if (!s) return;
2968
+ s.count -= 1;
2969
+ if (s.count > 0) return;
2970
+ states.delete(doc);
2971
+ html.style.overflow = s.html.overflow;
2972
+ html.style.paddingRight = s.html.paddingRight;
2973
+ body.style.overflow = s.body.overflow;
2974
+ body.style.paddingRight = s.body.paddingRight;
2975
+ };
2976
+ }
2977
+
2978
+ // src/plugins/guides/snooze.ts
2979
+ var KEY = (guideId) => `veo:snooze:${guideId}`;
2980
+ var UNIT_MS = {
2981
+ hour: 36e5,
2982
+ day: 864e5,
2983
+ week: 6048e5
2984
+ };
2985
+ function snoozeDurationMs(every, unit) {
2986
+ const n = Number.isFinite(every) && every > 0 ? Math.floor(every) : 1;
2987
+ return n * (UNIT_MS[unit] ?? UNIT_MS.day);
2988
+ }
2989
+ function snoozeGuide(guideId, every, unit, now = Date.now()) {
2990
+ try {
2991
+ localStorage.setItem(KEY(guideId), String(now + snoozeDurationMs(every, unit)));
2992
+ } catch {
2993
+ }
2994
+ }
2995
+ function isSnoozed(guideId, now = Date.now()) {
2996
+ try {
2997
+ const raw = localStorage.getItem(KEY(guideId));
2998
+ if (raw === null) return false;
2999
+ const until = Number(raw);
3000
+ if (!Number.isFinite(until) || until <= now) {
3001
+ localStorage.removeItem(KEY(guideId));
3002
+ return false;
3003
+ }
3004
+ return true;
3005
+ } catch {
3006
+ return false;
3007
+ }
3008
+ }
2272
3009
  var INFO_GLYPH = "M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021";
2273
3010
  var QUESTION_GLYPH = "M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75";
2274
3011
  var STAR_GLYPH = "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z";
@@ -2458,6 +3195,32 @@ var ROW_ALIGN = {
2458
3195
  center: "center",
2459
3196
  right: "flex-end"
2460
3197
  };
3198
+ var ROW_JUSTIFY = { ...ROW_ALIGN, distribute: "space-between" };
3199
+ var ROW_VALIGN = {
3200
+ top: "flex-start",
3201
+ center: "center",
3202
+ bottom: "flex-end"
3203
+ };
3204
+ function applyRowStyle(rowEl, style) {
3205
+ const s = rowEl.style;
3206
+ if (typeof style.background === "string" && style.background) s.background = style.background;
3207
+ for (const side of ["Top", "Right", "Bottom", "Left"]) {
3208
+ const p = style[`padding${side}`];
3209
+ if (typeof p === "number" && Number.isFinite(p))
3210
+ s.setProperty(`padding-${side.toLowerCase()}`, `${p}px`);
3211
+ const m = style[`margin${side}`];
3212
+ if (typeof m === "number" && Number.isFinite(m))
3213
+ s.setProperty(`margin-${side.toLowerCase()}`, `${m}px`);
3214
+ }
3215
+ if (typeof style.minHeight === "number" && Number.isFinite(style.minHeight) && style.minHeight > 0) {
3216
+ s.minHeight = `${style.minHeight}px`;
3217
+ }
3218
+ if (style.align && ROW_JUSTIFY[style.align]) {
3219
+ s.justifyContent = ROW_JUSTIFY[style.align];
3220
+ rowEl.classList.add("veo-row--aligned");
3221
+ }
3222
+ if (style.valign && ROW_VALIGN[style.valign]) s.alignItems = ROW_VALIGN[style.valign];
3223
+ }
2461
3224
  var CompositeRenderer = class extends BaseRenderer {
2462
3225
  async render(context) {
2463
3226
  const idx = context.nav?.stepIndex ?? 0;
@@ -2476,7 +3239,8 @@ var CompositeRenderer = class extends BaseRenderer {
2476
3239
  const rawDoc = anchor.ownerDocument ?? document;
2477
3240
  const { cleanup } = mountCodeBlock(soleCode, rawDoc, context, void 0, {
2478
3241
  anchor,
2479
- pos: inlinePosOf(step.inlinePosition)
3242
+ pos: inlinePosOf(step.inlinePosition),
3243
+ ...context.isPreview && soleCode.editorId ? { tagBlockId: soleCode.editorId } : {}
2480
3244
  });
2481
3245
  this.registerCleanup(cleanup);
2482
3246
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: idx, action: "shown" });
@@ -2489,6 +3253,10 @@ var CompositeRenderer = class extends BaseRenderer {
2489
3253
  const card = doc.createElement("div");
2490
3254
  card.className = "veo-card";
2491
3255
  applyCompositeDesign(card, design);
3256
+ if (typeof design.ariaLabel === "string" && design.ariaLabel.trim()) {
3257
+ card.setAttribute("role", "dialog");
3258
+ card.setAttribute("aria-label", design.ariaLabel.trim());
3259
+ }
2492
3260
  const dismiss = (action, meta) => {
2493
3261
  context.onInteraction({
2494
3262
  guideId: context.guide.guideId,
@@ -2499,6 +3267,28 @@ var CompositeRenderer = class extends BaseRenderer {
2499
3267
  };
2500
3268
  const readers = [];
2501
3269
  this.buildContent(card, step.blocks ?? [], doc, context, idx, readers, host);
3270
+ if (blocks.length === 0) {
3271
+ card.classList.add("veo-card--empty");
3272
+ if (context.isPreview) {
3273
+ const zone = doc.createElement("div");
3274
+ zone.className = "veo-card-empty-zone";
3275
+ const plus = doc.createElement("b");
3276
+ plus.textContent = "+";
3277
+ const text = doc.createElement("span");
3278
+ text.textContent = "Clic para agregar contenido";
3279
+ zone.append(plus, text);
3280
+ card.appendChild(zone);
3281
+ const applyNarrow = () => {
3282
+ card.classList.toggle("veo-card--narrow", card.clientWidth < NARROW_CARD_WIDTH);
3283
+ };
3284
+ if (typeof ResizeObserver !== "undefined") {
3285
+ const ro = new ResizeObserver(applyNarrow);
3286
+ ro.observe(card);
3287
+ this.registerCleanup(() => ro.disconnect());
3288
+ }
3289
+ applyNarrow();
3290
+ }
3291
+ }
2502
3292
  const lastBlock = card.lastElementChild;
2503
3293
  if (lastBlock && !lastBlock.style.marginBottom) lastBlock.style.marginBottom = "0";
2504
3294
  let closeTooltip = null;
@@ -2516,6 +3306,14 @@ var CompositeRenderer = class extends BaseRenderer {
2516
3306
  if (closeColor) closeBtn.style.color = closeColor;
2517
3307
  const closePos = typeof design.closePosition === "string" ? design.closePosition : "top-right";
2518
3308
  applyClosePosition(closeBtn, closePos);
3309
+ for (const side of ["Top", "Right", "Bottom", "Left"]) {
3310
+ const p = design[`closePadding${side}`];
3311
+ if (typeof p === "number")
3312
+ closeBtn.style.setProperty(`padding-${side.toLowerCase()}`, `${p}px`);
3313
+ const m = design[`closeMargin${side}`];
3314
+ if (typeof m === "number")
3315
+ closeBtn.style.setProperty(`margin-${side.toLowerCase()}`, `${m}px`);
3316
+ }
2519
3317
  if (hoverColor) {
2520
3318
  closeBtn.addEventListener("mouseenter", () => {
2521
3319
  closeBtn.style.color = hoverColor;
@@ -2525,6 +3323,7 @@ var CompositeRenderer = class extends BaseRenderer {
2525
3323
  });
2526
3324
  }
2527
3325
  card.appendChild(closeBtn);
3326
+ reserveCloseSpace(card, blocks, closePos, design);
2528
3327
  }
2529
3328
  if (badgeMode && anchor) {
2530
3329
  closeTooltip = this.renderBadgeMode(
@@ -2555,6 +3354,7 @@ var CompositeRenderer = class extends BaseRenderer {
2555
3354
  const bz = typeof design.backdropZIndex === "number" ? design.backdropZIndex : 2e5;
2556
3355
  bg.style.zIndex = String(bz);
2557
3356
  root.appendChild(bg);
3357
+ this.registerCleanup(lockScroll(doc));
2558
3358
  if (design.targetAbove === true) this.registerCleanup(liftTarget(anchor, bz + 1));
2559
3359
  }
2560
3360
  const wrapper = doc.createElement("div");
@@ -2581,6 +3381,7 @@ var CompositeRenderer = class extends BaseRenderer {
2581
3381
  this.closeOrSkip(context);
2582
3382
  }
2583
3383
  });
3384
+ this.registerCleanup(lockScroll(doc));
2584
3385
  }
2585
3386
  applyViewportPosition(card, step.position, design);
2586
3387
  overlay.appendChild(card);
@@ -2598,84 +3399,218 @@ var CompositeRenderer = class extends BaseRenderer {
2598
3399
  ...block.id ? { metadata: { buttonId: block.id } } : {}
2599
3400
  });
2600
3401
  };
2601
- let i = 0;
2602
- while (i < blocks.length) {
2603
- const b = blocks[i];
2604
- if (!b) {
2605
- i++;
2606
- continue;
2607
- }
2608
- if (b.type === "button") {
2609
- const row = doc.createElement("div");
2610
- row.className = "veo-guide-actions";
2611
- let rowAlign;
2612
- while (i < blocks.length && blocks[i]?.type === "button") {
2613
- const bb = blocks[i];
2614
- if (!rowAlign) {
2615
- const a = bb.style?.align;
2616
- if (a === "left" || a === "center" || a === "right") rowAlign = a;
3402
+ const tag = (node, block, index) => {
3403
+ node.dataset.veoBlock = String(index);
3404
+ if (block.editorId) node.dataset.veoBlockId = block.editorId;
3405
+ return node;
3406
+ };
3407
+ const placeholder = (block, label) => {
3408
+ if (!context.isPreview || !block.editorId) return null;
3409
+ const el = doc.createElement("div");
3410
+ el.className = "veo-block-placeholder";
3411
+ el.textContent = label;
3412
+ return el;
3413
+ };
3414
+ const buildSequence = (container, cell) => {
3415
+ let i = 0;
3416
+ while (i < cell.length) {
3417
+ const { block: b, index } = cell[i];
3418
+ if (b.type === "button") {
3419
+ const row = doc.createElement("div");
3420
+ row.className = "veo-guide-actions";
3421
+ let rowAlign;
3422
+ while (i < cell.length && cell[i]?.block.type === "button") {
3423
+ const { block: bb, index: bi } = cell[i];
3424
+ if (!rowAlign) {
3425
+ const a = bb.style?.align;
3426
+ if (a === "left" || a === "center" || a === "right") rowAlign = a;
3427
+ }
3428
+ row.appendChild(tag(this.buildButton(bb, doc, context, idx, readers, emit), bb, bi));
3429
+ i++;
2617
3430
  }
2618
- row.appendChild(this.buildButton(bb, doc, context, idx, readers, emit));
3431
+ if (rowAlign) row.style.justifyContent = ROW_ALIGN[rowAlign] ?? "flex-end";
3432
+ container.appendChild(row);
3433
+ continue;
3434
+ }
3435
+ if (b.type === "code") {
3436
+ const { node: node2, cleanup } = mountCodeBlock(b, doc, context, host);
3437
+ const el = node2 ?? placeholder(b, "Bloque de c\xF3digo");
3438
+ if (el) container.appendChild(tag(el, b, index));
3439
+ this.registerCleanup(cleanup);
2619
3440
  i++;
3441
+ continue;
2620
3442
  }
2621
- if (rowAlign) row.style.justifyContent = ROW_ALIGN[rowAlign] ?? "flex-end";
2622
- card.appendChild(row);
2623
- continue;
2624
- }
2625
- if (b.type === "code") {
2626
- const { node: node2, cleanup } = mountCodeBlock(b, doc, context, host);
2627
- if (node2) card.appendChild(node2);
2628
- this.registerCleanup(cleanup);
2629
- i++;
2630
- continue;
2631
- }
2632
- if (b.type === "form") {
2633
- const form = doc.createElement("div");
2634
- form.className = "veo-form";
2635
- for (const field of b.fields ?? []) {
2636
- const { wrapper, read } = buildField(field, doc);
2637
- readers.push({ field, read });
2638
- form.appendChild(wrapper);
3443
+ if (b.type === "form") {
3444
+ container.appendChild(tag(this.buildForm(b, doc, context, idx, readers, emit), b, index));
3445
+ i++;
3446
+ continue;
3447
+ }
3448
+ if (b.type === "checklist") {
3449
+ const el = buildChecklistBlock(b, doc, context);
3450
+ applyBlockStyle(el, b.style, "text");
3451
+ el.style.alignSelf = "stretch";
3452
+ el.style.width = "100%";
3453
+ container.appendChild(tag(el, b, index));
3454
+ i++;
3455
+ continue;
2639
3456
  }
2640
- card.appendChild(form);
3457
+ const node = buildContentBlock(b, doc, { editing: context.isPreview === true }) ?? placeholder(
3458
+ b,
3459
+ b.type === "image" ? "Imagen (sin URL)" : b.type === "video" ? "Video (elige proveedor y pega la URL)" : "Bloque vac\xEDo"
3460
+ );
3461
+ if (node) container.appendChild(tag(node, b, index));
2641
3462
  i++;
3463
+ }
3464
+ };
3465
+ const rowStyles = context.guide.guideSteps[idx]?.rows;
3466
+ for (const row of groupLayout(blocks)) {
3467
+ const rowStyle = Number.isInteger(row.row) ? rowStyles?.[String(row.row)] : void 0;
3468
+ if (row.columns.length === 1 && !rowStyle) {
3469
+ buildSequence(card, row.columns[0].blocks);
2642
3470
  continue;
2643
3471
  }
2644
- const node = buildContentBlock(b, doc);
2645
- if (node) card.appendChild(node);
2646
- i++;
3472
+ const rowEl = doc.createElement("div");
3473
+ rowEl.className = "veo-row";
3474
+ if (Number.isInteger(row.row)) rowEl.dataset.veoRow = String(row.row);
3475
+ for (const column of row.columns) {
3476
+ const colEl = doc.createElement("div");
3477
+ colEl.className = "veo-col";
3478
+ buildSequence(colEl, column.blocks);
3479
+ rowEl.appendChild(colEl);
3480
+ }
3481
+ if (rowStyle) applyRowStyle(rowEl, rowStyle);
3482
+ card.appendChild(rowEl);
2647
3483
  }
2648
3484
  }
2649
3485
  /** Botón con enrutado de acción (navegación / cierre / url / guía / envío de forms). */
2650
- buildButton(block, doc, context, idx, readers, emit) {
3486
+ /**
3487
+ * Bloque `form` (encuesta): pregunta (rich text del bloque) + campos + botón
3488
+ * de envío propio (`submit`). Con pregunta, el label del campo se omite (sería
3489
+ * la misma pregunta dos veces) y pasa a `aria-label` del input. El estilo del
3490
+ * bloque viste la pregunta (preset/alineación/color); los márgenes van al
3491
+ * contenedor del bloque entero.
3492
+ */
3493
+ buildForm(b, doc, context, idx, readers, emit) {
3494
+ const form = doc.createElement("div");
3495
+ form.className = "veo-form";
3496
+ const style = b.style ?? {};
3497
+ const { marginTop, marginBottom, ...questionStyle } = style;
3498
+ for (const [side, v] of [
3499
+ ["marginTop", marginTop],
3500
+ ["marginBottom", marginBottom]
3501
+ ]) {
3502
+ if (typeof v === "number" && Number.isFinite(v)) form.style[side] = `${v}px`;
3503
+ }
3504
+ let question = null;
3505
+ if (typeof b.html === "string" && b.html || typeof b.text === "string" && b.text) {
3506
+ question = doc.createElement("div");
3507
+ question.className = "veo-form-question";
3508
+ if (typeof b.html === "string" && b.html) question.appendChild(renderRichText(b.html, doc));
3509
+ else question.textContent = b.text;
3510
+ applyBlockStyle(question, questionStyle, "text");
3511
+ form.appendChild(question);
3512
+ }
3513
+ const questionText = question?.textContent?.trim() ?? "";
3514
+ const own = [];
3515
+ for (const field of b.fields ?? []) {
3516
+ const { wrapper, read } = buildField(field, doc);
3517
+ const st = b.style ?? {};
3518
+ if (typeof st.scaleColor === "string" && st.scaleColor) {
3519
+ wrapper.style.setProperty("--veo-scale-color", st.scaleColor);
3520
+ }
3521
+ if (typeof st.scaleSize === "number" && st.scaleSize >= 24 && st.scaleSize <= 96) {
3522
+ wrapper.style.setProperty("--veo-scale-size", `${st.scaleSize}px`);
3523
+ }
3524
+ if (question) {
3525
+ wrapper.querySelector(".veo-form-label")?.remove();
3526
+ if (questionText) {
3527
+ for (const inp of wrapper.querySelectorAll("input, textarea, select")) {
3528
+ inp.setAttribute("aria-label", questionText);
3529
+ }
3530
+ }
3531
+ }
3532
+ const reader = { field, read, blockId: b.id ?? null };
3533
+ readers.push(reader);
3534
+ own.push(reader);
3535
+ form.appendChild(wrapper);
3536
+ }
3537
+ if (b.submit) {
3538
+ const actions = doc.createElement("div");
3539
+ actions.className = "veo-guide-actions";
3540
+ const submit = { ...b.submit, type: "button" };
3541
+ const sa = submit.style?.align;
3542
+ if (sa === "left" || sa === "center" || sa === "right") {
3543
+ actions.style.justifyContent = ROW_ALIGN[sa] ?? "flex-end";
3544
+ }
3545
+ actions.appendChild(this.buildButton(submit, doc, context, idx, readers, emit, own));
3546
+ form.appendChild(actions);
3547
+ }
3548
+ return form;
3549
+ }
3550
+ buildButton(block, doc, context, idx, readers, emit, submitFirst) {
2651
3551
  const btn = doc.createElement("button");
2652
3552
  btn.type = "button";
2653
3553
  btn.className = "veo-guide-cta";
2654
3554
  btn.textContent = block.text?.trim() || "OK";
3555
+ const st = block.style ?? {};
3556
+ if (st.preset === "secondary" || st.preset === "link") {
3557
+ btn.classList.add(`veo-guide-cta--${st.preset}`);
3558
+ }
3559
+ if (typeof st.ariaLabel === "string" && st.ariaLabel.trim()) {
3560
+ btn.setAttribute("aria-label", st.ariaLabel.trim());
3561
+ }
2655
3562
  applyBlockStyle(btn, block.style, "button");
2656
3563
  const nav = context.nav?.callbacks;
2657
- btn.addEventListener("click", () => {
2658
- switch (block.action) {
3564
+ const runSimple = (spec) => {
3565
+ switch (spec.action) {
2659
3566
  case "next":
2660
- emit(block);
2661
3567
  if (nav) nav.onNext();
2662
3568
  else context.onClose();
2663
3569
  break;
2664
3570
  case "prev":
2665
- emit(block);
2666
3571
  nav?.onBack();
2667
3572
  break;
3573
+ case "goto":
3574
+ if (nav?.onGoto && typeof spec.targetStep === "number") nav.onGoto(spec.targetStep);
3575
+ else context.onClose();
3576
+ break;
3577
+ case "snooze":
3578
+ snoozeGuide(
3579
+ context.guide.guideId,
3580
+ typeof spec.snoozeEvery === "number" ? spec.snoozeEvery : 1,
3581
+ spec.snoozeUnit ?? "day"
3582
+ );
3583
+ context.onClose();
3584
+ break;
2668
3585
  case "url":
2669
- emit(block);
2670
- if (typeof block.url === "string" && isSafeUrl(block.url)) {
2671
- window.open(block.url, "_blank", "noopener,noreferrer");
3586
+ if (typeof spec.url === "string" && isSafeUrl(spec.url)) {
3587
+ if (spec.newTab === false) window.location.assign(spec.url);
3588
+ else window.open(spec.url, "_blank", "noopener,noreferrer");
2672
3589
  }
2673
3590
  break;
2674
3591
  case "guide":
2675
- emit(block);
2676
- if (block.targetGuideId) context.onOpenGuide?.(block.targetGuideId);
3592
+ if (spec.targetGuideId) context.onOpenGuide?.(spec.targetGuideId);
2677
3593
  this.closeOrSkip(context);
2678
3594
  break;
3595
+ default:
3596
+ this.closeOrSkip(context);
3597
+ }
3598
+ };
3599
+ const runSecond = () => {
3600
+ if (block.secondAction?.action) runSimple(block.secondAction);
3601
+ };
3602
+ const runActions = () => {
3603
+ switch (block.action) {
3604
+ case "next":
3605
+ case "prev":
3606
+ case "goto":
3607
+ case "snooze":
3608
+ case "url":
3609
+ case "guide":
3610
+ emit(block);
3611
+ runSimple(block);
3612
+ runSecond();
3613
+ break;
2679
3614
  case "submit-forms":
2680
3615
  case "submit-forms-advance":
2681
3616
  case "submit-forms-goto":
@@ -2693,7 +3628,12 @@ var CompositeRenderer = class extends BaseRenderer {
2693
3628
  default:
2694
3629
  emit(block);
2695
3630
  this.closeOrSkip(context);
3631
+ runSecond();
2696
3632
  }
3633
+ };
3634
+ btn.addEventListener("click", () => {
3635
+ if (submitFirst) this.submitForms(submitFirst, context, idx, btn, runActions);
3636
+ else runActions();
2697
3637
  });
2698
3638
  return btn;
2699
3639
  }
@@ -2712,17 +3652,27 @@ var CompositeRenderer = class extends BaseRenderer {
2712
3652
  }
2713
3653
  answers[field.key] = value;
2714
3654
  }
2715
- if (readers.length > 0 && Object.keys(answers).length === 0) return;
3655
+ if (Object.keys(answers).length === 0) {
3656
+ onDone();
3657
+ return;
3658
+ }
2716
3659
  btn.disabled = true;
2717
3660
  const submitFn = context.onFormSubmit ?? (() => Promise.resolve(true));
2718
- void submitFn(answers).then((ok) => {
3661
+ const blockIds = new Set(
3662
+ readers.filter((r) => answers[r.field.key] !== void 0).map((r) => r.blockId ?? "")
3663
+ );
3664
+ const pollId = blockIds.size === 1 ? [...blockIds][0] || void 0 : void 0;
3665
+ void submitFn(answers, { ...pollId ? { pollId } : {}, stepIndex: idx }).then((ok) => {
2719
3666
  btn.disabled = false;
2720
3667
  if (!ok) return;
2721
- context.onInteraction({
2722
- guideId: context.guide.guideId,
2723
- stepIndex: idx,
2724
- action: "completed"
2725
- });
3668
+ const lastStep = !context.nav || idx >= context.nav.totalSteps - 1;
3669
+ if (lastStep) {
3670
+ context.onInteraction({
3671
+ guideId: context.guide.guideId,
3672
+ stepIndex: idx,
3673
+ action: "completed"
3674
+ });
3675
+ }
2726
3676
  onDone();
2727
3677
  });
2728
3678
  }
@@ -2836,21 +3786,38 @@ function inlinePosOf(p) {
2836
3786
  function applyViewportPosition(card, position, design) {
2837
3787
  const s = card.style;
2838
3788
  s.position = "fixed";
2839
- const M = "16px";
3789
+ const BASE = 16;
3790
+ const m = (k) => typeof design[k] === "number" && Number.isFinite(design[k]) ? design[k] : 0;
3791
+ s.margin = "0";
2840
3792
  const hAlign = design.hAlign === "left" ? "left" : design.hAlign === "right" ? "right" : null;
2841
3793
  const v = position.includes("top") ? "top" : position.includes("bottom") ? "bottom" : "middle";
2842
3794
  const h = hAlign ?? (position.includes("left") ? "left" : position.includes("right") ? "right" : "middle");
2843
- if (v === "top") s.top = M;
2844
- else if (v === "bottom") s.bottom = M;
3795
+ if (v === "top") s.top = `${BASE + m("marginTop")}px`;
3796
+ else if (v === "bottom") s.bottom = `${BASE + m("marginBottom")}px`;
2845
3797
  else s.top = "50%";
2846
- if (h === "left") s.left = M;
2847
- else if (h === "right") s.right = M;
3798
+ if (h === "left") s.left = `${BASE + m("marginLeft")}px`;
3799
+ else if (h === "right") s.right = `${BASE + m("marginRight")}px`;
2848
3800
  else s.left = "50%";
2849
- const tx = h === "middle" ? "-50%" : "0";
2850
- const ty = v === "middle" ? "-50%" : "0";
3801
+ const dx = m("marginLeft") - m("marginRight");
3802
+ const dy = m("marginTop") - m("marginBottom");
3803
+ const tx = h === "middle" ? `calc(-50% + ${dx}px)` : "0";
3804
+ const ty = v === "middle" ? `calc(-50% + ${dy}px)` : "0";
2851
3805
  s.transform = tx === "0" && ty === "0" ? "" : `translate(${tx}, ${ty})`;
2852
3806
  }
2853
3807
  var CLOSE_INSET = "10px";
3808
+ var CLOSE_RESERVE_PX = 42;
3809
+ var CARD_DEFAULT_PAD_PX = 20;
3810
+ function reserveCloseSpace(card, blocks, position, design) {
3811
+ if (blocks.length === 0) return;
3812
+ const side = position.includes("bottom") ? "Bottom" : "Top";
3813
+ const edgeBlock = side === "Top" ? blocks[0] : blocks[blocks.length - 1];
3814
+ if (edgeBlock?.type === "image") return;
3815
+ const margin = design[`closeMargin${side}`];
3816
+ const need = CLOSE_RESERVE_PX + (typeof margin === "number" ? margin : 0);
3817
+ const current = design[`padding${side}`];
3818
+ const have = typeof current === "number" ? current : CARD_DEFAULT_PAD_PX;
3819
+ if (have < need) card.style.setProperty(`padding-${side.toLowerCase()}`, `${need}px`);
3820
+ }
2854
3821
  function applyClosePosition(btn, position) {
2855
3822
  const s = btn.style;
2856
3823
  s.top = "auto";
@@ -2915,10 +3882,17 @@ async function anchorPosition(anchor, wrapper, caret, preferred = "bottom") {
2915
3882
  window.removeEventListener("resize", reposition);
2916
3883
  };
2917
3884
  }
3885
+ var MIN_CARD_WIDTH = 120;
3886
+ var NARROW_CARD_WIDTH = 260;
2918
3887
  function applyCompositeDesign(card, design) {
2919
3888
  const s = card.style;
2920
3889
  const px = (v) => typeof v === "number" && Number.isFinite(v) ? `${v}px` : null;
2921
3890
  const strv = (v) => typeof v === "string" && v ? v : null;
3891
+ if (design.colorMode === "dark") {
3892
+ s.background = "#1f1d1b";
3893
+ s.color = "#f5f0eb";
3894
+ card.classList.add("veo-card--dark");
3895
+ }
2922
3896
  if (strv(design.background)) s.background = design.background;
2923
3897
  const textColor = strv(design.color) ?? strv(design.textColor);
2924
3898
  if (textColor) s.color = textColor;
@@ -2940,13 +3914,14 @@ function applyCompositeDesign(card, design) {
2940
3914
  const n = (k) => typeof design[k] === "number" ? design[k] : 0;
2941
3915
  s.boxShadow = `${n("shadowX")}px ${n("shadowY")}px ${n("shadowBlur")}px ${n("shadowSpread")}px ${strv(design.shadowColor) ?? "rgba(0,0,0,0.2)"}`;
2942
3916
  }
3917
+ const width = typeof design.width === "number" && Number.isFinite(design.width) ? `${Math.max(MIN_CARD_WIDTH, design.width)}px` : null;
2943
3918
  if (design.fullWidth === true) {
2944
3919
  s.width = "100%";
2945
3920
  } else if (design.widthType === "fluid") {
2946
- s.width = "auto";
2947
- if (px(design.width)) s.maxWidth = px(design.width);
2948
- } else if (px(design.width)) {
2949
- s.width = px(design.width);
3921
+ s.width = "100%";
3922
+ if (width) s.maxWidth = width;
3923
+ } else if (width) {
3924
+ s.width = width;
2950
3925
  }
2951
3926
  for (const side of ["Top", "Right", "Bottom", "Left"]) {
2952
3927
  const p = px(design[`padding${side}`]);
@@ -3050,6 +4025,7 @@ var FormRenderer = class extends BaseRenderer {
3050
4025
  mountFormContent(card, context, doc);
3051
4026
  overlay.appendChild(card);
3052
4027
  root.appendChild(overlay);
4028
+ this.registerCleanup(lockScroll(doc));
3053
4029
  const dismiss = () => {
3054
4030
  context.onInteraction({ guideId: context.guide.guideId, stepIndex: 0, action: "dismissed" });
3055
4031
  context.onClose();
@@ -3177,6 +4153,18 @@ var ModalRenderer = class extends BaseRenderer {
3177
4153
  constructor() {
3178
4154
  super(...arguments);
3179
4155
  this.overlay = null;
4156
+ /** Suelta el bloqueo de scroll del backdrop (null = sin backdrop). */
4157
+ this.releaseScroll = null;
4158
+ }
4159
+ /** Con backdrop la app de atrás queda congelada; sin él, scrollea normal. */
4160
+ syncScrollLock(step) {
4161
+ const wantLock = step.style?.backdrop !== false;
4162
+ if (wantLock && !this.releaseScroll) {
4163
+ this.releaseScroll = lockScroll(this.host?.ownerDocument ?? document);
4164
+ } else if (!wantLock && this.releaseScroll) {
4165
+ this.releaseScroll();
4166
+ this.releaseScroll = null;
4167
+ }
3180
4168
  }
3181
4169
  render(context) {
3182
4170
  const idx = context.nav?.stepIndex ?? 0;
@@ -3188,6 +4176,7 @@ var ModalRenderer = class extends BaseRenderer {
3188
4176
  const overlay = ownerDocument.createElement("div");
3189
4177
  overlay.className = overlayClassOf(step);
3190
4178
  this.overlay = overlay;
4179
+ this.syncScrollLock(step);
3191
4180
  const card = ownerDocument.createElement("div");
3192
4181
  card.className = "veo-modal-card";
3193
4182
  const dismiss = (action, ctaUrl, meta) => {
@@ -3245,9 +4234,12 @@ var ModalRenderer = class extends BaseRenderer {
3245
4234
  }
3246
4235
  onLiveUpdate(step) {
3247
4236
  if (this.overlay) this.overlay.className = overlayClassOf(step);
4237
+ this.syncScrollLock(step);
3248
4238
  }
3249
4239
  destroy() {
3250
4240
  this.overlay = null;
4241
+ this.releaseScroll?.();
4242
+ this.releaseScroll = null;
3251
4243
  super.destroy();
3252
4244
  }
3253
4245
  };
@@ -3473,6 +4465,7 @@ var GuidePreviewController = class {
3473
4465
  // Preview de forms: por defecto simula el envío SIN escribir props;
3474
4466
  // el caller puede pasar `formSubmit` real (link de preview).
3475
4467
  onFormSubmit: this.input?.formSubmit ?? (() => Promise.resolve(true)),
4468
+ ...this.input?.fetchProgress ? { fetchProgress: this.input.fetchProgress } : {},
3476
4469
  isPreview: true,
3477
4470
  ...this.input?.formSubmit && this.input.formSubmitNote !== void 0 ? { previewNote: this.input.formSubmitNote } : {}
3478
4471
  });
@@ -3658,9 +4651,35 @@ function matchesUrl(matcher, pageUrl) {
3658
4651
  }
3659
4652
  }
3660
4653
  function stepMatchesPage(loc, pageUrl) {
3661
- if (!loc || loc.scope === "sitewide" || !loc.pattern) return true;
4654
+ if (!loc || loc.scope === "sitewide") return true;
4655
+ if (loc.scope === "pages") {
4656
+ const rules = loc.rules ?? [];
4657
+ return rules.some((r) => pageRuleMatches(r, pageUrl));
4658
+ }
4659
+ if (!loc.pattern) return true;
3662
4660
  return matchesUrl({ type: loc.matchType ?? "prefix", pattern: loc.pattern }, pageUrl);
3663
4661
  }
4662
+ function pageRuleMatches(rule, pageUrl) {
4663
+ const pattern = rule.pattern.toLowerCase();
4664
+ const candidates = urlCandidates(pageUrl).map((c) => c.toLowerCase());
4665
+ switch (rule.type) {
4666
+ case "exact":
4667
+ return candidates.some((c) => c === pattern);
4668
+ case "prefix":
4669
+ return candidates.some((c) => c.startsWith(pattern));
4670
+ case "contains":
4671
+ return candidates.some((c) => c.includes(pattern));
4672
+ case "regex":
4673
+ try {
4674
+ const re = new RegExp(rule.pattern, "i");
4675
+ return candidates.some((c) => re.test(c));
4676
+ } catch {
4677
+ return false;
4678
+ }
4679
+ default:
4680
+ return false;
4681
+ }
4682
+ }
3664
4683
 
3665
4684
  // src/plugins/autocapture/constants.ts
3666
4685
  var DEFAULT_AUTOCAPTURE_CONFIG = {
@@ -3795,6 +4814,6 @@ function buildSelectorPath(element, maxAncestors = 5) {
3795
4814
  return parts.join(" > ");
3796
4815
  }
3797
4816
 
3798
- export { ALWAYS_BLOCK_SELECTORS, BadgeRenderer, BannerRenderer, CompositeRenderer, CustomRenderer, DEFAULT_AUTOCAPTURE_CONFIG, DEFAULT_TRACKER_BATCH_SIZE, DEFAULT_TRACKER_FLUSH_INTERVAL_MS, DEFAULT_TRACKER_MAX_RETRIES, FREQUENCY_CACHE_KEY_PREFIX, FREQUENCY_CACHE_MAX_ENTRIES, FREQUENCY_CACHE_TTL_MS, FormRenderer, InlineCustomRenderer, InlineFormRenderer, InlineRenderer, MAX_ANCESTORS, ModalRenderer, PRIORITY_ATTRIBUTES, SENSITIVE_ATTRIBUTES, TooltipRenderer, WALKTHROUGH_ABANDONMENT_TIMEOUT_MS, WALKTHROUGH_STATE_KEY_PREFIX, WalkthroughRenderer, activationDelayMs, buildSelectorPath, clearBuilderToken, clearPreviewToken, closeGuidePreview, filterHumanClasses, getPreviewToken, hasDocument, hasLocalStorage, hasNavigatorSendBeacon, hasWindow, isAutomaticActivation, isBuilderMode, isDeviceAllowed, previewGuide, resolveBuilderContext, resolveBuilderToken, stepMatchesPage, uuidv7 };
3799
- //# sourceMappingURL=chunk-W4R4XCWL.mjs.map
3800
- //# sourceMappingURL=chunk-W4R4XCWL.mjs.map
4817
+ export { ALWAYS_BLOCK_SELECTORS, BadgeRenderer, BannerRenderer, CompositeRenderer, CustomRenderer, DEFAULT_AUTOCAPTURE_CONFIG, DEFAULT_TRACKER_BATCH_SIZE, DEFAULT_TRACKER_FLUSH_INTERVAL_MS, DEFAULT_TRACKER_MAX_RETRIES, FREQUENCY_CACHE_KEY_PREFIX, FREQUENCY_CACHE_MAX_ENTRIES, FREQUENCY_CACHE_TTL_MS, FormRenderer, GUIDE_STYLES, InlineCustomRenderer, InlineFormRenderer, InlineRenderer, MAX_ANCESTORS, ModalRenderer, PRIORITY_ATTRIBUTES, RAW_BLOCK_ATTR, SENSITIVE_ATTRIBUTES, TooltipRenderer, WALKTHROUGH_ABANDONMENT_TIMEOUT_MS, WALKTHROUGH_STATE_KEY_PREFIX, WalkthroughRenderer, activationDelayMs, applyBlockStyle, buildContentBlock, buildSelectorPath, clearBuilderToken, clearPreviewToken, closeGuidePreview, filterHumanClasses, getPreviewToken, groupLayout, hasDocument, hasLocalStorage, hasNavigatorSendBeacon, hasWindow, isAutomaticActivation, isBuilderMode, isDeviceAllowed, isSnoozed, previewGuide, resolveBuilderContext, resolveBuilderToken, stepMatchesPage, uuidv7 };
4818
+ //# sourceMappingURL=chunk-O6USA4TU.mjs.map
4819
+ //# sourceMappingURL=chunk-O6USA4TU.mjs.map