// 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,
});