// vita-shell.jsx — Estructura de la aplicación web. // // Sustituye al marco de iPhone (IOSDevice). Aquí vive la arquitectura de // navegación: header fijo, sidebar en escritorio, barra inferior en móvil, // y un contenedor de contenido con ancho máximo. // // Reglas de responsive (los cortes salen de src/design/tokens.ts): // ≥1024px sidebar fija de 240px + contenido // 768-1023 sidebar como cajón deslizante (botón hamburguesa) // <768px sin sidebar, navegación inferior de 5 destinos // ── Arquitectura de navegación ────────────────────────────────── // Un único sitio donde se declara qué secciones existen. Añadir una sección // es añadir una fila aquí, no tocar tres componentes. const NAV_PRINCIPAL = [ { id: 'home', label: 'Inicio', icono: 'home', flow: 'home', enMovil: true }, { id: 'chat', label: 'Hablar con Vita', iconoCorto: 'Vita', icono: 'mic', flow: 'chat', enMovil: true, destacado: true }, { id: 'care', label: 'Mi plan', icono: 'check', flow: 'care', enMovil: false }, { id: 'history', label: 'Historia clínica', iconoCorto: 'Historia', icono: 'doc', flow: 'history', enMovil: true }, { id: 'services', label: 'Servicios', icono: 'grid', flow: 'services', enMovil: true }, { id: 'profile', label: 'Perfil', icono: 'user', flow: 'profile', enMovil: true }, ]; function useAnchoVentana() { const [ancho, setAncho] = React.useState(typeof window !== 'undefined' ? window.innerWidth : 1280); React.useEffect(() => { const medir = () => setAncho(window.innerWidth); window.addEventListener('resize', medir); return () => window.removeEventListener('resize', medir); }, []); return ancho; } // ── Header ────────────────────────────────────────────────────── function AppHeader({ t, titulo, notifCount, onOpenNotifs, onToggleTheme, onToggleMenu, compacto, pacienteNombre, onOpenProfile }) { const p = t.paleta; return (
{compacto && ( )}
VITA
{titulo &&
{titulo}
}
); } function botonIcono(p) { return { width: 36, height: 36, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: 'none', cursor: 'pointer', background: 'transparent', color: '#fff', transition: 'background 120ms ease', }; } // ── Sidebar (escritorio y cajón) ──────────────────────────────── function AppSidebar({ t, activo, onNavegar, comoCajon, abierto, onCerrar }) { const p = t.paleta; const contenido = ( ); if (!comoCajon) return contenido; return ( {abierto && (
)}
{contenido}
); } // ── Navegación inferior (móvil) ───────────────────────────────── function AppBottomNav({ t, activo, onNavegar }) { const p = t.paleta; const items = NAV_PRINCIPAL.filter((i) => i.enMovil); return ( ); } // ── Shell ─────────────────────────────────────────────────────── // `ancho` controla el contenedor: 'normal' centra a 1200px, 'completo' deja // que la pantalla gestione su propio ancho (lo usa el chat, que necesita // ocupar todo el alto disponible). function AppShell({ t, activo, titulo, notifCount, pacienteNombre, onNavegar, onOpenNotifs, onToggleTheme, onOpenProfile, ancho = 'normal', children }) { const p = t.paleta; const anchoVentana = useAnchoVentana(); const [menuAbierto, setMenuAbierto] = React.useState(false); const esMovil = anchoVentana < 768; const sidebarFija = anchoVentana >= 1024; const sidebarComoCajon = !esMovil && !sidebarFija; React.useEffect(() => { if (sidebarFija) setMenuAbierto(false); }, [sidebarFija]); return (
setMenuAbierto((v) => !v)} compacto={sidebarComoCajon} />
{sidebarFija && } {sidebarComoCajon && ( setMenuAbierto(false)} /> )}
{ancho === 'completo' ? children : (
{children}
)}
{esMovil && }
); } // ── Piezas reutilizables del sistema ──────────────────────────── function Card({ t, children, padding = 18, style = {} }) { const p = t.paleta; return (
{children}
); } function CardTitle({ t, children, accion }) { const p = t.paleta; return (

{children}

{accion &&
{accion}
}
); } function Button({ t, children, onClick, variante = 'primario', ancho, size = 'md', disabled }) { const p = t.paleta; const [hover, setHover] = React.useState(false); const estilos = { primario: { bg: hover ? p.primaryDark : p.primary, color: p.onPrimary, border: 'none' }, secundario: { bg: hover ? p.primarySoft : p.surface, color: p.primaryDark, border: `1px solid ${p.border}` }, fantasma: { bg: hover ? p.primarySoft : 'transparent', color: p.primaryDark, border: 'none' }, peligro: { bg: hover ? '#B91C1C' : p.danger, color: '#fff', border: 'none' }, }[variante] || {}; const alturas = { sm: 34, md: 42, lg: 48 }; return ( ); } /** Rejilla responsive automática: se adapta sin media queries. */ function Grid({ children, min = 260, gap = 16 }) { return (
{children}
); } function Badge({ t, children, tono = 'neutro' }) { const p = t.paleta; const tonos = { neutro: { bg: p.bg, color: p.textMuted, border: p.border }, exito: { bg: t.dark ? 'rgba(74,222,128,0.14)' : '#DCFCE7', color: p.success, border: 'transparent' }, aviso: { bg: t.dark ? 'rgba(251,191,36,0.14)' : '#FEF3C7', color: t.dark ? p.warning : '#B45309', border: 'transparent' }, error: { bg: t.dark ? 'rgba(248,113,113,0.14)' : '#FEE2E2', color: p.danger, border: 'transparent' }, marca: { bg: p.primarySoft, color: p.primaryDark, border: 'transparent' }, }[tono] || {}; return ( {children} ); } function PageTitle({ t, children, sub, acciones }) { const p = t.paleta; return (

{children}

{sub &&

{sub}

}
{acciones &&
{acciones}
}
); } Object.assign(window, { NAV_PRINCIPAL, AppShell, AppHeader, AppSidebar, AppBottomNav, Card, CardTitle, Button, Grid, Badge, PageTitle, useAnchoVentana, });