Solutio Design System — referência de componentes
Este app valida que apps/docs resolve @solutio/design-system via workspace do Yarn, sem publish, e serve como referência anotada de cada componente (nome, código, entrada/saída, mock) — ver openspec/changes/add-docs-component-reference. Com migrate-datatable, os 31 componentes vendorizados + o hook useDialogA11y estão migrados (roadmap de 9 changes, completo).
Esta página (/) é a referência completa, cada componente com código/entrada/saída/mock. /guia organiza os mesmos componentes em seções numeradas (3.1, 3.2…), pensado para navegar sequencialmente pelo catálogo em vez de procurar um componente específico.
Exemplo de aplicação real (réplica do mockup)
Réplica funcional, com os componentes reais deste pacote, da tela "Administração do Ambiente" de SolutioDesignSystemSAPFiori/SolutioDesignSystem.dc.html — o protótipo HTML usa um motor de template próprio (support.js) incompatível com React/Next.js, então a réplica é feita compondo NavBar, Banner e Tile reais, não copiando o HTML. Esta seção é uma tela composta, não um catálogo — por isso não é anotada instância a instância como as seções abaixo (ver "Non-Goals" de add-docs-component-reference).
Esta aplicação demonstra como o contexto visual irá funcionar. Todos os itens exibidos são ilustrativos e esta exibição não contempla as configurações de Identity (usuários, roles, permissões). Esta é a visão do administrador da plataforma, não a visão do cliente. A finalidade é apoiar o mapeamento das alterações de layout e levantar as discussões de arquitetura e infraestrutura.
Administração do Ambiente
Dados Mestres
Icon
<Icon name="chevron-left" /><Icon name="check-circle" /><Icon name="value-help" /><Icon name="apps" />Button
<Button variant="emphasized">Emphasized</Button><Button variant="default">Default</Button><Button variant="transparent">Transparent</Button><Button variant="destructive">Destructive</Button><Button disabled>Disabled</Button><Button variant="emphasized" loading>
Salvando…
</Button>Formulário
<FormField label="Nome" defaultValue="" placeholder="Digite para filtrar" /><FormField label="Código (imutável)" value="ABC-123" immutable /><TextArea label="Observações" defaultValue="" /><Select
label="Status"
options={[
{ value: "active", label: "Ativo" },
{ value: "draft", label: "Rascunho" },
]}
defaultValue="active"
/><Switch label="Sincronização automática" checked={checked} onChange={setChecked} /><StatusRadioGroup
value={status}
onChange={setStatus}
hint="Registros inativos não aparecem em buscas"
/><DatePicker label="Data de início" defaultValue="" /><ValueHelpInput
label="Cliente"
selected={selected}
rowKey={(row) => row.id}
renderChip={(row) => row.nome}
onRemove={(row) => setSelected((sel) => sel.filter((s) => s.id !== row.id))}
placeholder="Selecione um cliente"
onOpen={async () => {
// padrão recomendado: buscar por fora, o componente só recebe rows prontos
const rows = await fetchClientesDoEndpoint();
setRows(rows);
setDialogOpen(true);
}}
/>
<ValueHelpDialog
open={dialogOpen}
title="Selecionar cliente"
columns={[{ key: "nome", label: "Nome" }]}
rows={rows}
rowKey={(row) => row.id}
totalCount={rows.length}
selected={selected}
onToggleRow={(row) =>
setSelected((sel) =>
sel.some((s) => s.id === row.id) ? sel.filter((s) => s.id !== row.id) : [...sel, row],
)
}
onClearSelection={() => setSelected([])}
searchValue={search}
onSearchChange={setSearch}
onSearch={() => fetchClientesDoEndpoint(search).then(setRows)}
onOk={() => setDialogOpen(false)}
onCancel={() => setDialogOpen(false)}
/>
// "Relatório" — mesmo modo chips do "Cliente", tabela de 3 colunas, filterFields
// com um filtro type: "value-help" (País/região abre um 2º ValueHelpDialog aninhado)
<ValueHelpInput
label="Relatório"
selected={reportSelected}
rowKey={(row) => row.id}
renderChip={(row) => row.nome}
onRemove={(row) => setReportSelected((sel) => sel.filter((s) => s.id !== row.id))}
onOpen={() => setReportDialogOpen(true)}
/>
<ValueHelpDialog
open={reportDialogOpen}
columns={[{ key: "id", label: "ID" }, { key: "nome", label: "Nome" }, { key: "autoridade", label: "Autoridade" }]}
rows={filteredReportRows}
selected={reportSelected}
onToggleRow={(row) =>
setReportSelected((sel) =>
sel.some((s) => s.id === row.id) ? sel.filter((s) => s.id !== row.id) : [...sel, row],
)
}
filterFields={[
{ type: "text", key: "autoridade", label: "Autoridade", value: reportAutoridade, onChange: setReportAutoridade },
{
type: "value-help",
key: "pais",
label: "País/região",
selected: reportCountrySelected,
rowKey: (row) => row.id,
renderChip: (row) => row.nome,
onOpen: () => setReportCountryDialogOpen(true),
onRemove: (row) => setReportCountrySelected((sel) => sel.filter((s) => s.id !== row.id)),
},
]}
onOk={() => setReportDialogOpen(false)}
onCancel={() => setReportDialogOpen(false)}
/>
{/* ValueHelpDialog aninhado — aberto pelo filtro "value-help" acima */}
<ValueHelpDialog
open={reportCountryDialogOpen}
columns={[{ key: "nome", label: "Nome" }]}
rows={COUNTRY_ROWS}
selected={reportCountrySelected}
onOk={() => setReportCountryDialogOpen(false)}
onCancel={() => setReportCountryDialogOpen(false)}
/><ValueHelpInput
label="Fornecedor"
selected={selected}
valueColumn="codigo"
renderChip={(row) => `${row.razaoSocial} - ${row.cidade}`}
onRemove={() => setSelected([])}
onOpen={() => setDialogOpen(true)}
selectionMode="single"
rows={rows}
query={query}
onQueryChange={setQuery}
onSelectRow={(row) => {
setSelected([row]);
setQuery('');
}}
/>
<ValueHelpDialog
open={dialogOpen}
title="Selecionar fornecedor"
columns={[
{ key: "codigo", label: "Código" },
{ key: "razaoSocial", label: "Razão social" },
{ key: "cidade", label: "Cidade" },
]}
rows={rows}
valueColumn="codigo"
totalCount={rows.length}
selected={selected}
selectionMode="single"
onSelectRow={(row) => setSelected([row])}
onClearSelection={() => setSelected([])}
searchValue={search}
onSearchChange={setSearch}
onSearch={() => {}}
onOk={() => setDialogOpen(false)}
onCancel={() => setDialogOpen(false)}
/>{chips.map((chip) => (
<Chip key={chip} label={chip} onRemove={() => removerChip(chip)} />
))}
<Chip label="neutral" />
<Chip label="info" variant="info" />
<Chip label="success" variant="success" />
<Chip label="warning" variant="warning" />
<Chip label="error" variant="error" /><MultiSelectDropdown
searchValue={search}
onSearchChange={setSearch}
options={options}
open={open}
onToggleOpen={() => setOpen((o) => !o)}
/><SearchSelectInput
options={options}
query={query}
onQueryChange={setQuery}
selected={selected}
onSelect={setSelected}
onAdd={() => setMembros((m) => [...m, selected])}
/>Layout / navegação
<ObjectPageTabs
tabs={[{ key: "geral", label: "Geral" }, { key: "membros", label: "Membros" }]}
active={activeTab}
onChange={setActiveTab}
/><NavBar
groups={navGroups}
overflowOpen={overflowOpen}
onToggleOverflow={() => setOverflowOpen((o) => !o)}
/>Conteúdo de exemplo — role este container pra ver o item ativo mudar, e o scroll horizontal do NavBar acompanhar sozinho.
Conteúdo de exemplo — role este container pra ver o item ativo mudar, e o scroll horizontal do NavBar acompanhar sozinho.
Conteúdo de exemplo — role este container pra ver o item ativo mudar, e o scroll horizontal do NavBar acompanhar sozinho.
Conteúdo de exemplo — role este container pra ver o item ativo mudar, e o scroll horizontal do NavBar acompanhar sozinho.
Conteúdo de exemplo — role este container pra ver o item ativo mudar, e o scroll horizontal do NavBar acompanhar sozinho.
Conteúdo de exemplo — role este container pra ver o item ativo mudar, e o scroll horizontal do NavBar acompanhar sozinho.
Conteúdo de exemplo — role este container pra ver o item ativo mudar, e o scroll horizontal do NavBar acompanhar sozinho.
Conteúdo de exemplo — role este container pra ver o item ativo mudar, e o scroll horizontal do NavBar acompanhar sozinho.
// onSelect rola até a seção — não troca painel, NavBar não sabe de "seções"
<NavBar
variant="surface"
groups={sections.map((s) => ({ id: s.id, nome: s.nome, active: s.id === activeId, onSelect: () => scrollToSection(s.id) }))}
overflowOpen={overflowOpen}
onToggleOverflow={() => setOverflowOpen((o) => !o)}
/>
// activeId vem de um IntersectionObserver nas seções da página, não do cliqueClique no avatar ou no título "TC Cloud" no ShellBar, no topo da página, para ver os dois popovers.
{avatarMenuOpen && (
<AvatarMenu userName="Maria Silva" items={items} onClose={() => setAvatarMenuOpen(false)} />
)}
{appsMenuOpen && (
<AppsMenuPopover groups={groups} onClose={() => setAppsMenuOpen(false)} />
)}<CollapsibleGroup title="Permissões" count={2}>
<Chip label="fiscal:read" />
<Chip label="fiscal:write" />
</CollapsibleGroup><FilterBar onApply={aplicarFiltros}>
<FormField label="Nome" value={nome} onChange={(e) => setNome(e.target.value)} />
<Select
label="Status"
options={[{ value: "active", label: "Ativo" }, { value: "draft", label: "Rascunho" }]}
value={status}
onChange={(e) => setStatus(e.target.value)}
/>
</FilterBar><ShellBar
title="TC Cloud"
avatarInitials="MG"
titleMenuOpen={appsMenuOpen}
onTitleClick={() => setAppsMenuOpen((v) => !v)}
avatarMenuOpen={avatarMenuOpen}
onAvatarClick={() => setAvatarMenuOpen((v) => !v)}
/><ObjectPageHeader
kicker="Projeto"
title="Projeto Alpha"
status={{ variant: "success", label: "Ativo" }}
onSave={salvar}
onCancel={cancelar}
/>Dados
<StatusIndicator variant="success" label="Ativo" />
<StatusIndicator variant="error" label="Inativo" />
<StatusIndicator variant="warning" label="Em andamento" />
<StatusIndicator variant="info" label="Informação" />3 projetos ativos
<Card title="Projetos" subtitle="Workspace atual" action={<Button variant="transparent">Ver todos</Button>}>
<p>3 projetos ativos</p>
</Card><Tile title="Sem link" subtitle="cursor: help" titleHint="Não clicável" /><Tile title="Com next/link" subtitle="Navegação client-side" href="/" linkComponent={Link} /><Tile title="Fase futura" subtitle="Ainda não disponível" futurePhase />Feedback
<BusyIndicator size="small" />
<BusyIndicator size="medium" />
<BusyIndicator size="big" /><Banner variant="warning" onClose={fechar}>Este registro está pendente de aprovação.</Banner><EmptyState
title="Nenhum projeto encontrado"
subtitle="Crie o primeiro projeto do workspace para começar."
actionLabel="Criar projeto"
onAction={criarProjeto}
/>const { showToast } = useToast();
showToast({ message: "Salvo com sucesso!", variant: "success" });<ConfirmDialog
open={confirmOpen}
title="Confirmar exclusão"
message="Esta ação não pode ser desfeita. Deseja continuar?"
onConfirm={excluir}
onCancel={() => setConfirmOpen(false)}
/>DataTable
busy · onCreate + createLabel · onRefresh · onNavigate · onSelect
Selecionados: nenhum
navigateIcon/navigateIconSize — a coluna de navegação continua sempre a última e com largura fixa; só o ícone é configurável (default chevron-right):
onNavigate · navigateIcon="apps"
pagination modo cliente — rows já está inteiro em memória (34 itens); DataTable fatia sozinho por pageSize (default 10) e agrega mais a cada clique no rodapé, sem chamar nenhum endpoint:
pagination · pageSize (default 10, sem onLoadMore = modo cliente)
pagination modo servidor — presença de onLoadMore muda o modo: rows é só a página já carregada, e clicar no rodapé só chama onLoadMore (aqui simulado com setTimeout); DataTable nunca fatia ou acumula nada sozinho. loadingMore exibe um BusyIndicator real no rodapé, não um spinner reimplementado:
pagination · onLoadMore (presença = modo servidor) · hasMore · loadingMore · totalCount
onSortChange/onGroupChange tornam o cabeçalho clicável, abrindo o DataTableFeatureDialog ancorado na coluna. DataTable nunca reordena/reagrupa rows sozinho — quem aplica a ordenação e o agrupamento escolhidos é este mesmo componente de página (o "consumidor"), reordenando o array de 34 itens (3 status distintos) antes de repassá-lo. Agrupar não é ordenar: com groupBy, o próprio DataTable insere uma linha divisória (cor leve, label "Coluna: valor") antes da primeira linha de cada grupo — desde que as linhas já cheguem clusterizadas (o consumidor ainda decide a ordem, `DataTable` só detecta a mudança de valor entre linhas consecutivas):
onSortChange + sortBy/sortDirection · onGroupChange + groupBy
Sem agrupamento
Correção: a coluna de navegação continua sempre encostada na borda direita mesmo quando todas as colunas de columns têm width explícito (antes, o espaço sobrando do table-layout: fixed era redistribuído também para a coluna de navegação, afastando o ícone da borda):
columns[].width em todas as colunas · onNavigate
<DataTable
title="Projetos"
columns={[{ key: "nome", label: "Nome" }, { key: "status", label: "Status" }]}
rows={projects}
rowKey={(row) => row.id}
busy={isPending}
onCreate={criarProjeto}
createLabel="Criar Projeto"
onRefresh={atualizar}
onNavigate={(row) => router.push(`/projects/${row.id}`)}
onSelect={(row) => alternarSelecao(row.id)}
/>DetailPanel
Campo sem label — ocupa a linha inteira, não confinado à largura de uma coluna:
<DetailPanel
title="Apuração CBS"
columns={[
[
{ label: "Empresa", value: "1000" },
{ label: "Exercício", value: "2025" },
{ label: "Período", value: "1" },
],
[
{ label: "Data Limite Conclusão", value: "02.02.2025" },
{ label: "Data de Recolhimento", value: "10.02.2025" },
],
[{ label: "Status", value: <StatusIndicator variant="success" label="Concluído" /> }],
]}
/>
// campo sem label ocupa a linha inteira, não confinado à largura de uma coluna
<DetailPanel
title="Apuração CBS"
columns={[
[{ label: "Empresa", value: "1000" }],
[{ value: "Observação: todos os documentos fiscais deste período já foram conciliados." }],
]}
/>DetailPanelNav
Conteúdo de exemplo #1 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #2 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #3 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #4 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #5 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #6 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #7 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #8 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #9 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #10 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #11 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #12 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #13 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
Conteúdo de exemplo #14 — role a página pra ver o painel (ou, com "Fixar painel", o painel inteiro) congelar logo abaixo do ShellBar + NavBar de âncora.
<DetailPanelNav
title="Apuração CBS"
columns={[...]}
groups={[
{ id: "debitos", nome: "Débitos", active: tab === "debitos", onSelect: () => setTab("debitos") },
{ id: "creditos", nome: "Créditos", active: tab === "creditos", onSelect: () => setTab("creditos") },
{ id: "ajustes", nome: "Ajustes", active: tab === "ajustes", onSelect: () => setTab("ajustes") },
{ id: "apuracao", nome: "Apuração", active: tab === "apuracao", onSelect: () => setTab("apuracao") },
]}
overflowOpen={false}
onToggleOverflow={() => {}}
stickyOffset="calc(var(--sol-shell-h) * 2)"
fullBleed
/>Checkbox
Checkbox controlado com rótulo e descrição opcional. Um por linha (ex.: uma permissão por linha). Para um liga/desliga de configuração use Switch. Receita completa: docs/recipes/checkbox-e-layout-duas-colunas.md.
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| label | ReactNode | — | Rótulo principal. Clicar nele (ou na descrição) alterna o checkbox. |
| description | ReactNode | — | Texto secundário; ligado ao input por aria-describedby; quebra linha em qualquer ponto. |
| checked | boolean | — | Valor atual (controlado). Ignorado com locked: sempre aparece marcado. |
| onChange | (checked: boolean) => void | — | Nunca chamado com disabled ou locked. |
| disabled | boolean | false | Esmaecido, cursor not-allowed, não alterna, sem selo. |
| locked | boolean | false | Marcado e não editável, com selo dentro do <label> (anunciado por leitores de tela). |
| lockedLabel | string | 'obrigatória' | Texto do selo quando locked. |
<Checkbox label="Visualizar empresas" description="company.read" checked={v} onChange={setV} />
<Checkbox label="Editar empresas" description="company.write" checked disabled onChange={() => {}} />
<Checkbox label="Visualizar empresas" description="company.read" checked locked lockedLabel="mínima" onChange={() => {}} />
<Checkbox label="Acesso base" checked locked onChange={() => {}} /><div style={{ width: "15rem" }}>
<Checkbox label="Revogar o acesso permanente de empresas parceiras" description="company.access.revoke.permanently" ... />
</div>TwoColumnLayout
Formulário (main) à esquerda e painel lateral (aside) à direita. Em telas largas (≥ 900px) o painel fica fixo e rola por dentro quando é mais alto que a janela; abaixo de 900px vira coluna única. É só CSS — o breakpoint fica dentro do componente. Não é o DetailPanelNav (navegação por seções de uma página de objeto): é outro tipo de painel.
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| main | ReactNode | — | Coluna principal (formulário). Primeiro no DOM. |
| aside | ReactNode | — | Painel lateral; fixo (sticky) com rolagem interna em telas largas; abaixo de main em telas estreitas. |
| mainMaxWidth | string | number | '40rem' | Largura máxima de main (≥ 900px). Número = px. |
| stickyOffset | string | number | '7.5rem' | Distância do topo onde o aside fica fixo (altura dos cabeçalhos fixos acima). Número = px. |
| bottomOffset | string | number | '5.5rem' | Espaço do rodapé fixo, descontado da altura máxima do aside. Número = px. |
Atenção: position: sticky não funciona se algum ancestral tiver overflow diferente de visible. Os defaults (7.5rem/5.5rem) são os do cabeçalho de objeto + ShellBar e do rodapé fixo da tela de aplicativos do TCCloud — ajuste à sua página.
1 permissão(ões) selecionada(s).
<TwoColumnLayout
main={<form>{/* campos */}</form>}
aside={<Card>{/* CollapsibleGroup + Checkbox por permissão */}</Card>}
stickyOffset="4rem"
bottomOffset="1rem"
/>Lacunas da tela "Parâmetros de DF-e"
Componentes e props adicionados para a tela de parâmetros de DF-e do TCCloud (DS-GAP-1 a 7). Tudo é opcional e aditivo. Receitas: docs/recipes/dialog-erro-de-campo-e-breadcrumb.md e docs/recipes/datatable-report-screen.md (itens 17 a 19).
Dialog
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| open | boolean | — | false não renderiza nada. |
| title | ReactNode | — | Título e nome acessível (aria-labelledby com id único por instância). |
| children | ReactNode | — | Conteúdo livre; rola por dentro quando é alto. |
| footer | ReactNode | — | Ações fixas no rodapé. |
| onClose | () => void | — | Esc, clique no fundo e botão Fechar (exceto com busy). |
| width | number | string | 520 | Número = px. |
| closeOnBackdrop | boolean | true | false ignora o clique no fundo; Esc continua fechando. |
| busy | boolean | false | Bloqueia Esc, fundo e botão Fechar. |
| role | 'dialog' | 'alertdialog' | 'dialog' | alertdialog para exigir resposta. |
<Dialog open={aberto} title="Histórico de alterações" width={560} busy={busy} onClose={() => setAberto(false)}
footer={<Button onClick={() => setAberto(false)}>Fechar</Button>}>
conteúdo livre
</Dialog>ConfirmDialog — conteúdo e estado
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| children | ReactNode | — | Entre a mensagem e os botões (ex.: campo de motivo). |
| message | ReactNode | — | Antes só string. |
| confirmDisabled | boolean | false | Desabilita o botão de confirmar. |
| confirmLoading | boolean | false | Spinner no confirmar, botões desabilitados; Esc e fundo não fecham. |
| width | number | string | 420 | Número = px. |
<ConfirmDialog open title="Editar campos" message="Informe o motivo." confirmLabel="Salvar"
confirmDisabled={motivo.trim() === ''} confirmLoading={salvando} width={520} ...>
<TextArea label="Motivo" maxLength={500} showCount value={motivo} onChange={...} />
</ConfirmDialog>Switch e Checkbox — nome acessível
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| hideLabel | boolean | false | Esconde o rótulo só visualmente (continua para leitores de tela). |
| aria-label | string | — | Nome acessível explícito, repassado ao input. |
| aria-labelledby | string | — | Id de elemento que nomeia o controle. |
| Switch.label | ReactNode | — | Antes só string. |
| Checkbox.indeterminate | boolean | false | Estado misto (aria-checked="mixed"). |
<Switch label="Editável" hideLabel checked={v} onChange={setV} />
<Checkbox label="Selecionar C001" hideLabel checked={c} onChange={setC} />Erro de campo
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| error | ReactNode | — | FormField, TextArea e Select. Borda --sol-error, mensagem, aria-invalid e aria-describedby. |
| showCount | boolean | false | TextArea: contador n/maxLength (precisa de maxLength). |
<FormField label="Código" value={codigo} onChange={...} error={codigo ? undefined : 'Informe o código'} />
<TextArea label="Motivo" maxLength={500} showCount error={...} />
<Select label="Modelo" options={...} error={...} />DetailPanel — wrap
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| wrap | boolean | false | Painel: permite quebra de linha em todos os campos com rótulo. |
| DetailPanelField.wrap | boolean | — | Campo: vale só para ele e vence o painel. |
<DetailPanel title="Campo" columns={[[{ label: "Motivo", value: texto, wrap: true }]]} />Breadcrumb
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| items | BreadcrumbItem[] | — | { label, href?, onClick?(event) }. O último é a página atual (aria-current="page", sem link). |
| ariaLabel | string | 'Você está em' | Nome acessível do <nav>. |
<Breadcrumb items={[
{ label: 'Início', href: '/' },
{ label: 'Fiscal', onClick: () => router.push('/fiscal') },
{ label: 'Parâmetros de DF-e' },
]} />DataTable — seleção de linhas, faixa, overflow e tooltip
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| selectedKeys | string[] | Set<string> | — | Seleção controlada (chave = rowKey). Liga a coluna de seleção. |
| defaultSelectedKeys | string[] | — | Seleção inicial do modo não controlado. |
| onSelectedKeysChange | (keys: string[]) => void | — | Lista completa a cada mudança. |
| isRowSelectable | (row) => boolean | todas | false: checkbox desabilitado e fora do "selecionar todas". |
| selectAllScope | 'page' | 'all' | 'page' | 'all' = linhas que passam o filtro por coluna (servidor: as carregadas). |
| selectionActions | ReactNode | DataTableToolbarAction[] | — | Ações da faixa "N selecionadas" (no lugar de actions). |
| DataTableToolbarAction.overflow | boolean | false | Vai para o menu "…". |
| DataTableToolbarAction.group | string | — | Separa grupos de itens dentro do menu "…". |
| Column.tooltip | (row) => string | — | Tooltip de célula com render; célula focável por teclado. |
<DataTable title="Campos" columns={columns} rows={rows} rowKey={(r) => r.id}
selectedKeys={sel} onSelectedKeysChange={setSel}
isRowSelectable={(r) => r.id !== 'C003'} selectAllScope="all" filterableColumns
selectionActions={[{ key: 'ok', label: 'Aprovar', onClick }, { key: 'x', label: 'Excluir', onClick, overflow: true }]} />A implementação completa do site (Fumadocs, páginas MDX por componente) é escopo de uma proposta futura.