Estado inicial del mockup

This commit is contained in:
urieljareth
2026-09-27 19:17:33 -06:00
commit dc6e31a9d7
15 changed files with 6590 additions and 0 deletions
+699
View File
@@ -0,0 +1,699 @@
/* ==========================================================================
js/render.js — dueño B: pinta el chat de WhatsApp dentro del iPhone.
API (SPEC §2): WAM.render / renderHeader / renderMensaje / anadirMensajeDOM /
setTema / setEscribiendo / scrollAbajo / abrirSheet / cerrarSheet / tick.
Campos por tipo: SPEC §1. Colores, geometría y tipografía: docs/research-ui.md.
Cero listeners de flujo (los clics los resuelve js/interactivity.js con los
data-* estampados aquí; main.js abre el sheet del list con el data-flujo
"plano" del botón). Listeners propios: cierre en #sheet-fondo (elemento
creado por B), scroll de puntos del carousel y despliegue del plegado
">3 botones". No toca DOM al cargar.
========================================================================== */
(function () {
"use strict";
window.WAM = window.WAM || {};
/* ------------------------------ helpers ------------------------------ */
function byId(id) { return document.getElementById(id); }
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
function vacio(s) { return s == null || String(s) === ""; }
function estado() { return WAM.estado || { cuenta: {}, mensajes: [] }; }
var ESCRIBIENDO = false;
/* Placeholder local; en la práctica usa WAM.svgPlaceholder (js/store.js). */
function placeholder(texto) {
if (typeof WAM.svgPlaceholder === "function") {
try {
var u = WAM.svgPlaceholder(String(texto == null ? "" : texto));
if (u) return u;
} catch (e) { /* fallback abajo */ }
}
var t = esc(String(texto == null ? "" : texto).slice(0, 42));
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="320" height="240">' +
'<rect width="320" height="240" fill="#e7e3da"/>' +
'<rect x="122" y="70" width="76" height="60" rx="9" fill="none" stroke="#b9b2a4" stroke-width="4"/>' +
'<circle cx="143" cy="89" r="6" fill="#b9b2a4"/>' +
'<path d="M126 122 L148 104 L163 117 L176 106 L194 121" fill="none" stroke="#b9b2a4" stroke-width="4" stroke-linejoin="round"/>' +
'<text x="160" y="176" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" fill="#9a927f">' + t + "</text></svg>";
return "data:image/svg+xml," + encodeURIComponent(svg);
}
/* ------------------------------ iconos SVG inline ------------------------------ */
function svgChevronAtras() {
return '<svg width="12" height="20" viewBox="0 0 12 20" aria-hidden="true"><path d="M10.3 1.5 2 10l8.3 8.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
}
function svgChevronDer(px) {
var s = px || 14;
return '<svg width="' + s + '" height="' + s + '" viewBox="0 0 12 20" aria-hidden="true"><path d="M1.7 1.5 10 10l-8.3 8.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
}
function svgVideoLlamada() {
return '<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"><path d="M3.6 6.2h10.6a1.9 1.9 0 0 1 1.9 1.9v7.8a1.9 1.9 0 0 1-1.9 1.9H3.6a1.9 1.9 0 0 1-1.9-1.9V8.1a1.9 1.9 0 0 1 1.9-1.9Zm13.4 4.1 4.4-3a.55.55 0 0 1 .9.5v8.4a.55.55 0 0 1-.9.5l-4.4-3Z" fill="currentColor"/></svg>';
}
function svgLlamada(px) {
var s = px || 23;
return '<svg width="' + s + '" height="' + s + '" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 3.4c.6-.6 1.5-.5 2 .1l2.1 2.6c.4.6.4 1.4-.1 1.9l-1.2 1.4c.9 1.8 2.5 3.4 4.3 4.3l1.4-1.2c.5-.5 1.3-.5 1.9-.1l2.6 2.1c.6.5.7 1.4.1 2l-1.6 1.7c-.6.6-1.5.9-2.4.6-5.2-1.7-9.3-5.8-11-11-.3-.9 0-1.8.6-2.4Z" fill="currentColor"/></svg>';
}
function svgVerificado() {
return '<svg width="17" height="17" viewBox="0 0 16 16" aria-hidden="true">' +
'<polygon points="16,8 14.1,10.53 13.66,13.66 10.53,14.1 8,16 5.47,14.1 2.34,13.66 1.9,10.53 0,8 1.9,5.47 2.34,2.34 5.47,1.9 8,0 10.53,1.9 13.66,2.34 14.1,5.47" fill="#53bdeb"/>' +
'<path d="M4.6 8.3 7 10.6 11.4 5.7" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
}
function svgPlay(px) {
var s = px || 18;
return '<svg width="' + s + '" height="' + s + '" viewBox="0 0 24 24" aria-hidden="true"><path d="M8.2 5.4v13.2L19.4 12Z" fill="currentColor"/></svg>';
}
function svgMicrofono(px) {
var s = px || 19;
return '<svg width="' + s + '" height="' + s + '" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 14.6a3.4 3.4 0 0 0 3.4-3.4V5.9a3.4 3.4 0 0 0-6.8 0v5.3a3.4 3.4 0 0 0 3.4 3.4Zm-5.8-3.4a5.8 5.8 0 0 0 11.6 0M12 17v3.2M8.8 20.2h6.4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>';
}
function svgMusica(px) {
var s = px || 19;
return '<svg width="' + s + '" height="' + s + '" viewBox="0 0 24 24" aria-hidden="true"><path d="M9.2 18.3V5.6L18.4 3.5v12.7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6.7" cy="18.4" r="2.5" fill="currentColor"/><circle cx="15.9" cy="16.3" r="2.5" fill="currentColor"/></svg>';
}
function svgDescarga(px) {
var s = px || 15;
return '<svg width="' + s + '" height="' + s + '" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4.2v10.6M7.6 10.6l4.4 4.4 4.4-4.4M5 19.6h14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>';
}
function svgEnlace(px) {
var s = px || 18;
return '<svg width="' + s + '" height="' + s + '" viewBox="0 0 24 24" aria-hidden="true"><path d="M9.9 14.1a4.1 4.1 0 0 1 0-5.8l3-3a4.1 4.1 0 0 1 5.8 5.8l-1.7 1.7M14.1 9.9a4.1 4.1 0 0 1 0 5.8l-3 3a4.1 4.1 0 0 1-5.8-5.8l1.7-1.7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>';
}
function svgCorreo(px) {
var s = px || 15;
return '<svg width="' + s + '" height="' + s + '" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5.5" width="18" height="13" rx="2.4" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M4 7l8 6 8-6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
}
function svgEmpresa(px) {
var s = px || 15;
return '<svg width="' + s + '" height="' + s + '" viewBox="0 0 24 24" aria-hidden="true"><rect x="3.2" y="7.6" width="17.6" height="12.4" rx="2.4" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M9 7.6V6.2A2.2 2.2 0 0 1 11.2 4h1.6A2.2 2.2 0 0 1 15 6.2v1.4M3.2 12.4h17.6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>';
}
/* Icono de página coloreado por extensión (research-ui §6.6) */
var COLORES_DOC = {
pdf: "#e2574c", doc: "#4577e0", docx: "#4577e0",
xls: "#2f9e5b", xlsx: "#2f9e5b", csv: "#2f9e5b",
ppt: "#e5813d", pptx: "#e5813d",
txt: "#8592a0", zip: "#a5855e", rar: "#a5855e"
};
function extDe(nombre) {
var n = String(nombre == null ? "" : nombre);
var partes = n.split(".");
return partes.length > 1 ? partes.pop().toLowerCase() : "doc";
}
function docIcon(ext, px) {
var w = px || 30;
var h = Math.round(w * 36 / 30);
var color = COLORES_DOC[ext] || "#7c8894";
var etiqueta = ext.length <= 4 ? ext.toUpperCase() : "DOC";
return '<svg width="' + w + '" height="' + h + '" viewBox="0 0 30 36" aria-hidden="true">' +
'<path d="M3 4a3 3 0 0 1 3-3h14l7 7v24a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3Z" fill="' + color + '"/>' +
'<path d="M20 1l7 7h-6a1 1 0 0 1-1-1Z" fill="rgba(255,255,255,.4)"/>' +
'<text x="15" y="24.5" text-anchor="middle" font-family="Arial, sans-serif" font-size="7.5" font-weight="700" fill="#fff">' + esc(etiqueta) + "</text></svg>";
}
/* Mapa estático con pin rojo (research-ui §6.8, data-URI sin red) */
function svgMapa() {
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="320" height="190" viewBox="0 0 320 190">' +
'<rect width="320" height="190" fill="#e8e4da"/>' +
'<path d="M0 0h90L60 60H0Z" fill="#ded9cb"/>' +
'<path d="M230 190v-70l90-24v94Z" fill="#ded9cb"/>' +
'<path d="M115 0h75l-12 62h-70Z" fill="#e0dcd1"/>' +
'<path d="M0 150q80-26 160-6t160-14v60H0Z" fill="#cfe0e8"/>' +
'<path d="M118 0q10 60-8 120t-30 70" stroke="#fff" stroke-width="11" fill="none"/>' +
'<path d="M0 84q90 18 170 2t150-30" stroke="#fff" stroke-width="11" fill="none"/>' +
'<path d="M205 0q-6 50 12 95t40 95" stroke="#fff" stroke-width="7" fill="none"/>' +
'<path d="M40 40q60 24 120 12" stroke="#fff" stroke-width="5" fill="none"/>' +
'<path d="M252 40q40 10 68 6" stroke="#fff" stroke-width="5" fill="none"/>' +
'<ellipse cx="160" cy="103" rx="12" ry="4" fill="rgba(0,0,0,.18)"/>' +
'<path d="M160 34c-14.4 0-24 10-24 22.6C136 72 160 97 160 97s24-25 24-40.4C184 44 174.4 34 160 34Z" fill="#e14b4b"/>' +
'<circle cx="160" cy="56" r="8" fill="#fff"/>' +
"</svg>";
return "data:image/svg+xml," + encodeURIComponent(svg);
}
/* ------------------------------ texto enriquecido ------------------------------ */
/* Formato WhatsApp (research-api §2): *negrita* _cursiva_ ~tachado~ ```mono```
+ URLs hipervinculadas (color --wa-link, sin subrayado). */
function formatear(txt) {
var partes = String(txt == null ? "" : txt).split(/(https?:\/\/[^\s<>"]+)/g);
var html = "";
for (var i = 0; i < partes.length; i++) {
var p = partes[i];
if (p === "") continue;
if (/^https?:\/\//.test(p)) {
html += '<a class="wa-link" href="' + esc(p) + '" target="_blank" rel="noopener noreferrer">' + esc(p) + "</a>";
} else {
var t = esc(p);
t = t.replace(/```([\s\S]+?)```/g, "<code>$1</code>");
t = t.replace(/\*([^*\n]+)\*/g, "<strong>$1</strong>");
t = t.replace(/(^|[^\w])_([^_\n]+)_(?=[^\w]|$)/g, "$1<em>$2</em>");
t = t.replace(/(^|[^\w])~([^~\n]+)~(?=[^\w]|$)/g, "$1<del>$2</del>");
html += t;
}
}
return html;
}
/* ------------------------------ ticks y meta ------------------------------ */
/* ✓ enviado (gris) / ✓✓ entregado (gris) / ✓✓ leído (celeste #53bdeb) */
var TRAZO_TICK = '<path d="M1 5.4 4.1 8.5 10.4 1.6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>';
WAM.tick = function (estadoTick) {
var uno = '<svg class="tick" width="12" height="10" viewBox="0 0 11 10" aria-hidden="true">' + TRAZO_TICK + "</svg>";
var dos = '<svg class="tick" width="17" height="10" viewBox="0 0 16 10" aria-hidden="true">' + TRAZO_TICK +
'<path d="M6 5.4 9.1 8.5 15.4 1.6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
if (estadoTick === "read") return dos.replace('class="tick"', 'class="tick tick-read"');
if (estadoTick === "delivered") return dos;
return uno;
};
/* Hora (+ ticks solo en OUT) dentro de la burbuja */
function metaHTML(m, extra) {
var mm = m || {};
var html = '<span class="meta' + (extra ? " " + extra : "") + '">' + esc(vacio(mm.hora) ? "" : mm.hora);
if (mm.dir === "out") html += WAM.tick(mm.estado || "sent");
return html + "</span>";
}
/* ------------------------------ piezas de burbujas ------------------------------ */
function insigniaPlay(px) {
return '<span class="wa-play-badge' + (px && px <= 38 ? " wa-play-sm" : "") + '">' + svgPlay(px || 26) + "</span>";
}
function burbujaTexto(m) {
return '<div class="bubble"><div class="wa-texto">' + formatear(m.texto) + metaHTML(m) + "</div></div>";
}
/* image / video: media full-bleed; caption debajo; hora encima o en caption.
El placeholder SVG (data:image/svg+xml) solo lo decodifican los <img>: un
<video> con ese src lanza MEDIA_ELEMENT_ERROR, así que el vídeo sin src
degrada al placeholder como imagen (SPEC §5.10). */
function burbujaMedia(m, tipo) {
var tieneCaption = !vacio(m.caption);
var esVideo = tipo === "video";
var sinSrc = vacio(m.src);
var src = sinSrc ? placeholder(esVideo ? "video" : (m.caption || "imagen")) : m.src;
var media;
if (esVideo) {
var nodo = sinSrc
? '<img class="wa-media" src="' + esc(src) + '" alt="" draggable="false">'
: '<video class="wa-media" src="' + esc(src) + '" muted playsinline preload="metadata"></video>';
media = '<div class="wa-media-wrap">' + nodo + insigniaPlay(52) + "</div>";
} else {
media = '<img class="wa-media" src="' + esc(src) + '" alt="" draggable="false">';
}
var html = '<div class="bubble wa-bubble-media' + (tieneCaption ? " wa-con-caption" : "") + '">' + media;
if (tieneCaption) {
html += '<div class="wa-caption">' + formatear(m.caption) + metaHTML(m) + "</div>";
} else {
html += '<div class="wa-gradiente"></div>' + metaHTML(m, "meta-en-media");
}
return html + "</div>";
}
/* Waveform determinista a partir del id del mensaje */
function waveform(m) {
var n = 26, paso = 4.6, alto = 18;
var semilla = 0, id = String((m && m.id) || "wam");
for (var c = 0; c < id.length; c++) semilla = (semilla + id.charCodeAt(c) * (c + 7)) % 9973;
var barras = "";
for (var i = 0; i < n; i++) {
var h = 4 + Math.abs(Math.sin(semilla * 0.37 + i * 1.7)) * (alto - 5);
var y = (alto - h) / 2;
barras += '<rect x="' + (i * paso).toFixed(1) + '" y="' + y.toFixed(1) + '" width="2" height="' + h.toFixed(1) + '" rx="1"/>';
}
var w = (n * paso - 2.6).toFixed(1);
return '<svg class="audio-wave" width="' + w + '" height="' + alto + '" viewBox="0 0 ' + w + " " + alto + '" fill="currentColor" aria-hidden="true">' + barras + "</svg>";
}
function burbujaAudio(m) {
var voz = !!m.voz;
var dur = vacio(m.duracion) ? "" : '<span class="audio-dur">' + esc(m.duracion) + "</span>";
return '<div class="bubble wa-audio"><div class="audio-line">' +
'<span class="audio-play">' + svgPlay(15) + "</span>" +
waveform(m) + dur +
'<span class="audio-icono' + (voz ? " wa-voz" : "") + '">' + (voz ? svgMicrofono() : svgMusica()) + "</span>" +
"</div>" + metaHTML(m) + "</div>";
}
function burbujaDocumento(m) {
var nombre = vacio(m.filename) ? "documento.pdf" : String(m.filename);
var ext = extDe(nombre);
var html = '<div class="bubble wa-doc"><div class="doc-line">' +
'<span class="doc-icono">' + docIcon(ext, 30) + "</span>" +
'<span class="doc-datos"><span class="doc-nombre">' + esc(nombre) + "</span>" +
'<span class="doc-sub">' + esc(ext.toUpperCase()) + " · Documento</span></span>" +
'<span class="doc-descarga">' + svgDescarga() + "</span>" +
"</div>";
if (!vacio(m.caption)) html += '<div class="wa-caption">' + formatear(m.caption) + metaHTML(m) + "</div>";
else html += metaHTML(m);
return html + "</div>";
}
function stickerHTML(m) {
var src = vacio(m.src) ? placeholder("sticker") : m.src;
return '<div class="sticker-box" data-hora="' + esc(vacio(m.hora) ? "" : m.hora) + '">' +
'<img class="sticker-img" src="' + esc(src) + '" alt="Sticker" draggable="false"></div>';
}
function burbujaUbicacion(m) {
var tieneDatos = !vacio(m.nombre) || !vacio(m.direccion);
var html = '<div class="bubble wa-loc"><img class="loc-mapa" src="' + svgMapa() + '" alt="Ubicación" draggable="false">';
if (tieneDatos) {
html += '<div class="loc-datos">' +
(vacio(m.nombre) ? "" : '<div class="loc-nombre">' + esc(m.nombre) + "</div>") +
(vacio(m.direccion) ? "" : '<div class="loc-dir">' + esc(m.direccion) + "</div>") +
metaHTML(m) + "</div>";
} else {
html += '<div class="wa-gradiente"></div>' + metaHTML(m, "meta-en-media");
}
return html + "</div>";
}
function inicialesDe(nombre) {
var partes = String(nombre == null ? "" : nombre).trim().split(/\s+/);
var s = "";
for (var i = 0; i < partes.length && s.length < 2; i++) {
if (partes[i]) s += partes[i].charAt(0).toUpperCase();
}
return s || "?";
}
function colorDe(nombre) {
var paleta = ["#7f8ea8", "#8a7fa8", "#a87f96", "#7fa88f", "#a89a7f", "#7f9aa8", "#a8847f", "#6f8ba4"];
var h = 0, s = String(nombre == null ? "" : nombre);
for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9973;
return paleta[h % paleta.length];
}
function burbujaContacto(m) {
var nombre = vacio(m.nombre) ? "Contacto" : String(m.nombre);
var filas = "";
if (!vacio(m.telefono)) filas += '<div class="contact-fila">' + svgLlamada(15) + "<span>" + esc(m.telefono) + "</span></div>";
if (!vacio(m.email)) filas += '<div class="contact-fila">' + svgCorreo() + "<span>" + esc(m.email) + "</span></div>";
if (!vacio(m.empresa)) filas += '<div class="contact-fila">' + svgEmpresa() + "<span>" + esc(m.empresa) + "</span></div>";
return '<div class="bubble wa-contact">' +
'<div class="contact-cab">' +
'<span class="contact-avatar" style="background:' + colorDe(nombre) + '">' + esc(inicialesDe(nombre)) + "</span>" +
'<span class="contact-nombre">' + esc(nombre) + "</span>" +
'<span class="contact-chev">' + svgChevronDer(14) + "</span>" +
"</div>" +
(filas ? '<div class="contact-filas">' + filas + "</div>" : "") +
metaHTML(m) +
"</div>";
}
/* Botones apilados (template / quick replies). Con url → <a>; sin url → flujo.
Con >3 botones (plantillas) el cliente real muestra 2 y un plegado
"See all options" (research-api §7.2 / research-ui §6.4): se pintan los
2 primeros + "Ver todas las opciones", que despliega el resto. */
function tarjetaBotones(botones, i, clase) {
var bs = Array.isArray(botones) ? botones : [];
var total = Math.min(bs.length, 10);
var plegar = total > 3;
var html = "", ocultos = "";
for (var j = 0; j < total; j++) {
var b = bs[j] || {};
var etiqueta = vacio(b.texto) ? "Botón " + (j + 1) : String(b.texto);
var nodo;
if (!vacio(b.url)) {
nodo = '<a class="qr-btn" href="' + esc(b.url) + '" target="_blank" rel="noopener noreferrer">' + esc(etiqueta) + "</a>";
} else {
nodo = '<span class="qr-btn" role="button" tabindex="0" data-flujo="' + i + '" data-btn="' + j + '">' + esc(etiqueta) + "</span>";
}
if (plegar && j < 2) html += nodo;
else if (plegar) ocultos += nodo.replace(/^<(a|span) /, "<$1 hidden ");
else html += nodo;
}
if (!html && !ocultos) return "";
if (ocultos) {
html += '<span class="qr-btn" role="button" tabindex="0" data-ver-todas="1">Ver todas las opciones (' + (total - 2) + ")</span>" + ocultos;
}
return '<div class="qr-btns' + (clase ? " " + clase : "") + '">' + html + "</div>";
}
/* Despliegue del plegado ">3 botones" (listener propio de B, solo presentación) */
function activarPlegado(li) {
var toggle = li.querySelector("[data-ver-todas]");
if (!toggle) return;
toggle.addEventListener("click", function () {
var ocultos = li.querySelectorAll(".qr-btn[hidden]");
for (var k = 0; k < ocultos.length; k++) ocultos[k].removeAttribute("hidden");
if (toggle.parentNode) toggle.parentNode.removeChild(toggle);
});
}
/* Header compartido por template y buttons (research-api §6.1/§7.2):
imagen / vídeo / documento / texto. Vídeo sin headerMedia → placeholder
como <img> (un <video> no decodifica el SVG; ver burbujaMedia). */
function headerHTML(m) {
var ht = m.headerTipo || "";
if (ht === "imagen") {
var src = vacio(m.headerMedia) ? placeholder(vacio(m.headerTexto) ? "imagen" : m.headerTexto) : m.headerMedia;
return '<img class="tpl-media" src="' + esc(src) + '" alt="" draggable="false">';
}
if (ht === "video") {
var sinVideo = vacio(m.headerMedia);
var srcv = sinVideo ? placeholder("video") : m.headerMedia;
var nodo = sinVideo
? '<img class="tpl-media" src="' + esc(srcv) + '" alt="" draggable="false">'
: '<video class="tpl-media" src="' + esc(srcv) + '" muted playsinline preload="metadata"></video>';
return '<div class="tpl-media-wrap">' + nodo + insigniaPlay(38) + "</div>";
}
if (ht === "documento") {
var nom = vacio(m.headerTexto) ? "documento.pdf" : String(m.headerTexto);
return '<div class="tpl-header-doc">' + docIcon(extDe(nom), 22) + "<span>" + esc(nom) + "</span></div>";
}
if (ht === "texto") {
return vacio(m.headerTexto) ? "" : '<div class="tpl-header-txt">' + esc(m.headerTexto) + "</div>";
}
return "";
}
/* template: header → body → footer → botones (research-ui §6.4) */
function tplHTML(m, i) {
var header = headerHTML(m);
var body = vacio(m.body) ? "" : '<div class="tpl-body">' + formatear(m.body) + "</div>";
var footer = vacio(m.footer) ? "" : '<span class="template-footer">' + formatear(m.footer) + "</span>";
return '<div class="tpl">' + header + body +
'<div class="wa-pie">' + footer + metaHTML(m) + "</div>" +
tarjetaBotones(m.botones, i, "tpl-btns") +
"</div>";
}
/* Pie común dentro de burbuja interactiva: footer gris + hora/ticks */
function pieBurbuja(m, footer) {
return '<div class="wa-pie">' + (vacio(footer) ? "" : '<span class="template-footer">' + formatear(footer) + "</span>") + metaHTML(m) + "</div>";
}
/* buttons: quick replies (máx 3) con header opcional (research-api §6.1) */
function buttonsHTML(m, i) {
return '<div class="wa-stack">' +
'<div class="bubble">' + headerHTML(m) + '<div class="wa-texto">' + formatear(m.body) + "</div>" + pieBurbuja(m, m.footer) + "</div>" +
tarjetaBotones(m.botones, i, "") +
"</div>";
}
/* list: header + body + footer + botón único. Sin data-btn/sec/fila/card:
es el marcador "plano" que main.js resuelve llamando WAM.abrirSheet(i). */
function listHTML(m, i) {
var cabecera = vacio(m.header) ? "" : '<div class="tpl-header-txt">' + esc(m.header) + "</div>";
var etiqueta = vacio(m.boton) ? "Ver opciones" : String(m.boton);
return '<div class="wa-stack">' +
'<div class="bubble">' + cabecera +
'<div class="wa-texto">' + formatear(m.body) + "</div>" +
pieBurbuja(m, m.footer) + "</div>" +
'<div class="qr-btns"><span class="qr-btn" role="button" tabindex="0" data-flujo="' + i + '">' + esc(etiqueta) + "</span></div>" +
"</div>";
}
function dominioDe(url) {
var u = String(url == null ? "" : url);
if (!u) return "";
try { return new URL(u).hostname || u; } catch (e) { return u.replace(/^[a-z]+:\/\//i, "").split("/")[0]; }
}
/* cta_url: headerMedia opcional + body + footer + botón con dominio.
Sin url el botón queda inerte (research-api §6.3: nada que abrir) y la
fila de dominio no se pinta (dominioDe("") = ""). */
function ctaHTML(m, i) {
var html = '<div class="wa-stack">';
if (!vacio(m.headerMedia)) {
html += '<div class="bubble wa-bubble-media"><img class="wa-media" src="' + esc(m.headerMedia) + '" alt="" draggable="false"></div>';
}
html += '<div class="bubble"><div class="wa-texto">' + formatear(m.body) + "</div>" + pieBurbuja(m, m.footer) + "</div>";
var etiqueta = vacio(m.textoBoton) ? "Abrir enlace" : String(m.textoBoton);
var dominio = dominioDe(m.url);
var boton;
if (vacio(m.url)) {
boton = '<span class="cta-btn cta-btn-inactiva" aria-disabled="true">' + svgEnlace() + '<span class="btn-label">' + esc(etiqueta) + "</span></span>";
} else {
boton = '<a class="cta-btn" href="' + esc(m.url) + '" target="_blank" rel="noopener noreferrer">' + svgEnlace() + '<span class="btn-label">' + esc(etiqueta) + "</span></a>";
}
html += '<div class="cta-card">' + boton +
(dominio ? '<div class="cta-domain">' + esc(dominio) + "</div>" : "") +
"</div></div>";
return html;
}
/* carousel: burbuja body + tarjetas con scroll-snap + puntos */
function carouselHTML(m, i) {
var cards = Array.isArray(m.cards) ? m.cards : [];
var html = '<div class="wa-stack wa-stack-full">';
if (!vacio(m.body)) {
html += '<div class="bubble"><div class="wa-texto">' + formatear(m.body) + "</div>" + pieBurbuja(m, "") + "</div>";
}
html += '<div class="carousel">';
for (var j = 0; j < cards.length; j++) {
var c = cards[j] || {};
var media = vacio(c.media) ? placeholder(vacio(c.texto) ? "card" : c.texto) : c.media;
var boton;
if (!vacio(c.url)) {
boton = '<a class="qr-btn" href="' + esc(c.url) + '" target="_blank" rel="noopener noreferrer">' + esc(vacio(c.textoBoton) ? "Ver más" : c.textoBoton) + "</a>";
} else {
boton = '<span class="qr-btn" role="button" tabindex="0" data-flujo="' + i + '" data-card="' + j + '">' + esc(vacio(c.textoBoton) ? "Ver más" : c.textoBoton) + "</span>";
}
html += '<div class="card">' +
'<img class="card-media" src="' + esc(media) + '" alt="" draggable="false">' +
(vacio(c.texto) ? "" : '<div class="card-body">' + formatear(c.texto) + "</div>") +
'<div class="card-btns">' + boton + "</div></div>";
}
html += "</div>";
if (cards.length > 1) {
html += '<div class="car-puntos">';
for (j = 0; j < cards.length; j++) html += '<span class="car-punto' + (j === 0 ? " activo" : "") + '"></span>';
html += "</div>";
}
return html + "</div>";
}
/* Puntos del carousel (listener de presentación, no de flujo) */
function activarPuntos(li) {
var car = li.querySelector(".carousel");
if (!car) return;
var puntos = li.querySelectorAll(".car-punto");
if (!puntos.length) return;
car.addEventListener("scroll", function () {
var hijo = car.firstElementChild;
var paso = hijo ? hijo.offsetWidth + 8 : 0;
if (!paso) return;
var idx = Math.round(car.scrollLeft / paso);
if (idx < 0) idx = 0;
if (idx > puntos.length - 1) idx = puntos.length - 1;
for (var k = 0; k < puntos.length; k++) {
puntos[k].className = "car-punto" + (k === idx ? " activo" : "");
}
}, { passive: true });
}
/* ------------------------------ API pública ------------------------------ */
function textoSubtitulo() {
if (ESCRIBIENDO) return "escribiendo…";
var c = estado().cuenta || {};
return vacio(c.subtitulo) ? "en línea" : String(c.subtitulo);
}
/* Avatar/nombre/subtítulo/verificado en #chat-header (chevron + iconos) */
WAM.renderHeader = function () {
var header = byId("chat-header");
if (!header) return;
var c = estado().cuenta || {};
var nombre = vacio(c.nombre) ? "Chat" : String(c.nombre);
var avatar = vacio(c.avatar) ? placeholder(nombre) : c.avatar;
header.innerHTML =
'<span class="wa-back">' + svgChevronAtras() + "</span>" +
'<img id="chat-avatar" class="wa-avatar" src="' + esc(avatar) + '" alt="" draggable="false">' +
'<div class="wa-header-tit">' +
'<div class="header-title"><span id="chat-nombre">' + esc(nombre) + "</span>" +
'<span id="chat-verificado" class="wa-verificado"' + (c.verificado ? "" : " hidden") + ">" + svgVerificado() + "</span></div>" +
'<div class="header-sub' + (ESCRIBIENDO ? " wa-sub-verde" : "") + '" id="chat-subtitulo">' + esc(textoSubtitulo()) + "</div>" +
"</div>" +
'<span class="wa-hicon">' + svgVideoLlamada() + "</span>" +
'<span class="wa-hicon">' + svgLlamada() + "</span>";
};
/* li.wa-row con todo el contenido del tipo + data-* para la interactividad */
WAM.renderMensaje = function (msg, i) {
var m = msg || {};
var idx = typeof i === "number" ? i : 0;
var dir = m.dir === "out" ? "out" : "in";
var mensajes = estado().mensajes || [];
var prev = idx > 0 ? mensajes[idx - 1] : null;
var grupo = !prev || prev.dir !== m.dir ? " grupo" : "";
var cuerpo;
switch (m.tipo) {
case "image": cuerpo = burbujaMedia(m, "imagen"); break;
case "video": cuerpo = burbujaMedia(m, "video"); break;
case "audio": cuerpo = burbujaAudio(m); break;
case "document": cuerpo = burbujaDocumento(m); break;
case "sticker": cuerpo = stickerHTML(m); break;
case "location": cuerpo = burbujaUbicacion(m); break;
case "contact": cuerpo = burbujaContacto(m); break;
case "template": cuerpo = tplHTML(m, idx); break;
case "buttons": cuerpo = buttonsHTML(m, idx); break;
case "list": cuerpo = listHTML(m, idx); break;
case "cta_url": cuerpo = ctaHTML(m, idx); break;
case "carousel": cuerpo = carouselHTML(m, idx); break;
default: cuerpo = burbujaTexto(m);
}
var li = document.createElement("li");
li.className = "wa-row " + dir + grupo;
if (!vacio(m.id)) li.setAttribute("data-id", String(m.id));
li.innerHTML = cuerpo;
if (m.tipo === "carousel") activarPuntos(li);
activarPlegado(li);
return li;
};
/* renderHeader() + repinta TODOS los mensajes en #chat-cuerpo */
WAM.render = function () {
WAM.renderHeader();
var cuerpo = byId("chat-cuerpo");
if (!cuerpo) return;
var ind = byId("chat-escribiendo");
var indEnCuerpo = !!(ind && ind.parentNode === cuerpo);
var mensajes = estado().mensajes || [];
var frag = document.createDocumentFragment();
for (var i = 0; i < mensajes.length; i++) frag.appendChild(WAM.renderMensaje(mensajes[i], i));
cuerpo.innerHTML = "";
if (indEnCuerpo && ind) frag.appendChild(ind); /* conserva el indicador */
cuerpo.appendChild(frag);
WAM.scrollAbajo();
};
/* append + scrollAbajo (lo usa la interactividad, sin re-render total) */
WAM.anadirMensajeDOM = function (msg) {
var cuerpo = byId("chat-cuerpo");
if (!cuerpo) return;
var mensajes = estado().mensajes || [];
var i = mensajes.indexOf ? mensajes.indexOf(msg) : -1;
if (i < 0) i = mensajes.length;
var nodo = WAM.renderMensaje(msg, i);
var ind = byId("chat-escribiendo");
if (ind && ind.parentNode === cuerpo && !ind.hidden) cuerpo.insertBefore(nodo, ind);
else cuerpo.appendChild(nodo);
WAM.scrollAbajo();
};
/* body[data-tema] + clase oscuro en #pantalla */
WAM.setTema = function (tema) {
var oscuro = tema === "oscuro";
if (document.body) document.body.setAttribute("data-tema", oscuro ? "oscuro" : "claro");
var pantalla = byId("pantalla");
if (pantalla) {
if (oscuro) pantalla.classList.add("oscuro");
else pantalla.classList.remove("oscuro");
}
};
/* Muestra/oculta #chat-escribiendo; subtítulo "escribiendo…" en verde */
WAM.setEscribiendo = function (visible) {
ESCRIBIENDO = !!visible;
var cuerpo = byId("chat-cuerpo");
var ind = byId("chat-escribiendo");
if (!ind && cuerpo) {
ind = document.createElement("li");
ind.id = "chat-escribiendo";
ind.className = "wa-row in";
ind.innerHTML = '<div class="bubble wa-bubble-escribiendo">' +
'<span class="wam-dot"></span><span class="wam-dot"></span><span class="wam-dot"></span></div>';
cuerpo.appendChild(ind);
}
if (ind) {
/* Respeta la colocación de A (index.html:147, fuera de #chat-cuerpo):
rellena contenido, alterna hidden y hereda la fila visual. */
if (String(ind.className).indexOf("wa-row") === -1) {
ind.className = String(ind.className ? ind.className + " " : "") + "wa-row in";
}
if (String(ind.innerHTML).indexOf("wa-bubble-escribiendo") === -1) {
ind.innerHTML = '<div class="bubble wa-bubble-escribiendo">' +
'<span class="wam-dot"></span><span class="wam-dot"></span><span class="wam-dot"></span></div>';
}
ind.hidden = !ESCRIBIENDO;
if (ESCRIBIENDO && ind.parentNode === cuerpo) cuerpo.appendChild(ind); /* al final */
}
var sub = byId("chat-subtitulo");
if (sub) {
sub.textContent = textoSubtitulo();
sub.className = "header-sub" + (ESCRIBIENDO ? " wa-sub-verde" : "");
}
if (ESCRIBIENDO) WAM.scrollAbajo();
};
WAM.scrollAbajo = function () {
var cuerpo = byId("chat-cuerpo");
if (cuerpo) cuerpo.scrollTop = cuerpo.scrollHeight;
};
/* Sheet de lista (B crea/destruye) en #pantalla */
WAM.abrirSheet = function (i) {
var pantalla = byId("pantalla");
if (!pantalla) return;
WAM.cerrarSheet();
var mensajes = estado().mensajes || [];
var m = (typeof i === "number" ? mensajes[i] : null) || {};
var secciones = Array.isArray(m.secciones) ? m.secciones : [];
var cuenta = estado().cuenta || {};
var titulo = vacio(m.header) ? (vacio(cuenta.nombre) ? "Opciones" : String(cuenta.nombre)) : String(m.header);
var html = "", total = 0, s, f;
for (s = 0; s < secciones.length && total < 10; s++) {
var sec = secciones[s] || {};
var filas = Array.isArray(sec.filas) ? sec.filas : [];
if (secciones.length > 1 && !vacio(sec.titulo)) {
html += '<div class="sheet-seccion">' + esc(sec.titulo) + "</div>";
}
html += '<div class="sheet-grupo">';
for (f = 0; f < filas.length && total < 10; f++) {
var fila = filas[f] || {};
total++;
html += '<span class="sheet-fila" role="button" tabindex="0" data-flujo="' + i + '" data-sec="' + s + '" data-fila="' + f + '">' +
'<span class="sheet-fila-txt"><span class="sheet-fila-titulo">' + esc(vacio(fila.titulo) ? "Opción" : fila.titulo) + "</span>" +
(vacio(fila.descripcion) ? "" : '<span class="sheet-fila-desc">' + esc(fila.descripcion) + "</span>") +
"</span>" + svgChevronDer(13) + "</span>";
}
html += "</div>";
}
var hoja = document.createElement("div");
hoja.id = "sheet";
hoja.className = "sheet";
hoja.innerHTML = '<div class="sheet-fondo" id="sheet-fondo"></div>' +
'<div class="sheet-panel"><div class="sheet-cabecera"><span class="sheet-titulo" id="sheet-titulo">' + esc(titulo) + "</span></div>" +
'<div class="sheet-opciones" id="sheet-opciones">' + html +
(total ? "" : '<div class="sheet-vacio">Sin opciones</div>') + "</div></div>";
pantalla.appendChild(hoja);
var fondo = hoja.querySelector("#sheet-fondo");
if (fondo) fondo.addEventListener("click", function () { WAM.cerrarSheet(); });
};
WAM.cerrarSheet = function () {
var hoja = byId("sheet");
if (hoja && hoja.parentNode) hoja.parentNode.removeChild(hoja);
};
})();