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
+196
View File
@@ -0,0 +1,196 @@
# 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…).