App Shell
Esqueleto de sistema com menu lateral recolhível, cabeçalho com trilha de navegação e conteúdo em cards.
'use client';
import {
Bell,
Boxes,
CalendarDays,
LayoutDashboard,
LifeBuoy,
Plus,
Receipt,
Settings,
ShoppingBag,
Store,
Users,
} from 'lucide-react';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Breadcrumb, Breadcrumbs } from '@/components/ui/breadcrumbs';
import { Button } from '@/components/ui/button';
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Link } from '@/components/ui/link';
import { Separator } from '@/components/ui/separator';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from '@/components/ui/sidebar';
const menu = [
{ label: 'Painel', icon: <LayoutDashboard />, active: true },
{ label: 'Pedidos', icon: <ShoppingBag />, badge: '8' },
{ label: 'Produtos', icon: <Boxes /> },
{ label: 'Clientes', icon: <Users /> },
{ label: 'Agenda de entregas', icon: <CalendarDays /> },
{ label: 'Financeiro', icon: <Receipt /> },
];
const indicadores = [
{ titulo: 'Vendas hoje', valor: 'R$ 3.482,90', detalhe: '+18% vs. ontem' },
{ titulo: 'Pedidos em aberto', valor: '8', detalhe: '3 aguardando pagamento' },
{ titulo: 'Ticket médio', valor: 'R$ 127,40', detalhe: 'Últimos 30 dias' },
];
const pedidos = [
{ id: '#10482', cliente: 'Fernanda Ribeiro', valor: 'R$ 289,90', status: 'Pago' },
{ id: '#10481', cliente: 'Lucas Martins', valor: 'R$ 74,50', status: 'Separando' },
{ id: '#10480', cliente: 'Patrícia Gomes', valor: 'R$ 512,00', status: 'Enviado' },
{ id: '#10479', cliente: 'Roberto Nunes', valor: 'R$ 139,90', status: 'Aguardando Pix' },
];
export default function AppShellDemo() {
return (
// Na sua aplicação, remova a altura fixa: o SidebarProvider ocupa a tela toda (min-h-svh).
<SidebarProvider className="h-[640px] min-h-0 overflow-hidden rounded-xl">
<Sidebar collapsible="icon" className="h-full">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" href="#">
<span className="flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<Store className="size-4" />
</span>
<span className="grid flex-1 text-left leading-tight">
<span className="truncate font-heading font-semibold">Empório Bom Grão</span>
<span className="truncate text-xs text-sidebar-foreground/70">Loja online</span>
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Loja</SidebarGroupLabel>
<SidebarMenu>
{menu.map((item) => (
<SidebarMenuItem key={item.label}>
<SidebarMenuButton href="#" icon={item.icon} isActive={item.active}>
{item.label}
</SidebarMenuButton>
{item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Sistema</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton href="#" icon={<Settings />}>
Configurações
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton href="#" icon={<LifeBuoy />}>
Suporte
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg">
<Avatar className="size-8 rounded-lg">
<AvatarFallback className="rounded-lg">CA</AvatarFallback>
</Avatar>
<span className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">Camila Azevedo</span>
<span className="truncate text-xs text-sidebar-foreground/70">Administradora</span>
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
<SidebarInset className="overflow-y-auto">
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 border-b bg-background/95 px-4 backdrop-blur">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4 self-center" />
<Breadcrumbs>
<Breadcrumb href="#" className="hidden sm:inline-flex">
Empório Bom Grão
</Breadcrumb>
<Breadcrumb>Painel</Breadcrumb>
</Breadcrumbs>
<div className="ml-auto flex items-center gap-2">
<Button variant="ghost" size="icon-sm" aria-label="Notificações">
<Bell />
</Button>
<Button size="sm">
<Plus /> Novo pedido
</Button>
</div>
</header>
<div className="flex flex-1 flex-col gap-6 p-4 md:p-6">
<div>
<h1 className="font-heading text-2xl font-semibold tracking-tight">Bom dia, Camila</h1>
<p className="text-sm text-muted-foreground">Resumo da loja nesta terça-feira.</p>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{indicadores.map((i) => (
<Card key={i.titulo} className="gap-2">
<CardHeader>
<CardDescription>{i.titulo}</CardDescription>
<CardTitle className="font-mono text-2xl tabular-nums">{i.valor}</CardTitle>
</CardHeader>
<CardContent className="text-xs text-muted-foreground">{i.detalhe}</CardContent>
</Card>
))}
</div>
<div className="grid gap-4 xl:grid-cols-3">
<Card className="xl:col-span-2">
<CardHeader>
<CardTitle>Pedidos recentes</CardTitle>
<CardDescription>Últimos pedidos recebidos pela loja online.</CardDescription>
<CardAction>
<Link href="#">Ver todos</Link>
</CardAction>
</CardHeader>
<CardContent>
<ul className="divide-y text-sm">
{pedidos.map((p) => (
<li key={p.id} className="flex items-center gap-3 py-3">
<span className="font-mono text-muted-foreground tabular-nums">{p.id}</span>
<span className="flex-1 truncate font-medium">{p.cliente}</span>
<span className="hidden text-muted-foreground sm:inline">{p.status}</span>
<span className="font-mono tabular-nums">{p.valor}</span>
</li>
))}
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Estoque baixo</CardTitle>
<CardDescription>Produtos com menos de 5 unidades.</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-3 text-sm">
{[
['Café especial 250g', '3 un.'],
['Granola artesanal 500g', '2 un.'],
['Mel orgânico 300g', '4 un.'],
].map(([produto, qtd]) => (
<li key={produto} className="flex items-center justify-between">
<span>{produto}</span>
<span className="font-mono text-muted-foreground tabular-nums">{qtd}</span>
</li>
))}
</ul>
</CardContent>
<CardFooter className="mt-auto">
<Button variant="outline" className="w-full">
Repor estoque
</Button>
</CardFooter>
</Card>
</div>
</div>
</SidebarInset>
</SidebarProvider>
);
}Instalação
O App Shell é uma composição de componentes do registry — não há um arquivo próprio. Instale as peças e copie o código do exemplo.
Instale os componentes usados:
npx shadcn@latest add @lenstech/sidebar @lenstech/breadcrumbs @lenstech/card @lenstech/separator @lenstech/avatar @lenstech/linkCopie o código da aba Código da prévia acima para o layout do seu sistema (ex.: app/(painel)/layout.tsx).
Uso
Coloque o shell no layout das páginas internas, para que o menu não seja recriado a cada navegação:
// app/(painel)/layout.tsx
export default function PainelLayout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<AppSidebar /> {/* Sidebar com SidebarHeader, SidebarContent e SidebarFooter */}
<SidebarInset>
<header className="sticky top-0 z-10 flex h-14 items-center gap-2 border-b bg-background/95 px-4 backdrop-blur">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4 self-center" />
<Breadcrumbs>
<Breadcrumb href="/painel">Empório Bom Grão</Breadcrumb>
<Breadcrumb>Pedidos</Breadcrumb>
</Breadcrumbs>
</header>
<div className="flex flex-1 flex-col gap-6 p-4 md:p-6">{children}</div>
</SidebarInset>
</SidebarProvider>
)
}Remova a altura fixa
Na prévia o SidebarProvider usa className="h-[640px] min-h-0 overflow-hidden" e a Sidebar
className="h-full" só para caber na página. No seu sistema, remova essas classes: o provider ocupa
a tela toda e o menu fica fixo enquanto o conteúdo rola.
Exemplos
Menu offcanvas
Para telas com muito conteúdo (fichas, tabelas largas), comece com o menu escondido: defaultOpen={false} + collapsible="offcanvas".
'use client';
import { Briefcase, FileText, LayoutDashboard, Users } from 'lucide-react';
import { Breadcrumb, Breadcrumbs } from '@/components/ui/breadcrumbs';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupLabel,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from '@/components/ui/sidebar';
const menu = [
{ label: 'Painel', icon: <LayoutDashboard /> },
{ label: 'Clientes', icon: <Users />, active: true },
{ label: 'Processos', icon: <Briefcase /> },
{ label: 'Documentos', icon: <FileText /> },
];
export default function AppShellOffcanvas() {
return (
<SidebarProvider defaultOpen={false} className="h-[480px] min-h-0 overflow-hidden rounded-xl">
<Sidebar collapsible="offcanvas" className="h-full">
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Andrade & Lima Advocacia</SidebarGroupLabel>
<SidebarMenu>
{menu.map((item) => (
<SidebarMenuItem key={item.label}>
<SidebarMenuButton href="#" icon={item.icon} isActive={item.active}>
{item.label}
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset className="overflow-y-auto">
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4 self-center" />
<Breadcrumbs>
<Breadcrumb href="#">Clientes</Breadcrumb>
<Breadcrumb>Padaria Pão Dourado Ltda.</Breadcrumb>
</Breadcrumbs>
</header>
<div className="grid gap-4 p-4 md:grid-cols-2 md:p-6">
<Card>
<CardHeader>
<CardTitle>Dados do cliente</CardTitle>
<CardDescription>CNPJ 98.765.432/0001-10</CardDescription>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Responsável: Sérgio Almeida · (19) 3251-7788
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Processos ativos</CardTitle>
<CardDescription>2 em andamento</CardDescription>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Próxima audiência em 21/10, às 14h.
</CardContent>
</Card>
</div>
</SidebarInset>
</SidebarProvider>
);
}Quando usar
- Use como ponto de partida de sistemas para clientes: gestão de loja, agenda de clínica, área do cliente de escritórios.
- Não use em sites institucionais — use
Site HeadereSite Footer. - Troque os itens de menu, os indicadores e os cards pelos do seu sistema; a estrutura (provider → sidebar → inset → header → conteúdo) deve ser mantida.
API
O App Shell não tem props próprias. As peças principais:
Prop
Type