Design
Sistema de Diseño
Para qué

Las decisiones visuales canónicas del producto: paleta, tipografía, componentes, movimiento y accesibilidad.

Audiencia

Diseño e ingeniería frontend.

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 (fondo rgba(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):

  1. 0ms - Checkmark aparece con celebrate animation (600ms)
  2. 300ms - Streak counter incrementa con countup
  3. 600ms - Particulas/sparkles aparecen alrededor del counter (CSS particles, 8 particulas, 900ms)
  4. 1000ms - Mensaje “Vas muy bien” fade-in (200ms)
  5. 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:

  1. Tap checkbox -> haptic light
  2. Checkmark SVG dibuja stroke (250ms)
  3. Background del checkbox: transparent -> #0f2fc7 (150ms)
  4. Texto: aplica line-through con opacity 0.5 (200ms)
  5. 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.

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:

  1. Shell (header, tab bar) - inmediato, no skeleton
  2. Hero card - primera en cargar
  3. Cards secundarias - staggered 100ms entre cada una
  4. 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 IntersectionObserver con rootMargin: "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+)

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)
  • 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-label descriptivo
  • Animaciones de score usan aria-live="polite" para anunciar el valor final
  • Toasts usan role="status" con aria-live="polite"
  • Modals usan role="dialog" con aria-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