Interaction Patterns
Especificación de interacciones para ingenieros
Propósito: Definir cada patrón de interacción con valores exactos de timing, easing y comportamiento
Audiencia: Engineers (Frontend / Mobile)
Alcance: Toda la app ADEN
Relacionado: states, edge-cases, flows/
1. Principios de Interacción
Touch-first (mobile primary)
- Todo target interactivo mide mínimo 44x44px (Apple HIG) / 48x48dp (Material)
- Padding entre targets: mínimo 8px
- Las acciones primarias viven en la mitad inferior del screen (zona de pulgar)
- Bottom sheet preferido sobre modal centrado en mobile
- No hover-only interactions: todo lo que responde a hover debe tener tap equivalent
Mínima carga cognitiva
- Máximo 1 decisión por pantalla (ej: check-in = 1 pregunta a la vez)
- Progressive disclosure: mostrar solo lo necesario, expandir bajo demanda
- Defaults inteligentes: pre-seleccionar la respuesta más probable
- Copy corto: máximo 2 líneas por instrucción
Feedback inmediato
- Todo tap produce respuesta visual en <100ms
- Si una operación toma >300ms, mostrar skeleton o spinner
- Optimistic updates para acciones de escritura (check-in, toggle, favorito)
- Nunca dejar al usuario sin saber si su acción fue recibida
Progressive disclosure
- Nivel 1: Resumen (card, score, estado)
- Nivel 2: Detalle (tap para expandir, ver gráfico, ver rango)
- Nivel 3: Histórico (navegación a pantalla completa)
- Cada nivel revela más información sin abrumar
2. Gestos
Tap (acción primaria)
| Contexto | Comportamiento |
|---|---|
| Botón | Ejecuta acción, feedback visual scale(0.98) 150ms |
| Card clickable | Navega a detalle, card eleva translateY(-2px) 150ms |
| Checkbox | Toggle inmediato + animación checkmark 200ms |
| Biomarcador | Expande inline para mostrar range bar + trend |
| Sparkline | Navega a gráfico completo |
| Icono notificación | Abre panel de notificaciones |
Long press (menu contextual)
| Contexto | Comportamiento | Duración |
|---|---|---|
| Biomarcador card | Muestra opciones: “Ver histórico”, “Compartir”, “Preguntar al coach” | 500ms hold |
| Plan item | Muestra opciones: “Editar recordatorio”, “Marcar completado”, “Mas info” | 500ms hold |
| Mensaje chat | Muestra opciones: “Copiar”, “Reportar” | 500ms hold |
Feedback: Haptic medium al activar + fondo oscurece a rgba(0,0,0,0.05) durante hold.
Cancel: Mover dedo >10px cancela el long press.
Swipe left/right
| Contexto | Dirección | Comportamiento |
|---|---|---|
| Notificación | Izquierda | Dismiss con fade + translateX(-100%) 200ms ease-out |
| Notificación | Derecha | Marcar como leida, fondo verde 300ms, vuelve a posición |
| Timeline días | Izquierda/derecha | Navega entre días, snap al dia más cercano |
| Alerta | Izquierda | Dismiss (fade out + slide) |
Threshold: Swipe activa a partir de 60px de desplazamiento horizontal.
Velocity: Si velocidad >500px/s, activar aunque no alcance threshold.
Bounce: Si no alcanza threshold, vuelve con spring(damping: 20, stiffness: 300).
Pull to refresh
| Contexto | Comportamiento |
|---|---|
| Dashboard | Re-fetch todos los datos, skeleton shimmer 200ms |
| Resultados lab | Verificar si hay nuevos resultados |
| Lista notificaciones | Cargar nuevas notificaciones |
Mecanica:
- Pull distance para activar: 80px
- Indicator: círculo de progreso que rota (no spinner genérico)
- Copy durante refresh: “Actualizando…”
- Copy post-refresh: “Actualizado” (toast, 1.5s)
- Max duration: si >5s, fallback a error state
Pinch to zoom
| Contexto | Comportamiento |
|---|---|
| Gráficos (línea, área) | Zoom in/out del eje X (tiempo), min 7 días, max 365 días |
| Timeline | Zoom para ver más o menos detalle temporal |
| PDF orden médica | Zoom estándar del documento |
Limites: Min zoom 1x, max zoom 3x.
Snap: Al soltar, snap al nivel de zoom más cercano (7d, 30d, 90d, 365d).
Easing: cubic-bezier(0.25, 0.1, 0.25, 1) 300ms al hacer snap.
Scroll
| Tipo | Contexto | Comportamiento |
|---|---|---|
| Vertical (primario) | Dashboard, listas, detalles | Scroll nativo con momentum, overscroll bounce (iOS) |
| Horizontal | Timeline, carrusel de dominios | Scroll con snap points (scroll-snap-type: x mandatory) |
| Horizontal | Sparklines en progreso semanal | Scroll libre, sin snap |
Header behavior: Sticky header con backdrop-filter: blur(12px) + reduce altura de 64px a 48px on scroll (compactar).
Bottom tab bar: Ocultar al scrollear hacia abajo, mostrar al scrollear hacia arriba. Transition 200ms translateY.
3. Animaciones & Transiciones
Skeleton shimmer
.skeleton {
background: linear-gradient(
90deg,
#f0f0ec 25%, /* aden-cream variant */
#e8e8e4 50%,
#f0f0ec 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
| Escenario | Duración visible |
|---|---|
| Primera carga (cold start) | 500ms mínimo antes de mostrar contenido |
| Cargas subsecuentes (hot) | 200ms mínimo (evitar flash) |
| Re-fetch (pull to refresh) | 200ms mínimo |
Regla: Si datos llegan antes del mínimo, mantener skeleton hasta completar el mínimo para evitar flash.
FadeInUp (entrada de contenido)
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-up {
animation: fadeInUp 260ms cubic-bezier(0.4, 0, 0.2, 1) both;
}
Stagger: 0.2s entre elementos (card 1 a 0s, card 2 a 0.2s, card 3 a 0.4s).
Max stagger: 5 elementos (después, todos aparecen a 1.0s).
No usar si: Elemento ya estaba visible (ej: volver atrás a una página cacheada).
Transiciones de página
| Tipo | Duración | Easing | Cuando |
|---|---|---|---|
| Push (forward) | 300ms | cubic-bezier(0.4, 0, 0.2, 1) |
Navegar a detalle |
| Pop (back) | 250ms | cubic-bezier(0.4, 0, 0.6, 1) |
Volver atrás |
| Fade (tabs) | 200ms | ease-in-out |
Cambiar entre tabs |
| Modal enter | 200ms | cubic-bezier(0, 0, 0.2, 1) |
Abrir modal |
| Modal exit | 150ms | cubic-bezier(0.4, 0, 1, 1) |
Cerrar modal |
| Bottom sheet enter | 300ms | spring(damping: 25, stiffness: 300) |
Abrir bottom sheet |
| Bottom sheet exit | 200ms | ease-in |
Cerrar bottom sheet |
Push animation:
- Página nueva entra desde la derecha (
translateX(100%)->translateX(0)) - Página actual sale hacia la izquierda (
translateX(0)->translateX(-30%)) +opacity 0.5 - Ambas simultaneas
Modal animation:
- Backdrop:
opacity 0->opacity 1(fondorgba(0,0,0,0.4)) en 200ms - Contenido:
scale(0.95) opacity(0)->scale(1) opacity(1)en 200ms
Button press
.btn:active {
transform: scale(0.98);
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
.btn {
transition: all 150ms ease-out;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(15, 47, 199, 0.15);
}
Estado loading del botón:
- Texto cambia a “Guardando…” con spinner inline (16px)
- Botón se deshabilita (
pointer-events: none, opacity: 0.7) - Transición de texto: crossfade 150ms
Score ring fill
@keyframes ringFill {
from { stroke-dashoffset: 283; } /* circumference of r=45 circle */
to { stroke-dashoffset: calc(283 - (283 * var(--score)) / 100); }
}
.score-ring circle.progress {
animation: ringFill 800ms cubic-bezier(0.4, 0, 0.2, 1) both;
animation-delay: 400ms; /* esperar a que la card aparezca */
}
Comportamiento: El ring se llena desde 0 hasta el score actual.
Color: #0f2fc7 (aden-blue) para el progreso, #e8ebf8 (aden-blue-light) para el fondo.
Número central: Countup animation sincronizado con el ring (ver sección microinteracciones).
Streak celebration
@keyframes celebrate {
0% { transform: scale(0.8); opacity: 0; }
40% { transform: scale(1.1); opacity: 1; }
60% { transform: scale(0.95); }
100% { transform: scale(1); opacity: 1; }
}
.streak-celebrate {
animation: celebrate 600ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
Secuencia completa (1.5s total):
0ms- Checkmark aparece concelebrateanimation (600ms)300ms- Streak counter incrementa con countup600ms- Particulas/sparkles aparecen alrededor del counter (CSS particles, 8 particulas, 900ms)1000ms- Mensaje “Vas muy bien” fade-in (200ms)1500ms- Animación completa
Particulas: 8 circulos de 4px, color #0f2fc7 al 60% opacity, se expanden radialmente 20px desde el centro y desaparecen.
Trigger: Solo al completar check-in o alcanzar milestone (7, 14, 30, 60, 90 días).
Card hover/press
.card-interactive {
transition: transform 150ms ease-out, box-shadow 150ms ease-out;
}
.card-interactive:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
.card-interactive:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
4. Microinteracciones
Check-in completion
Secuencia (2.5s total):
[0ms] Usuario toca "Guardar"
-> Boton: scale(0.98) + "Guardando..."
-> Optimistic: enviar request en background
[150ms] Boton disabled, spinner inline
[300ms] Modal se transforma en success state:
-> Contenido actual: fadeOut 200ms
-> Checkmark: animate in (celebrate 600ms)
[600ms] Streak counter: countup desde n a n+1
-> Number morphing: old number slide up + fade out,
new number slide up from below + fade in (300ms)
[900ms] Sparkles alrededor del counter (si streak > 1)
[1200ms] Mensaje celebración fade-in:
"Excelente, María! Mantén el ritmo"
[2000ms] Auto-close modal con fade 300ms
[2300ms] Dashboard visible, streak actualizado
Si falla la request:
- A los 3s sin respuesta: “Algo tardo. Reintentando…”
- Revert optimistic update si falla después de 3 reintentos
- Guardar offline como fallback (ver sección 9)
Score update (number countup)
// Configuración
const DURATION = 600; // ms
const EASING = (t) => 1 - Math.pow(1 - t, 3); // ease-out cubic
function countUp(from, to, element) {
const start = performance.now();
function tick(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / DURATION, 1);
const easedProgress = EASING(progress);
const current = from + (to - from) * easedProgress;
element.textContent = Math.round(current);
if (progress < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
Trigger: Cuando el componente entra en viewport O cuando data se actualiza.
Formato: Número entero (sin decimales) para health score, 1 decimal para edad biológica.
Biomarcador tap (expand detail)
Secuencia:
[0ms] Tap en biomarcador card
-> Card background: sutil highlight (#f5f5f0 -> #e8ebf8) 100ms
[100ms] Card se expande verticalmente:
-> height: auto con max-height transition 300ms ease-out
-> Contenido expandido: fadeInUp 260ms (delay 100ms)
[200ms] Range bar aparece:
-> Barra fondo: fade-in 150ms
-> Indicador de valor: slide desde izquierda hasta posición, 400ms ease-out
[400ms] Trend sparkline aparece:
-> Line draws from left to right (SVG stroke-dashoffset animation) 500ms
[600ms] Labels de rango ("Bajo", "Óptimo", "Alto") fade-in 150ms
Collapse: Tap de nuevo o tap en otro biomarcador.
Collapse animation: Inversa, 200ms (más rápido que expand).
Regla: Solo 1 biomarcador expandido a la vez (accordion pattern).
Alert dismiss
.alert-dismiss {
animation: dismissSlide 200ms ease-out forwards;
}
@keyframes dismissSlide {
to {
transform: translateX(-100%);
opacity: 0;
height: 0;
margin: 0;
padding: 0;
}
}
Trigger: Swipe left >60px O tap en icono X.
Height collapse: 200ms delay después del slide para que los items de abajo suban suavemente.
Undo: No hay undo para alerts (a diferencia de notificaciones).
Lab upload
Secuencia:
[0ms] Usuario selecciona archivo (foto o PDF)
-> Preview thumbnail aparece (200ms fade-in)
-> Boton "Subir" se activa
[tap] Tap "Subir"
-> Progress bar aparece: 0% -> avanza según upload real
-> Progress bar color: #0f2fc7
-> Progress bar height: 4px, border-radius: 2px
[upload] Upload completo
-> Progress bar: 100%, se transforma en texto "Procesando..."
-> Spinner circular reemplaza progress bar (crossfade 200ms)
[process] Procesamiento (30min - 2h, background)
-> Card muestra: "Procesando tus exámenes..."
-> Icono: spinner animado (no bloquea UI)
-> Push notification cuando termine
[done] Notificación push: "Tu análisis está listo"
-> Tap notif -> navega a resultados
-> Checkmark animation en la card (celebrate 600ms)
Streak freeze activation
Trigger: Usuario activa “congelar streak” (1 uso por ciclo).
[0ms] Tap "Congelar streak"
-> Confirmación modal (200ms enter)
[confirm] Tap "Confirmar"
-> Streak counter: número se "congela" visualmente
-> Efecto cristal de hielo: overlay semitransparente azul claro
sobre el counter (#e8ebf8 al 80% opacity)
-> Frost particles: 6 particulas blancas, drift lento hacia abajo
(2s, ease-in, opacity fade)
[500ms] Badge "Congelado" aparece debajo del counter
-> fadeInUp 260ms
-> Color: #e8ebf8 bg, #0f2fc7 text
[1000ms] Toast: "Streak congelado por hoy" (3s)
Plan item toggle
@keyframes checkboxCheck {
0% {
stroke-dashoffset: 20;
opacity: 0;
}
50% {
opacity: 1;
}
100% {
stroke-dashoffset: 0;
opacity: 1;
}
}
.checkbox-mark {
animation: checkboxCheck 250ms cubic-bezier(0.4, 0, 0.2, 1) both;
}
Secuencia:
- Tap checkbox -> haptic light
- Checkmark SVG dibuja stroke (250ms)
- Background del checkbox:
transparent->#0f2fc7(150ms) - Texto: aplica
line-throughcon opacity 0.5 (200ms) - Counter “X de Y completados” actualiza (countup instant)
Uncheck: Inverso pero más rápido (150ms). Checkmark desaparece, background vuelve a transparent.
5. Patrones de Feedback
Toast notifications
Posición: bottom 80px (sobre tab bar), centrado horizontal
Ancho: max 90% del viewport, min-width 280px
Padding: 12px 16px
Border-radius: 12px
Background: #1a1a1a (dark) con opacity 0.95
Text: white, 14px, font-weight 500
Shadow: 0 4px 12px rgba(0,0,0,0.15)
| Propiedad | Valor |
|---|---|
| Entrada | translateY(20px) opacity(0) -> translateY(0) opacity(1), 200ms ease-out |
| Salida | translateY(0) opacity(1) -> translateY(20px) opacity(0), 150ms ease-in |
| Auto-dismiss | 3000ms |
| Max simultaneos | 1 (nuevo reemplaza anterior) |
| Swipe dismiss | Si, swipe down >40px |
| Acción inline | Opcional, ej: “Deshacer” como link a la derecha |
Tipos:
| Tipo | Icono | Ejemplo |
|---|---|---|
| Success | Checkmark circle (verde) | “Check-in guardado” |
| Info | Info circle (azul) | “Sincronizado” |
| Warning | Alert triangle (azul) | “1 cambio pendiente” |
| Error | X circle (rojo) | “No se pudo guardar” |
Inline errors
.input-error {
border-color: #ef4444;
background-color: rgba(239, 68, 68, 0.04);
animation: shake 400ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
.error-message {
color: #ef4444;
font-size: 13px;
margin-top: 4px;
animation: fadeIn 200ms ease-out both;
}
@keyframes shake {
10%, 90% { transform: translateX(-1px); }
20%, 80% { transform: translateX(2px); }
30%, 50%, 70% { transform: translateX(-3px); }
40%, 60% { transform: translateX(3px); }
}
Trigger: On blur (cuando usuario sale del input) O on submit.
Clear: Cuando usuario empieza a escribir de nuevo, error desaparece (fade-out 150ms).
Icono: AlertCircleIcon de HugeIcons, 16px, inline antes del texto de error.
Modal confirmations
Usar solo para: Acciones destructivas (borrar cuenta, cancelar ciclo, descartar cambios no guardados).
Backdrop: rgba(0,0,0,0.4), fade-in 200ms
Modal:
- Centrado vertical y horizontal
- Max-width: 380px
- Padding: 24px
- Border-radius: 16px
- Background: white
- Shadow: 0 24px 48px rgba(0,0,0,0.16)
Estructura:
[Icono] (opcional, 48px, color según tipo)
[Título] (18px, font-weight 600)
[Descripción] (14px, color gray-600)
[Boton destructivo] (rojo) + [Cancelar] (gris)
Dismiss: Tap backdrop, botón Cancelar, o swipe down. ESC en desktop.
Focus trap: Tab cycle dentro del modal unicamente.
Haptic feedback
| Tipo | Intensidad | Cuando |
|---|---|---|
| Light tap | UIImpactFeedbackGenerator(.light) |
Toggle checkbox, tab switch, tap interactivo |
| Medium | UIImpactFeedbackGenerator(.medium) |
Check-in completado, streak increment, acción confirmada |
| Heavy | UIImpactFeedbackGenerator(.heavy) |
Error, acción destructiva confirmada |
| Success | UINotificationFeedbackGenerator(.success) |
Guardado exitoso, sync completo |
| Warning | UINotificationFeedbackGenerator(.warning) |
Alerta nueva, campo con error |
| Error | UINotificationFeedbackGenerator(.error) |
Fallo de red, submit rechazado |
Android equivalent: HapticFeedbackConstants.CONFIRM, REJECT, GESTURE_START.
Regla: Nunca haptic en scroll o navegación normal. Solo en acciones con significado.
Sonido
- Default: Off (sin sonido)
- Configurable en: Settings > Notificaciones > Sonidos
- Excepción: Alertas críticas de biomarcadores en estado “ATENCION” pueden vibrar (no sonido) incluso con sonido apagado
- Si activado: Sonidos cortos (<500ms), tonos suaves, sin melodia
6. Patrones de Carga
Skeleton screens (preferido)
Reglas de skeleton:
- Las shapes del skeleton deben coincidir con el layout final (mismas alturas, anchos, posiciones)
- Usar border-radius identico al componente final
- No animar el skeleton con bounce o movimiento, solo shimmer
- Mínimo visible: 200ms (evitar flash)
Prioridad de carga:
- Shell (header, tab bar) - inmediato, no skeleton
- Hero card - primera en cargar
- Cards secundarias - staggered 100ms entre cada una
- Contenido below-the-fold - lazy load
Optimistic updates
| Acción | Update optimista | Rollback si falla |
|---|---|---|
| Check-in submit | Streak +1, success animation inmediata | Revert streak, mostrar error toast |
| Plan item toggle | Checkbox marcado inmediato | Deseleccionar, shake animation |
| Alerta dismiss | Alerta desaparece inmediato | Alerta reaparece con fade-in |
| Perfil update | Datos actualizados inmediato | Revert a valores anteriores + error toast |
Regla: El request al backend se envía simultáneamente. Si falla, revert con animación suave (300ms) + error toast.
Progressive loading (dashboard)
[0ms] Shell renderiza (header sticky, tab bar, layout grid)
-> No skeleton, es estatico
[0ms] Hero card skeleton shimmer aparece
[~200ms] Hero card data llega:
-> Skeleton -> contenido con fadeInUp 260ms
-> Score ring empieza fill animation (800ms)
[~400ms] Cards secundarias data llega:
-> Staggered fadeInUp (0.2s entre cada una)
-> Plan del dia, próxima cita
[~600ms] Cards terciarias:
-> Progreso semanal, edad biológica
[~800ms] Accesos rápidos (bottom):
-> FadeIn simple 200ms
Lazy loading (off-screen content)
- Usar
IntersectionObserverconrootMargin: "200px"(precargar 200px antes de ser visible) - Placeholder: skeleton shape con altura estimada
- Transition skeleton -> contenido: fade 200ms
- Imágenes: blur-up technique (thumbnail 20px de ancho -> imagen completa)
7. Interacciones de Navegación
Bottom tab bar
Tabs: 5
1. Inicio (dashboard)
2. Mi Plan
3. Exámenes
4. Progreso
5. Perfil
Altura: 56px (iOS safe área adicional abajo)
Background: white con border-top 1px #e5e5e5
Icono: 24px (HugeIcons), color inactivo #9ca3af, activo #0f2fc7
Label: 11px, misma logica de color
Interacción:
- Tap -> haptic light + transición de contenido (fade 200ms)
- Icono activo: transición de color 150ms
- Indicador activo: dot 4px debajo del icono,
#0f2fc7, fade-in 150ms - Double-tap en tab activo: scroll to top (animated 300ms)
- Tab bar se oculta al scrollear hacia abajo:
translateY(100%)200ms ease-in - Tab bar reaparece al scrollear hacia arriba:
translateY(0)200ms ease-out
Back button / gesture
Botón:
- Posición: top-left, 44x44px touch target
- Icono: ArrowLeftIcon 24px, color
#1a1a1a - Visible siempre excepto en root screens (tabs)
- Tap -> navega atrás, animación pop (250ms)
Gesto (iOS):
- Swipe from left edge (20px zona de activación)
- Threshold: 40% del ancho de pantalla para confirmar
- Preview: página anterior visible debajo con parallax
- Cancel: si no alcanza threshold, spring back
Android:
- Back button del sistema
- Predictive back gesture (Android 14+)
Breadcrumbs
Cuando: Profundidad > 2 niveles (ej: Dashboard > Exámenes > Glucosa > Histórico).
Posición: debajo del header, sobre el contenido
Formato: "Inicio / Exámenes / Glucosa"
Font: 13px, color #6b7280
Separador: " / " (con espacios)
Último item: color #1a1a1a, font-weight 500
Items clickables: todos excepto el último
Overflow: truncar items intermedios con "..." si >3 niveles
Pull-down to close modals
Aplicable a: Bottom sheets, modals full-screen en mobile.
- Drag handle visible: 36x4px, border-radius 2px, color
#d1d5db, centrado, top 8px - Drag threshold: 150px hacia abajo para cerrar
- Velocidad: si >800px/s hacia abajo, cerrar sin importar distancia
- Backdrop opacity: se reduce proporcionalmente al drag
- Spring back si no alcanza threshold:
spring(damping: 20, stiffness: 300)
8. Interacciones de Visualización de Datos
Graph scrubbing (drag para ver valores)
Contexto: Gráficos de línea (biomarcadores en el tiempo, health score histórico).
Interacción:
1. Touch/hold en gráfico -> haptic light
2. Línea vertical (crosshair) aparece en punto tocado
-> Color: #0f2fc7 al 40% opacity
-> Ancho: 1px
-> Altura: completa del gráfico
3. Tooltip aparece arriba del punto:
-> Background: #1a1a1a, opacity 0.9
-> Border-radius: 8px
-> Padding: 6px 10px
-> Text: white, 13px
-> Contenido: "78 - 15 Mar"
-> Flecha apuntando al punto
4. Drag horizontal -> crosshair sigue el dedo
-> Snap a data points (no interpolar)
-> Haptic light en cada data point
5. Release -> crosshair desaparece (fade 150ms)
-> Tooltip desaparece (fade 150ms)
Performance: Usar requestAnimationFrame para el movimiento. No re-render el gráfico completo, solo mover el crosshair.
Biomarcador range bar
Estructura:
[Bajo | Óptimo | Alto ]
[---rojo--|--verde---------|--rojo------]
^
Tu valor: 98 mg/dL
Colores:
- Bajo: #ef4444 (aden-negative) al 20% opacity
- Óptimo: #10b981 (aden-positive) al 20% opacity
- Alto: #ef4444 al 20% opacity
- Indicador: círculo 12px, color según zona
Animación (on expand):
- Barra fade-in 200ms
- Indicador: slide desde izquierda hasta posición, 400ms ease-out
- Label del valor: fadeIn 200ms, delay 200ms
Score ring (animated fill)
Ver sección 3 “Score ring fill” para la animación CSS.
Adicional:
- El número central hace countup sincronizado con el ring
- Color del ring cambia según score:
- 0-39:
#ef4444(rojo) - 40-69:
#0f2fc7(azul) - 70-100:
#10b981(verde)
- 0-39:
- Background ring: siempre
#e8ebf8 - Stroke-width: 6px (ring) / 6px (background)
- Radio: 45px (mobile), 56px (desktop)
Sparkline tap
En contexto compacto (dashboard card):
- Sparkline muestra últimos 7 o 30 datos
- Tap -> navega a pantalla completa de ese gráfico
- Transition: la sparkline se “expande” al gráfico completo (shared element transition si posible, sino push animation)
Timeline horizontal scroll
.timeline-container {
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scrollbar-width: none; /* Firefox */
}
.timeline-container::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.timeline-item {
scroll-snap-align: center;
min-width: 80px;
}
Snap points: Cada día/semana/mes según nivel de zoom.
Indicador de posición: Línea fina debajo del timeline (scroll indicator), ancho proporcional a la porcion visible.
Auto-scroll: Al cargar, scroll automático al dia actual (300ms ease-out).
9. Interacciones Offline
Estado general offline
Detección: navigator.onLine + ping periódico cada 30s a health endpoint.
Banner: Aparece en top del screen, debajo del header.
Banner offline:
Background: #f5f5f0 (aden-cream) con border-bottom 1px #d1d5db
Icono: WifiOffIcon 16px, color #6b7280
Texto: "Sin conexión" (14px, #6b7280)
Altura: 36px
Animación entrada: slideDown 200ms ease-out
Animación salida: slideUp 200ms ease-in (cuando reconecta)
Inputs offline
| Componente | Estado offline |
|---|---|
| Botones de acción (guardar, enviar) | Disabled, opacity 0.5 |
| Botón “Guardar offline” | Enabled, texto: “Se sincronizara al reconectar” |
| Forms de input | Habilitados (escribir si, enviar no) |
| Checkboxes del plan | Habilitados, guardados en cola local |
| Chat health coach | Disabled, “Requiere conexión” |
| Navegación | Habilitada (datos en cache) |
Queue indicator
Posición: Sobre el tab bar, barra fina
Background: #e8ebf8
Texto: "1 cambio pendiente" / "3 cambios pendientes"
Icono: CloudOffIcon 14px
Tap: Abre lista de cambios en cola
Animación: pulse sutil cada 3s (opacity 0.7 -> 1 -> 0.7)
Auto-sync on reconnect
Secuencia:
[0ms] Conexión detectada (online event + ping exitoso)
-> Banner offline: texto cambia a "Reconectando..."
-> Icono: spinner reemplaza WifiOffIcon
[~500ms] Sync inicia:
-> Cola de cambios se envía en orden (FIFO)
-> Queue indicator: "Sincronizando 1 de 3..."
[sync] Cada item sincronizado:
-> Counter actualiza
-> Haptic light por cada sync exitoso
[done] Todo sincronizado:
-> Banner: texto cambia a "Sincronizado" + checkmark icon
-> Color: fondo #10b981 al 10%, texto #10b981
-> Duración: 2s, luego slideUp 200ms
-> Queue indicator desaparece (fade 200ms)
-> Toast: "Todos los cambios sincronizados"
Conflict resolution
Estrategia: Last-write-wins (por timestamp).
Escenario: Usuario edito dato offline. Otro dispositivo edito el mismo dato online.
Resolución:
1. Comparar timestamps
2. El más reciente gana
3. Toast informativo: "Tus cambios fueron actualizados con la versión más reciente"
4. NO modal de conflicto (demasiada fricción)
5. Log del conflicto en analytics para monitoreo
Excepción: Check-in duplicado
-> Si ya existe check-in para ese dia (enviado desde otro dispositivo):
-> Rechazar el offline, toast: "Ya completaste check-in hoy desde otro dispositivo"
-> No contar doble en streak
10. Accesibilidad en Interacciones
Reduce motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Efecto: Todas las animaciones se vuelven instantaneas. Funcionalidad identica, sin movimiento.
Focus visible
*:focus-visible {
outline: 2px solid #0f2fc7;
outline-offset: 2px;
border-radius: 4px;
}
Tab order: Lógico (izquierda a derecha, arriba a abajo).
Skip link: “Ir al contenido” como primer elemento focusable, visible solo con keyboard.
Screen reader
- Todo icono interactivo tiene
aria-labeldescriptivo - Animaciones de score usan
aria-live="polite"para anunciar el valor final - Toasts usan
role="status"conaria-live="polite" - Modals usan
role="dialog"conaria-modal="true" - Skeleton screens tienen
aria-busy="true"en el contenedor padre
Referencia Rápida de Timings
| Elemento | Duración | Easing |
|---|---|---|
| Skeleton shimmer | loop 1.5s | linear |
| Skeleton visible (cold) | min 500ms | – |
| Skeleton visible (hot) | min 200ms | – |
| FadeInUp | 260ms | cubic-bezier(0.4, 0, 0.2, 1) |
| FadeInUp stagger | +200ms entre items | – |
| Page push | 300ms | cubic-bezier(0.4, 0, 0.2, 1) |
| Page pop | 250ms | cubic-bezier(0.4, 0, 0.6, 1) |
| Tab switch | 200ms | ease-in-out |
| Modal enter | 200ms | cubic-bezier(0, 0, 0.2, 1) |
| Modal exit | 150ms | cubic-bezier(0.4, 0, 1, 1) |
| Bottom sheet enter | 300ms | spring(25, 300) |
| Bottom sheet exit | 200ms | ease-in |
| Button press | 150ms | cubic-bezier(0.4, 0, 0.2, 1) |
| Button hover | 150ms | ease-out |
| Card hover | 150ms | ease-out |
| Score ring fill | 800ms | cubic-bezier(0.4, 0, 0.2, 1) |
| Number countup | 600ms | ease-out cubic |
| Streak celebration | 1500ms total | mixed |
| Checkbox check | 250ms | cubic-bezier(0.4, 0, 0.2, 1) |
| Toast enter | 200ms | ease-out |
| Toast exit | 150ms | ease-in |
| Toast auto-dismiss | 3000ms | – |
| Error shake | 400ms | cubic-bezier(0.36, 0.07, 0.19, 0.97) |
| Inline error fade | 200ms | ease-out |
| Alert dismiss | 200ms | ease-out |
| Swipe threshold | 60px | – |
| Long press hold | 500ms | – |
| Pull to refresh | 80px pull distance | – |
| Haptic: each action | 0ms (instant) | – |
| Offline banner enter | 200ms | ease-out |
| Sync complete banner | 2000ms visible | – |
ADDENDUM: Patrones de Interacción Nuevos (Refero Research)
Origen: Investigación Refero (abril 2026)
Patrón: Readiness Level Bottom Sheet
Trigger: Tap en cualquier biomarcador (dashboard, resultados, reporte)
Referencia: The Outsiders
| Propiedad | Valor |
|---|---|
| Entrada | slide-up 200ms, ease-out cubic-bezier(.22,.9,.32,1) |
| Backdrop | rgba(0,0,0,0.4) con blur 8px |
| Corner radius | 18px (top) |
| Drag handle | 36x4px, centered, color #9CA3AF |
| Color bar lateral | 4px width, pill radius, colores por estado |
| Sections stagger | 180ms delay entre secciones |
| Pull-down to close | threshold 120px |
| Haptic | light impact al abrir |
Patrón: Health Coach Keyword Chips
Trigger: Respuesta del Health Coach contiene términos reconocidos
Referencia: Kin
| Propiedad | Valor |
|---|---|
| Chip padding | 4px 10px |
| Chip border-radius | 6px |
| Chip font-size | 14px, semibold |
| Tap feedback | scale(0.95) 100ms + haptic light |
| Chip → action | navigate to relevant screen (200ms transition) |
| Tipos | Biomarcador (#e8ebf8/#0f2fc7), Acción (#d1fae5/#065f46), Suplemento (#dbeafe/#1e40af), Medicamento (#fef3c7/#92400e) |
Patrón: Daily Moment Mode Selector
Trigger: Onboarding Step 6 o Settings → Check-in
Referencia: Pi
| Propiedad | Valor |
|---|---|
| Card selection | tap toggles checkmark, max 2 seleccionados |
| Card selected state | subtle border #0f2fc7 + checkmark top-right |
| Card unselected state | border #e5e7eb, bg white |
| Transition | background-color 150ms ease-out |
| Haptic | light impact on select |
Patrón: Reporte 90 Días Score Animation
Trigger: Primera vista del reporte de cierre
Referencia: The Outsiders hero
| Propiedad | Valor |
|---|---|
| Score count-up | 0 → valor real, 1200ms, ease-out |
| Progress bar fill | 0% → actual%, 800ms, ease-out, delay 400ms |
| Celebration card | fade-in 300ms, delay 1600ms (after score lands) |
| Confetti (si delta >= +10) | 2s duration, 50 particles, gold + blue |
| Delta badge | slide-in from right, 200ms, delay 1200ms |
Anterior: states – Global States
Siguiente: edge-cases – Error Recovery & Business Rules