Files
whatsapp-api-mockup/SPEC.md
T
2026-09-27 19:17:33 -06:00

197 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC — WhatsApp Cloud API Mockup (demo visual 100 % estática)
Web estática: `index.html` + `css/*.css` + `js/*.js`. Sin frameworks, sin build, sin módulos ES,
sin `fetch()` de archivos locales; funciona abriendo `index.html` por `file://` con doble clic.
Único global: `window.WAM` (cada JS hace `window.WAM = window.WAM || {}` y añade propiedades).
Estado único y vivo en `WAM.estado`; tras toda mutación: `WAM.guardar()` + re-render.
Bases: `docs/research-api.md` (payloads y límites) y `docs/research-ui.md` (colores, burbujas, marco). UI en español.
## 1. Modelo de datos (`WAM.estado`, JSON serializable → localStorage / export)
```json
{
"version": 1,
"tema": "claro",
"cuenta": {
"nombre": "Café Aurora",
"avatar": "",
"subtitulo": "en línea",
"verificado": true
},
"mensajes": [ ]
}
```
Sobre común de CADA mensaje (`Mensaje`): `{ "id": "m1", "dir": "in"|"out",
"tipo": "<ver union>", "hora": "10:24" (texto literal HH:MM), "estado": "sent"|"delivered"|"read" }`.
`estado` solo se renderiza si `dir:"out"` (ticks abajo-derecha); por defecto `"sent"`.
Campos EXACTOS por `tipo` (los que faltan no aplican; `respuestas` = array de `Mensaje` completos
con `dir:"in"`, anidados, NO se renderizan en el chat: los consume la interactividad):
1. `text`: `texto`
2. `image`: `src`, `caption`
3. `video`: `src`, `caption`
4. `audio`: `src` (opcional), `voz` (bool: nota de voz), `duracion` ("0:12")
5. `document`: `src` (ignorado), `caption`, `filename` (con extensión; el icono depende de ella)
6. `sticker`: `src` (WebP/imagen; sin burbuja de texto ni hora dentro)
7. `location`: `latitud`, `longitud`, `nombre`, `direccion`
8. `contact`: `nombre` (formatted_name), `telefono`, `email`, `empresa`
9. `template`: `headerTipo` (""|"texto"|"imagen"|"video"|"documento"), `headerTexto`, `headerMedia`,
`body`, `footer`, `botones`: `[ { "texto", "url" (opcional, ≤2 con url), "respuestas": [Mensaje…] } ]`
10. `buttons` (quick replies, máx 3): `headerTipo` (""|"texto"|"imagen"|"video"|"documento"),
`headerTexto`, `headerMedia`, `body`, `footer`, `botones`: `[ { "texto", "respuestas" } ]`
11. `list`: `header`, `body`, `footer`, `boton` (etiqueta ≤20), `secciones`:
`[ { "titulo", "filas": [ { "titulo", "descripcion", "respuestas" } ] } ]`
12. `cta_url`: `headerMedia` ("" = sin header), `body`, `footer`, `textoBoton`, `url`
13. `carousel` (2–5 cards): `body`, `cards`:
`[ { "media", "texto", "textoBoton", "url", "respuestas" } ]`
Convenciones: `src`/`avatar`/`headerMedia`/`media` = dataURL o URL remota; `""` → placeholder SVG
de `WAM.svgPlaceholder(texto)`. Botones con `url` no vacía se renderizan como `<a target="_blank">`
(cta_url, template URL, card de carousel); sin `url` → flujo interactivo con `respuestas`.
`WAM.normalizar(o)` rellena campos faltantes por tipo y devuelve el estado válido (import/upgrade).
## 2. Contratos de módulos (namespace único `WAM`) e IDs DOM compartidos
### js/store.js (dueño D) — estado, persistencia, demo
- `WAM.CLAVE = "wam-mockup-v1"` · `WAM.estado` (objeto vivo)
- `WAM.datosDemo()` → estado nuevo de la demo (§6) · `WAM.cargar()` → estado (LS o demo; normaliza)
- `WAM.guardar()` → void (JSON a localStorage) · `WAM.resetear()` → void (borra LS, estado = demo)
- `WAM.normalizar(o)` → estado · `WAM.generarId()` → string · `WAM.nuevoMensaje(tipo)` → Mensaje vacío
- `WAM.horaAhora()` → "HH:MM" · `WAM.svgPlaceholder(texto)` → dataURI `data:image/svg+xml`
### js/render.js (dueño B) — pinta el chat, cero listeners de flujo
- `WAM.render()` → void: `renderHeader()` + repinta TODOS los mensajes de `estado.mensajes` en `#chat-cuerpo`
- `WAM.renderHeader()` → void: avatar/nombre/subtítulo/verificado en `#chat-header`
- `WAM.renderMensaje(msg, i)` → Node `li.wa-row` con todo el contenido del tipo (incluye `data-*`, § abajo)
- `WAM.anadirMensajeDOM(msg)` → void: append + `scrollAbajo()` (lo usa la interactividad, sin re-render total)
- `WAM.setTema(tema)` → void: `body[data-tema]` + clase `oscuro` en `#pantalla`
- `WAM.setEscribiendo(visible)` → void: muestra/oculta `#chat-escribiendo`; subtítulo "escribiendo…" en verde
- `WAM.scrollAbajo()` → void · `WAM.abrirSheet(i)` / `WAM.cerrarSheet()` → void (sheet de lista en `#pantalla`)
- `WAM.tick(estado)` → string SVG inline (✓ gris / ✓✓ gris / ✓✓ celeste `#53bdeb`)
### js/editor.js (dueño C) — formularios; listeners SOLO dentro de `#panel-editor` e inputs propios
- `WAM.renderEditor()` → void: formulario de cuenta + repinta tarjetas
- `WAM.renderTarjetas()` → void: una tarjeta colapsable por mensaje en `#lista-mensajes` (todos los campos del tipo)
- `WAM.anadirMensaje(tipo)` → void · `WAM.moverMensaje(i, delta)` → void · `WAM.duplicarMensaje(i)` → void
- `WAM.borrarMensaje(i)` → void · `WAM.setCampo(ruta, valor)` → void (ruta `"mensajes.3.body"`; muta + guardar + `WAM.render()`)
- `WAM.aDataUrl(archivo)` → Promise<dataURL> (FileReader) · `WAM.exportarJSON()` → void (Blob + `<a download>`)
- `WAM.importarJSON(archivo)` → Promise<void> (parse + `normalizar` + guardar + re-render total)
### js/interactivity.js (dueño D) — flujos, typing, demo
- `WAM.resolverFlujo(el)` → `{ texto, respuestas }` a partir de los `data-*` del elemento clicado
- `WAM.flujo(texto, respuestas)` → Promise: burbuja OUT con `texto` (ticks auto sent→delivered→read, ~800 ms c/u),
typing ON ~900 ms, luego por cada `respuestas`: typing ON ~900 ms → `anadirMensajeDOM` → scroll; typing OFF
- `WAM.enviarLibre(texto)` → void: burbuja OUT del composer (hora `horaAhora()`, estado `sent`, ticks auto)
- `WAM.reproducirDemo()` / `WAM.detenerDemo()` → void: vacía `#chat-cuerpo` y revela `estado.mensajes` uno a uno
(~600 ms; mensaje `in` precedido de typing ~700 ms; cancelable)
- `WAM.espera(ms)` → Promise cancelable (todo timer pasa por aquí para poder detener)
### js/main.js (dueño D) — bootstrap
- `WAM.init()` → void (se ejecuta al final del body): `estado = cargar()`; `setTema`; `render()`; `renderEditor()`;
delegación de clics en `#chat-cuerpo` (ver data-*), submit de `#composer`, `#btn-reproducir`, toolbar
(`#btn-tema`, `#btn-presentacion`, `#btn-exportar`, `#btn-importar`→`#importar-archivo`, `#btn-reset`)
### IDs DOM (el HTML lo escribe A; el contenido lo rellena B o C en runtime)
| ID | Rellena | Papel |
|---|---|---|
| `app`, `panel-editor`, `panel-movil`, `toolbar` | A | shell de dos columnas y barra superior |
| `btn-tema`, `btn-presentacion`, `btn-exportar`, `btn-importar`, `btn-reset`, `importar-archivo` (hidden) | A/C | toolbar; main (D) los enlaza |
| `editor-cuenta` | C | form identidad: `cuenta-nombre`, `cuenta-subtitulo`, `cuenta-verificado`, `cuenta-avatar-vista`, `cuenta-avatar-archivo`, `cuenta-avatar-url` |
| `nuevo-tipo` (paleta de tarjetas de los 13 tipos), `nuevo-tipo-valor` (hidden), `btn-anadir`, `lista-mensajes` | C | alta por paleta (el tipo elegido vive en `#nuevo-tipo-valor` y `data-valor`) y tarjetas de mensajes |
| `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 |
| `chat-header` (`chat-avatar`, `chat-nombre`, `chat-subtitulo`, `chat-verificado`), `chat-cuerpo`, `chat-escribiendo` | A estructura / B contenido | header del chat, scroll de mensajes, typing |
| `composer` (form), `composer-input`, `composer-enviar` | A estructura / D lógica | campo "Mensaje" + enviar |
| `sheet`, `sheet-fondo`, `sheet-titulo`, `sheet-opciones` | B crea/desstruye | modal de selección de lista |
Contrato de clics B→D (delegación en `#chat-cuerpo`): B estampa en cada elemento interactivo
`data-flujo="<índice del mensaje>"` y además `data-btn` (template/buttons), `data-sec`+`data-fila` (list),
`data-card` (carousel). D resuelve con `resolverFlujo(el)` y llama `flujo()`. Los `<a>` de URL no llevan flujo.
### Convención CSS
- `css/base.css` (A): reset, layout grid de `#app`, `.toolbar`, variables `--wam-*` (fondo, panel, texto,
borde, acento) con overrides en `body[data-tema="oscuro"]`; `body.presentacion` oculta `#panel-editor`.
- `css/iphone.css` (A): `#panel-movil`, `.iphone` (393×852, radio 55, negro), `.pantalla`, `.island`
(126×37, top 11), `.statusbar` (54 pt), `.home-indicator` (139×5).
- `css/whatsapp.css` (B): todo bajo `.wa`; variables `--wa-*` del research-ui y overrides `.wa.oscuro`;
clases: `.wa-row .in .out .bubble .meta .tick .tick-read`, cola con `clip-path`, `.qr-btns .qr-btn`,
`.tpl-*`, `.cta-*`, `.carousel .card`, `.doc-* .audio-* .loc-* .contact-* .sticker-*`, `.sheet*`;
keyframes de typing con prefijo `wam-`.
- `css/editor.css` (C): prefijo `ed-` (`.ed-seccion .ed-campo .ed-tarjeta .ed-btn .ed-fila .ed-badge`).
- Tema: B alterna `oscuro` en `#pantalla`; editor/shell ya reaccionan por `body[data-tema]` (lo setea `setTema`).
## 3. Orden exacto de scripts en index.html (todos al final del `<body>`, sin `defer`/`module`)
```html
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/iphone.css">
<link rel="stylesheet" href="css/whatsapp.css">
<link rel="stylesheet" href="css/editor.css">
…
<script src="js/store.js"></script> <!-- define WAM.CLAVE/estado/normalizar; NO toca DOM al cargar -->
<script src="js/render.js"></script> <!-- define funciones de render; NO toca DOM al cargar -->
<script src="js/editor.js"></script> <!-- define funciones del editor; NO toca DOM al cargar -->
<script src="js/interactivity.js"></script> <!-- define flujos/typing/demo; NO toca DOM al cargar -->
<script src="js/main.js"></script> <!-- única ejecución: WAM.init() -->
```
Ningún archivo lee `WAM.estado` en carga (solo dentro de funciones) → el orden garantiza cero
`undefined` y cero errores por `file://`.
## 4. División de trabajo (archivos sin solapar)
**A — shell:** `index.html` + `css/base.css` + `css/iphone.css`. HTML completo con TODOS los IDs de la
tabla (contenedores del editor y del chat vacíos), `<body data-tema="claro">`, `#pantalla class="wa"`,
grid de dos columnas, toolbar, marco iPhone con Dynamic Island y status bar. Sin JS propio.
**B — chat:** `css/whatsapp.css` + `js/render.js`. Render de los 13 tipos según research-ui (burbuja
17 px, cola arriba, meta flotante abajo-derecha, media full-bleed, template header→body→footer→botones,
tarjeta apilada de quick replies con hairlines, sheet de lista, waveform de audio, tarjeta de contacto,
pin de mapa, scroll-snap del carousel), ticks SVG, indicador "escribiendo…", claro/oscuro. Sin listeners
de flujo; solo el cierre de `#sheet-fondo` (elemento suyo) y `data-*`.
**C — editor:** `css/editor.css` + `js/editor.js`. Formulario de cuenta (nombre, avatar por archivo vía
`aDataUrl` o por URL, subtítulo, verificado), alta por la paleta de tarjetas `#nuevo-tipo` (una tarjeta con
icono por tipo; el valor elegido se guarda en `#nuevo-tipo-valor`), tarjetas por mensaje con TODOS los
campos del §1 (incluye sub-editor de `botones`/`secciones`/`cards` y sus `respuestas`), reordenar
(drag & drop por asa o flechas ↑/↓)/duplicar/borrar, exportar/importar JSON. Mutación solo vía
`setCampo` + `guardar` + `render`.
**D — núcleo:** `js/store.js` + `js/interactivity.js` + `js/main.js`. Demo por defecto (§6), localStorage,
`normalizar`, delegación de clics del chat, composer, `flujo` con typing y retrasos, ticks automáticos,
reproducir/detener demo, toggle tema/presentación/export/import/reset, bootstrap.
Regla de convivencia: la única API entre módulos son las funciones `WAM.*` y los IDs/data-* de esta SPEC.
Prohibido `document.getElementById` de IDs ajenos a tu panel salvo los listados aquí.
## 5. Criterios de aceptación (Chrome/Edge, abriendo por `file://`)
1. Doble clic en `index.html` → la demo carga SIN ningún error ni warning de consola.
2. Se renderizan los 13 tipos del §6 con su estructura esperada (burbuja+cola, hora, ticks en OUT,
botones apilados, sheet funcional, carousel desplazable).
3. Clic en quick reply / fila de lista / botón de plantilla / botón de card → burbuja OUT con ese texto,
indicador "escribiendo…" ~900 ms y llegada de las `respuestas` encadenadas con scroll al fondo.
4. `#composer-input` + Enter/enviar → mensaje libre OUT con hora actual y ticks sent→delivered→read.
5. "Reproducir demo" vacía el chat y lo revela paso a paso (~600 ms/mensaje, typing antes de cada IN);
pulsar de nuevo lo detiene.
6. F5 conserva TODO el estado (localStorage `wam-mockup-v1`; nota: en file:// Chrome/Edge/Firefox lo
comparten por origen file://); `#btn-reset` restaura la demo.
7. Exportar descarga `wam-demo.json`; importar ese mismo archivo restaura un estado idéntico.
8. Tema alterna editor+chat; modo presentación oculta el editor y centra/escala el iPhone.
9. Subir avatar o imagen por archivo funciona sin red (dataURL) y persiste tras recargar.
10. Sin dependencias de red obligatorias: con `src` vacíos se usa `svgPlaceholder` (las URLs remotas
de la demo son opcionales y su ausencia degrada al placeholder).
## 6. Demo por defecto (D, en `datosDemo()`): "Café Aurora" ☕ — recorre los 13 tipos
IN text saludo · OUT text cliente · IN image (caption "Nuevo horario") · IN location (la cafetería) ·
IN contact (barista) · IN document (menu.pdf) · IN audio voz 0:12 · IN sticker · IN template
(header imagen, body confirmación de reserva, footer, botones "Confirmar"/"Cambiar" con respuestas) ·
IN buttons (body "¿Qué te muestro?", botones "Horarios"/"Promociones"/"Hablar" — 1-2 respuestas cada uno) ·
IN list (botón "Ver opciones", secciones Bebidas/Postres, filas con descripción y respuestas) ·
IN cta_url ("Abrir tienda") · IN carousel (3 cards "Los favoritos" con botón y respuestas).
Medias: URLs de picsum.photos con seed fija; textos cortos españoles creíbles. Verificados los límites
del research-api (≤3 quick replies, títulos 20/24, body 1024, footer 60…).