codigo0/docs/ANALISIS_UX_UI_FRONTEND.md

1188 lines
32 KiB
Markdown

# 🎨 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```ts
// ✅ FIX: tailwind.config.ts
extend: {
// ... existing config
ringOffsetWidth: {
DEFAULT: '2px',
},
// Asegurar que focus:outline-none está en base
}
```
**Añadir a index.css:**
```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**
```tsx
// ❌ 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:**
```tsx
// ✅ FIX: Mejorar alt text fallback
alt={imageAlt || imageCaption || `Imagen en ${filePath.split('/').pop()}`}
```
**4. Falta de Skip Links**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ⚠️ 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**
```tsx
// ❌ 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**
```tsx
// ⚠️ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ⚠️ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ PROBLEMA: ManualViewer.tsx no tiene breadcrumbs
// Usuario no sabe dónde está en la jerarquía
// ✅ SOLUCIÓN: Añadir breadcrumbs
```
**Fix sugerido:**
```tsx
// ✅ 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**
```tsx
// ⚠️ 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:**
```tsx
// ✅ 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**
```tsx
// ⚠️ 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** | ✅ Sí | ✅ Sí | ✅ Sí | ✅ Completo |
| **ManualViewer** | ⚠️ Parcial | ⚠️ Parcial | ✅ Sí | ⚠️ Mejorable |
| **SearchModal** | ❌ No | ❌ No | ✅ Sí | ❌ Falta |
| **ProcedureCard** | ❌ No | ❌ No | ❌ No | ❌ Falta |
#### ❌ Problemas Identificados
**1. MarkdownViewer Re-renderiza en Cada filePath Change**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ⚠️ 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:**
```tsx
// ✅ 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**
```tsx
// ⚠️ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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:**
```tsx
// ✅ 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**
```tsx
// ❌ 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):**
```tsx
// ✅ 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**
```tsx
// ❌ 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)
4. **`src/components/procedures/ProcedureCard.tsx`**
- ✅ Memoizar componente
- ✅ Extraer a BaseCard
- ✅ Optimizar re-renders
5. **`src/pages/Index.tsx`**
- ✅ Añadir aria-labels
- ✅ Fix responsive grid
- ✅ Fix floating button safe area
6. **`src/pages/ManualViewer.tsx`**
- ✅ Añadir breadcrumbs
- ✅ Sticky navigation
- ✅ Indicador de progreso
#### 🟢 Mejoras (1-2 semanas)
7. Crear componentes compartidos (BaseCard, LoadingState, ErrorState)
8. Implementar onboarding flow
9. Optimización de imágenes completa
10. 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 | 1º |
| **U2** | Race condition en MarkdownViewer | 🔴 Alta | `MarkdownViewer.tsx` | 54-81 | 1 hora | 2º |
| **U3** | Hydration mismatch en useIsMobile | 🔴 Alta | `use-mobile.tsx` | 6-18 | 1 hora | 3º |
| **U4** | Sin debounce en búsqueda | 🟡 Media | `SearchModal.tsx` | 54-58 | 1 hora | 4º |
| **U5** | Duplicación lógica en Cards | 🟡 Media | `ProcedureCard.tsx` | Todo | 4 horas | 5º |
| **U6** | Re-renders innecesarios | 🟡 Media | `ProcedureCard.tsx` | 22 | 2 horas | 6º |
| **U7** | Sin skeleton loaders | 🟡 Media | Múltiples | - | 3 horas | 7º |
| **U8** | Sin breadcrumbs | 🟡 Media | `ManualViewer.tsx` | - | 2 horas | 8º |
| **U9** | Imágenes sin optimizar | 🟡 Media | `MarkdownViewer.tsx` | 278-354 | 4 horas | 9º |
| **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)