DataTable
Tabela de dados genérica baseada em TanStack Table com API composicional - sorting, paginação client-side, toolbar e pagination como children.
DataTable é a opinião da Uranus sobre tabelas de dados, construída sobre TanStack Table v8 e o primitivo Table do design system. A API é composicional: você combina DataTable.Provider, DataTable.Root, DataTable.Toolbar e DataTable.Pagination como children, e qualquer célula custom acessa a Table instance via useDataTable<T>().
Esta é a única dependência runtime que o @uranus-workspace/blocks adiciona em relação ao primitivo Table.
Clientes ativos
8 clientes neste workspace.
AC Alice Costaalice@uranus.com.br | Enterprise | ativo | R$ 12.800,00 |
BL Bruno Limabruno@uranus.com.br | Pro | ativo | R$ 870,00 |
CS Camila Souzacamila@uranus.com.br | Free | convidado | R$ 0,00 |
DA Diego Almeidadiego@uranus.com.br | Pro | pausado | R$ 99,00 |
EM Erika Martinserika@uranus.com.br | Enterprise | ativo | R$ 19.900,00 |
'use client';
import type { ColumnDef } from '@tanstack/react-table';
import { DataTable, useDataTable } from '@uranus-workspace/blocks';
import {
Avatar,
AvatarFallback,
Badge,
Card,
CardContent,
CardHeader,
CardTitle,
Input,
} from '@uranus-workspace/design-system';
import { Search } from 'lucide-react';
interface Customer {
id: string;
name: string;
email: string;
plan: 'Free' | 'Pro' | 'Enterprise';
status: 'ativo' | 'convidado' | 'pausado';
amount: number;
}
const customers: Customer[] = [
{
id: '1',
name: 'Alice Costa',
email: 'alice@uranus.com.br',
plan: 'Enterprise',
status: 'ativo',
amount: 12_800,
},
{
id: '2',
name: 'Bruno Lima',
email: 'bruno@uranus.com.br',
plan: 'Pro',
status: 'ativo',
amount: 870,
},
{
id: '3',
name: 'Camila Souza',
email: 'camila@uranus.com.br',
plan: 'Free',
status: 'convidado',
amount: 0,
},
{
id: '4',
name: 'Diego Almeida',
email: 'diego@uranus.com.br',
plan: 'Pro',
status: 'pausado',
amount: 99,
},
{
id: '5',
name: 'Erika Martins',
email: 'erika@uranus.com.br',
plan: 'Enterprise',
status: 'ativo',
amount: 19_900,
},
{
id: '6',
name: 'Felipe Andrade',
email: 'felipe@uranus.com.br',
plan: 'Pro',
status: 'ativo',
amount: 615,
},
{
id: '7',
name: 'Gabriela Tavares',
email: 'gabriela@uranus.com.br',
plan: 'Free',
status: 'convidado',
amount: 0,
},
{
id: '8',
name: 'Henrique Pires',
email: 'henrique@uranus.com.br',
plan: 'Pro',
status: 'ativo',
amount: 740,
},
];
const currency = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
const statusVariant: Record<Customer['status'], 'default' | 'secondary' | 'outline'> = {
ativo: 'default',
convidado: 'secondary',
pausado: 'outline',
};
const initialsOf = (name: string) =>
name
.split(' ')
.map((part) => part[0])
.filter(Boolean)
.slice(0, 2)
.join('')
.toUpperCase();
const columns: ColumnDef<Customer, unknown>[] = [
{
accessorKey: 'name',
header: 'Cliente',
cell: ({ row }) => (
<div className="flex items-center gap-3">
<Avatar className="size-8">
<AvatarFallback className="text-[11px] font-medium">
{initialsOf(row.original.name)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="font-medium text-foreground">{row.original.name}</span>
<span className="text-xs text-muted-foreground">{row.original.email}</span>
</div>
</div>
),
},
{
accessorKey: 'plan',
header: 'Plano',
cell: ({ row }) => <span className="text-sm">{row.original.plan}</span>,
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => (
<Badge variant={statusVariant[row.original.status]} className="capitalize">
{row.original.status}
</Badge>
),
},
{
accessorKey: 'amount',
header: () => <div className="text-right">MRR</div>,
cell: ({ row }) => (
<div className="text-right font-mono text-sm tabular-nums">
{currency.format(row.original.amount)}
</div>
),
},
];
function NameSearch() {
const { table } = useDataTable<Customer>();
const value = (table.getColumn('name')?.getFilterValue() as string) ?? '';
return (
<div className="relative w-full sm:w-72">
<Search
aria-hidden
className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
/>
<Input
type="search"
aria-label="Buscar cliente"
placeholder="Buscar por nome…"
value={value}
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
className="pl-8"
/>
</div>
);
}
export default function DataTableDefault() {
return (
<div className="flex w-full justify-center">
<Card className="w-full max-w-3xl">
<CardHeader className="flex flex-row items-start justify-between gap-3 space-y-0">
<div className="flex flex-col gap-1">
<CardTitle className="text-base">Clientes ativos</CardTitle>
<p className="text-xs text-muted-foreground">
{customers.length} clientes neste workspace.
</p>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<DataTable.Provider data={customers} columns={columns} pageSize={5}>
<DataTable.Toolbar className="justify-start">
<NameSearch />
</DataTable.Toolbar>
<DataTable.Root caption="Clientes ativos no workspace" />
<DataTable.Pagination />
</DataTable.Provider>
</CardContent>
</Card>
</div>
);
}
Uso
import { DataTable } from '@uranus-workspace/blocks';
import type { ColumnDef } from '@tanstack/react-table';
interface Customer {
id: string;
name: string;
email: string;
}
const columns: ColumnDef<Customer>[] = [
{ accessorKey: 'name', header: 'Nome' },
{ accessorKey: 'email', header: 'Email' },
];
<DataTable.Provider data={customers} columns={columns} pageSize={10}>
<DataTable.Root caption="Lista de clientes" />
<DataTable.Pagination />
</DataTable.Provider>Toolbar com acesso à Table
O useDataTable<T>() lê a instância TanStack do contexto, com o T re-aplicado no call-site para tipagem das linhas:
import { DataTable, useDataTable } from '@uranus-workspace/blocks';
import { Input } from '@uranus-workspace/design-system';
function SearchInput() {
const { table } = useDataTable<Customer>();
return (
<Input
placeholder="Buscar por nome…"
value={(table.getColumn('name')?.getFilterValue() as string) ?? ''}
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
className="w-64"
/>
);
}
<DataTable.Provider data={customers} columns={columns}>
<DataTable.Toolbar>
<SearchInput />
</DataTable.Toolbar>
<DataTable.Root caption="Clientes" />
<DataTable.Pagination />
</DataTable.Provider>Subcomponentes
DataTable.Provider— cria ouseReactTablee provê o contexto. Aceitadata,columns,enableSorting?,enableRowSelection?,pageSize?(padrão10,0desabilita paginação client-side).DataTable.Root— renderiza a tabela. Requercaption(a11y), aceitaemptyState?,onRowClick?.DataTable.Toolbar— wrapper composicional comflex gap-2. Children acessam aTableviauseDataTable.DataTable.Pagination— controles default ("Anterior" / "Próxima"). AceitapreviousLabel?enextLabel?para custom copy.useDataTable<T>()— hook para custom toolbar/pagination cells.
Acessibilidade
captioné obrigatório noDataTable.Root— passamos comsr-onlypara que a tabela tenha rótulo para leitores de tela.- Headers com sort renderizam como
<button>dentro do<th>, e o<th>recebearia-sort="ascending|descending|none". onRowClickadicionacursor-pointermas não torna a linha um botão — para ações principais use uma célula com botão real.
Server-side
Para paginação/sorting server-side, passe pageSize={0} para desabilitar a paginação client-side e use useDataTable em uma toolbar custom para refletir suas queries.