Contrato visual — Solutio IT / TCCloud
Réplica de GuiaVisualContrato.html usando os componentes reais de @solutio/design-system — tema claro único (sem modo escuro). Cada bloco "Ver código" mostra o uso real do pacote, não o HTML/TSX do protótipo original (que em alguns pontos já documentados em CLAUDE.md ficou desatualizado em relação à API real — ex. DataTable, SearchSelectInput).
Ver código — uso real (@solutio/design-system)
<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>Ver código — uso real (@solutio/design-system)
<StatusIndicator variant="success" label="Ativo" />
<StatusIndicator variant="error" label="Inativo" />
<StatusIndicator variant="warning" label="Em andamento" />
<StatusIndicator variant="info" label="Informação" />Ver código — uso real (@solutio/design-system)
<BusyIndicator size="small" />
<BusyIndicator size="medium" />
<BusyIndicator size="big" />Ver código — uso real (@solutio/design-system)
<FormField label="Nome" defaultValue="" placeholder="Digite para filtrar" />
<FormField label="Código (imutável)" value="ABC-123" immutable />Ver código — uso real (@solutio/design-system)
<FilterBar onApply={() => {}}>
<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>busy · onCreate + createLabel · onRefresh · onNavigate · onSelect
Selecionados: nenhum
navigateIcon customiza o ícone da coluna (sempre última, sempre largura fixa):
onNavigate · navigateIcon="apps"
Ver código — uso real (@solutio/design-system)
<DataTable
title="Projetos"
columns={[{ key: 'nome', label: 'Nome' }, { key: 'status', label: 'Status' }]}
rows={rows}
rowKey={(row) => row.id}
busy={busy}
onCreate={() => {}}
createLabel="Criar Projeto"
onRefresh={() => {}}
onNavigate={() => {}}
onSelect={(row) => { /* ... */ }}
/>Modo cliente — 34 itens já em memória, DataTable fatia sozinho por pageSize (default 10):
pagination · pageSize (default 10, sem onLoadMore = modo cliente)
Modo servidor — onLoadMore simulado com setTimeout; DataTable nunca fatia/acumula sozinho. loadingMore exibe um BusyIndicator real, não um spinner reimplementado:
pagination · onLoadMore (presença = modo servidor) · hasMore · loadingMore · totalCount
Ver código — uso real (@solutio/design-system)
{/* Modo cliente: rows já está inteiro em memória, DataTable fatia sozinho por pageSize */}
<DataTable title="Projetos" columns={columns} rows={todosOsProjetos} rowKey={(row) => row.id} pagination />
{/* Modo servidor: presença de onLoadMore muda o modo — rows é só a página atual */}
<DataTable
title="Projetos"
columns={columns}
rows={paginaAtual}
rowKey={(row) => row.id}
totalCount={total}
pagination
onLoadMore={buscarProximaPagina}
hasMore={paginaAtual.length < total}
loadingMore={carregando}
/>Clique no cabeçalho de uma coluna para abrir o DataTableFeatureDialog. DataTable nunca reordena/reagrupa rows sozinho — este exemplo (o "consumidor") aplica a ordenação e o agrupamento escolhidos ao 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:
onSortChange + sortBy/sortDirection · onGroupChange + groupBy
Sem agrupamento
Ver código — uso real (@solutio/design-system)
<DataTable
title="Projetos"
columns={columns}
rows={rowsOrdenadas}
rowKey={(row) => row.id}
sortBy={sort?.key}
sortDirection={sort?.direction}
onSortChange={(key, direction) => setSort({ key, direction })}
groupBy={groupBy}
onGroupChange={setGroupBy}
/>columns[].width em todas as colunas · onNavigate
Ver código — uso real (@solutio/design-system)
{/* Antes: com todas as colunas dimensionadas, o ícone de navegação se afastava da borda.
Agora: DataTable injeta uma célula spacer sem width, protegendo a coluna de navegação. */}
<DataTable title="Projetos" columns={[{ key: 'nome', label: 'Nome', width: '200px' }, { key: 'status', label: 'Status', width: '160px' }]} rows={rows} rowKey={(row) => row.id} onNavigate={() => {}} />Ver código — uso real (@solutio/design-system)
<NavBar
groups={[
{ id: 'grupo-a', nome: 'Grupo A', active: true, onSelect: () => {} },
{ id: 'grupo-b', nome: 'Grupo B', active: false, onSelect: () => {} },
]}
overflowOpen={overflowOpen}
onToggleOverflow={() => {}}
/>Navegação por âncora, não troca de painel: as seções abaixo ficam todas renderizadas; clicar rola até a seção correspondente, e o item "ativo" é decidido por um IntersectionObserver olhando o scroll do container abaixo — não pelo clique em si.
Conteúdo de exemplo da seção "Filiais". Ela nunca é desmontada — só entra/sai de vista.
Conteúdo de exemplo da seção "Centros de custo". Ela nunca é desmontada — só entra/sai de vista.
Ver código — uso real (@solutio/design-system)
// onSelect rola até a seção — não troca painel, NavBar não sabe de "seções"
<NavBar
variant="surface"
groups={[
{ id: 'filiais', nome: 'Filiais', active: activeKey === 'filiais', onSelect: () => scrollToSection('filiais') },
{ id: 'centros-custo', nome: 'Centros de custo', active: activeKey === 'centros-custo', onSelect: () => scrollToSection('centros-custo') },
]}
overflowOpen={overflowOpen}
onToggleOverflow={() => setOverflowOpen((o) => !o)}
// showOverflowButton default já é 'auto' em variant="surface" — não precisa passar
/>
// activeKey vem de um IntersectionObserver nas seções da página, não do clique —
// mesmo padrão de CompanyDetailPage.jsx (tccloud-web). O NavBar já rola sozinho
// seu scroller horizontal pra manter o item ativo visível (comportamento padrão).Ver código — uso real (@solutio/design-system)
{/* Sem prop "open" — o pai controla a visibilidade montando/desmontando */}
<div style={{ position: 'relative' }}>
<AvatarMenu
userName="Maria Silva"
items={[{ key: 'logout', label: 'Sair', onSelect: () => {} }]}
onClose={() => setAvatarMenuOpen(false)}
/>
</div>Ver código — uso real (@solutio/design-system)
<MultiSelectDropdown
searchValue={search}
onSearchChange={setSearch}
options={[
{ value: 'a', label: 'Opção A', checked, onToggle: () => {} },
{ value: 'b', label: 'Opção B', checked, onToggle: () => {} },
]}
open={open}
onToggleOpen={() => setOpen((o) => !o)}
/>Ver código — uso real (@solutio/design-system)
<ValueHelpInput
label="Cliente"
selected={selected}
rowKey={(row) => row.id}
renderChip={(row) => row.nome}
onRemove={(row) => setSelected((sel) => sel.filter((s) => s.id !== row.id))}
onOpen={() => setDialogOpen(true)}
/>
<ValueHelpDialog
open={dialogOpen}
title="Selecionar cliente"
columns={[{ key: 'nome', label: 'Nome' }]}
rows={rows}
rowKey={(row) => row.id}
selected={selected}
onToggleRow={(row) =>
setSelected((sel) =>
sel.some((s) => s.id === row.id) ? sel.filter((s) => s.id !== row.id) : [...sel, row],
)
}
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)}
/>Ver código — uso real (@solutio/design-system)
<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([])}
onOk={() => setDialogOpen(false)}
onCancel={() => setDialogOpen(false)}
/>Ver código — uso real (@solutio/design-system)
import { Icon } from '@solutio/design-system';
<Icon name="chevron-right" size={16} />Ver código — uso real (@solutio/design-system)
<EmptyState
title="Nenhum projeto encontrado"
subtitle="Crie o primeiro projeto do workspace para começar."
actionLabel="Criar projeto"
onAction={() => {}}
/>Ver código — uso real (@solutio/design-system)
<Select
label="Status"
options={[{ value: 'active', label: 'Ativo' }, { value: 'draft', label: 'Rascunho' }]}
defaultValue="active"
/>Ver código — uso real (@solutio/design-system)
<DatePicker label="Data de início" defaultValue="" />Ver código — uso real (@solutio/design-system)
<TextArea label="Observações" defaultValue="" />Ver código — uso real (@solutio/design-system)
const { showToast } = useToast();
<Button onClick={() => showToast({ message: 'Salvo com sucesso!', variant: 'success' })}>
Disparar toast
</Button>Preview dentro do card só para esta página — na aplicação real fica fixo no topo (ver o topo desta própria página).
Ver código — uso real (@solutio/design-system)
<ShellBar
title="TC Cloud"
avatarInitials="MG"
logo={<img src="/brand/logoIcon.png" alt="Solutio TCCloud" style={{ height: 28 }} />}
onLogoClick={() => {}}
onTitleClick={() => {}}
onAvatarClick={() => {}}
onSearchClick={() => {}}
onHelpClick={() => {}}
onNotificationsClick={() => {}}
/>Ver código — uso real (@solutio/design-system)
<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 />3 projetos ativos
Ver código — uso real (@solutio/design-system)
<Card title="Projetos" subtitle="Workspace atual" action={<Button variant="transparent">Ver todos</Button>}>
<p>3 projetos ativos</p>
</Card>Ver código — uso real (@solutio/design-system)
{bannerVisible && (
<Banner variant="warning" onClose={() => setBannerVisible(false)}>
Este registro está pendente de aprovação.
</Banner>
)}Ver código — uso real (@solutio/design-system)
<StatusRadioGroup value={status} onChange={setStatus} hint="Registros inativos não aparecem em buscas" />Ver código — uso real (@solutio/design-system)
<Switch label="Sincronização automática" checked={checked} onChange={setChecked} />Ver código — uso real (@solutio/design-system)
<Chip label="workspace.read" onRemove={() => {}} />
<Chip label="success" variant="success" />
<Chip label="warning" variant="warning" />
<Chip label="error" variant="error" />Ver código — uso real (@solutio/design-system)
<CollapsibleGroup title="Permissões" count={2}>
<Chip label="fiscal:read" />
<Chip label="fiscal:write" />
</CollapsibleGroup>Ver código — uso real (@solutio/design-system)
<ObjectPageHeader
kicker="Projeto"
title="Projeto Alpha"
status={{ variant: 'success', label: 'Ativo' }}
onSave={() => {}}
onCancel={() => {}}
/>Ver código — uso real (@solutio/design-system)
<ObjectPageTabs
tabs={[{ key: 'geral', label: 'Geral' }, { key: 'membros', label: 'Membros' }]}
active={activeTab}
onChange={setActiveTab}
/>Ver código — uso real (@solutio/design-system)
<ConfirmDialog
open={open}
title="Confirmar exclusão"
message="Esta ação não pode ser desfeita. Deseja continuar?"
onConfirm={() => setOpen(false)}
onCancel={() => setOpen(false)}
/>Ver código — uso real (@solutio/design-system)
<SearchSelectInput
options={options}
query={query}
onQueryChange={setQuery}
selected={selected}
onSelect={setSelected}
onAdd={() => { /* adiciona "selected" a uma lista de chips */ }}
/>Ver código — uso real (@solutio/design-system)
<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" /> }],
]}
/>stickyOffset é calc(var(--sol-shell-h) * 2), pra colar logo abaixo do ShellBar + NavBar de âncora reais desta página (ambos sticky) em vez de sobrepor os dois — clique em "Fixar painel" e role a página. NavBar variant="surface" é full-bleed (alcança a borda real da viewport): não funciona dentro de um container com overflow diferente de visible, por isso este demo não usa um quadro de scroll local.
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.
Ver código — uso real (@solutio/design-system)
<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)"
/>- Grid base 4px — escala 4 / 8 / 12 / 16 / 24 / 32 / 48.
- Altura da shell bar: 44px. Barra de navegação horizontal: 44px.
- Altura de linha de tabela: 40px (compact) / 44px (cozy, padrão). Cabeçalho: 44px.
- Altura de campo e botão: 36px. Raio de canto: 4px em cartões, campos e botões.
- Tile: 176 × 176px, gap de 12px. Padding de página: 24px 32px.
- Sombra de popover: 0 8px 24px rgba(0,0,0,0.24). Sem sombra em cartões (usar borda).