Dashboard
BetaPanel de inicio de una app: shell con Navbar y Sidebar, resumen en cards, tabla con pestañas y columna de eventos y avisos.
registry/examples/templates/dashboard.tsx"use client";
import { useState } from "react";
import { Alert, Banner } from "@/registry/ds/alert";
import { Avatar } from "@/registry/ds/avatar";
import { Badge, Chip } from "@/registry/ds/badge";
import { Breadcrumb, BreadcrumbItem } from "@/registry/ds/breadcrumb";
import { Button } from "@/registry/ds/button";
import { Card, CardBody, CardFooter, CardTitle } from "@/registry/ds/card";
import { EmptyState } from "@/registry/ds/empty-state";
import { Footer, FooterBottom, FooterLegal, FooterLink } from "@/registry/ds/footer";
import { BarChart, Bell, BookOpen, Calendar, CheckCircle, FileText, Home, LogOut, Plus, Search, Settings, User } from "@/registry/ds/icons";
import { Input } from "@/registry/ds/input";
import { Link } from "@/registry/ds/link";
import { Menu, MenuItem, MenuSeparator } from "@/registry/ds/menu";
import { Navbar, NavbarActions, NavbarBrand, NavbarMobile } from "@/registry/ds/navbar";
import { Progress } from "@/registry/ds/progress";
import { Sidebar, SidebarGroup, SidebarItem, SidebarNav } from "@/registry/ds/sidebar";
import { Tab, TabList, TabPanel, Tabs } from "@/registry/ds/tabs";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/registry/ds/table";
import { Tooltip, TooltipProvider } from "@/registry/ds/tooltip";
const Logo = () => (
<svg viewBox="0 0 24 24" aria-hidden>
<rect width="24" height="24" rx="6" fill="var(--primary)" />
<path d="M7 12.5l3.5 3.5L17 9" stroke="var(--primary-fg)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</svg>
);
const materias = [
{ codigo: "MAT201", nombre: "Cálculo II", docente: "Julia Arce", proxima: "Lun 8:00 · Aula 204", asistencia: 92, estado: "success" as const },
{ codigo: "FIS101", nombre: "Física I", docente: "Martín Ferro", proxima: "Mar 14:00 · Lab 2", asistencia: 78, estado: "warning" as const },
{ codigo: "PRG102", nombre: "Programación II", docente: "Lucía Paz", proxima: "Mié 10:00 · Lab 1", asistencia: 100, estado: "success" as const },
{ codigo: "ALG101", nombre: "Álgebra", docente: "Pedro Núñez", proxima: "Jue 8:00 · Aula 110", asistencia: 64, estado: "danger" as const },
{ codigo: "ING201", nombre: "Inglés técnico", docente: "Ana Rey", proxima: "Vie 16:00 · Virtual", asistencia: 88, estado: "success" as const },
];
const estadoLabel = { success: "Regular", warning: "En riesgo", danger: "Libre" };
const colorDe = (p: number): "success" | "warning" | "danger" => (p >= 75 ? "success" : p >= 65 ? "warning" : "danger");
const finales = [
{ materia: "Cálculo II", fecha: "Jue 28 oct · 9:00", lugar: "Aula 204", inscripto: true },
{ materia: "Física I", fecha: "Vie 30 oct · 14:00", lugar: "Lab 2", inscripto: false },
];
const eventos = [
{ dia: "28", mes: "oct", titulo: "Final de Cálculo II", detalle: "9:00 · Aula 204" },
{ dia: "30", mes: "oct", titulo: "Cierra la inscripción a finales", detalle: "23:59" },
{ dia: "4", mes: "nov", titulo: "Entrega del TP de Programación II", detalle: "Campus virtual" },
];
const nav = (
<>
<SidebarItem href="#" icon={<Home />} active>
Inicio
</SidebarItem>
<SidebarGroup label="Académico">
<SidebarItem href="#" icon={<BookOpen />}>
Materias
</SidebarItem>
<SidebarItem href="#" icon={<Calendar />} badge="2">
Calendario
</SidebarItem>
<SidebarItem href="#" icon={<BarChart />}>
Notas
</SidebarItem>
<SidebarItem href="#" icon={<FileText />}>
Certificados
</SidebarItem>
</SidebarGroup>
<SidebarGroup label="Cuenta">
<SidebarItem href="#" icon={<Settings />}>
Configuración
</SidebarItem>
</SidebarGroup>
</>
);
/**
* Template Dashboard — Panel de inicio: shell con Navbar y Sidebar, banner, resumen en cards, tabla con pestañas, eventos y avisos.
*/
export default function Dashboard() {
const [banner, setBanner] = useState(true);
return (
<TooltipProvider>
<div className="flex min-h-dvh flex-col bg-bg-subtle">
<Navbar collapse="md" width="full">
<NavbarBrand href="#">
<Logo />
Campus
</NavbarBrand>
<NavbarActions>
<div className="hidden w-64 lg:block">
<Input size="sm" aria-label="Buscar" placeholder="Buscar materias, docentes…" iconLeft={<Search />} />
</div>
<Tooltip content="Notificaciones">
<Button variant="text" size="sm" iconOnly={<Bell />} aria-label="Notificaciones" />
</Tooltip>
<Menu
align="end"
trigger={
<button type="button" aria-label="Cuenta de Julia Arce" className="rounded-full focus-visible:shadow-[0_0_0_2px_var(--focus-ring)] focus-visible:outline-none">
<Avatar name="Julia Arce" size="sm" />
</button>
}
>
<MenuItem icon={<User />}>Mi perfil</MenuItem>
<MenuItem icon={<Settings />} href="#">
Configuración
</MenuItem>
<MenuSeparator />
<MenuItem icon={<LogOut />} destructive>
Cerrar sesión
</MenuItem>
</Menu>
<NavbarMobile>{nav}</NavbarMobile>
</NavbarActions>
</Navbar>
<div className="flex flex-1">
<Sidebar collapsible className="max-md:hidden sticky top-14 h-[calc(100dvh-3.5rem)] self-start">
<SidebarNav>{nav}</SidebarNav>
</Sidebar>
<main className="min-w-0 flex-1">
<div className="mx-auto w-full max-w-6xl space-y-6 p-4 sm:p-6 lg:p-8">
<Breadcrumb size="sm">
<BreadcrumbItem href="#" separator={false} icon={<Home />}>
Inicio
</BreadcrumbItem>
<BreadcrumbItem current>Panel</BreadcrumbItem>
</Breadcrumb>
<header className="flex flex-wrap items-end justify-between gap-4">
<div>
<h1 className="text-heading-sm font-semibold text-fg">Hola, Julia</h1>
<p className="mt-1 text-sm text-fg-muted">Segundo cuatrimestre 2026 · Ingeniería en Sistemas · 3.º año</p>
</div>
<div className="flex flex-wrap gap-2">
<Button variant="stroke" iconLeft={<FileText />}>
Descargar analítico
</Button>
<Button iconLeft={<Plus />}>Inscribirme a un final</Button>
</div>
</header>
{banner && (
<Banner color="info" title="Inscripción a finales abierta" action={<Button size="sm" variant="text">Ver fechas</Button>} onDismiss={() => setBanner(false)}>
Cierra el 30 de octubre a las 23:59.
</Banner>
)}
<section aria-label="Resumen del cuatrimestre" className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
{[
{ label: "Materias en curso", value: "5", detail: "2 finales este mes" },
{ label: "Promedio", value: "8,4", detail: "+0,3 vs. el cuatrimestre pasado" },
{ label: "Asistencia", value: "84%", detail: "Mínimo para regularizar: 75%" },
].map((s) => (
<Card key={s.label} padding="sm">
<CardBody>
<p className="text-sm text-fg-muted">{s.label}</p>
<p className="text-heading-sm font-semibold text-fg">{s.value}</p>
<p className="text-xs text-fg-subtle">{s.detail}</p>
</CardBody>
</Card>
))}
<Card padding="sm">
<CardBody>
<p className="text-sm text-fg-muted">Créditos</p>
<p className="text-heading-sm font-semibold text-fg">
64 <span className="text-md font-normal text-fg-subtle">/ 240</span>
</p>
<Progress value={27} size="sm" aria-label="Créditos completados: 27 %" />
</CardBody>
</Card>
</section>
<div className="grid gap-6 lg:grid-cols-3">
<Card className="lg:col-span-2">
<CardBody>
<Tabs defaultValue="materias">
<TabList aria-label="Secciones del panel">
<Tab value="materias" badge={<Badge size="sm">5</Badge>}>
Materias
</Tab>
<Tab value="finales" badge={<Badge size="sm">2</Badge>}>
Finales
</Tab>
<Tab value="pagos">Pagos</Tab>
</TabList>
<TabPanel value="materias" className="mt-4">
<Table size="sm">
<TableHead>
<TableRow>
<TableHeader>Materia</TableHeader>
<TableHeader className="max-lg:hidden">Próxima clase</TableHeader>
<TableHeader className="max-md:hidden">Asistencia</TableHeader>
<TableHeader>Estado</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{materias.map((m) => (
<TableRow key={m.codigo}>
<TableCell>
<span className="block font-medium text-fg">{m.nombre}</span>
<span className="block text-xs text-fg-muted">{m.docente}</span>
</TableCell>
<TableCell className="text-fg-muted max-lg:hidden">{m.proxima}</TableCell>
<TableCell className="max-md:hidden">
<div className="flex items-center gap-3">
<Progress value={m.asistencia} size="sm" color={colorDe(m.asistencia)} aria-label={`Asistencia en ${m.nombre}`} className="w-24" />
<span className="text-xs text-fg-muted">{m.asistencia}%</span>
</div>
</TableCell>
<TableCell>
<Chip size="sm" status={m.estado}>
{estadoLabel[m.estado]}
</Chip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabPanel>
<TabPanel value="finales" className="mt-4">
<ul className="divide-y divide-border-subtle">
{finales.map((f) => (
<li key={f.materia} className="flex flex-wrap items-center justify-between gap-3 py-3">
<div>
<p className="font-medium text-fg">{f.materia}</p>
<p className="text-xs text-fg-muted">
{f.fecha} · {f.lugar}
</p>
</div>
{f.inscripto ? (
<Chip size="sm" status="success">
Inscripta
</Chip>
) : (
<Button size="sm" variant="tonal">
Inscribirme
</Button>
)}
</li>
))}
</ul>
</TabPanel>
<TabPanel value="pagos" className="mt-2">
<EmptyState size="sm" icon={<CheckCircle />} title="No tenés pagos pendientes" description="La próxima cuota vence el 10 de noviembre." actions={<Button variant="stroke" size="sm">Ver historial</Button>} />
</TabPanel>
</Tabs>
</CardBody>
<CardFooter>
<span className="text-xs text-fg-muted">Actualizado hoy a las 8:12</span>
<Link href="#" variant="standalone">
Ver todas las materias
</Link>
</CardFooter>
</Card>
<div className="space-y-6">
<Card>
<CardBody>
<CardTitle as="h2">Próximos eventos</CardTitle>
<ul className="mt-2 space-y-4">
{eventos.map((e) => (
<li key={e.titulo} className="flex gap-3">
<div className="flex w-11 shrink-0 flex-col items-center rounded-md bg-primary-subtle py-1 text-primary-text">
<span className="text-md font-semibold leading-5">{e.dia}</span>
<span className="text-[10px] uppercase leading-3">{e.mes}</span>
</div>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-fg">{e.titulo}</p>
<p className="text-xs text-fg-muted">{e.detalle}</p>
</div>
</li>
))}
</ul>
</CardBody>
<CardFooter>
<Link href="#" variant="standalone">
Ver calendario
</Link>
</CardFooter>
</Card>
<div className="space-y-3">
<Alert color="warning" size="sm" title="Asistencia baja en Álgebra" action={<Link href="#">Ver detalle</Link>}>
Con 64 % quedás libre. Necesitás ir a las próximas 3 clases.
</Alert>
<Alert color="neutral" size="sm" title="Encuesta docente" action={<Link href="#">Responder</Link>}>
Abierta hasta el 15 de noviembre. Lleva 5 minutos.
</Alert>
</div>
</div>
</div>
</div>
</main>
</div>
<Footer compact>
<FooterBottom>
<p>© 2026 Campus · Universidad</p>
<FooterLegal>
<FooterLink href="#">Ayuda</FooterLink>
<FooterLink href="#">Privacidad</FooterLink>
</FooterLegal>
</FooterBottom>
</Footer>
</div>
</TooltipProvider>
);
}Qué incluye
- Shell. Navbar con búsqueda, notificaciones y Menu de cuenta; Sidebar plegable desde 768 px y la misma navegación en el Drawer de ☰ por debajo.
- Breadcrumb, un
h1y dos acciones: una primaria y unastroke. - Banner cerrable para el aviso de página.
- Cuatro Card de resumen; la de créditos lleva un
Progress con
aria-label. - Tabs con Badge de conteo:
una Table
size="sm"con Chip de estado, una lista de finales y un Empty state en Pagos. - Columna lateral con eventos y dos Alert
size="sm"con acción. Footercompact.
Decisiones
- Una pantalla, una primaria. "Inscribirme a un final" es la única
acción
fill; descargar el analítico esstrokey los links de las cards son Linkstandalone. - La tabla pierde columnas antes que legibilidad. Próxima clase se oculta
bajo 640 px y Asistencia bajo 768 (
max-sm:hidden,max-md:hidden); Materia y Estado quedan siempre. - El color del Progress sale de un umbral, no del gusto: verde desde 75 %, ámbar desde 65, rojo por debajo. Es el mismo criterio que el Chip de estado.
- Pagos muestra el vacío como buena noticia: "No tenés pagos pendientes", con la próxima fecha y una acción secundaria.
- Avisos como Alert chicos, no como cards: tienen rol de estado, se anuncian solos y cada uno lleva su acción.
Cómo adaptarlo
- Las listas (
materias,finales,eventos) son el contrato con tu API: mantené los campos y cambiá la fuente. - Si tu app no tiene Sidebar, sacá el
<Sidebar>y pasá la navegación aNavbarNav; elNavbarMobilequeda igual. - Para más de 5 filas, pasá a la Table con paginación del Listado.
Instalación
npx shadcn@latest add https://polen.cdc.cool/r/template-dashboard.json