codigo0/docs/ANALISIS_UX_UI_FRONTEND.md

32 KiB

🎨 ANÁLISIS UX/UI Y FRONTEND

Fecha: 2025-01-07
Analista: Experto UX/UI + Desarrollador Frontend
Versión del Proyecto: 1.0.0


📊 RESUMEN EJECUTIVO

Aspecto Estado Calificación
Componentes UI Bueno (shadcn/ui) 7.5/10
Reutilización ⚠️ Mejorable 6/10
Accesibilidad ⚠️ Básica 5/10
Responsividad Buena 8/10
Rendimiento Optimizado (lazy loading) 7.5/10
Flujos de Usuario ⚠️ Algunos puntos de fricción 6.5/10
Estados de Carga/Error ⚠️ Inconsistentes 6/10

Calificación General: 6.6/10 ⚠️ BUENO CON MEJORAS NECESARIAS


1. 🔍 ANÁLISIS DE COMPONENTES UI

1.1 Reutilización de Componentes

Componentes Bien Reutilizados

Componente Usos Estado Ubicación
Badge 50+ Excelente components/shared/Badge.tsx
Button 100+ Excelente components/ui/button.tsx
Card 30+ Bueno components/ui/card.tsx
EmergencyButton 10+ Bueno components/shared/EmergencyButton.tsx
MarkdownViewer 15+ Bueno components/content/MarkdownViewer.tsx

⚠️ Componentes con Duplicación o Inconsistencias

Problema Ubicaciones Impacto Prioridad
Card Variants Duplicados ProcedureCard, DrugCard, GuideCard 🟡 Media Media
Loading States Inconsistentes Múltiples páginas 🟡 Media Media
Error States Inconsistentes ManualViewer, NotFound, otros 🟡 Media Media
Button Styles Inconsistentes Varios componentes 🟡 Baja Baja

Problemas Identificados

1. Card Components con Lógica Similar pero No Unificada

// ❌ PROBLEMA: ProcedureCard.tsx (198 líneas)
// Lógica de expand/collapse, favoritos, compartir duplicada

// ❌ PROBLEMA: DrugCard.tsx
// Misma lógica pero implementación diferente

// ✅ SOLUCIÓN: Crear componente base <BaseCard>

Archivos afectados:

  • src/components/procedures/ProcedureCard.tsx (198 líneas)
  • src/components/drugs/DrugCard.tsx
  • src/components/guide/GuideCard.tsx

Fix sugerido:

// ✅ Crear: src/components/shared/BaseCard.tsx
interface BaseCardProps {
  title: string;
  subtitle?: string;
  badges?: BadgeProps[];
  expandable?: boolean;
  defaultExpanded?: boolean;
  onFavorite?: () => void;
  onShare?: () => void;
  children?: React.ReactNode;
}

export const BaseCard = ({ 
  title, 
  subtitle, 
  badges, 
  expandable,
  defaultExpanded = false,
  onFavorite,
  onShare,
  children 
}: BaseCardProps) => {
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
  // ... lógica unificada
};

2. Loading States Inconsistentes

// ❌ PROBLEMA: MarkdownViewer.tsx (línea 84-90)
if (loading && showLoading) {
  return (
    <div className={`flex flex-col items-center justify-center py-12 ${className}`}>
      <Loader2 className="w-8 h-8 animate-spin text-primary mb-4" />
      <p className="text-muted-foreground">Cargando contenido...</p>
    </div>
  );
}

// ❌ PROBLEMA: ManualViewer.tsx (línea 32-45)
// Loading state diferente (solo "Capítulo no encontrado")
// Sin estado de carga real

// ✅ SOLUCIÓN: Crear componente <LoadingState>

Fix sugerido:

// ✅ Crear: src/components/shared/LoadingState.tsx
interface LoadingStateProps {
  message?: string;
  size?: 'sm' | 'md' | 'lg';
  fullScreen?: boolean;
}

export const LoadingState = ({ 
  message = 'Cargando...', 
  size = 'md',
  fullScreen = false 
}: LoadingStateProps) => {
  const sizeClasses = {
    sm: 'w-4 h-4',
    md: 'w-8 h-8',
    lg: 'w-12 h-12'
  };
  
  return (
    <div className={`flex flex-col items-center justify-center ${
      fullScreen ? 'min-h-[60vh]' : 'py-12'
    }`}>
      <Loader2 className={`${sizeClasses[size]} animate-spin text-primary mb-4`} />
      <p className="text-muted-foreground">{message}</p>
    </div>
  );
};

3. Error States Inconsistentes

// ❌ PROBLEMA: NotFound.tsx (línea 11-21)
// Error state sin accesibilidad, sin navegación clara

// ❌ PROBLEMA: ManualViewer.tsx (línea 32-45)
// Error state diferente (usa "Capítulo no encontrado")

// ❌ PROBLEMA: MarkdownViewer.tsx (línea 94-102)
// Error state con más detalles pero sin acciones claras

Fix sugerido:

// ✅ Crear: src/components/shared/ErrorState.tsx
interface ErrorStateProps {
  title?: string;
  message: string;
  action?: {
    label: string;
    onClick: () => void;
  };
  icon?: React.ReactNode;
}

export const ErrorState = ({ 
  title = 'Error',
  message,
  action,
  icon = <AlertCircle className="w-12 h-12 text-destructive" />
}: ErrorStateProps) => {
  return (
    <div className="flex flex-col items-center justify-center min-h-[60vh] space-y-4" role="alert">
      {icon}
      <h2 className="text-xl font-semibold text-foreground">{title}</h2>
      <p className="text-muted-foreground text-center max-w-md">{message}</p>
      {action && (
        <Button onClick={action.onClick}>
          {action.label}
        </Button>
      )}
    </div>
  );
};

1.2 Responsividad y Breakpoints

Breakpoints Implementados

Breakpoint Valor Uso Estado
Mobile < 768px Hook useIsMobile() Funcional
Tablet 768px - 1024px Tailwind md: Implementado
Desktop > 1024px Tailwind lg:, xl:, 2xl: Implementado

⚠️ Problemas de Responsividad

1. Hook useIsMobile() con Problema de Hydration

// ❌ PROBLEMA: src/hooks/use-mobile.tsx (línea 6)
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);

// ⚠️ PROBLEMA: undefined inicial causa hydration mismatch en SSR
// ⚠️ PROBLEMA: window.innerWidth no está disponible en SSR

Fix sugerido:

// ✅ FIX: src/hooks/use-mobile.tsx
import { useState, useEffect } from 'react';

const MOBILE_BREAKPOINT = 768;

export function useIsMobile() {
  const [isMobile, setIsMobile] = useState<boolean>(false);

  useEffect(() => {
    // Solo ejecutar en cliente
    if (typeof window === 'undefined') return;

    const checkMobile = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
    };

    // Check inicial
    checkMobile();

    // Media query listener (más eficiente que resize)
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
    const handleChange = (e: MediaQueryListEvent) => {
      setIsMobile(e.matches);
    };

    // Modern API
    if (mql.addEventListener) {
      mql.addEventListener('change', handleChange);
      return () => mql.removeEventListener('change', handleChange);
    } else {
      // Fallback para navegadores antiguos
      mql.addListener(handleChange);
      return () => mql.removeListener(handleChange);
    }
  }, []);

  return isMobile;
}

2. Grids sin Breakpoints Específicos

// ❌ PROBLEMA: Index.tsx (línea 52)
<div className="grid grid-cols-2 gap-3">
  {/* Emergency Buttons */}
</div>

// ⚠️ PROBLEMA: grid-cols-2 siempre, incluso en mobile pequeño
// Debería ser grid-cols-1 en mobile muy pequeño

Fix sugerido:

// ✅ FIX: Responsive grid
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
  {/* Emergency Buttons */}
</div>

3. Floating Button Sin Consideración Mobile

// ❌ PROBLEMA: Index.tsx (línea 151-157)
<Link
  to="/rcp"
  className="fixed bottom-24 right-4 z-40 w-16 h-16 rounded-full bg-primary flex items-center justify-center shadow-lg animate-pulse-ring"
  aria-label="Emergencia - RCP"
>
  <AlertTriangle className="w-8 h-8 text-primary-foreground" />
</Link>

// ⚠️ PROBLEMA: Puede taparse con BottomNav en mobile
// ⚠️ PROBLEMA: No considera safe-area-inset-bottom

Fix sugerido:

// ✅ FIX: Safe area aware floating button
<Link
  to="/rcp"
  className="fixed bottom-[calc(4rem+env(safe-area-inset-bottom))] right-4 z-40 w-16 h-16 rounded-full bg-primary flex items-center justify-center shadow-lg animate-pulse-ring touch-manipulation"
  aria-label="Emergencia - RCP"
  style={{ bottom: 'calc(4rem + env(safe-area-inset-bottom, 0px))' }}
>
  <AlertTriangle className="w-8 h-8 text-primary-foreground" />
</Link>

1.3 Accesibilidad (ARIA, Contraste, Navegación por Teclado)

Buenas Prácticas Implementadas

Práctica Estado Ejemplo
aria-expanded Implementado ProcedureCard.tsx línea 76
aria-label Implementado Botones de acción (línea 99, 109)
Loading lazy en imágenes Implementado MarkdownViewer.tsx línea 324, 345
Enlaces externos con rel Implementado MarkdownViewer.tsx línea 259

Problemas Críticos de Accesibilidad

1. Falta de ARIA Labels en Elementos Interactivos

// ❌ PROBLEMA: Index.tsx (línea 37-45)
<button
  onClick={onSearchClick}
  className="w-full flex items-center gap-4 p-4 bg-card border border-border rounded-xl hover:border-primary/50 transition-colors"
>
  {/* ⚠️ PROBLEMA: Sin aria-label, sin type="button" */}
  <Search className="w-6 h-6 text-muted-foreground" />
  <span className="text-muted-foreground">
    Buscar protocolo, fármaco, calculadora...
  </span>
</button>

Fix sugerido:

// ✅ FIX: Añadir aria-label y type
<button
  type="button"
  onClick={onSearchClick}
  className="w-full flex items-center gap-4 p-4 bg-card border border-border rounded-xl hover:border-primary/50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
  aria-label="Abrir búsqueda de protocolos, fármacos y calculadoras"
>
  <Search className="w-6 h-6 text-muted-foreground" aria-hidden="true" />
  <span className="text-muted-foreground">
    Buscar protocolo, fármaco, calculadora...
  </span>
</button>

2. Falta de Focus Visible

// ❌ PROBLEMA: Muchos botones sin focus visible
// No hay estilos focus en Tailwind config

// ✅ SOLUCIÓN: Añadir focus ring a todos los botones

Fix en Tailwind Config:

// ✅ FIX: tailwind.config.ts
extend: {
  // ... existing config
  ringOffsetWidth: {
    DEFAULT: '2px',
  },
  // Asegurar que focus:outline-none está en base
}

Añadir a index.css:

/* ✅ FIX: Focus visible para todos los elementos interactivos */
*:focus-visible {
  outline: 2px solid hsl(var(--primary));
  outline-offset: 2px;
  border-radius: calc(var(--radius) - 2px);
}

3. Imágenes Sin Alt Text Consistente

// ❌ PROBLEMA: MarkdownViewer.tsx (línea 344)
alt={imageAlt || 'Imagen'}

// ⚠️ PROBLEMA: 'Imagen' no es descriptivo
// ⚠️ PROBLEMA: No hay validación de alt text en Markdown

Fix sugerido:

// ✅ FIX: Mejorar alt text fallback
alt={imageAlt || imageCaption || `Imagen en ${filePath.split('/').pop()}`}

4. Falta de Skip Links

// ❌ PROBLEMA: No hay skip links para navegación por teclado
// Usuarios de screen readers deben tabular por toda la navegación

// ✅ SOLUCIÓN: Añadir skip link al inicio de App.tsx

Fix sugerido:

// ✅ FIX: Añadir en App.tsx después del ErrorBoundary
<div className="min-h-screen bg-background flex flex-col">
  {/* Skip to main content link */}
  <a
    href="#main-content"
    className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg"
  >
    Saltar al contenido principal
  </a>

  <Header ... />

  <main id="main-content" className="pt-14 pb-safe flex-1">
    {/* ... */}
  </main>
</div>

5. Contraste de Colores

// ⚠️ PROBLEMA: No hay verificación de contraste WCAG AA
// Colores definidos en HSL pueden no cumplir contraste mínimo

// ✅ SOLUCIÓN: Usar herramienta de verificación
// Recomendación: @storybook/addon-a11y o pa11y

1.4 Inconsistencias Visuales

1. Espaciado Inconsistente

// ❌ PROBLEMA: Diferentes sistemas de espaciado
// Index.tsx: space-y-6
// ManualViewer.tsx: space-y-6
// RCP.tsx: espacio inconsistente

// ✅ SOLUCIÓN: Usar sistema de diseño consistente
const spacing = {
  section: 'space-y-6',
  card: 'space-y-3',
  list: 'space-y-2',
};

2. Typography Scale Inconsistente

// ⚠️ PROBLEMA: Diferentes tamaños de fuente sin sistema
// h1: text-2xl, text-3xl, text-4xl (inconsistente)

// ✅ SOLUCIÓN: Usar sistema de tipografía

2. 🔄 FLUJOS DE USUARIO EN CÓDIGO

2.1 Flujo Principal: Búsqueda → Resultado → Detalle

Mapeo del Flujo:

1. Usuario abre app (Home)
   ↓
2. Click en Search Bar (Index.tsx línea 37)
   ↓
3. Abre SearchModal (SearchModal.tsx)
   ↓
4. Escribe query (debounce implícito, línea 54-58)
   ↓
5. Muestra resultados (línea 60-118)
   ↓
6. Click en resultado → Navigate (línea 137-146)
   ↓
7. Página de detalle (ProcedureCard, DrugCard, etc.)

⚠️ Puntos de Fricción Identificados

1. Sin Debounce en Búsqueda

// ❌ PROBLEMA: SearchModal.tsx (línea 54-58)
useEffect(() => {
  if (query.length < 2) {
    setResults([]);
    return;
  }
  // Búsqueda inmediata sin debounce
  // Puede causar múltiples renders innecesarios
}, [query, typeFilter, categoryFilter]);

Fix sugerido:

// ✅ FIX: Añadir debounce
import { useDebouncedValue } from '@/hooks/useDebounce';

const SearchModal = ({ isOpen, onClose }: SearchModalProps) => {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebouncedValue(query, 300); // 300ms debounce
  
  useEffect(() => {
    if (debouncedQuery.length < 2) {
      setResults([]);
      return;
    }
    // Búsqueda con debouncedQuery
  }, [debouncedQuery, typeFilter, categoryFilter]);
};

2. Navegación Sin Estado de Carga

// ❌ PROBLEMA: Al hacer click en resultado, no hay feedback inmediato
// El usuario no sabe si está cargando o si hay error

// ✅ SOLUCIÓN: Añadir loading state en navigate

Fix sugerido:

// ✅ FIX: Añadir loading state
const handleResultClick = (result: SearchResult) => {
  setResults([]); // Limpiar resultados
  addToHistory({ ...result, path: result.path });
  navigate(result.path);
  onClose(); // Cerrar modal
};

// En el botón de resultado:
<button
  onClick={() => {
    setIsNavigating(true);
    handleResultClick(result);
  }}
  disabled={isNavigating}
  className={cn(
    "flex items-center gap-3 p-3 rounded-lg transition-colors",
    isNavigating && "opacity-50 cursor-not-allowed"
  )}
>
  {isNavigating ? <Loader2 className="w-4 h-4 animate-spin" /> : <ArrowRight />}
</button>

3. Sin Historial de Búsqueda Visible en Modal

// ⚠️ PROBLEMA: Historial solo visible en Home, no en SearchModal
// Usuario debe cerrar modal y volver a Home para ver historial

// ✅ SOLUCIÓN: Mostrar historial reciente en SearchModal cuando está vacío

2.2 Flujo: Onboarding / Primera Visita

Estado Actual: NO EXISTE

// ❌ PROBLEMA: No hay onboarding para nuevos usuarios
// No hay explicación de features
// No hay tour de la app

// ✅ SOLUCIÓN: Crear componente de onboarding

Fix sugerido:

// ✅ CREAR: src/components/onboarding/OnboardingFlow.tsx
interface OnboardingStep {
  id: string;
  title: string;
  description: string;
  target: string; // CSS selector
  position: 'top' | 'bottom' | 'left' | 'right';
}

export const OnboardingFlow = () => {
  const [currentStep, setCurrentStep] = useState(0);
  const [isComplete, setIsComplete] = useState(
    localStorage.getItem('onboarding-complete') === 'true'
  );

  if (isComplete) return null;

  const steps: OnboardingStep[] = [
    {
      id: 'search',
      title: 'Busca rápidamente',
      description: 'Usa la barra de búsqueda para encontrar protocolos, fármacos o calculadoras',
      target: '[aria-label="Abrir búsqueda"]',
      position: 'bottom',
    },
    {
      id: 'emergency',
      title: 'Emergencias críticas',
      description: 'Acceso rápido a los protocolos más importantes',
      target: '[aria-label="Emergencia - RCP"]',
      position: 'left',
    },
    // ... más pasos
  ];

  // Implementar overlay y tooltip
};

2.3 Flujo: Navegación Nested (Manual, Guías)

Mapeo del Flujo:

1. Usuario va a /manual (ManualIndex.tsx)
   ↓
2. Click en bloque → Expande (acordeón)
   ↓
3. Click en capítulo → Navigate a /manual/:parte/:bloque/:capitulo
   ↓
4. ManualViewer carga MarkdownViewer (línea 4)
   ↓
5. Navegación anterior/siguiente (línea 48-53)

⚠️ Puntos de Fricción

1. Sin Breadcrumbs

// ❌ PROBLEMA: ManualViewer.tsx no tiene breadcrumbs
// Usuario no sabe dónde está en la jerarquía

// ✅ SOLUCIÓN: Añadir breadcrumbs

Fix sugerido:

// ✅ FIX: Añadir breadcrumbs
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink } from '@/components/ui/breadcrumb';

<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink asChild>
        <Link to="/manual">Manual</Link>
      </BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbItem>
      <BreadcrumbLink>{capituloData.bloque}</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbItem>
      <span>{capituloData.titulo}</span>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>

2. Navegación Anterior/Siguiente No Visible Hasta Scroll

// ⚠️ PROBLEMA: ManualViewer.tsx (línea 55-126)
// Navegación anterior/siguiente solo visible al final del contenido
// En contenido largo, usuario no ve opciones de navegación hasta el final

// ✅ SOLUCIÓN: Añadir sticky navigation

Fix sugerido:

// ✅ FIX: Sticky navigation
<div className="sticky top-14 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b border-border py-3">
  <div className="flex items-center justify-between">
    {capituloAnterior && (
      <Link to={capituloAnterior.ruta} className="flex items-center gap-2">
        <ChevronLeft className="w-5 h-5" />
        <span className="hidden sm:inline">Anterior</span>
      </Link>
    )}
    {capituloSiguiente && (
      <Link to={capituloSiguiente.ruta} className="flex items-center gap-2 ml-auto">
        <span className="hidden sm:inline">Siguiente</span>
        <ChevronRight className="w-5 h-5" />
      </Link>
    )}
  </div>
</div>

3. Sin Indicador de Progreso de Lectura

// ⚠️ PROBLEMA: No hay indicador de cuánto ha leído el usuario
// No hay "X% completado" o barra de progreso

// ✅ SOLUCIÓN: Añadir indicador de progreso

2.4 Estados de Carga/Error

Estados Implementados

Componente Loading Error Empty Estado
MarkdownViewer Completo
ManualViewer ⚠️ Parcial ⚠️ Parcial ⚠️ Mejorable
SearchModal No No Falta
ProcedureCard No No No Falta

Problemas Identificados

1. MarkdownViewer Re-renderiza en Cada filePath Change

// ❌ PROBLEMA: MarkdownViewer.tsx (línea 54-81)
useEffect(() => {
  // Fetch se ejecuta cada vez que filePath cambia
  // No hay cancelación de fetch anterior si cambia rápido
  fetch(normalizedPath)
    .then(...)
    .catch(...);
}, [filePath, onError]);

// ⚠️ PROBLEMA: Si filePath cambia rápidamente, múltiples fetches concurrentes
// ⚠️ PROBLEMA: Race condition - respuesta antigua puede sobrescribir nueva

Fix sugerido:

// ✅ FIX: Cancelar fetch anterior
useEffect(() => {
  const normalizedPath = filePath.startsWith('/') ? filePath : `/${filePath}`;
  
  setLoading(true);
  setError(null);
  
  const abortController = new AbortController();
  
  fetch(normalizedPath, { signal: abortController.signal })
    .then((res) => {
      if (!res.ok) {
        throw new Error(`Error ${res.status}: ${res.statusText}`);
      }
      return res.text();
    })
    .then((text) => {
      // Solo actualizar si el fetch no fue cancelado
      if (!abortController.signal.aborted) {
        setContent(text);
        setLoading(false);
      }
    })
    .catch((err) => {
      // Ignorar errores de cancelación
      if (err.name === 'AbortError') return;
      
      const errorMessage = `No se pudo cargar el archivo: ${err.message}`;
      if (!abortController.signal.aborted) {
        setError(errorMessage);
        setLoading(false);
        
        if (onError) {
          onError(new Error(errorMessage));
        }
      }
    });
  
  return () => {
    abortController.abort();
  };
}, [filePath, onError]);

2. Sin Skeleton Loading

// ❌ PROBLEMA: Loading states son spinners genéricos
// No hay skeleton loaders que muestren estructura del contenido esperado

// ✅ SOLUCIÓN: Añadir skeleton loaders

Fix sugerido:

// ✅ CREAR: src/components/shared/SkeletonLoader.tsx
export const MarkdownSkeleton = () => (
  <div className="space-y-4 animate-pulse">
    <div className="h-8 bg-muted rounded w-3/4" />
    <div className="h-4 bg-muted rounded w-full" />
    <div className="h-4 bg-muted rounded w-5/6" />
    <div className="h-64 bg-muted rounded" /> {/* Imagen skeleton */}
    <div className="h-4 bg-muted rounded w-full" />
    <div className="h-4 bg-muted rounded w-4/5" />
  </div>
);

// Usar en MarkdownViewer:
if (loading && showLoading) {
  return <MarkdownSkeleton />;
}

3. RENDIMIENTO FRONTEND

3.1 Bundle Analysis

Optimizaciones Implementadas

Optimización Estado Detalles
Lazy Loading de Páginas Implementado App.tsx línea 23-54
Code Splitting Implementado Vite config manual chunks
Tree Shaking Automático Vite + ES Modules
Image Lazy Loading Implementado loading="lazy" en imágenes

⚠️ Problemas Identificados

1. Bundle Size Grande por react-markdown

// ⚠️ PROBLEMA: react-markdown + plugins es pesado (~150KB gzipped)
// Se carga incluso si no se usa MarkdownViewer

// ✅ SOLUCIÓN: Lazy load react-markdown también

Fix sugerido:

// ✅ FIX: Lazy load MarkdownViewer completo
const MarkdownViewer = lazy(() => import('@/components/content/MarkdownViewer'));

// En ManualViewer.tsx:
<Suspense fallback={<MarkdownSkeleton />}>
  <MarkdownViewer filePath={filePath} />
</Suspense>

2. Vendor Chunks Muy Grandes

// ⚠️ PROBLEMA: vite.config.ts (línea 59)
chunkSizeWarningLimit: 1000, // 1MB - muy alto

// ⚠️ PROBLEMA: vendor-react puede ser > 500KB
// Debería optimizarse más

Fix sugerido:

// ✅ FIX: Optimizar chunks más agresivamente
build: {
  chunkSizeWarningLimit: 500, // Reducir a 500KB
  rollupOptions: {
    output: {
      manualChunks: (id) => {
        // Separar react-markdown en chunk propio
        if (id.includes('react-markdown')) {
          return 'vendor-markdown';
        }
        if (id.includes('remark') || id.includes('rehype')) {
          return 'vendor-markdown-plugins';
        }
        // ... resto de lógica
      },
    },
  },
}

3. Imágenes Sin Optimización

// ❌ PROBLEMA: Imágenes en public/assets/ no están optimizadas
// No hay WebP, no hay responsive images, no hay srcset

// ✅ SOLUCIÓN: Implementar optimización de imágenes

Fix sugerido:

// ✅ FIX: Usar next/image o similar (o implementar manual)
// Crear componente OptimizedImage

interface OptimizedImageProps {
  src: string;
  alt: string;
  width?: number;
  height?: number;
  sizes?: string;
}

export const OptimizedImage = ({ src, alt, width, height, sizes }: OptimizedImageProps) => {
  // Detectar soporte WebP
  const [supportsWebP, setSupportsWebP] = useState(false);
  
  useEffect(() => {
    const webP = new Image();
    webP.onload = webP.onerror = () => {
      setSupportsWebP(webP.height === 2);
    };
    webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
  }, []);

  const webPSrc = src.replace(/\.(jpg|jpeg|png)$/, '.webp');
  
  return (
    <picture>
      {supportsWebP && <source srcSet={webPSrc} type="image/webp" />}
      <img
        src={src}
        alt={alt}
        width={width}
        height={height}
        sizes={sizes}
        loading="lazy"
        decoding="async"
      />
    </picture>
  );
};

3.2 Re-renders Innecesarios

Problemas Identificados

1. ProcedureCard Re-renderiza en Cada Toggle

// ❌ PROBLEMA: ProcedureCard.tsx (línea 22-33)
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const { isFavorite, toggleFavorite: toggleFavoriteHook } = useFavorites();

// ⚠️ PROBLEMA: useFavorites() puede causar re-renders en todos los cards
// cuando cambia un favorito

// ✅ SOLUCIÓN: Memoizar componente

Fix sugerido:

// ✅ FIX: Memoizar ProcedureCard
import { memo, useCallback } from 'react';

const ProcedureCard = memo(({ procedure, defaultExpanded = false }: ProcedureCardProps) => {
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
  const { isFavorite, toggleFavorite } = useFavorites();

  const handleToggleFavorite = useCallback((e: React.MouseEvent) => {
    e.stopPropagation();
    toggleFavorite({
      id: procedure.id,
      type: 'procedure',
      title: procedure.shortTitle,
      path: `/soporte-vital?id=${procedure.id}`,
    });
  }, [procedure.id, procedure.shortTitle, toggleFavorite]);

  // ... resto del componente
}, (prevProps, nextProps) => {
  // Comparación personalizada para evitar re-renders innecesarios
  return (
    prevProps.procedure.id === nextProps.procedure.id &&
    prevProps.defaultExpanded === nextProps.defaultExpanded
  );
});

ProcedureCard.displayName = 'ProcedureCard';

2. SearchModal Re-renderiza en Cada Query Change

// ❌ PROBLEMA: SearchModal.tsx (línea 54-118)
// useEffect ejecuta búsqueda en cada cambio de query
// Sin debounce, sin memoización de resultados

// ✅ SOLUCIÓN: Debounce + memoización

Fix sugerido (ver sección 2.1 para debounce):

// ✅ FIX: Memoizar resultados de búsqueda
import { useMemo } from 'react';

const searchResults = useMemo(() => {
  if (debouncedQuery.length < 2) return [];
  
  const expandedQuery = expandQueryWithAcronyms(debouncedQuery);
  // ... lógica de búsqueda
  return [...procedures, ...drugs];
}, [debouncedQuery, typeFilter, categoryFilter]);

3. MarkdownViewer Re-renderiza Props Sin Cambios

// ❌ PROBLEMA: MarkdownViewer.tsx
// No está memoizado, re-renderiza incluso si filePath no cambia

// ✅ SOLUCIÓN: Memoizar

3.3 Optimización de Assets

Problemas:

  1. Sin compresión de imágenes (WebP, AVIF)
  2. Sin responsive images (srcset)
  3. Sin preload de assets críticos
  4. ⚠️ Sin service worker para cache de assets

Soluciones:

  1. Implementar optimización de imágenes (ver 3.1)
  2. Añadir preload de assets críticos en index.html
  3. Mejorar service worker para cache agresivo de assets

4. 🔧 MEJORAS CONCRETAS - REFACTORS ESPECÍFICOS

4.1 Refactor de Componentes

Prioridad Alta

Componente Problema Fix Esfuerzo Impacto
BaseCard Duplicación lógica Crear componente base 4 horas Alto
LoadingState Estados inconsistentes Unificar componente 2 horas Medio
ErrorState Estados inconsistentes Unificar componente 2 horas Medio
OptimizedImage Sin optimización Implementar componente 4 horas Alto
useDebounce Falta debounce Crear hook 1 hora Medio

Prioridad Media

Componente Problema Fix Esfuerzo Impacto
OnboardingFlow No existe onboarding Crear componente 1 día Alto
Breadcrumbs Falta navegación clara Añadir a ManualViewer 2 horas Medio
SkeletonLoader Loading genérico Crear skeletons específicos 3 horas Medio
StickyNavigation Navegación no visible Implementar sticky nav 2 horas Medio

4.2 Librerías Recomendadas

Problema Librería Razón Instalación
Debounce use-debounce Hook ya implementado npm i use-debounce
Accesibilidad @react-aria/* Componentes accesibles npm i @react-aria/button
Imágenes next/image (adaptar) Optimización automática O implementar manual
Análisis web-vitals Métricas de rendimiento npm i web-vitals
Testing A11y @axe-core/react Testing accesibilidad npm i @axe-core/react

4.3 Archivos que Necesitan Atención Urgente

🔴 Crítico (1-2 días)

  1. src/components/content/MarkdownViewer.tsx

    • Añadir cancelación de fetch (race condition)
    • Memoizar componente
    • Mejorar manejo de errores
  2. src/hooks/use-mobile.tsx

    • Fix hydration mismatch
    • Usar MediaQueryList API
  3. src/components/layout/SearchModal.tsx

    • Añadir debounce
    • Memoizar resultados
    • Añadir loading state

🟡 Importante (3-5 días)

  1. src/components/procedures/ProcedureCard.tsx

    • Memoizar componente
    • Extraer a BaseCard
    • Optimizar re-renders
  2. src/pages/Index.tsx

    • Añadir aria-labels
    • Fix responsive grid
    • Fix floating button safe area
  3. src/pages/ManualViewer.tsx

    • Añadir breadcrumbs
    • Sticky navigation
    • Indicador de progreso

🟢 Mejoras (1-2 semanas)

  1. Crear componentes compartidos (BaseCard, LoadingState, ErrorState)
  2. Implementar onboarding flow
  3. Optimización de imágenes completa
  4. Añadir skeleton loaders

5. 📊 TABLA RESUMEN DE PROBLEMAS

ID Problema Severidad Archivo Línea Esfuerzo Fix Prioridad
U1 Falta aria-labels en botones 🔴 Alta Index.tsx 37 30 min
U2 Race condition en MarkdownViewer 🔴 Alta MarkdownViewer.tsx 54-81 1 hora
U3 Hydration mismatch en useIsMobile 🔴 Alta use-mobile.tsx 6-18 1 hora
U4 Sin debounce en búsqueda 🟡 Media SearchModal.tsx 54-58 1 hora
U5 Duplicación lógica en Cards 🟡 Media ProcedureCard.tsx Todo 4 horas
U6 Re-renders innecesarios 🟡 Media ProcedureCard.tsx 22 2 horas
U7 Sin skeleton loaders 🟡 Media Múltiples - 3 horas
U8 Sin breadcrumbs 🟡 Media ManualViewer.tsx - 2 horas
U9 Imágenes sin optimizar 🟡 Media MarkdownViewer.tsx 278-354 4 horas
U10 Sin focus visible 🟡 Media Múltiples - 2 horas 10º
U11 Sin skip links 🟢 Baja App.tsx - 30 min 11º
U12 Sin onboarding 🟢 Baja Nuevo - 1 día 12º

Total Problemas: 12
Críticos: 3
Importantes: 7
Mejoras: 2


6. CHECKLIST DE IMPLEMENTACIÓN

Quick Wins (1 día)

  • U1: Añadir aria-labels a botones principales
  • U2: Fix race condition en MarkdownViewer
  • U3: Fix hydration mismatch en useIsMobile
  • U4: Añadir debounce en búsqueda
  • U11: Añadir skip links

Refactors Importantes (1 semana)

  • U5: Crear BaseCard component
  • U6: Memoizar ProcedureCard
  • U7: Implementar skeleton loaders
  • U8: Añadir breadcrumbs
  • U10: Añadir focus visible styles

Optimizaciones (1-2 semanas)

  • U9: Optimizar imágenes (WebP, srcset)
  • U12: Implementar onboarding flow
  • Mejorar bundle splitting
  • Añadir web-vitals tracking

Última actualización: 2025-01-07
Próxima revisión recomendada: 2025-02-07 (1 mes)