7.8 KiB
Componentes de Layout Padrão
Este diretório contém componentes reutilizáveis para manter um design system consistente em todas as páginas de listagem do dashboard.
Componentes Disponíveis
1. PageHeader
Header padrão com título, descrição e botão de ação.
import PageHeader from '@/components/layout/PageHeader';
<PageHeader
title="Agências"
description="Gerencie seus parceiros e acompanhe o desempenho."
actionLabel="Nova Agência"
onAction={() => setModalOpen(true)}
/>
2. SearchBar
Barra de busca padrão com ícone de lupa.
import SearchBar from '@/components/layout/SearchBar';
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
placeholder="Buscar por nome, email..."
/>
3. StatusFilter
Dropdown de filtro de status com Headless UI.
import StatusFilter from '@/components/layout/StatusFilter';
const STATUS_OPTIONS = [
{ id: 'all', name: 'Todos os Status' },
{ id: 'active', name: 'Ativos' },
{ id: 'inactive', name: 'Inativos' },
];
<StatusFilter
options={STATUS_OPTIONS}
selected={selectedStatus}
onChange={setSelectedStatus}
/>
4. EmptyState
Estado vazio padrão com ícone, título e descrição.
import EmptyState from '@/components/layout/EmptyState';
import { BuildingOfficeIcon } from '@heroicons/react/24/outline';
<EmptyState
icon={<BuildingOfficeIcon className="w-8 h-8 text-zinc-400" />}
title="Nenhuma agência encontrada"
description="Não encontramos resultados para os filtros selecionados."
actionLabel="Limpar todos os filtros"
onAction={clearFilters}
/>
5. LoadingState
Estado de carregamento com spinner.
import LoadingState from '@/components/layout/LoadingState';
{loading && <LoadingState />}
6. StatusBadge
Badge de status ativo/inativo com toggle opcional.
import StatusBadge from '@/components/layout/StatusBadge';
<StatusBadge
active={item.is_active}
onClick={() => toggleStatus(item.id, item.is_active)}
activeLabel="Ativo"
inactiveLabel="Inativo"
/>
7. Pagination ⭐ NEW
Paginação funcional com navegação e indicador de páginas.
import Pagination from '@/components/layout/Pagination';
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const totalItems = filteredItems.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const paginatedItems = filteredItems.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
/>
8. ConfirmDialog ⭐ NEW
Modal de confirmação profissional (substitui confirm()).
import { useState } from 'react';
import ConfirmDialog from '@/components/layout/ConfirmDialog';
const [confirmOpen, setConfirmOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState<string | null>(null);
const handleDeleteClick = (id: string) => {
setItemToDelete(id);
setConfirmOpen(true);
};
const handleConfirmDelete = () => {
if (itemToDelete) {
// Executar exclusão
deleteItem(itemToDelete);
}
};
<ConfirmDialog
isOpen={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirm={handleConfirmDelete}
title="Excluir Item"
message="Tem certeza que deseja excluir este item? Esta ação não pode ser desfeita."
confirmText="Excluir"
cancelText="Cancelar"
variant="danger"
/>
9. ToastContext & useToast ⭐ NEW
Sistema de notificações toast (substitui alert()).
Setup no layout:
import { ToastProvider } from '@/components/layout/ToastContext';
<ToastProvider>
{children}
</ToastProvider>
Uso nas páginas:
import { useToast } from '@/components/layout/ToastContext';
const toast = useToast();
// Sucesso
toast.success('Item criado!', 'O item foi criado com sucesso.');
// Erro
toast.error('Erro ao excluir', 'Não foi possível excluir o item.');
// Info
toast.info('Informação', 'Ação concluída.');
Exemplo de Uso Completo
"use client";
import { useState, useEffect } from 'react';
import PageHeader from '@/components/layout/PageHeader';
import SearchBar from '@/components/layout/SearchBar';
import StatusFilter from '@/components/layout/StatusFilter';
import LoadingState from '@/components/layout/LoadingState';
import EmptyState from '@/components/layout/EmptyState';
import StatusBadge from '@/components/layout/StatusBadge';
import TableFooter from '@/components/layout/TableFooter';
import { BuildingOfficeIcon } from '@heroicons/react/24/outline';
const STATUS_OPTIONS = [
{ id: 'all', name: 'Todos os Status' },
{ id: 'active', name: 'Ativos' },
{ id: 'inactive', name: 'Inativos' },
];
export default function MyListPage() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [selectedStatus, setSelectedStatus] = useState(STATUS_OPTIONS[0]);
const filteredItems = items.filter(item => {
const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus =
selectedStatus.id === 'all' ? true :
selectedStatus.id === 'active' ? item.is_active :
!item.is_active;
return matchesSearch && matchesStatus;
});
return (
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
<PageHeader
title="Minha Lista"
description="Descrição da página"
actionLabel="Novo Item"
onAction={() => {}}
/>
<div className="flex flex-col lg:flex-row gap-4 items-center justify-between">
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
placeholder="Buscar..."
/>
<StatusFilter
options={STATUS_OPTIONS}
selected={selectedStatus}
onChange={setSelectedStatus}
/>
</div>
{loading ? (
<LoadingState />
) : filteredItems.length === 0 ? (
<EmptyState
icon={<BuildingOfficeIcon className="w-8 h-8 text-zinc-400" />}
title="Nenhum item encontrado"
description="Tente ajustar os filtros."
/>
) : (
<div className="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 overflow-hidden">
<table className="w-full">
{/* Sua tabela aqui */}
</table>
<TableFooter count={filteredItems.length} />
</div>
)}
</div>
);
}
Design System
Cores
--gradient: linear-gradient(135deg, #ff3a05, #ff0080)
--brand-color: #ff0080
Classes Tailwind Padrão
Container principal:
p-6 max-w-[1600px] mx-auto space-y-6
Tabela:
bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800
Header da tabela:
bg-zinc-50/50 dark:bg-zinc-800/50 border-b border-zinc-200 dark:border-zinc-800
Linha hover:
hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors
Benefícios
✅ Consistência visual - Todas as páginas seguem o mesmo padrão ✅ Manutenção fácil - Altere um componente, atualiza em todas as páginas ✅ Desenvolvimento rápido - Reutilize componentes prontos ✅ Design system - Cores e estilos centralizados ✅ Acessibilidade - Componentes já otimizados