Integrar rama de editor y toolbar: jerarquía, foco seguro y autoPulsar estable

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
urieljareth
2026-09-27 21:33:16 -06:00
co-authored by Claude Opus 5.5
6 changed files with 762 additions and 134 deletions
+10 -4
View File
@@ -54,7 +54,8 @@ de `WAM.svgPlaceholder(texto)`. Botones con `url` no vacía se renderizan como `
`WAM.normalizar(o)` rellena campos faltantes por tipo y devuelve el estado válido (import/upgrade).
Campo opcional `autoPulsar` (entero ≥ 0) en `template`/`buttons`/`list`/`carousel` (también en respuestas
anidadas): índice del botón, de la fila aplanada de la lista o de la card que «Reproducir demo» pulsa sola
~1 s después de revelar el mensaje. `normalizar` lo omite si no es válido.
~1 s después de revelar el mensaje. `normalizar` lo omite si no es válido. El editor lo mantiene al
reordenar o borrar botones / filas / cards (sigue al elemento; se quita si se borró el pulsado).
**Guion frente a sesión.** `estado.mensajes` es el guion (persistente). Lo que se pulsa o escribe en el
teléfono es la sesión: `WAM.sesion` (array en memoria, IDs `s1`, `s2`…), pintada con `anadirMensajeDOM`
@@ -124,7 +125,8 @@ cancela demo y flujos (interactivity.js envuelve `WAM.render`).
nombre = slug de la demo guardada activa o de `cuenta.nombre`, p. ej. `cafe-aurora.json`; `wam-demo.json` si vacío)
- `WAM.importarJSON(archivo)` → Promise<void> (parse + `normalizar` sin reservar los IDs del estado vivo + guardar +
re-render total; deja punto de deshacer)
- `WAM.toast(texto, {tipo:"error", ms, accion:{texto, fn}})` → fn cerrar: aviso `#ed-toasts` fijo abajo a la izquierda
- `WAM.toast(texto, {tipo:"error", ms, accion:{texto, fn}})` → fn cerrar: aviso `#ed-toasts` fijo abajo, centrado
sobre la vista previa. Solo hay UN aviso con acción a la vista: uno nuevo con acción retira los anteriores con acción
- `WAM.enfocarEnChat(id)` → bool: centra y resalta 1,2 s (`.ed-resaltado`) la burbuja en `#chat-cuerpo`; lo usa el
editor al abrir/enfocar una tarjeta · `WAM.abrirTarjeta(id)` → bool: abre y enfoca la tarjeta (desde el chat)
- Validación propia (no pública): chip ⚠ por tarjeta, caja «La Cloud API rechazaría…» y `#ed-avisos-global`;
@@ -145,7 +147,10 @@ cancela demo y flujos (interactivity.js envuelve `WAM.render`).
- `WAM.marcarEditado()` → indicador `#estado-guardado` («Guardando…» → «Guardado · hace N s»; rojo con
`WAM.onErrorGuardar`, que ux.js sustituye por toast con «Exportar JSON»)
- `WAM.setVistaMovil("editor"|"vista")` (≤ 900 px; `body.vista-movil`, `wam-ui-vista`) · `WAM.recordarEnfoque(id)`
- Atajos globales (ignorados en campos): P, Espacio, T, F, N, Esc, Ctrl+S, Ctrl+Z, ?; Alt+↑/↓ mueve en el editor.
- Deshacer / rehacer conservan el tema actual (el tema no forma parte del historial de contenido); cambiar el
tema pasa por `WAM.marcarEditado()`. Cargar escenario / demo / Café Aurora deja SIEMPRE punto de deshacer.
- Atajos globales (ignorados en campos): P, Espacio, T, F, N, G, Esc, Ctrl+S, Ctrl+Z, ?; F6 también en campos;
G / F6 y el enlace `#saltar-vista` mueven el foco entre el editor y `#chat-cuerpo`; Alt+↑/↓ mueve en el editor.
Alt+clic o doble clic en una burbuja abre su tarjeta. Envuelve `WAM.anadirMensajeDOM` para anunciar el mensaje
en `#wam-anuncios` (sr-only) y quita `aria-live` de `#chat-cuerpo`.
@@ -189,7 +194,8 @@ cancela demo y flujos (interactivity.js envuelve `WAM.render`).
| `editor-cuenta` | C | form identidad: `cuenta-nombre`, `cuenta-subtitulo`, `cuenta-verificado`, `cuenta-avatar-vista`, `cuenta-avatar-archivo`, `cuenta-avatar-url` |
| `nuevo-tipo` (paleta `role=toolbar` de los 13 tipos: un clic AÑADE), `nuevo-tipo-valor` (hidden), `btn-anadir` («+ Otro …»), `ed-destino-alta`, `lista-mensajes` | C | alta por paleta (el último tipo usado vive en `#nuevo-tipo-valor` y `data-valor`) y tarjetas de mensajes |
| `bloque-cuenta` (`<details>`), `cuenta-resumen`, `ed-avisos-global`, `btn-ver-telefono` | C | cuenta plegable, contador de avisos, salto a la vista previa (≤ 900 px) |
| `btn-deshacer`, `btn-rehacer`, `btn-demos`, `btn-atajos`, `btn-mas` + `tb-menu`, `estado-guardado` | C (ux.js) | toolbar: historial, demos, atajos, menú «⋯» (≤ 900 px), indicador de guardado |
| `btn-deshacer`, `btn-rehacer`, `btn-demos`, `btn-atajos`, `btn-mas` + `tb-menu`, `estado-guardado` | C (ux.js) | toolbar: historial, «Nueva demo» (primario, junto a la marca), atajos, menú «⋯» (siempre visible; `#btn-reset` «Volver al ejemplo Café Aurora» solo vive dentro del menú; ≤ 900 px agrupa además Exportar / Importar / Atajos), indicador de guardado |
| `saltar-vista` (enlace), `ed-lista-herr`, `body[data-wam-vacia]` | C | salto de teclado a la vista previa; herramientas de lista (ocultas sin mensajes); demo vacía → pista en el chat |
| `tabs-movil`, `tab-editor`, `tab-vista` | C (ux.js) | pestañas Editor / Vista previa (≤ 900 px) |
| `barra-movil`, `btn-reproducir` | A | barra sobre el teléfono (Reproducir demo) |
| `iphone`, `pantalla` (class `wa`), `isla`, `statusbar` ("9:41" + señal/wifi/batería SVG), `home-indicator` | A | marco iPhone 393×852 |
+146 -12
View File
@@ -187,9 +187,43 @@ body {
border-color: var(--wam-acento);
color: var(--wam-acento-hover);
}
#btn-reset:hover { color: var(--wam-peligro); border-color: var(--wam-peligro); }
#btn-mas { display: none; }
/* «Nueva demo»: punto de partida (botón primario junto a la marca) */
.tb-btn-primario {
margin-left: 4px;
background: var(--wam-primario);
border-color: var(--wam-primario);
color: #fff;
}
.tb-btn-primario:hover { background: var(--wam-primario-hover); border-color: var(--wam-primario-hover); }
.tb-kbd {
display: inline-block;
min-width: 18px;
padding: 1px 5px;
border: 1px solid rgba(255, 255, 255, .45);
border-radius: 5px;
font: 600 11px/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
text-align: center;
opacity: .9;
}
/* Acción destructiva: siempre dentro del menú «⋯», al final */
.tb-btn-peligro { color: var(--wam-peligro); }
.tb-btn-peligro:hover { color: var(--wam-peligro); border-color: var(--wam-peligro); background: rgba(185, 58, 46, .08); }
.tb-menu-sep { display: none; }
.tb-solo-menu { display: none; }
/* Escritorio: el grupo secundario va en línea y «⋯» despliega
«Volver al ejemplo Café Aurora» como un menú de una opción. */
#btn-reset { display: none; }
.toolbar.tb-menu-abierto #btn-reset {
display: inline-flex;
position: absolute;
top: calc(100% + 6px);
right: 10px;
z-index: 40;
min-height: 40px;
padding: 10px 14px;
border-color: var(--wam-borde);
box-shadow: var(--wam-sombra);
}
/* --- Editor (columna izquierda · cascarón; contenido: editor.js) -------- */
#panel-editor {
@@ -375,6 +409,20 @@ button.wam-dlg-peligro.wam-dlg-btn:not(.wam-dlg-mini):hover { background: #9c2f2
.wam-esc-nombre { font-size: 14px; font-weight: 700; }
.wam-esc-desc { font-size: 12.5px; line-height: 1.4; color: var(--wam-texto-suave); }
.wam-esc-meta { font-size: 11.5px; font-weight: 600; color: var(--wam-acento-hover); }
.wam-esc-tipos { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
.wam-esc-tipo {
padding: 1px 7px;
border: 1px solid var(--wam-borde);
border-radius: 999px;
font-size: 11px;
font-weight: 600;
color: var(--wam-texto-suave);
background: var(--wam-panel-alt);
}
@media (min-width: 1280px) {
.wam-dlg-grande { width: min(1040px, calc(100vw - 64px)); max-height: min(92vh, 860px); }
.wam-dlg-grande .wam-esc-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* Demos guardadas */
.wam-demos-lista { list-style: none; display: grid; gap: 6px; }
@@ -429,8 +477,12 @@ button.wam-dlg-peligro.wam-dlg-btn:not(.wam-dlg-mini):hover { background: #9c2f2
.tb-guardado-rel { display: none; }
}
@media (max-width: 1100px) {
.toolbar-acciones .tb-btn > span:not(.tb-solo-menu) { display: none; }
.toolbar-acciones .tb-btn { padding: 7px 9px; }
/* Iconos sin texto, salvo en «Nueva demo» y «Presentación» */
.toolbar-acciones .tb-grupo-sec .tb-btn > span:not(.tb-solo-menu) { display: none; }
.toolbar-acciones .tb-grupo-sec .tb-btn { padding: 7px 9px; }
.toolbar.tb-menu-abierto #btn-reset > span { display: inline; }
.toolbar.tb-menu-abierto #btn-reset { padding: 10px 14px; }
.tb-kbd { display: none; }
}
/* --- Ventana estrecha (≤ 900 px): una columna con pestañas -------------- */
@@ -489,9 +541,12 @@ button.wam-dlg-peligro.wam-dlg-btn:not(.wam-dlg-mini):hover { background: #9c2f2
.tab-movil:focus-visible { outline-offset: -4px; }
/* «Ver teléfono»: botón flotante dentro del editor */
/* centrado: nunca tapa las acciones (derecha) ni el asa (izquierda) de
las tarjetas; #lista-mensajes reserva su hueco al final (editor.css) */
.ed-ver-telefono {
position: fixed;
right: 16px;
left: 50%;
transform: translateX(-50%);
bottom: calc(var(--wam-tabs-alto) + 14px + env(safe-area-inset-bottom, 0px));
z-index: 25;
display: inline-flex;
@@ -515,10 +570,15 @@ button.wam-dlg-peligro.wam-dlg-btn:not(.wam-dlg-mini):hover { background: #9c2f2
.toolbar-logo { width: 30px; height: 30px; }
.toolbar h1 { font-size: 14px; }
.tb-guardado { padding: 4px 8px; margin-left: 2px; }
.tb-guardado-txt { display: none; }
.tb-guardado[data-estado="error"] .tb-guardado-txt { display: inline; }
#btn-mas { display: inline-flex; }
/* el texto sigue disponible para lectores de pantalla (y en el title) */
.tb-guardado-txt {
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.tb-guardado[data-estado="error"] .tb-guardado-txt { position: static; width: auto; height: auto; margin: 0; clip: auto; }
.tb-sep-sec { display: none; }
.toolbar-acciones #btn-presentacion > span { display: none; }
#btn-presentacion { padding: 7px 9px; }
.tb-grupo-sec {
display: none;
position: absolute;
@@ -536,15 +596,26 @@ button.wam-dlg-peligro.wam-dlg-btn:not(.wam-dlg-mini):hover { background: #9c2f2
box-shadow: var(--wam-sombra);
}
.toolbar.tb-menu-abierto .tb-grupo-sec { display: flex; }
.tb-grupo-sec .tb-btn {
.toolbar-acciones .tb-grupo-sec .tb-btn {
justify-content: flex-start;
border-color: transparent;
padding: 10px 12px;
min-height: 40px;
}
.toolbar-acciones .tb-grupo-sec .tb-btn > span,
.tb-grupo-sec .tb-solo-menu { display: inline; }
.toolbar-acciones .tb-grupo-sec .tb-btn > span:not(.tb-solo-menu),
.toolbar-acciones .tb-grupo-sec .tb-solo-menu { display: inline; }
.tb-grupo-sec .tb-btn:hover { border-color: transparent; }
.tb-grupo-sec .tb-btn-peligro:hover { background: rgba(185, 58, 46, .08); }
.tb-menu-sep { display: block; height: 1px; margin: 4px 6px; background: var(--wam-borde); }
/* dentro del menú desplegable «Volver al ejemplo» es una fila más */
#btn-reset,
.toolbar.tb-menu-abierto #btn-reset {
display: inline-flex;
position: static;
padding: 10px 12px;
box-shadow: none;
border-color: transparent;
}
/* Escala del teléfono con el ancho completo (sin restar el editor) y la
altura real de la toolbar + barra de vista previa + pestañas. */
@@ -566,10 +637,19 @@ button.wam-dlg-peligro.wam-dlg-btn:not(.wam-dlg-mini):hover { background: #9c2f2
}
}
@media (max-width: 560px) {
#btn-demos > span { display: none; }
#btn-demos { padding: 7px 9px; }
}
@media (max-width: 520px) {
/* la pestaña ya dice «Vista previa»: se gana una fila para el teléfono */
#barra-movil .barra-titulo { display: none; }
#barra-movil { justify-content: center; }
#barra-movil .barra-etq-txt {
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
}
@media (max-width: 420px) {
@@ -577,6 +657,60 @@ button.wam-dlg-peligro.wam-dlg-btn:not(.wam-dlg-mini):hover { background: #9c2f2
.toolbar-marca { flex: 1; }
}
/* --- Enlace «Saltar a la vista previa» (visible solo con el foco) ------ */
.wam-saltar {
position: fixed;
left: 12px;
top: 8px;
z-index: 200;
padding: 10px 14px;
border-radius: 10px;
background: var(--wam-primario);
color: #fff;
font-size: 14px;
font-weight: 700;
text-decoration: none;
transform: translateY(-160%);
transition: transform .15s ease;
}
.wam-saltar:focus { transform: none; outline: 2px solid #fff; outline-offset: -4px; }
#chat-cuerpo:focus { outline: none; }
#chat-cuerpo:focus-visible { outline: 2px solid var(--wam-acento); outline-offset: -3px; }
#lista-mensajes:focus { outline: none; }
/* --- Etiqueta de la velocidad en la barra de la vista previa ---------- */
#barra-movil .barra-etq {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
color: var(--wam-texto-suave);
cursor: pointer;
}
#barra-movil #btn-reiniciar { display: inline-flex; align-items: center; gap: 6px; }
#barra-movil #btn-reiniciar svg { flex: none; }
/* --- Chat vacío: pista dentro del teléfono -----------------------------
body[data-wam-vacia] lo pone js/editor.js cuando la demo no tiene
mensajes (así no parpadea al arrancar «Reproducir demo»). */
body[data-wam-vacia] #chat-cuerpo:not(:has(> .wa-row))::after {
content: "Añade mensajes desde el editor";
display: block;
width: max-content;
max-width: 80%;
margin: 16px auto;
padding: 6px 12px;
border-radius: 8px;
background: rgba(255, 255, 255, .92);
color: #54656f;
font-size: 12.5px;
text-align: center;
box-shadow: 0 1px .5px rgba(11, 20, 26, .13);
}
body[data-wam-vacia] .pantalla.oscuro #chat-cuerpo:not(:has(> .wa-row))::after { background: #182229; color: #8696a0; }
body.presentacion[data-wam-vacia] #chat-cuerpo:not(:has(> .wa-row))::after { content: "Sin mensajes todavía"; }
/* Destinos táctiles más grandes */
@media (pointer: coarse) {
.tb-btn { min-height: 40px; }
@@ -585,5 +719,5 @@ button.wam-dlg-peligro.wam-dlg-btn:not(.wam-dlg-mini):hover { background: #9c2f2
/* --- Accesibilidad: menos movimiento ------------------------------------ */
@media (prefers-reduced-motion: reduce) {
#chat-cuerpo { scroll-behavior: auto; }
.tb-btn, .wam-btn-anadir, .wam-esc { transition: none; }
.tb-btn, .wam-btn-anadir, .wam-esc, .wam-saltar { transition: none; }
}
+156 -28
View File
@@ -276,6 +276,7 @@ textarea.ed-input { resize: vertical; min-height: 46px; }
.ed-cab-principal {
flex: 1;
min-width: 0;
overflow: hidden;
display: flex;
align-items: center;
gap: 6px;
@@ -293,14 +294,22 @@ textarea.ed-input { resize: vertical; min-height: 46px; }
flex: 0 0 auto;
min-width: 18px;
height: 18px;
display: inline-grid;
place-items: center;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 1px;
padding: 0 5px 0 4px;
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--ed-acento-hondo);
background: var(--ed-acento-suave);
border-radius: 999px;
}
/* Dirección: flecha ↙ entrada / ↗ salida (además del color) */
.ed-num-dir { font-size: 11px; line-height: 1; }
.ed-num.ed-num-in { color: var(--ed-entrada); background: var(--ed-entrada-suave); }
.ed-num.ed-num-resp { padding: 0 5px; }
.ed-dir {
flex: 0 0 auto;
@@ -321,19 +330,28 @@ textarea.ed-input { resize: vertical; min-height: 46px; }
}
.ed-badge {
flex: 0 0 auto;
padding: 1px 6px;
flex: 0 1 auto;
min-width: 0;
display: inline-flex;
align-items: center;
gap: 3px;
padding: 1px 6px 1px 4px;
overflow: hidden;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.3px;
text-transform: uppercase;
white-space: nowrap;
color: var(--wam-texto, var(--ed-texto-suave));
border: 1px solid var(--wam-borde, var(--ed-borde));
border-radius: 999px;
}
.ed-badge .ed-ico { flex: none; display: inline-grid; place-items: center; color: var(--wam-texto-suave, var(--ed-texto-suave)); }
.ed-badge .ed-ico svg { display: block; width: 13px; height: 13px; }
.ed-badge-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ed-resumen {
flex: 1;
flex: 1 1 60px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@@ -341,6 +359,26 @@ textarea.ed-input { resize: vertical; min-height: 46px; }
font-size: 12px;
color: var(--wam-texto, var(--ed-texto-suave));
}
/* Tarjeta abierta: el resumen baja a su propia línea bajo la cabecera
(así siempre se lee qué mensaje se está editando) */
.ed-tarjeta { --ed-acc-w: 146px; } /* + ↑ ↓ ⧉ ✕ (26 px) + huecos */
.ed-tarjeta.ed-anidada { --ed-acc-w: 88px; } /* ↑ ↓ ✕ */
.ed-tarjeta.ed-abierta > .ed-tarjeta-cab { align-items: flex-start; }
.ed-tarjeta.ed-abierta > .ed-tarjeta-cab > .ed-asa { margin-top: 2px; }
.ed-tarjeta.ed-abierta > .ed-tarjeta-cab > .ed-cab-principal { flex-wrap: wrap; row-gap: var(--ed-acc-h, 8px); overflow: visible; }
/* la 2.ª línea pasa por debajo de las acciones: usa todo el ancho de la tarjeta */
.ed-tarjeta.ed-abierta > .ed-tarjeta-cab > .ed-cab-principal > .ed-resumen {
order: 10;
flex: 0 0 calc(100% + var(--ed-acc-w)); /* tamaño exterior = 100 %: fuerza la 2.ª línea */
max-width: none;
margin-right: calc(-1 * var(--ed-acc-w));
white-space: normal;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
padding-left: 2px;
}
.ed-acciones {
flex: 0 0 auto;
@@ -825,15 +863,22 @@ textarea.ed-input { resize: vertical; min-height: 46px; }
font-size: 12.5px;
}
.ed-paleta .ed-tipo-card .ed-tipo-card-e { flex: none; overflow: visible; }
.ed-paleta .ed-tipo-card.ed-seleccionada {
/* último tipo usado: marca sutil (la paleta añade al hacer clic) */
background: var(--wam-panel, var(--ed-panel));
color: var(--wam-texto, var(--ed-texto));
font-weight: 500;
box-shadow: none;
border-color: var(--ed-seleccion);
}
/* Sin estado «seleccionado»: cada clic añade. El «+» aparece con hover/foco. */
.ed-paleta .ed-tipo-card:hover { background: var(--ed-seleccion-suave); }
.ed-tipo-mas {
display: inline-block;
width: 0;
overflow: hidden;
font-weight: 800;
line-height: 1;
opacity: 0;
transition: width 0.12s ease, opacity 0.12s ease;
}
.ed-paleta .ed-tipo-card:hover .ed-tipo-mas,
.ed-paleta .ed-tipo-card:focus-visible .ed-tipo-mas { width: 0.7em; opacity: 1; }
@media (hover: none) {
.ed-tipo-mas { width: 0.7em; opacity: 0.55; }
}
.ed-alta-pie {
display: flex;
flex-wrap: wrap;
@@ -845,6 +890,52 @@ textarea.ed-input { resize: vertical; min-height: 46px; }
font-size: 12px;
color: var(--wam-texto-suave, var(--ed-texto-suave));
}
/* Chip visible del destino del próximo alta */
.ed-destino-alta.ed-destino-chip {
display: inline-flex;
align-items: center;
gap: 4px;
min-height: 26px;
padding: 2px 10px;
border: 1px solid var(--wam-borde, var(--ed-borde));
border-radius: 999px;
background: var(--ed-fondo-suave);
font-weight: 600;
color: var(--wam-texto, var(--ed-texto));
}
.ed-destino-alta.ed-destino-chip::before {
content: "↧";
font-weight: 700;
color: var(--ed-seleccion);
}
.ed-destino-alta.ed-destino-dentro {
border-color: var(--ed-seleccion);
background: var(--ed-seleccion-suave);
color: var(--ed-seleccion);
padding-right: 3px;
}
.ed-destino-quitar {
display: inline-grid;
place-items: center;
width: 22px;
height: 22px;
padding: 0;
border: 0;
border-radius: 50%;
background: transparent;
color: inherit;
font: inherit;
font-size: 15px;
line-height: 1;
cursor: pointer;
}
.ed-destino-quitar:hover { background: rgba(43, 107, 228, 0.18); }
.ed-destino-quitar:focus-visible { outline: 2px solid var(--ed-seleccion); outline-offset: 1px; }
.ed-destino-cambio { animation: ed-destino-cambio 0.9s ease-out; }
@keyframes ed-destino-cambio {
0% { box-shadow: 0 0 0 3px var(--ed-seleccion-suave), 0 0 0 1px var(--ed-seleccion); }
100% { box-shadow: 0 0 0 0 transparent; }
}
.ed-enlace {
padding: 0;
border: 0;
@@ -955,20 +1046,43 @@ textarea.ed-input { resize: vertical; min-height: 46px; }
}
.ed-chip-aviso { color: var(--ed-ambar); background: rgba(180, 83, 9, 0.12); }
.ed-chip-flujo { color: var(--ed-acento-hondo); background: var(--ed-acento-suave); }
.ed-chip-auto { color: var(--ed-seleccion); background: var(--ed-seleccion-suave); }
/* «Se pulsa sola en la demo»: select arriba del formulario y casilla por elemento */
.ed-campo-auto { max-width: 320px; }
.ed-check-auto {
justify-self: start;
padding: 3px 8px 3px 6px;
border: 1px dashed var(--ed-seleccion);
border-radius: 999px;
font-size: 12px;
color: var(--ed-seleccion);
}
.ed-check-auto input { accent-color: var(--ed-seleccion); }
.ed-check-auto:has(input:checked) { border-style: solid; background: var(--ed-seleccion-suave); font-weight: 600; }
.ed-chip-flujo-incompleto { color: var(--ed-entrada); background: var(--ed-entrada-suave); }
.ed-num-resp { font-size: 12px; }
/* Las flechas ↑/↓ se muestran al abrir, al pasar el ratón o con el foco
dentro (siguen en el orden de Tab: el foco las hace visibles) */
dentro. Su hueco se reserva siempre (visibility, no display): el hover no
provoca saltos de layout ni recorta el resumen. Con el foco en la
cabecera se vuelven visibles y entran en el orden de Tab. */
.ed-tarjeta { position: relative; }
.ed-tarjeta:not(.ed-abierta):not(:hover):not(:focus-within) > .ed-tarjeta-cab .ed-mov {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
padding: 0;
border: 0;
visibility: hidden;
}
/* Editor estrecho (contenido < 400 px, p. ej. ventanas de 1024 y 1280): en plegadas las
flechas no ocupan sitio (se reordena con el asa o Alt+↑/↓) y aparecen
al abrir la tarjeta, cuando el resumen ya va en su propia línea. */
.ed-bloque-mensajes { container-type: inline-size; container-name: ed-mensajes; }
@container ed-mensajes (max-width: 400px) {
.ed-tarjeta:not(.ed-abierta) > .ed-tarjeta-cab .ed-mov { display: none; }
}
/* Muy estrecho (editor < 380 px, contenido ~330): la etiqueta de tipo queda solo como icono */
@container ed-mensajes (max-width: 330px) {
.ed-tarjeta-cab .ed-badge-txt { display: none; }
.ed-tarjeta-cab .ed-badge { padding: 2px 4px; }
}
.ed-acciones .ed-btn-mini { min-width: 26px; min-height: 26px; padding: 2px 6px; }
.ed-btn-mini:disabled { opacity: 0.35; }
@@ -1033,16 +1147,23 @@ textarea.ed-input { resize: vertical; min-height: 46px; }
.ed-vacio-acciones { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 4px; }
/* ---------- Toasts (fuera del scroll del panel) ---------- */
/* Abajo, centrados sobre la columna de la vista previa: no tapan las
tarjetas ni sus acciones mientras se trabaja en el editor. */
.ed-toasts {
--ed-col-editor: clamp(340px, 34vw, 480px); /* = columna del editor (base.css) */
position: fixed;
left: 16px;
left: calc(var(--ed-col-editor) + (100vw - var(--ed-col-editor)) / 2);
bottom: 16px;
z-index: 1000;
display: grid;
justify-items: center;
gap: 8px;
max-width: min(440px, calc(100vw - 32px));
width: max-content;
max-width: min(440px, calc(100vw - var(--ed-col-editor) - 32px));
transform: translateX(-50%);
pointer-events: none;
}
body.presentacion .ed-toasts { left: 50%; max-width: min(440px, calc(100vw - 32px)); }
.ed-toast {
display: flex;
align-items: center;
@@ -1090,7 +1211,14 @@ textarea.ed-input { resize: vertical; min-height: 46px; }
to { opacity: 1; transform: none; }
}
@media (max-width: 900px) {
.ed-toasts { bottom: calc(var(--wam-tabs-alto, 58px) + 70px); }
.ed-toasts {
left: 50%;
max-width: min(440px, calc(100vw - 32px));
bottom: calc(var(--wam-tabs-alto, 58px) + 64px + env(safe-area-inset-bottom, 0px));
}
body.vista-movil .ed-toasts { bottom: calc(var(--wam-tabs-alto, 58px) + 12px + env(safe-area-inset-bottom, 0px)); }
/* hueco al final de la lista para el botón flotante «Ver teléfono» */
#lista-mensajes { padding-bottom: 56px; }
}
/* ---------- Enlace con la vista previa ---------- */
@@ -1108,6 +1236,8 @@ body.presentacion #chat-cuerpo .wa-row.ed-resaltado { outline: 0; }
/* ---------- Destinos táctiles ---------- */
@media (pointer: coarse) {
.ed-tarjeta { --ed-acc-w: 176px; --ed-acc-h: 14px; }
.ed-tarjeta.ed-anidada { --ed-acc-w: 106px; }
.ed-btn-mini,
.ed-acciones .ed-btn-mini { min-width: 32px; min-height: 32px; }
.ed-fmt-btn { min-width: 32px; height: 32px; }
@@ -1116,14 +1246,12 @@ body.presentacion #chat-cuerpo .wa-row.ed-resaltado { outline: 0; }
}
@media (max-width: 480px) {
/* cabecera de tarjeta: la dirección se lee en el color del número */
.ed-tarjeta-cab .ed-dir { display: none; }
.ed-num.ed-num-in { color: var(--ed-entrada); background: var(--ed-entrada-suave); }
.ed-paleta-grupo { grid-template-columns: 1fr; gap: 4px; }
.ed-paleta-titulo { padding-top: 0; }
}
@media (prefers-reduced-motion: reduce) {
.ed-toast, .ed-tarjeta.ed-destello { animation: none; }
.ed-toast, .ed-tarjeta.ed-destello, .ed-destino-cambio { animation: none; }
.ed-tipo-mas { transition: none; }
.ed-bloque-cuenta > .ed-cuenta-sumario::before, .ed-seg-op span { transition: none; }
}
+38 -23
View File
@@ -16,6 +16,9 @@
</head>
<body data-tema="claro">
<!-- Teclado: salta las tarjetas del editor directo al teléfono (atajo G / F6) -->
<a id="saltar-vista" class="wam-saltar" href="#chat-cuerpo">Saltar a la vista previa</a>
<div id="app">
<!-- ============ Barra superior de la app (shell A) ============ -->
@@ -28,6 +31,12 @@
<h1>WhatsApp Cloud API</h1>
<span class="toolbar-subtitulo">Mockup visual · demo estática</span>
</div>
<!-- Punto de partida del consultor: escenarios por industria y demos guardadas -->
<button id="btn-demos" class="tb-btn tb-btn-primario" type="button" title="Nueva demo: escenarios por industria y demos guardadas (N)" aria-keyshortcuts="N">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><path d="M17 14v6M14 17h6"/></svg>
<span>Nueva demo</span>
<kbd class="tb-kbd" aria-hidden="true">N</kbd>
</button>
<!-- Estado del guardado local (lo pinta js/ux.js) -->
<span id="estado-guardado" class="tb-guardado" data-estado="ok" title="Guardado en este navegador">
<span class="tb-guardado-punto" aria-hidden="true"></span>
@@ -44,24 +53,18 @@
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 14 5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 0 0 0 11H13"/></svg>
</button>
<span class="tb-separador" aria-hidden="true"></span>
<button id="btn-tema" class="tb-btn" type="button" title="Alternar tema claro / oscuro (T)" aria-label="Tema oscuro" aria-pressed="false">
<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor" aria-hidden="true"><path d="M12 2.6a9.4 9.4 0 1 0 0 18.8 9.4 9.4 0 0 0 0-18.8zm0 16.9V4.5a7.5 7.5 0 0 1 0 15z"/></svg>
<span>Tema</span>
</button>
<button id="btn-presentacion" class="tb-btn" type="button" title="Modo presentación: oculta el editor (P · Esc para salir)" aria-pressed="false">
<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor" fill-rule="evenodd" aria-hidden="true"><path d="M3 4.5h18a1 1 0 0 1 1 1V16a1 1 0 0 1-1 1h-6.4v2.1h1.9a.9.9 0 1 1 0 1.8H7.5a.9.9 0 1 1 0-1.8h1.9V17H3a1 1 0 0 1-1-1V5.5a1 1 0 0 1 1-1zm8 3.4v5.7l4.6-2.85z"/></svg>
<span>Presentación</span>
</button>
<button id="btn-tema" class="tb-btn tb-btn-icono" type="button" title="Alternar tema claro / oscuro (T)" aria-label="Tema oscuro" aria-pressed="false">
<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor" aria-hidden="true"><path d="M12 2.6a9.4 9.4 0 1 0 0 18.8 9.4 9.4 0 0 0 0-18.8zm0 16.9V4.5a7.5 7.5 0 0 1 0 15z"/></svg>
</button>
<span class="tb-separador tb-sep-sec" aria-hidden="true"></span>
<!-- «⋯»: en ventana estrecha agrupa las acciones secundarias -->
<button id="btn-mas" class="tb-btn tb-btn-icono" type="button" aria-label="Más acciones" aria-expanded="false" aria-controls="tb-menu" title="Más acciones">
<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.9"/><circle cx="12" cy="12" r="1.9"/><circle cx="19" cy="12" r="1.9"/></svg>
</button>
<!-- Acciones secundarias: en línea en escritorio; en ventana estrecha
forman el menú «⋯». «Volver al ejemplo» (destructiva) vive siempre
dentro del menú «⋯», al final y con estilo de peligro. -->
<div id="tb-menu" class="tb-grupo-sec">
<button id="btn-demos" class="tb-btn" type="button" title="Nueva demo: escenarios por industria y demos guardadas (N)">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><path d="M17 14v6M14 17h6"/></svg>
<span>Demos</span>
</button>
<button id="btn-exportar" class="tb-btn" type="button" title="Exportar la demo a JSON (Ctrl+S)">
<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor" aria-hidden="true"><path d="M12 3.2c.5 0 .9.4.9.9v8.2l2.7-2.7a.9.9 0 1 1 1.3 1.3l-4.2 4.2a.9.9 0 0 1-1.3 0L7.2 11a.9.9 0 1 1 1.3-1.3l2.7 2.7V4.1c0-.5.4-.9.9-.9zM4.9 18.5h14.2a.9.9 0 1 1 0 1.8H4.9a.9.9 0 1 1 0-1.8z"/></svg>
<span>Exportar</span>
@@ -71,15 +74,19 @@
<span>Importar</span>
</button>
<input type="file" id="importar-archivo" accept="application/json,.json" hidden>
<button id="btn-reset" class="tb-btn" type="button" title="Restablecer la demo de ejemplo (pide confirmación)">
<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor" aria-hidden="true"><path d="M12 5V2.8a.7.7 0 0 0-1.13-.55L6.9 5.2a.7.7 0 0 0 0 1.1l3.97 2.95A.7.7 0 0 0 12 8.7V6.5a5.5 5.5 0 1 1-5.5 5.5H4.75A7.25 7.25 0 1 0 12 5z"/></svg>
<span>Restablecer</span>
</button>
<button id="btn-atajos" class="tb-btn tb-btn-icono" type="button" title="Atajos de teclado (?)" aria-label="Atajos de teclado">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="6" width="19" height="12" rx="2.2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7.5 14h9"/></svg>
<span class="tb-solo-menu">Atajos</span>
</button>
<span class="tb-menu-sep" aria-hidden="true"></span>
<button id="btn-reset" class="tb-btn tb-btn-peligro" type="button" title="Sustituye la demo actual por el ejemplo Café Aurora (pide confirmación y se puede deshacer)">
<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor" aria-hidden="true"><path d="M12 5V2.8a.7.7 0 0 0-1.13-.55L6.9 5.2a.7.7 0 0 0 0 1.1l3.97 2.95A.7.7 0 0 0 12 8.7V6.5a5.5 5.5 0 1 1-5.5 5.5H4.75A7.25 7.25 0 1 0 12 5z"/></svg>
<span>Volver al ejemplo Café Aurora</span>
</button>
</div>
<button id="btn-mas" class="tb-btn tb-btn-icono" type="button" aria-label="Más acciones" aria-expanded="false" aria-controls="tb-menu" title="Más acciones">
<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.9"/><circle cx="12" cy="12" r="1.9"/><circle cx="19" cy="12" r="1.9"/></svg>
</button>
</div>
</header>
@@ -103,19 +110,20 @@
<div class="wam-alta">
<!-- Paleta de los 13 tipos (la rellena js/editor.js): un clic AÑADE el mensaje.
El último tipo usado queda en #nuevo-tipo-valor (hidden) y en data-valor. -->
<div id="nuevo-tipo" class="ed-paleta" role="toolbar" aria-label="Añadir un mensaje: elige el tipo"></div>
<div id="nuevo-tipo" class="ed-paleta" role="toolbar" aria-label="Añadir un mensaje: cada tipo añade uno"></div>
<input type="hidden" id="nuevo-tipo-valor" value="text">
<div class="ed-alta-pie">
<span id="ed-destino-alta" class="ed-destino-alta" aria-live="polite"></span>
<button id="btn-anadir" class="wam-btn-anadir" type="button">+ Otro «Texto»</button>
<!-- «Repetir último»: aparece tras el primer alta de la sesión -->
<button id="btn-anadir" class="wam-btn-anadir" type="button" hidden>+ Otro «Texto»</button>
</div>
</div>
<div class="ed-lista-herr">
<div class="ed-lista-herr" id="ed-lista-herr">
<button type="button" class="ed-btn ed-btn-mini" data-acc="horas-secuencia" title="Reparte las horas desde la del primer mensaje, +1 min por mensaje">Horas en secuencia</button>
<button type="button" class="ed-btn ed-btn-mini" data-acc="plegar-todo">Plegar todo</button>
</div>
<!-- Una tarjeta colapsable por mensaje la pinta js/editor.js (C) -->
<div id="lista-mensajes"></div>
<div id="lista-mensajes" tabindex="-1" aria-label="Tarjetas de mensajes"></div>
</section>
<!-- Solo en ventana estrecha: salta a la vista previa del mensaje en edición -->
@@ -130,12 +138,19 @@
<div id="barra-movil">
<span class="barra-titulo">Vista previa</span>
<div class="barra-acciones">
<select id="sel-velocidad" title="Velocidad de la reproducción" aria-label="Velocidad de la reproducción">
<label class="barra-etq" for="sel-velocidad" title="Velocidad de la reproducción">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="13" r="8"/><path d="M12 13V9M10 2.8h4"/></svg>
<span class="barra-etq-txt">Velocidad</span>
</label>
<select id="sel-velocidad" title="Velocidad de la reproducción">
<option value="0.5">0,5x</option>
<option value="1" selected>1x</option>
<option value="2">2x</option>
</select>
<button id="btn-reiniciar" type="button" title="Descartar lo pulsado y escrito en el teléfono y volver al guion">Reiniciar chat</button>
<button id="btn-reiniciar" type="button" title="Descarta lo pulsado y escrito en el teléfono y vuelve al guion desde el primer mensaje">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/></svg>
<span>Repetir desde el inicio</span>
</button>
<button id="btn-reproducir" type="button" title="Reproducir la demo paso a paso (clic de nuevo para detener)">
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M8.4 5.6v12.8c0 .8.9 1.3 1.6.9l10-6.4c.6-.4.6-1.4 0-1.8l-10-6.4c-.7-.4-1.6.1-1.6.9z"/></svg>
<span>Reproducir demo</span>
@@ -199,7 +214,7 @@
</header>
<!-- Scroll de mensajes (estructura A · filas li.wa-row de B) -->
<ul id="chat-cuerpo" aria-live="polite"></ul>
<ul id="chat-cuerpo" aria-live="polite" tabindex="-1" aria-label="Conversación"></ul>
<!-- Indicador "escribiendo…" (estructura A · contenido y animación de B) -->
<div id="chat-escribiendo"></div>
+209 -33
View File
@@ -110,6 +110,7 @@
var ultimaAbierta = null; // id top-level abierto más reciente (posición de alta)
var insertarTras = null; // id tras el que insertar (botón «+» de una tarjeta)
var ultimoTipo = "text";
var yaAnadido = false; // ¿se añadió algo desde la paleta en esta sesión?
var ultimoEnlazado = null; // id resaltado en la vista previa
var tResalte = 0;
var contadorIds = 0; // ids únicos para <label for>
@@ -244,9 +245,9 @@
return false;
}
/* Aviso flotante (toast) abajo a la izquierda, fuera de la zona con
scroll del panel. opts: { tipo: "error", ms, accion: { texto, fn } }.
Devuelve una función que lo cierra. */
/* Aviso flotante (toast) abajo, centrado sobre la vista previa (fuera de
la lista de tarjetas). opts: { tipo: "error", ms, accion: { texto, fn } }.
Solo hay un aviso con acción a la vez. Devuelve una función que lo cierra. */
function contenedorToasts() {
var c = document.getElementById("ed-toasts");
if (!c) {
@@ -280,14 +281,24 @@
var x = h("button", { type: "button", class: "ed-toast-cerrar", "aria-label": "Cerrar aviso", text: "×" });
x.addEventListener("click", cerrar);
t.appendChild(x);
// Como mucho 2 a la vista: primero se retiran los que no tienen acción
var hijos = Array.prototype.slice.call(c.children);
// Un aviso con acción nuevo invalida los anteriores con acción: su
// «Deshacer» ya no desharía lo que dice (se deshace de uno en uno).
if (opts.accion && typeof opts.accion.fn === "function") {
hijos = hijos.filter(function (x) {
if (!x.querySelector(".ed-toast-accion")) return true;
if (typeof x.__cerrar === "function") x.__cerrar(); else if (x.parentNode) x.parentNode.removeChild(x);
return false;
});
}
// Como mucho 2 a la vista: primero se retiran los que no tienen acción
var sinAccion = hijos.filter(function (x) { return !x.querySelector(".ed-toast-accion"); });
while (hijos.length >= 2) {
var quitar = sinAccion.length ? sinAccion.shift() : hijos[0];
hijos.splice(hijos.indexOf(quitar), 1);
if (quitar.parentNode) quitar.parentNode.removeChild(quitar);
}
t.__cerrar = cerrar;
c.appendChild(t);
timer = setTimeout(cerrar, opts.ms || (esError ? 7000 : 3000));
return cerrar;
@@ -500,14 +511,51 @@
if (car2) car2.scrollLeft = sl;
}
// Reemplaza un array completo vía setCampo (mutación contractual) y repinta tarjetas
/* --- autoPulsar (SPEC §1): índice del elemento que «Reproducir demo»
pulsa solo. Botones (template/buttons) y tarjetas (carousel) usan su
índice en el array; las listas, el índice aplanado de todas sus filas. */
function pulsablesDe(m) {
if (!m || typeof m !== "object") return [];
if (m.tipo === "list") {
var out = [];
(m.secciones || []).forEach(function (s) { ((s && s.filas) || []).forEach(function (f) { out.push(f); }); });
return out;
}
if (m.tipo === "carousel") return Array.isArray(m.cards) ? m.cards : [];
if (m.tipo === "template" || m.tipo === "buttons") return Array.isArray(m.botones) ? m.botones : [];
return [];
}
function tieneAuto(m) { return !!m && typeof m.autoPulsar === "number" && isFinite(m.autoPulsar) && m.autoPulsar >= 0; }
// Ruta del mensaje dueño de una lista de botones / cards / secciones / filas
function rutaMensajeDe(rutaArr) {
var r = String(rutaArr || "");
var m = /^(.*)\.secciones\.\d+\.filas$/.exec(r) || /^(.*)\.(?:botones|cards|secciones)$/.exec(r);
return m ? m[1] : null;
}
// Reemplaza un array completo vía setCampo (mutación contractual) y repinta tarjetas.
// Si el array es de botones / cards / secciones / filas, autoPulsar sigue
// al elemento pulsado (se mueve con él; se quita si se borró).
function mutarArray(ruta, fn) {
var arr = rutaGet(ruta);
if (!Array.isArray(arr)) { aviso("Estructura no encontrada: " + ruta, "error"); return null; }
var copia = fn(arr.slice());
if (!copia) return null; // validación rechazada (fn ya avisó)
snap();
var rutaMsg = rutaMensajeDe(ruta);
var dueno = rutaMsg ? rutaGet(rutaMsg) : null;
var pulsado = tieneAuto(dueno) ? pulsablesDe(dueno)[dueno.autoPulsar] : undefined;
WAM.setCampo(ruta, copia);
if (pulsado !== undefined) {
var nuevo = pulsablesDe(rutaGet(rutaMsg)).indexOf(pulsado);
if (nuevo !== dueno.autoPulsar) {
var padre = rutaGet(rutaMsg);
if (nuevo < 0) delete padre.autoPulsar; else padre.autoPulsar = nuevo;
WAM.setCampo(rutaMsg, padre);
}
}
WAM.renderTarjetas();
return copia;
}
@@ -816,7 +864,8 @@
snap();
WAM.setCampo("mensajes", copia);
WAM.renderTarjetas();
// Foco a la tarjeta vecina (la siguiente o, si era la última, la anterior)
// Foco a la CABECERA de la tarjeta vecina (la siguiente o, si era la
// última, la anterior), nunca a su botón borrar; sin tarjetas, la paleta.
var vecino = copia[i] || copia[i - 1];
if (vecino) enfocarTarjeta(vecino.id, false);
else { var pal = paletaFoco(); if (pal) pal.focus(); }
@@ -990,6 +1039,11 @@
function largo(v) { return String(v === undefined || v === null ? "" : v).length; }
function vacioTxt(v) { return !String(v === undefined || v === null ? "" : v).trim(); }
function urlValida(v) { return /^https?:\/\/[^\s/$.?#][^\s]*$/i.test(String(v || "").trim()); }
// Botón de llamada de plantilla: tel: con al menos 3 dígitos (espacios, guiones y paréntesis permitidos)
function telValido(v) {
var s = String(v || "").trim();
return /^tel:\+?[\d\s().-]+$/i.test(s) && (s.replace(/\D/g, "").length >= 3);
}
function validar(m, prefijo, profundidad) {
var av = [];
@@ -1032,12 +1086,16 @@
lim(m.footer, 60, "El pie");
var bt = m.botones || [];
if (bt.length > LIMITES.botonesPlantilla) add("Máximo " + LIMITES.botonesPlantilla + " botones");
var conUrl = bt.filter(function (b) { return b && !vacioTxt(b.url); }).length;
// tel:+… es el botón de llamada (PHONE_NUMBER de la API): máximo 1 y
// no cuenta entre los 2 de URL
var conTel = bt.filter(function (b) { return b && telValido(b.url); }).length;
var conUrl = bt.filter(function (b) { return b && !vacioTxt(b.url) && !telValido(b.url); }).length;
if (conUrl > 2) add("Máximo 2 botones con URL (" + conUrl + ")");
if (conTel > 1) add("Máximo 1 botón de llamada tel: (" + conTel + ")");
bt.forEach(function (b, i) {
if (vacioTxt(b.texto)) add("Botón " + (i + 1) + ": sin texto");
else lim(b.texto, 25, "Botón " + (i + 1));
if (!vacioTxt(b.url) && !urlValida(b.url)) add("Botón " + (i + 1) + ": URL no válida (usa https://…)");
if (!vacioTxt(b.url) && !urlValida(b.url) && !telValido(b.url)) add("Botón " + (i + 1) + ": URL no válida (usa https://… o tel:+34…)");
});
break;
}
@@ -1110,6 +1168,13 @@
}
default: break;
}
// «Se pulsa sola» apunta a una opción que ya no existe o que abre una URL
if (tieneAuto(m) && ["template", "buttons", "list", "carousel"].indexOf(m.tipo) >= 0) {
var objAuto = pulsablesDe(m)[m.autoPulsar];
var nAuto = "«Se pulsa sola en la demo»: la opción " + (m.autoPulsar + 1);
if (!objAuto) add(nAuto + " ya no existe (la demo no pulsará nada)");
else if (!vacioTxt(objAuto.url)) add(nAuto + " abre una URL y la demo no la pulsará");
}
if ((profundidad || 0) < 6) {
origenesRespuesta(m).forEach(function (o) {
o.respuestas.forEach(function (r) {
@@ -1150,6 +1215,16 @@
"aria-label": av.length + (av.length === 1 ? " aviso" : " avisos")
}, [h("span", { "aria-hidden": "true", text: "⚠ " + av.length })]));
}
// ▶ auto: la demo pulsa sola una opción de este mensaje
if (tieneAuto(m)) {
var objA = pulsablesDe(m)[m.autoPulsar];
if (objA && vacioTxt(objA.url)) {
var etA = objA.texto || objA.titulo || objA.textoBoton || ("opción " + (m.autoPulsar + 1));
var tA = "En «Reproducir demo» se pulsa sola: «" + etA + "»";
frag.appendChild(h("span", { class: "ed-chip ed-chip-auto", title: tA, "aria-label": tA },
[h("span", { "aria-hidden": "true", text: "▶ auto" })]));
}
}
var f = contarFlujos(m);
if (f.total) {
var txt = "↳ " + f.con + (f.sin ? "/" + f.total : "");
@@ -1188,6 +1263,8 @@
if (!cab) return;
var res = cab.querySelector(".ed-resumen");
if (res) res.textContent = resumenDe(m);
var cp = cab.querySelector(".ed-cab-principal");
if (cp) cp.title = (t.classList.contains("ed-abierta") ? "Plegar" : "Desplegar") + " · " + resumenDe(m);
var chips = cab.querySelector(".ed-chips");
if (chips) {
while (chips.firstChild) chips.removeChild(chips.firstChild);
@@ -1477,7 +1554,8 @@
btnMini("✕", "Quitar " + o.etiqueta.toLowerCase(), "del-boton", { "data-ruta": ruta, "data-idx": i }, true)
]),
edCampo("Texto", ruta + "." + i + ".texto", b.texto, { limite: o.maxTexto, placeholder: o.ejemplo || "" }),
o.conUrl ? edCampo("URL (si la pones, abre el enlace en vez de responder)", ruta + "." + i + ".url", b.url, { placeholder: "https://…" }) : null,
o.conUrl ? edCampo("URL (si la pones, abre el enlace en vez de responder; tel:+34… llama)", ruta + "." + i + ".url", b.url, { placeholder: "https://…" }) : null,
edCheckAuto(rutaMensajeDe(ruta), i, b),
edRespuestas(ruta + "." + i + ".respuestas", b.respuestas, o.raiz)
]);
});
@@ -1503,9 +1581,13 @@
function edSeccionesEd(ruta, secciones, raiz) {
var lista = Array.isArray(secciones) ? secciones : [];
var rutaMsgLista = rutaMensajeDe(ruta);
var base = 0; // índice aplanado de la primera fila de la sección (autoPulsar)
var hijos = lista.map(function (s, si) {
var filas = Array.isArray(s.filas) ? s.filas : [];
var rutaFilas = ruta + "." + si + ".filas";
var offset = base;
base += filas.length;
var filaEls = filas.map(function (f, fi) {
return h("div", { class: "ed-item", "data-drag-ruta": rutaFilas, "data-idx": fi }, [
h("div", { class: "ed-sub-cab" }, [
@@ -1516,6 +1598,7 @@
]),
edCampo("Título", rutaFilas + "." + fi + ".titulo", f.titulo, { limite: 24 }),
edCampo("Descripción", rutaFilas + "." + fi + ".descripcion", f.descripcion, { limite: 72 }),
edCheckAuto(rutaMsgLista, offset + fi, f),
edRespuestas(rutaFilas + "." + fi + ".respuestas", f.respuestas, raiz)
]);
});
@@ -1572,6 +1655,7 @@
edCampo("Texto del botón", ruta + "." + ci + ".textoBoton", c.textoBoton, { limite: 20 }),
edCampo("URL (opcional)", ruta + "." + ci + ".url", c.url, { placeholder: "https://…" })
]),
edCheckAuto(rutaMensajeDe(ruta), ci, c),
edRespuestas(ruta + "." + ci + ".respuestas", c.respuestas, raiz)
]);
});
@@ -1598,10 +1682,14 @@
var caja = h("div", { class: "ed-header-bloque" });
caja.appendChild(h("div", { class: "ed-sub-cab" }, [
h("span", { class: "ed-sub-titulo", text: "Cabecera" }),
// Etiqueta fija (no se invierte con el estado): el interruptor dice si se muestra
h("label", { class: "ed-switch" }, [
h("input", { type: "checkbox", "data-rol": "header-toggle", "data-ruta": ruta, checked: tipo !== "" }),
h("input", {
type: "checkbox", role: "switch", "aria-checked": tipo !== "" ? "true" : "false",
"data-rol": "header-toggle", "data-ruta": ruta, checked: tipo !== ""
}),
h("span", { class: "ed-switch-pista" }, [h("span", { class: "ed-switch-nudo" })]),
h("span", { class: "ed-switch-texto", text: tipo === "" ? "Mostrar cabecera" : "Ocultar cabecera" })
h("span", { class: "ed-switch-texto", text: "Mostrar cabecera" })
])
]));
if (tipo !== "") {
@@ -1666,12 +1754,34 @@
});
}
var v = (m.autoPulsar === undefined || m.autoPulsar === null) ? "" : String(m.autoPulsar);
return edSelect("En «Reproducir demo», pulsar sola", ruta + ".autoPulsar", v, ops, false);
// Valor guardado que ya no corresponde a ninguna opción (borrado, o con URL):
// se muestra tal cual para que no parezca «No» en silencio (y validar() avisa)
if (v !== "" && !ops.some(function (op) { return op[0] === v; })) {
ops.push([v, "⚠ Opción inexistente (" + (Number(v) + 1) + ")"]);
}
var campo = edSelect("En «Reproducir demo», se pulsa sola", ruta + ".autoPulsar", v, ops, true);
campo.classList.add("ed-campo-auto");
return campo;
}
// Casilla «Se pulsa sola en la demo» dentro de cada botón / fila / tarjeta
// (exclusiva dentro del mensaje: marcar una desmarca la anterior)
function edCheckAuto(rutaMsg, indice, elemento) {
var m = rutaGet(rutaMsg);
if (!m || !elemento || !vacioTxt(elemento.url)) return null; // con URL abre el enlace
var marcado = tieneAuto(m) && m.autoPulsar === indice;
return h("label", { class: "ed-check ed-check-auto", title: "En «Reproducir demo» se pulsa sola 1 s después de aparecer" }, [
h("input", { type: "checkbox", "data-rol": "auto-item", "data-ruta": rutaMsg + ".autoPulsar", "data-idx": indice, checked: marcado }),
h("span", { text: "Se pulsa sola en la demo" })
]);
}
function edFormTipo(m, ruta, raiz) {
var frag = document.createDocumentFragment();
var ap = function (n) { if (n) frag.appendChild(n); };
// Clave para las demos: arriba del todo, justo bajo Dirección / Hora
// (cada botón / fila / tarjeta tiene además su casilla «Se pulsa sola»)
if (["template", "buttons", "list", "carousel"].indexOf(m.tipo) >= 0) ap(edAutoPulsar(ruta, m));
switch (m.tipo) {
case "text":
ap(edTextarea("Texto", ruta + ".texto", m.texto, { limite: 4096, rows: 3, formato: true, placeholder: "Hola 👋" }));
@@ -1724,7 +1834,6 @@
max: LIMITES.botonesPlantilla, maxTexto: 25, conUrl: true,
etiqueta: "Botón", ejemplo: "Confirmar", raiz: raiz
}));
ap(edAutoPulsar(ruta, m));
break;
}
case "buttons":
@@ -1735,7 +1844,6 @@
max: LIMITES.botonesRapidos, maxTexto: 20, conUrl: false,
etiqueta: "Botón rápido", ejemplo: "Horarios", raiz: raiz
}));
ap(edAutoPulsar(ruta, m));
break;
case "list":
ap(edCampo("Cabecera", ruta + ".header", m.header, { limite: 60 }));
@@ -1743,7 +1851,6 @@
ap(edCampo("Pie (footer)", ruta + ".footer", m.footer, { limite: 60 }));
ap(edCampo("Etiqueta del botón", ruta + ".boton", m.boton, { limite: 20, placeholder: "Ver opciones" }));
ap(edSeccionesEd(ruta + ".secciones", m.secciones, raiz));
ap(edAutoPulsar(ruta, m));
break;
case "cta_url":
ap(edMedia("Cabecera («» = sin cabecera)", ruta + ".headerMedia", m.headerMedia, { aceptar: "image/*", textoDrop: "Clic o arrastra una imagen aquí" }));
@@ -1757,7 +1864,6 @@
case "carousel":
ap(edTextarea("Cuerpo (opcional)", ruta + ".body", m.body, { limite: 1024, rows: 2 }));
ap(edCardsEd(ruta + ".cards", m.cards, raiz));
ap(edAutoPulsar(ruta, m));
break;
default:
ap(h("p", { class: "ed-nota", text: "Tipo desconocido: " + m.tipo }));
@@ -1775,9 +1881,10 @@
var total = o.anidado ? (o.total || 1) : ((WAM.estado && WAM.estado.mensajes) || []).length;
var idx = o.anidado ? o.indice : o.i;
if (!o.anidado) {
acciones.push(btnMini("+", "Insertar un mensaje debajo", "insertar-debajo", { "data-i": o.i, "data-id": m.id }));
// ↑ ↓ primero: su hueco reservado (ocultas en reposo) queda como margen
acciones.push(btnMini("↑", "Subir", "mover", { "data-i": o.i, "data-delta": -1, disabled: idx <= 0 || null }, false, "ed-mov"));
acciones.push(btnMini("↓", "Bajar", "mover", { "data-i": o.i, "data-delta": 1, disabled: idx >= total - 1 || null }, false, "ed-mov"));
acciones.push(btnMini("+", "Insertar un mensaje debajo", "insertar-debajo", { "data-i": o.i, "data-id": m.id }));
acciones.push(btnMini("⧉", "Duplicar", "dup", { "data-i": o.i }));
acciones.push(btnMini("✕", "Borrar mensaje (se puede deshacer)", "borrar", { "data-i": o.i }, true));
} else {
@@ -1804,12 +1911,19 @@
var cab = h("button", {
type: "button", class: "ed-cab-principal", "data-acc": "toggle",
"aria-expanded": abierta ? "true" : "false",
title: abierta ? "Plegar" : "Desplegar"
title: (abierta ? "Plegar" : "Desplegar") + " · " + resumenDe(m)
}, [
// Número + flecha de dirección (↙ entrada / ↗ salida): no depende solo del color
o.anidado ? h("span", { class: "ed-num ed-num-resp", text: "↳" })
: h("span", { class: "ed-num ed-num-" + (esOut ? "out" : "in"), title: esOut ? "Salida (enviado)" : "Entrada (recibido)", text: String(o.i + 1) }),
o.anidado ? null : h("span", { class: "ed-dir ed-dir-" + (esOut ? "out" : "in"), text: esOut ? "Salida" : "Entrada" }),
h("span", { class: "ed-badge", text: CORTO[m.tipo] || etiquetaTipo(m.tipo) }),
: h("span", { class: "ed-num ed-num-" + (esOut ? "out" : "in"), title: esOut ? "Salida (enviado)" : "Entrada (recibido)" }, [
h("span", { class: "ed-num-dir", "aria-hidden": "true", text: esOut ? "↗" : "↙" }),
document.createTextNode(String(o.i + 1)),
h("span", { class: "wam-sr", text: esOut ? " · salida" : " · entrada" })
]),
h("span", { class: "ed-badge", title: etiquetaTipo(m.tipo) }, [
svgIcono(ICONOS_TIPO[m.tipo] || ""),
h("span", { class: "ed-badge-txt", text: CORTO[m.tipo] || etiquetaTipo(m.tipo) })
]),
h("span", { class: "ed-resumen", text: resumenDe(m) }),
h("span", { class: "ed-chips" }, o.anidado ? null : chipsDe(m))
]);
@@ -1853,6 +1967,11 @@
pintarDestinoAlta();
actualizarAvisosGlobal();
actualizarResumenCuenta();
// Demo vacía: sin herramientas de lista (no harían nada) y pista en el teléfono
var herr = document.getElementById("ed-lista-herr") || document.querySelector(".ed-lista-herr");
if (herr) herr.hidden = !mensajes.length;
if (mensajes.length) document.body.removeAttribute("data-wam-vacia");
else document.body.setAttribute("data-wam-vacia", "");
if (!mensajes.length) {
lista.appendChild(h(etiquetaItem, { class: "ed-vacio" }, [
h("p", { class: "ed-vacio-titulo", text: "Sin mensajes todavía" }),
@@ -1887,8 +2006,13 @@
return f;
}
// Botones que borran: tras usarlos el foco NUNCA vuelve a su gemelo (un
// segundo Intro/Espacio borraría otro elemento); lo recoloca quien borra.
function esAccionBorrar(acc) { return acc === "borrar" || /^del-/.test(String(acc || "")); }
function restaurarFoco(cont, f) {
if (!f || !cont) return;
if (esAccionBorrar(f.acc)) return;
var sel = "";
if (f.ruta) sel += '[data-ruta="' + cssEsc(f.ruta) + '"]';
if (f.acc) sel += '[data-acc="' + cssEsc(f.acc) + '"]';
@@ -2017,17 +2141,18 @@
var pal = document.getElementById("nuevo-tipo");
if (pal && pal.tagName !== "SELECT") {
pal.setAttribute("data-valor", tipo);
// Sin estado «seleccionado» visible: cada clic AÑADE (no es un selector).
// Solo se conserva la parada de Tab (roving tabindex) en el último usado.
var hijos = pal.querySelectorAll("[data-tipo]");
for (var i = 0; i < hijos.length; i++) {
var activo = hijos[i].getAttribute("data-tipo") === tipo;
hijos[i].classList.toggle("ed-seleccionada", activo);
hijos[i].tabIndex = activo ? 0 : -1;
hijos[i].tabIndex = hijos[i].getAttribute("data-tipo") === tipo ? 0 : -1;
}
}
var btn = document.getElementById("btn-anadir");
if (btn) {
btn.textContent = "+ Otro «" + etiquetaTipo(tipo) + "»";
btn.title = "Añade otro mensaje de tipo " + etiquetaTipo(tipo);
btn.title = "Repite el último tipo añadido: " + etiquetaTipo(tipo);
btn.hidden = !yaAnadido;
}
}
@@ -2049,8 +2174,13 @@
"data-acc": "anadir-tipo",
"data-tipo": tipo,
tabindex: "-1",
title: "Añadir un mensaje de tipo " + etiquetaTipo(tipo)
}, [svgIcono(ICONOS_TIPO[tipo] || ""), h("span", { class: "ed-tipo-card-e", text: CORTO[tipo] || etiquetaTipo(tipo) })]));
title: "Añadir un mensaje de tipo " + etiquetaTipo(tipo),
"aria-label": "Añadir " + etiquetaTipo(tipo)
}, [
svgIcono(ICONOS_TIPO[tipo] || ""),
h("span", { class: "ed-tipo-card-e", text: CORTO[tipo] || etiquetaTipo(tipo) }),
h("span", { class: "ed-tipo-mas", "aria-hidden": "true", text: "+" })
]));
});
cont.appendChild(fila);
});
@@ -2102,15 +2232,31 @@
while (el.firstChild) el.removeChild(el.firstChild);
var ms = (WAM.estado && WAM.estado.mensajes) || [];
var pos = posicionAlta();
var previo = el.getAttribute("data-pos");
var clave = ms.length ? String(pos >= ms.length ? "fin" : pos) : "vacio";
el.setAttribute("data-pos", clave);
// Chip visible del destino: «al final» o «debajo del n.º N ×» (× = al final)
el.classList.toggle("ed-destino-chip", !!ms.length);
el.classList.toggle("ed-destino-dentro", !!ms.length && pos < ms.length);
if (pos >= ms.length) {
el.appendChild(document.createTextNode(ms.length ? "Se añade al final" : "Elige un tipo para empezar"));
return;
} else {
el.appendChild(document.createTextNode("Se inserta debajo del n.º " + pos));
el.appendChild(h("button", {
type: "button", class: "ed-destino-quitar", "data-acc": "destino-final",
title: "Añadir al final", "aria-label": "Añadir al final", text: "×"
}));
}
// Aviso visual breve cuando el destino cambia solo (al abrir otra tarjeta)
if (previo !== null && previo !== clave && ms.length && !reducirMovimiento()) {
el.classList.remove("ed-destino-cambio");
void el.offsetWidth;
el.classList.add("ed-destino-cambio");
}
el.appendChild(document.createTextNode("Se inserta debajo del n.º " + pos + " "));
el.appendChild(h("button", { type: "button", class: "ed-enlace", "data-acc": "destino-final", text: "Al final" }));
}
function anadirDesdePaleta(tipo) {
yaAnadido = true; // desde ahora se ofrece «+ Otro «X»» (repetir el último)
seleccionarTipo(tipo);
var pos = posicionAlta();
insertarTras = null;
@@ -2239,7 +2385,7 @@
snap();
WAM.setCampo("mensajes", copia);
WAM.renderTarjetas();
WAM.toast("Horas repartidas desde las " + inicio + " (+1 min por mensaje)",
WAM.toast("Horas repartidas desde " + inicio,
typeof WAM.deshacer === "function" ? { accion: { texto: "Deshacer", fn: WAM.deshacer } } : null);
}
@@ -2509,12 +2655,29 @@
var ruta = el.getAttribute("data-ruta");
var idx = Number(el.getAttribute("data-idx"));
var min = Number(el.getAttribute("data-min")) || 0;
mutarArray(ruta, function (arr) {
var res = mutarArray(ruta, function (arr) {
if (arr.length <= min) { aviso("Debe quedar al menos " + min, "error"); return null; }
if (idx < 0 || idx >= arr.length) return null;
arr.splice(idx, 1);
return arr;
});
if (res) enfocarTrasBorrar(ruta, idx, res.length);
}
/* Tras quitar un elemento: foco a la cabecera / primer campo del que
ocupa su lugar (o del anterior); si la lista quedó vacía, a su botón
«+ Añadir». Nunca a otro botón ✕. */
function enfocarTrasBorrar(ruta, idx, n) {
var lista = listaNodo();
if (!lista) return;
var j = idx < n ? idx : n - 1;
var r = cssEsc(ruta);
var it = j >= 0 ? lista.querySelector('[data-drag-ruta="' + r + '"][data-idx="' + j + '"], [data-mov-ruta="' + r + '"][data-idx="' + j + '"]') : null;
var foco = it && (it.querySelector(".ed-cab-principal") || it.querySelector("input.ed-input, textarea"));
if (!foco) foco = lista.querySelector('[data-acc^="add-"][data-ruta="' + r + '"]');
if (!foco) return;
try { foco.focus({ preventScroll: true }); } catch (e) { foco.focus(); }
if (foco.scrollIntoView) foco.scrollIntoView({ block: "nearest" });
}
function alternarModoMedia(el) {
@@ -2536,7 +2699,8 @@
abiertas[id] = !abiertas[id];
t.classList.toggle("ed-abierta", abiertas[id]);
el.setAttribute("aria-expanded", abiertas[id] ? "true" : "false");
el.title = abiertas[id] ? "Plegar" : "Desplegar";
var resT = el.querySelector(".ed-resumen");
el.title = (abiertas[id] ? "Plegar" : "Desplegar") + (resT ? " · " + resT.textContent : "");
var raizT = t.getAttribute("data-raiz") || id;
if (!t.classList.contains("ed-anidada")) {
if (abiertas[id]) ultimaAbierta = id;
@@ -2719,6 +2883,18 @@
}
if (el.type === "checkbox") {
snap();
if (el.getAttribute("data-rol") === "auto-item") {
// Exclusiva dentro del mensaje: marcar fija autoPulsar; desmarcar lo quita
var rutaA = String(el.getAttribute("data-ruta") || "").replace(/\.autoPulsar$/, "");
var msgA = rutaGet(rutaA);
if (msgA && typeof msgA === "object") {
if (el.checked) msgA.autoPulsar = Number(el.getAttribute("data-idx")) || 0;
else delete msgA.autoPulsar;
WAM.setCampo(rutaA, msgA);
}
repintarDesde(el);
return;
}
if (el.getAttribute("data-rol") === "header-toggle") {
var rt = el.getAttribute("data-ruta");
WAM.setCampo(rt + ".headerTipo", el.checked ? "texto" : "");
+203 -34
View File
@@ -5,7 +5,8 @@
· Indicador de guardado en la toolbar (#estado-guardado)
· Confirmación de Restablecer / Importar (WAM.confirmarReemplazo)
· Diálogo «Demos»: escenarios por industria + demos guardadas (wam-demos)
· Atajos de teclado globales y diálogo de ayuda (?)
· Atajos de teclado globales y diálogo de ayuda (?); G / F6 y el enlace
«Saltar a la vista previa» mueven el foco entre editor y teléfono
· Pestañas Editor / Vista previa en ventana estrecha y menú «⋯»
· Enlace vista previa → tarjeta (Alt+clic o doble clic en una burbuja)
· Región sr-only que anuncia los mensajes nuevos del chat
@@ -137,10 +138,17 @@
WAM.puedeDeshacer = function () { return pilaDeshacer.length > 0; };
// El tema es una preferencia de visualización, no contenido: deshacer y
// rehacer conservan el tema actual en vez del de la instantánea.
function conTemaActual(x) {
if (x && WAM.estado) x.tema = WAM.estado.tema;
return x;
}
WAM.deshacer = function () {
if (!pilaDeshacer.length) { toast("No hay nada que deshacer"); return false; }
pilaRehacer.push(clonar(WAM.estado));
sustituirEstado(pilaDeshacer.pop());
sustituirEstado(conTemaActual(pilaDeshacer.pop()));
pintarBotonesHistorial();
toast("Cambio deshecho", { accion: { texto: "Rehacer", fn: WAM.rehacer } });
return true;
@@ -149,7 +157,7 @@
WAM.rehacer = function () {
if (!pilaRehacer.length) { toast("No hay nada que rehacer"); return false; }
pilaDeshacer.push(clonar(WAM.estado));
sustituirEstado(pilaRehacer.pop());
sustituirEstado(conTemaActual(pilaRehacer.pop()));
pintarBotonesHistorial();
toast("Cambio rehecho");
return true;
@@ -253,14 +261,17 @@
if (!dentro) dlg.close();
});
var foco = null;
var restaurar = true;
dlg.addEventListener("close", function () {
setTimeout(function () { dlg.remove(); }, 0);
if (foco && foco.isConnected && typeof foco.focus === "function") foco.focus();
if (restaurar && foco && foco.isConnected && typeof foco.focus === "function") foco.focus();
});
document.body.appendChild(dlg);
return {
dlg: dlg, cuerpo: cuerpo, pie: pie,
abrir: function () { foco = document.activeElement; dlg.showModal(); }
abrir: function () { foco = document.activeElement; dlg.showModal(); },
// La acción elegida mueve el foco a otro sitio (p. ej. el nombre de la cuenta)
sinRestaurar: function () { restaurar = false; }
};
}
@@ -281,7 +292,7 @@
lsDel(CLAVE_ACTIVA);
ejecutar();
marcarEditado();
toast("Demo de ejemplo restablecida", { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 });
toast("Demo de ejemplo «Café Aurora» restablecida", { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 });
} else {
ejecutar(); // el punto de deshacer lo toma importarJSON al aplicar el archivo
}
@@ -290,11 +301,11 @@
var nombre = (WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "sin nombre";
var resumen = "«" + nombre + "», " + nMensajes() + " mensaje" + (nMensajes() === 1 ? "" : "s");
if (!soportaDialog) {
if (window.confirm(esReset ? "¿Restablecer la demo de ejemplo? Se sustituirá " + resumen + "."
if (window.confirm(esReset ? "¿Volver al ejemplo Café Aurora? Se sustituirá " + resumen + "."
: "¿Importar un JSON? Se sustituirá " + resumen + ".")) hacer();
return;
}
var d = crearDialogo("dlg-confirmar", esReset ? "¿Restablecer la demo de ejemplo?" : "¿Importar una demo desde JSON?", "wam-dlg-chico");
var d = crearDialogo("dlg-confirmar", esReset ? "¿Volver al ejemplo Café Aurora?" : "¿Importar una demo desde JSON?", "wam-dlg-chico");
d.cuerpo.appendChild(h("p", { text: (esReset
? "La demo actual (" + resumen + ") se sustituirá por la demo de ejemplo «Café Aurora»."
: "El archivo que elijas sustituirá la demo actual (" + resumen + ").") }));
@@ -372,14 +383,59 @@
return true;
}
/* Tras cargar un escenario lo primero es poner el nombre del cliente:
se despliega «Cuenta» y se enfoca (y selecciona) su nombre. */
function enfocarNombreCuenta() {
if (esEstrecha() && document.body.classList.contains("vista-movil")) return;
if (document.body.classList.contains("presentacion")) return;
var det = porId("bloque-cuenta");
if (det && !det.open) det.open = true;
var inp = porId("cuenta-nombre");
if (!inp) return;
try { inp.focus({ preventScroll: true }); } catch (e) { inp.focus(); }
if (inp.scrollIntoView) inp.scrollIntoView({ block: "nearest" });
if (typeof inp.select === "function") inp.select();
}
/* Siempre deja un punto de deshacer (también desde una demo vacía) y un
único aviso con «Deshacer»: WAM.toast retira los avisos con acción
anteriores, que ya no deshacen lo que dicen. */
function aplicarDemoCompleta(crudo, texto) {
WAM.puntoDeshacer();
sustituirEstado(normalizado(crudo));
lsDel(CLAVE_ACTIVA);
toast(texto, { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 });
}
function cargarEscenario(esc) {
var crudo = WAM.datosEscenario ? WAM.datosEscenario(esc.id) : null;
if (!crudo) return;
var habiaAlgo = nMensajes() > 0;
if (habiaAlgo) WAM.puntoDeshacer();
sustituirEstado(normalizado(crudo));
lsDel(CLAVE_ACTIVA);
toast("Escenario «" + esc.nombre + "» cargado", habiaAlgo ? { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 } : null);
aplicarDemoCompleta(crudo, "Escenario «" + esc.nombre + "» cargado. Cambia el nombre del negocio en «Cuenta».");
enfocarNombreCuenta();
}
// Tipos de mensaje que usa un escenario (chips del diálogo Demos)
var NOMBRE_TIPO = {
text: "Texto", image: "Imagen", video: "Vídeo", audio: "Audio", document: "Documento",
sticker: "Sticker", location: "Ubicación", contact: "Contacto", template: "Plantilla",
buttons: "Botones", list: "Lista", cta_url: "Botón URL", carousel: "Carrusel"
};
// Los más distintivos primero (texto casi siempre está)
var ORDEN_TIPOS = ["template", "list", "carousel", "buttons", "cta_url", "location", "document",
"contact", "image", "video", "audio", "sticker", "text"];
function chipsTipos(mensajes) {
var usados = Object.create(null);
(mensajes || []).forEach(function (m) { if (m && m.tipo) usados[m.tipo] = true; });
var lista = ORDEN_TIPOS.filter(function (t) { return usados[t]; });
if (!lista.length) return null;
var max = 4;
var cont = h("span", { class: "wam-esc-tipos", "aria-label": "Incluye: " + lista.map(function (t) { return NOMBRE_TIPO[t]; }).join(", ") });
lista.slice(0, max).forEach(function (t) {
cont.appendChild(h("span", { class: "wam-esc-tipo", "aria-hidden": "true", text: NOMBRE_TIPO[t] || t }));
});
if (lista.length > max) cont.appendChild(h("span", { class: "wam-esc-tipo", "aria-hidden": "true", text: "+" + (lista.length - max) }));
return cont;
}
function fechaCorta(iso) {
@@ -397,29 +453,28 @@
// --- Escenarios ---
var gridEsc = h("div", { class: "wam-esc-grid" });
(WAM.ESCENARIOS || []).forEach(function (esc) {
var n = 0;
try { n = (esc.datos().mensajes || []).length; } catch (e) { n = 0; }
var ms = [];
try { ms = esc.datos().mensajes || []; } catch (e) { ms = []; }
var b = h("button", { type: "button", class: "wam-esc", "data-esc": esc.id }, [
h("span", { class: "wam-esc-nombre", text: esc.nombre }),
h("span", { class: "wam-esc-desc", text: esc.descripcion }),
h("span", { class: "wam-esc-meta", text: n ? n + " mensajes" : "Vacía" })
chipsTipos(ms),
h("span", { class: "wam-esc-meta", text: ms.length ? ms.length + " mensajes" : "Vacía" })
]);
b.addEventListener("click", function () { d.dlg.close(); cargarEscenario(esc); });
b.addEventListener("click", function () { d.sinRestaurar(); d.dlg.close(); cargarEscenario(esc); });
gridEsc.appendChild(b);
});
var bAurora = h("button", { type: "button", class: "wam-esc" }, [
var msAurora = [];
try { msAurora = (typeof WAM.datosDemo === "function" && WAM.datosDemo().mensajes) || []; } catch (e) { msAurora = []; }
var bAurora = h("button", { type: "button", class: "wam-esc", "data-esc": "aurora" }, [
h("span", { class: "wam-esc-nombre", text: "Catálogo · Café Aurora" }),
h("span", { class: "wam-esc-desc", text: "Los 13 tipos de mensaje de la Cloud API. Útil como referencia." }),
h("span", { class: "wam-esc-meta", text: "Demo de ejemplo" })
h("span", { class: "wam-esc-meta", text: "Demo de ejemplo" + (msAurora.length ? " · " + msAurora.length + " mensajes" : "") })
]);
bAurora.addEventListener("click", function () {
d.dlg.close();
if (typeof WAM.datosDemo !== "function") return;
var habiaAlgo = nMensajes() > 0;
if (habiaAlgo) WAM.puntoDeshacer();
sustituirEstado(normalizado(WAM.datosDemo()));
lsDel(CLAVE_ACTIVA);
toast("Demo «Café Aurora» cargada", habiaAlgo ? { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 } : null);
aplicarDemoCompleta(WAM.datosDemo(), "Demo «Café Aurora» cargada");
});
gridEsc.appendChild(bAurora);
@@ -432,13 +487,17 @@
// --- Demos guardadas ---
var lista = h("ul", { class: "wam-demos-lista" });
var activa = demoActiva();
// Sin nombre precargado: el consultor escribe el del cliente (si lo deja
// vacío se usa el nombre de la cuenta).
var nombreCuenta = (WAM.estado && WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "";
var inputNombre = h("input", {
type: "text", class: "wam-dlg-input", id: "dlg-demo-nombre", maxlength: "60",
value: activa ? activa.nombre : ((WAM.estado && WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "Mi demo"),
value: activa ? activa.nombre : "",
placeholder: "Nombre de la demo (p. ej. el cliente)" + (nombreCuenta ? " · si lo dejas vacío: «" + nombreCuenta + "»" : ""),
"aria-label": "Nombre de la demo"
});
var bGuardar = boton(activa ? "Actualizar" : "Guardar la actual", "wam-dlg-primario", function () {
var nombre = String(inputNombre.value || "").trim() || "Mi demo";
var nombre = String(inputNombre.value || "").trim() || nombreCuenta || "Mi demo";
var misma = activa && activa.nombre === nombre;
var e = guardarDemoComo(nombre, misma ? activa.id : null);
if (e) { toast("Demo «" + e.nombre + "» guardada"); pintarLista(); activa = demoActiva(); }
@@ -539,27 +598,43 @@
/* ------------------------------------------------------------------
Atajos de teclado
------------------------------------------------------------------ */
// «Ctrl» se pinta como ⌘ en Mac (los atajos aceptan ctrlKey o metaKey)
var ATAJOS = [
["N", "Nueva demo: escenarios y demos guardadas"],
["P", "Modo presentación"],
["Espacio", "Reproducir / detener la demo"],
["G", "Saltar entre el editor y el teléfono (también F6)"],
["T", "Tema claro / oscuro"],
["F", "Pantalla completa"],
["N", "Demos y escenarios"],
["Esc", "Cerrar la lista del chat o salir de la presentación"],
["Ctrl + S", "Exportar JSON"],
["Ctrl + Z", "Deshacer (fuera de un campo de texto)"],
["Ctrl + Mayús + Z", "Rehacer"],
["Alt + ↑ / ↓", "Mover el mensaje o elemento con el foco"],
["Alt + clic en una burbuja", "Abrir su tarjeta en el editor (también doble clic)"],
["Alt + clic", "Sobre una burbuja: abre su tarjeta en el editor (también doble clic)"],
["?", "Esta ayuda"]
];
function esMac() {
var nav = window.navigator || {};
var plat = (nav.userAgentData && nav.userAgentData.platform) || nav.platform || nav.userAgent || "";
return /mac|iphone|ipad|ipod/i.test(String(plat));
}
function teclaVisible(k) {
if (!esMac()) return k;
if (k === "Ctrl") return "⌘";
if (k === "Alt") return "⌥";
if (k === "Mayús") return "⇧";
return k;
}
WAM.abrirAtajos = function () {
if (!soportaDialog) return;
var d = crearDialogo("dlg-atajos", "Atajos de teclado", "wam-dlg-chico");
var tabla = h("dl", { class: "wam-atajos" });
ATAJOS.forEach(function (a) {
tabla.appendChild(h("dt", {}, a[0].split(" + ").map(function (k, i, arr) {
tabla.appendChild(h("dt", {}, a[0].split(" + ").map(teclaVisible).map(function (k, i, arr) {
var frag = document.createDocumentFragment();
frag.appendChild(h("kbd", { text: k }));
if (i < arr.length - 1) frag.appendChild(document.createTextNode(" + "));
@@ -591,12 +666,66 @@
return !!(s && !s.hidden && s.offsetParent !== null);
}
/* ------------------------------------------------------------------
Saltar entre paneles: enlace «Saltar a la vista previa» y G / F6
------------------------------------------------------------------ */
var ultimoFocoEditor = null;
function enfocar(el) {
if (!el) return false;
try { el.focus({ preventScroll: true }); } catch (e) { el.focus(); }
return document.activeElement === el;
}
function irAVistaPrevia() {
if (esEstrecha() && !document.body.classList.contains("presentacion")) WAM.setVistaMovil("vista");
var cuerpo = porId("chat-cuerpo");
if (!cuerpo) return;
// tras el cambio de pestaña (display) el nodo ya es enfocable
requestAnimationFrame(function () { enfocar(cuerpo); });
}
function irAlEditor() {
if (document.body.classList.contains("presentacion")) return false;
if (esEstrecha()) WAM.setVistaMovil("editor");
requestAnimationFrame(function () {
var lista = porId("lista-mensajes");
var previo = ultimoFocoEditor && ultimoFocoEditor.isConnected && ultimoFocoEditor.getClientRects().length ? ultimoFocoEditor : null;
if (previo && enfocar(previo)) return;
var cab = lista && lista.querySelector(".ed-cab-principal");
if (cab && enfocar(cab)) { if (cab.scrollIntoView) cab.scrollIntoView({ block: "nearest" }); return; }
var pal = document.querySelector('#nuevo-tipo [data-tipo][tabindex="0"]') || document.querySelector("#nuevo-tipo [data-tipo]");
enfocar(pal || lista);
});
return true;
}
function alternarPanel() {
var movil = porId("panel-movil");
var a = document.activeElement;
if (movil && a && movil.contains(a)) irAlEditor();
else irAVistaPrevia();
}
function enlazarSaltos() {
var enlace = porId("saltar-vista");
if (enlace) enlace.addEventListener("click", function (e) { e.preventDefault(); irAVistaPrevia(); });
var ed = porId("panel-editor");
if (ed) ed.addEventListener("focusin", function (e) { ultimoFocoEditor = e.target; });
}
function alTecla(e) {
if (e.defaultPrevented || e.isComposing) return;
var k = e.key;
var mod = e.ctrlKey || e.metaKey;
var hayDialogo = !!document.querySelector("dialog[open]");
if (k === "F6" && !mod && !e.altKey && !hayDialogo) {
e.preventDefault();
alternarPanel();
return;
}
if (mod && !e.altKey && (k === "s" || k === "S")) {
e.preventDefault();
if (!hayDialogo && typeof WAM.exportarJSON === "function") WAM.exportarJSON();
@@ -625,6 +754,7 @@
case "t": case "T": e.preventDefault(); clicEn("btn-tema"); break;
case "f": case "F": e.preventDefault(); alternarPantallaCompleta(); break;
case "n": case "N": e.preventDefault(); WAM.abrirDemos(); break;
case "g": case "G": e.preventDefault(); alternarPanel(); break;
case "?": e.preventDefault(); WAM.abrirAtajos(); break;
case " ":
if (esInteractivo(e.target)) return; // Espacio activa el botón con foco
@@ -638,15 +768,28 @@
/* ------------------------------------------------------------------
Menú «⋯» de la toolbar (ventana estrecha) y pestañas Editor / Vista
------------------------------------------------------------------ */
function cerrarMenu() {
function cerrarMenu(devolverFoco) {
var tb = porId("toolbar");
var mas = porId("btn-mas");
if (!tb || !tb.classList.contains("tb-menu-abierto")) return false;
var menu = porId("tb-menu");
var dentro = menu && menu.contains(document.activeElement);
tb.classList.remove("tb-menu-abierto");
if (mas) mas.setAttribute("aria-expanded", "false");
if (mas) {
mas.setAttribute("aria-expanded", "false");
if (devolverFoco !== false && dentro) mas.focus();
}
return true;
}
// Opciones visibles del menú «⋯» abierto (en escritorio solo «Volver al ejemplo»)
function opcionesMenu() {
var menu = porId("tb-menu");
if (!menu) return [];
var bs = esEstrecha() ? menu.querySelectorAll("button:not([disabled])") : menu.querySelectorAll("#btn-reset");
return Array.prototype.filter.call(bs, function (b) { return b.getClientRects().length > 0; });
}
function enlazarMenu() {
var tb = porId("toolbar"), mas = porId("btn-mas"), menu = porId("tb-menu");
if (!tb || !mas || !menu) return;
@@ -654,10 +797,27 @@
e.stopPropagation();
var abierto = tb.classList.toggle("tb-menu-abierto");
mas.setAttribute("aria-expanded", abierto ? "true" : "false");
if (abierto) { var b = menu.querySelector("button:not([disabled])"); if (b) b.focus(); }
if (abierto) { var ops = opcionesMenu(); if (ops[0]) ops[0].focus(); }
});
// Flechas dentro del menú abierto
tb.addEventListener("keydown", function (e) {
if (!tb.classList.contains("tb-menu-abierto") || (e.key !== "ArrowDown" && e.key !== "ArrowUp")) return;
var ops = opcionesMenu();
var i = ops.indexOf(document.activeElement);
if (i < 0 && document.activeElement !== mas) return;
e.preventDefault();
var j = i < 0 ? 0 : (i + (e.key === "ArrowDown" ? 1 : -1) + ops.length) % ops.length;
if (ops[j]) ops[j].focus();
});
// Al salir con Tab del menú abierto, se cierra
tb.addEventListener("focusout", function (e) {
if (!tb.classList.contains("tb-menu-abierto")) return;
var sig = e.relatedTarget;
if (sig && (menu.contains(sig) || sig === mas)) return;
if (sig) cerrarMenu(false);
});
menu.addEventListener("click", function (e) {
if (e.target.closest && e.target.closest("button")) cerrarMenu();
if (e.target.closest && e.target.closest("button")) cerrarMenu(false);
});
document.addEventListener("click", function (e) {
if (!tb.classList.contains("tb-menu-abierto")) return;
@@ -782,9 +942,17 @@
/* ------------------------------------------------------------------
Estado de los botones de la toolbar
------------------------------------------------------------------ */
var temaPintado = null;
function sincronizarTema() {
var tema = document.body.getAttribute("data-tema");
var b = porId("btn-tema");
if (b) b.setAttribute("aria-pressed", document.body.getAttribute("data-tema") === "oscuro" ? "true" : "false");
if (b) {
b.setAttribute("aria-pressed", tema === "oscuro" ? "true" : "false");
b.setAttribute("aria-label", "Tema oscuro");
}
// El cambio de tema (botón o T) también pasa por «Guardando…»
if (temaPintado !== null && tema !== temaPintado) marcarEditado();
temaPintado = tema;
}
/* ------------------------------------------------------------------
@@ -809,6 +977,7 @@
enlazarPestanas();
observarToolbar();
enlazarChat();
enlazarSaltos();
envolverAnuncios();
sincronizarTema();