WIP: mejoras de fidelidad, editor, lógica y suite e2e (checkpoint tras reinicio)
This commit is contained in:
@@ -16,7 +16,8 @@ Bases: `docs/research-api.md` (payloads y límites) y `docs/research-ui.md` (col
|
||||
"nombre": "Café Aurora",
|
||||
"avatar": "",
|
||||
"subtitulo": "en línea",
|
||||
"verificado": true
|
||||
"verificado": true,
|
||||
"avisoCifrado": true
|
||||
},
|
||||
"mensajes": [ ]
|
||||
}
|
||||
@@ -51,44 +52,122 @@ Convenciones: `src`/`avatar`/`headerMedia`/`media` = dataURL o URL remota; `""`
|
||||
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).
|
||||
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.
|
||||
|
||||
**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`
|
||||
sin tocar el estado ni localStorage. Todo `WAM.render()` vuelve al guion: vacía la sesión, cierra la hoja y
|
||||
cancela demo y flujos (interactivity.js envuelve `WAM.render`).
|
||||
|
||||
## 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.guardar()` → void (JSON a localStorage; la 1.ª llamada escribe al instante y las de los 250 ms
|
||||
siguientes se agrupan; se vacía en `pagehide`) · `WAM.guardarYa()` → bool (escritura inmediata)
|
||||
- Error de guardado (cuota llena / LS bloqueado): llama UNA vez al hook opcional `WAM.onErrorGuardar(texto, error)`
|
||||
(main.js lo pinta con `WAM.avisar`) hasta el siguiente guardado correcto.
|
||||
- `WAM.resetear()` → void (borra LS, estado = demo con IDs desde `m1`)
|
||||
- `WAM.normalizar(o)` → estado (acepta un array de mensajes como raíz; IDs duplicados o inválidos se
|
||||
reasignan; vacía URLs con esquema no permitido; respuestas string → text; conserva campos extra primitivos)
|
||||
- `WAM.generarId()` → string (nunca repite un ID ya entregado aunque aún no esté en el estado)
|
||||
- `WAM.crearAlocador(extra?)` → `function tomar()` que reparte IDs únicos frente al estado, a los ya
|
||||
entregados y a los de `extra` (para clonar mensajes con respuestas anidadas)
|
||||
- `WAM.urlSegura(url, "enlace"|"media")` → bool (enlace: http(s)/mailto/tel/sin esquema; media: http(s)/blob/
|
||||
data:image|video|audio/sin esquema)
|
||||
- `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.render()` → void: `cerrarSheet()` + `renderHeader()` + (si `cuenta.avisoCifrado !== false`) dos
|
||||
`li.wa-sistema` (pastilla "Hoy" y aviso de cifrado de negocio) + TODOS los mensajes de `estado.mensajes`
|
||||
- `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.renderMensaje(msg, i)` → Node `li.wa-row` (+ `.grupo` si cambia el emisor, `.ultimo` si es el último
|
||||
del grupo: solo ese lleva la cola de la burbuja, abajo) con todo el contenido del tipo y sus `data-*`
|
||||
- `WAM.anadirMensajeDOM(msg)` → void: append + `scrollAbajo()`; recalcula `.grupo`/`.ultimo` contra la
|
||||
última fila del DOM y reinserta los `li.wa-sistema` si faltan
|
||||
- `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`)
|
||||
- `WAM.scrollAbajo()` → void · `WAM.abrirSheet(i | Mensaje)` / `WAM.cerrarSheet()` → void (hoja de lista en
|
||||
`#pantalla`: título = `boton`, filas `button.sheet-fila[data-sec][data-fila]`, botón `.sheet-cerrar`)
|
||||
- `WAM.abrirOpciones(i | Mensaje, li?)` → void: hoja "Todas las opciones" de un mensaje con >3 botones
|
||||
(la abre el propio B al pulsar `[data-ver-todas]`). Sus filas de respuesta llevan `data-opcion="<j>"`
|
||||
y B reenvía el clic al `[data-btn="<j>"]` oculto de la burbuja, que atiende la delegación normal de main.js.
|
||||
- `WAM.tick(estado)` → string SVG inline (✓ / ✓✓ del color de la hora; ✓✓ leído azul iOS `#027bfc`)
|
||||
- Botones del teléfono: `<button type="button" class="qr-btn">` (Enter/Espacio nativos); URLs solo con
|
||||
esquema `http(s):`, `mailto:` o `tel:` (otro esquema → botón inerte `.qr-btn-inactiva`); `tel:` pinta
|
||||
el icono de llamada. Con >3 botones se ven 2 + "Ver todas las opciones" y el resto va `hidden` en la fila.
|
||||
|
||||
### 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)
|
||||
- `WAM.anadirMensaje(tipo, indice?)` → void: inserta en `indice` (defecto: al final) con texto de ejemplo y hora =
|
||||
anterior + 1 min; abre su tarjeta y enfoca su primer campo · `WAM.moverMensaje(i, delta)` · `WAM.duplicarMensaje(i)`
|
||||
(ids del clon y de sus respuestas con un solo alocador `WAM.crearAlocador`)
|
||||
- `WAM.borrarMensaje(i)` → void: sin `confirm()`; toast «Mensaje borrado · Deshacer» (6 s)
|
||||
- `WAM.setCampo(ruta, valor, opts?)` → void (ruta `"mensajes.3.body"`; muta + guardar + `WAM.render()`).
|
||||
`opts.diferido` (edición tecla a tecla): el chat se repinta en el siguiente frame y SOLO la fila
|
||||
`li.wa-row[data-id]` afectada (usa `WAM.renderUno(i)` si existe; si no, `renderMensaje` + `replaceChild`), sin scroll
|
||||
- `WAM.aDataUrl(archivo)` → Promise<dataURL> (FileReader) · `WAM.exportarJSON()` → void (Blob + `<a download>`;
|
||||
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.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`;
|
||||
chip ↳ con botones/filas/cards con y sin respuesta.
|
||||
|
||||
### js/escenarios.js (C) — datos, sin DOM
|
||||
- `WAM.ESCENARIOS` → `[{ id, nombre, descripcion, datos() }]` (en blanco, restaurante, e-commerce, clínica,
|
||||
inmobiliaria, soporte, cobranza; textos ficticios) · `WAM.datosEscenario(id)` → estado crudo (se normaliza al usarlo)
|
||||
|
||||
### js/ux.js (C) — capa de UX de la app; `WAM.initUX()` lo llama `renderEditor()` una sola vez
|
||||
- Historial: `WAM.puntoDeshacer()` (instantánea antes de mutar, máx. 30; los strings/dataURL se comparten),
|
||||
`WAM.deshacer()`, `WAM.rehacer()`, `WAM.puedeDeshacer()`; `#btn-deshacer` / `#btn-rehacer`, Ctrl+Z / Ctrl+Mayús+Z
|
||||
fuera de campos de texto
|
||||
- `WAM.confirmarReemplazo("reset"|"importar", ejecutar)`: `<dialog>` con «Exportar antes» (main.js lo usa en
|
||||
`#btn-reset` y `#btn-importar`; sin mensajes no pregunta). Reset deja punto de deshacer.
|
||||
- `WAM.abrirDemos(seccion?)`: escenarios + demos guardadas (`wam-demos` = índice, `wam-demo-<id>` = estado,
|
||||
`wam-demo-activa`) con guardar/abrir/renombrar/duplicar/borrar · `WAM.nombreDemoActiva()` · `WAM.abrirAtajos()`
|
||||
- `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.
|
||||
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`.
|
||||
|
||||
### 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
|
||||
- `WAM.flujo(texto, respuestas, cita?)`: `cita` opcional `{nombre, titulo, extracto, miniatura, foto}` → bloque
|
||||
`.wa-cita` dentro de la burbuja OUT. La burbuja sale al instante; las respuestas de flujos simultáneos se
|
||||
encolan. El indicador «escribiendo…» es un contador de referencias.
|
||||
- `WAM.resolverFlujo(el)` → `{ texto, respuestas, cita, msg, tipo }`; resuelve el mensaje por el `data-id` de la
|
||||
fila (válido para mensajes de sesión); etiquetas vacías = las del render («Botón n», «Ver más», «Opción»);
|
||||
fila de lista: `titulo + "
|
||||
" + descripcion`.
|
||||
- `WAM.mensajeDeElemento(el)` → Mensaje · `WAM.abrirLista(msg)` → abre la hoja (`abrirSheet(i | msg)`)
|
||||
- `WAM.marcarUsado(el)` → clase `.usado` (gris, sin más clics) · `WAM.reiniciarChat()` (= `render()`)
|
||||
- `WAM.sesionAGuion()` → n: añade la sesión al guion (acción explícita) + guardar + re-render
|
||||
- `WAM.getVelocidad()` / `WAM.setVelocidad(0.5|1|2)` (persistida en `wam-mockup-v1-velocidad`)
|
||||
- `WAM.demoActiva()` → bool · hook opcional `WAM.onDemoChange(activa)`
|
||||
- `WAM.reproducirDemo()` / `WAM.detenerDemo()` → void: cancela flujos en curso, vacía `#chat-cuerpo` y revela
|
||||
`estado.mensajes` uno a uno (pulsando solas las opciones `autoPulsar`); detener vuelve al guion
|
||||
(~600 ms; mensaje `in` precedido de typing ~700 ms; cancelable)
|
||||
- `WAM.espera(ms)` → Promise cancelable (todo timer pasa por aquí para poder detener)
|
||||
- `WAM.espera(ms, grupo?)` → Promise cancelable; grupo `"flujo"` (defecto) o `"demo"`; la duración se divide
|
||||
por la velocidad (todo timer pasa por aquí para poder detener)
|
||||
|
||||
### js/main.js (dueño D) — bootstrap
|
||||
- `WAM.avisar(texto, {tipo:"info"|"error", duracion?})` → aviso flotante `#wam-aviso` (estilos por CSSOM)
|
||||
- Controles de `#barra-movil`: `#sel-velocidad`, `#btn-reiniciar`, `#btn-reproducir` (estilos en `css/interactividad.css`, D).
|
||||
Teclado en `#pantalla`: Enter/Espacio activan `[data-flujo]`/`[data-ver-todas]`/`.sheet-cerrar`; Escape cierra la hoja.
|
||||
`#btn-reset` pide confirmación. Enlaces del chat con esquema no permitido no se abren.
|
||||
- `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`)
|
||||
@@ -99,7 +178,10 @@ de `WAM.svgPlaceholder(texto)`. Botones con `url` no vacía se renderizan como `
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@@ -116,10 +198,15 @@ Contrato de clics B→D (delegación en `#chat-cuerpo`): B estampa en cada eleme
|
||||
- `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`,
|
||||
clases: `.wa-row .in .out .bubble .meta .tick .tick-read`, cola con `clip-path` (abajo, en `.ultimo`), `.qr-btns .qr-btn .qr-ico`, `.wa-sistema`,
|
||||
`.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`).
|
||||
- `css/editor.css` (C): prefijo `ed-` (`.ed-seccion .ed-campo .ed-tarjeta .ed-btn .ed-fila .ed-badge`,
|
||||
`.ed-chip .ed-toast .ed-seg .ed-validacion`, `#chat-cuerpo .wa-row.ed-resaltado`). `.ed-campo` es un `<div>` con
|
||||
`<label for>` solo sobre la etiqueta (un label envolvente activaba el primer botón de la barra de formato).
|
||||
- `css/base.css`: además, layout de una columna con pestañas ≤ 900 px (escala del teléfono con el ancho completo y
|
||||
`--wam-toolbar-alto` medido por ux.js), `.wam-dlg*` (diálogos), `.wam-sr`, `--wam-primario` (#008069, AA con blanco).
|
||||
- `css/interactividad.css` (D): `.wa-cita*` (respuesta citada), `.qr-btn.usado`, `.wam-pulsando`, controles de `#barra-movil`.
|
||||
- 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`)
|
||||
@@ -129,11 +216,14 @@ Contrato de clics B→D (delegación en `#chat-cuerpo`): B estampa en cada eleme
|
||||
<link rel="stylesheet" href="css/iphone.css">
|
||||
<link rel="stylesheet" href="css/whatsapp.css">
|
||||
<link rel="stylesheet" href="css/editor.css">
|
||||
<link rel="stylesheet" href="css/interactividad.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/escenarios.js"></script> <!-- datos de escenarios; NO toca DOM al cargar -->
|
||||
<script src="js/ux.js"></script> <!-- WAM.initUX (lo invoca renderEditor); NO toca DOM al cargar -->
|
||||
<script src="js/main.js"></script> <!-- única ejecución: WAM.init() -->
|
||||
```
|
||||
|
||||
@@ -146,11 +236,13 @@ Ningún archivo lee `WAM.estado` en carga (solo dentro de funciones) → el orde
|
||||
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-*`.
|
||||
**B — chat:** `css/whatsapp.css` + `js/render.js`. Render de los 13 tipos con el aspecto de WhatsApp iOS
|
||||
según `docs/referencias/README.md` (burbuja 17 px con radio 18 y cola abajo solo en la última del grupo,
|
||||
media inset 4 px, template/buttons/list/cta_url en UNA burbuja con separador y botones con icono verde,
|
||||
hoja inferior iOS, carrusel sin puntos, nota de voz con avatar, documento en panel gris, contacto con
|
||||
"Mensaje | Guardar contacto", "Hoy" + aviso de cifrado), ticks SVG, indicador "escribiendo…", claro/oscuro
|
||||
(negro neutro). Sin listeners de flujo; solo presentación: cierre de la hoja, "Ver todas las opciones" y
|
||||
el reenvío del clic de sus filas al botón oculto de la burbuja.
|
||||
|
||||
**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
|
||||
@@ -178,7 +270,8 @@ Prohibido `document.getElementById` de IDs ajenos a tu panel salvo los listados
|
||||
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.
|
||||
7. Exportar descarga `<nombre-de-la-demo>.json` (`wam-demo.json` si no hay nombre); 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
|
||||
|
||||
Reference in New Issue
Block a user