Componente Resumo
Status: Estavel | Versao: 2.0.0
O componente Resumo padroniza a exibicao de dados em formato de leitura (read-only), organizados em secoes logicas. Ele foi projetado para substituir a construcao manual de HTML em telas de revisao, detalhes de entidades ou etapas finais de formularios (wizards/steppers).
Novidades da Versao 2.0.0
- Validacao Inline: Erros exibidos diretamente no item com destaque visual
- Area de Erros: Resumo de erros no topo com links para correcao
- Compartilhamento: Atributos
data-share-*para extracao de dados - Modo Readonly: Esconde botoes de acao (para uso em Detalhar)
- Separador Vertical: Linha visual entre labels e valores
- Dark Mode Completo: Suporte total a tema escuro
- Responsividade Mobile: Layout adaptado para telas pequenas
Quando Usar
| Cenario | Recomendado |
|---|---|
| Ultima etapa de Wizards (Steppers) | Sim - Para revisao antes de salvar |
| Modais de Detalhes ("Ver Detalhes") | Sim - Com readonly: true |
| Compartilhamento via Email | Sim - Com atributos data-share-* |
| Cards de Informacao em dashboards | Sim |
| Formularios de edicao | Nao - Use componentes de input |
| Tabelas de dados massivos | Nao - Use o componente Table |
Estrutura de Classes
.resumo -> Container principal
|-- .resumo--compact -> Variante compacta
|-- .resumo--full-width -> Largura total (sem max-width)
|-- .resumo--readonly -> Modo somente leitura
|
|-- .resumo__alerts -> Area de alertas/erros
| +-- .resumo__error-list -> Lista de erros
|
|-- .resumo__section -> Secao de dados
| |-- .resumo__section--bordered -> Com borda inferior
| |
| |-- .resumo__header -> Cabecalho da secao
| | |-- .resumo__title -> Titulo da secao
| | |-- .resumo__title--large -> Titulo maior
| | +-- .resumo__action -> Botao de acao (ex: Editar)
| | |-- .resumo__action-icon -> Icone
| | +-- .resumo__action-label -> Texto
| |
| +-- .resumo__content -> Container com separador
| |-- .resumo__separator -> Linha vertical
| +-- .resumo__grid -> Grid de itens
| |-- .resumo__grid--table -> Layout tabela
| |-- .resumo__grid--wide-labels -> Labels largos
| +-- .resumo__grid--narrow-labels -> Labels estreitos
|
|-- .resumo__item -> Item (label + valor)
| |-- .resumo__item--empty -> Valor vazio
| |-- .resumo__item--error -> Com erro de validacao
| |-- .resumo__item--highlight -> Destaque verde
| |-- .resumo__item--warning -> Alerta laranja
| |-- .resumo__item--danger -> Erro vermelho
| |-- .resumo__item--required -> Campo obrigatorio (*)
| |
| |-- .resumo__item-label -> Rotulo do campo
| |-- .resumo__item-value -> Valor do campo
| +-- .resumo__item-fix -> Botao "Corrigir"
|
+-- .resumo__footer -> Rodape com acoes
|-- .resumo__footer--start -> Alinhamento a esquerda
|-- .resumo__footer--center -> Alinhamento centralizado
+-- .resumo__footer--between -> Espaco entre elementosImportacao
<!-- CSS -->
<link rel="stylesheet" href="/css/design-system/components/resumo.css">
<!-- JavaScript -->
<script src="/css/design-system/components/resumo.js"></script>API JavaScript
window.createResumo(selector, options)
Cria uma instancia do componente Resumo.
Parametros
| Parametro | Tipo | Padrao | Descricao |
|---|---|---|---|
selector | string|Element | - | Seletor CSS ou elemento DOM |
options.data | Array | [] | Array de secoes (ver estrutura abaixo) |
options.footerActions | string|Element | null | HTML ou elemento para o rodape |
options.variant | string | 'default' | 'default', 'compact', 'full-width' |
options.readonly | boolean | false | Esconde botoes de acao |
options.showSeparator | boolean | true | Mostra linha separadora vertical |
options.errors | Array | [] | Array de erros de validacao |
options.showErrorSummary | boolean | true | Mostra area de resumo de erros |
options.onFieldClick | Function | null | Callback: function(fieldKey, stepIndex) |
options.entityType | string | '' | Tipo da entidade (para data-share) |
options.entityId | string|number | '' | ID da entidade (para data-share) |
options.entityName | string | '' | Nome da entidade (para data-share) |
Retorno
{
render(), // Re-renderiza o componente
update(newData), // Atualiza todos os dados
updateSection(id, data), // Atualiza uma secao especifica
addSection(section, pos), // Adiciona nova secao
removeSection(id), // Remove uma secao
setErrors(errors), // Define erros de validacao
clearErrors(), // Limpa todos os erros
hasErrors(), // Verifica se ha erros
getData(), // Obtem dados atuais
getErrors(), // Obtem erros atuais
destroy(), // Destroi o componente
container // Referencia ao elemento DOM
}Estrutura de Dados
Secao (ResumoSection)
{
id: "string", // Identificador unico (obrigatorio)
title: "string", // Titulo da secao (opcional)
action: { // Botao de acao no header (opcional)
label: "string", // Texto do botao (padrao: "Editar etapa")
onClick: function // Callback ao clicar
},
items: [ // Lista de campos (obrigatorio)
// ... ver ResumoItem
]
}Item (ResumoItem)
{
label: "string", // Rotulo do campo (obrigatorio)
value: "any", // Valor (aceita HTML string)
fieldKey: "string", // Identificador do campo (para validacao/share)
stepIndex: number, // Indice do step (para navegacao)
isRequired: boolean, // Mostra asterisco (*) (opcional)
highlight: boolean, // Destaque verde (opcional)
warning: boolean, // Alerta laranja (opcional)
danger: boolean, // Erro vermelho (opcional)
shareable: boolean, // Incluir em compartilhamento (padrao: true)
shareType: "string" // Tipo para share ('text', 'currency', 'date')
}Erro (ResumoError)
{
field: "string", // fieldKey do item com erro
fieldLabel: "string", // Label para exibicao (opcional)
message: "string", // Mensagem de erro
stepIndex: number // Indice do step (para navegacao)
}Exemplos de Uso
Basico
var resumo = window.createResumo('#meu-container', {
data: [
{
id: 'pessoal',
title: 'Dados Pessoais',
items: [
{ label: 'Nome', value: 'Joao da Silva', fieldKey: 'ds_nome' },
{ label: 'CPF', value: '123.456.789-00', fieldKey: 'nr_cpf' },
{ label: 'Status', value: 'Ativo', fieldKey: 'ds_status', highlight: true }
]
}
]
});Com Acao de Editar (Stepper)
var resumo = window.createResumo('#container', {
data: [
{
id: 'endereco',
title: 'Endereco',
action: {
label: 'Editar etapa',
onClick: function() { StepperStateStore.goToStep(2); }
},
items: [
{ label: 'Logradouro', value: 'Rua das Flores, 123', fieldKey: 'ds_logradouro', stepIndex: 2 },
{ label: 'Cidade/UF', value: 'Goiania - GO', fieldKey: 'ds_cidade', stepIndex: 2 }
]
}
]
});Com Validacao Inline
var resumo = window.createResumo('#container', {
data: [
{
id: 'dados',
title: 'Dados',
items: [
{ label: 'CPF', value: '', fieldKey: 'nr_cpf', stepIndex: 0, isRequired: true },
{ label: 'Email', value: 'invalido', fieldKey: 'ds_email', stepIndex: 0 }
]
}
],
errors: [
{ field: 'nr_cpf', fieldLabel: 'CPF', message: 'Campo obrigatorio', stepIndex: 0 },
{ field: 'ds_email', fieldLabel: 'Email', message: 'Email invalido', stepIndex: 0 }
],
showErrorSummary: true,
onFieldClick: function(fieldKey, stepIndex) {
// Navegar para o step e focar no campo
StepperStateStore.goToStep(stepIndex);
setTimeout(function() {
var input = document.querySelector('[name="' + fieldKey + '"]');
if (input) input.focus();
}, 300);
}
});Modo Readonly (Detalhar)
var resumo = window.createResumo('#modal-detalhar .modal-body', {
readonly: true, // Esconde botoes de acao
variant: 'full-width',
entityType: 'pessoa', // Para compartilhamento
entityId: 12345,
entityName: 'Joao Silva',
data: [
{
id: 'identificacao',
title: 'Identificacao',
items: [
{ label: 'Codigo', value: '12345', fieldKey: 'id_pessoa' },
{ label: 'Nome', value: 'Joao Silva', fieldKey: 'ds_nome' },
{ label: 'Status', value: 'Ativo', fieldKey: 'ds_status', highlight: true }
]
}
]
});Com Footer de Acoes
var resumo = window.createResumo('#container', {
data: dadosResumo,
footerActions: '<button class="btn btn-outline" onclick="voltar()">Voltar</button>' +
'<button class="btn btn-primary" onclick="confirmar()">Confirmar e Salvar</button>'
});Validacao Inline
Exibindo Erros
Os erros sao exibidos de duas formas:
- Area de Resumo (topo): Lista de todos os erros com links "Corrigir"
- Inline no Item: Destaque visual com borda vermelha e mensagem
// Erros vindos do servidor
var errosValidacao = [
{ field: 'nr_cpf', fieldLabel: 'CPF', message: 'CPF invalido', stepIndex: 1 },
{ field: 'ds_email', fieldLabel: 'Email', message: 'Email ja cadastrado', stepIndex: 0 }
];
// Aplicar erros ao resumo
resumo.setErrors(errosValidacao);
// Verificar se ha erros
if (resumo.hasErrors()) {
alert('Corrija os erros antes de continuar');
}
// Limpar erros apos correcao
resumo.clearErrors();Callback de Navegacao
onFieldClick: function(fieldKey, stepIndex) {
// 1. Navegar para o step
StepperStateStore.goToStep(stepIndex);
// 2. Aguardar transicao e focar no campo
setTimeout(function() {
var input = document.querySelector('[name="' + fieldKey + '"]');
if (input) {
input.focus();
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}Compartilhamento (data-share-*)
Atributos Gerados
O componente gera atributos semanticos para extracao de conteudo:
<div class="resumo"
data-share-entity="pessoa"
data-share-entity-id="123"
data-share-entity-name="Joao Silva">
<div class="resumo__section"
data-share-section="dados-pessoais"
data-share-section-label="Dados Pessoais">
<div class="resumo__item"
data-share-item="ds_nome"
data-share-label="Nome">
<div class="resumo__item-value">
<span data-share-value>Joao Silva</span>
</div>
</div>
</div>
</div>Extraindo Dados
function extrairParaCompartilhamento(containerEl) {
var resultado = {
entity: containerEl.getAttribute('data-share-entity'),
entityId: containerEl.getAttribute('data-share-entity-id'),
entityName: containerEl.getAttribute('data-share-entity-name'),
sections: []
};
var sections = containerEl.querySelectorAll('[data-share-section]');
for (var i = 0; i < sections.length; i++) {
var section = sections[i];
var sectionData = {
key: section.getAttribute('data-share-section'),
label: section.getAttribute('data-share-section-label'),
items: []
};
var items = section.querySelectorAll('[data-share-item]');
for (var j = 0; j < items.length; j++) {
var item = items[j];
var valueEl = item.querySelector('[data-share-value]');
sectionData.items.push({
key: item.getAttribute('data-share-item'),
label: item.getAttribute('data-share-label'),
value: valueEl ? valueEl.textContent.trim() : ''
});
}
resultado.sections.push(sectionData);
}
return resultado;
}
// Gerar texto para email
function gerarTextoEmail(dados) {
var linhas = [];
linhas.push(dados.entityName + ' (' + dados.entity + ' #' + dados.entityId + ')');
linhas.push('');
dados.sections.forEach(function(section) {
linhas.push('## ' + section.label);
section.items.forEach(function(item) {
linhas.push(item.label + ': ' + item.value);
});
linhas.push('');
});
return linhas.join('\n');
}Variantes
Default (Padrao)
Largura maxima de 630px, ideal para modais e paineis laterais.
window.createResumo('#container', {
variant: 'default',
data: [...]
});Compact
Gap menor entre secoes, para espacos limitados.
window.createResumo('#container', {
variant: 'compact',
data: [...]
});Full Width
Sem limite de largura, ocupa todo o container pai.
window.createResumo('#container', {
variant: 'full-width',
data: [...]
});Sem Separador
Esconde a linha vertical entre labels e valores.
window.createResumo('#container', {
showSeparator: false,
data: [...]
});Padrao Mapper
Para alimentar o componente, utilize uma funcao transformadora (Mapper) em vez de construir o array manualmente.
// mappers/produtorMapper.js
function mapProdutorToResumo(produtor, steps) {
return [
{
id: 'identificacao',
title: 'Identificacao',
action: steps ? {
label: 'Editar etapa',
onClick: function() { StepperStateStore.goToStep(0); }
} : null,
items: [
{ label: 'Nome', value: produtor.nome, fieldKey: 'ds_nome', stepIndex: 0 },
{
label: 'CPF/CNPJ',
value: formatarDocumento(produtor.documento),
fieldKey: 'nr_documento',
stepIndex: 0
},
{
label: 'Situacao',
value: produtor.ativo ? 'Ativo' : 'Inativo',
fieldKey: 'bo_ativo',
highlight: produtor.ativo,
danger: !produtor.ativo
}
]
},
{
id: 'contato',
title: 'Contato',
action: steps ? {
label: 'Editar etapa',
onClick: function() { StepperStateStore.goToStep(1); }
} : null,
items: [
{
label: 'Telefone',
value: formatarTelefone(produtor.telefone) || '',
fieldKey: 'nr_telefone',
stepIndex: 1
},
{
label: 'E-mail',
value: produtor.email || '',
fieldKey: 'ds_email',
stepIndex: 1
}
]
}
];
}
// Uso no Stepper
var dadosResumo = mapProdutorToResumo(formData, true);
var resumo = window.createResumo('#container', { data: dadosResumo });
// Uso no Detalhar
var dadosDetalhe = mapProdutorToResumo(entityData, false);
var resumo = window.createResumo('#container', {
data: dadosDetalhe,
readonly: true
});Integracao com Stepper
O componente Resumo e ideal para a ultima etapa de um stepper/wizard:
// No callback da etapa "Resumo"
function renderEtapaResumo() {
// Coleta dados de todas as etapas anteriores
var formData = StepperStateStore.getFormData();
// Transforma usando mapper
var dadosResumo = mapFormularioToResumo(formData);
// Renderiza com validacao
window.createResumo('#step-resumo-content', {
data: dadosResumo,
errors: [], // Sera preenchido apos validacao
showErrorSummary: true,
onFieldClick: function(fieldKey, stepIndex) {
StepperStateStore.goToStep(stepIndex);
},
footerActions: '<button class="btn btn-primary" type="submit">Confirmar e Salvar</button>'
});
}
// Apos submit, se houver erros do servidor
function handleServerErrors(response) {
if (response.errors) {
resumo.setErrors(response.errors.map(function(e) {
return {
field: e.campo,
fieldLabel: e.label,
message: e.mensagem,
stepIndex: e.etapa
};
}));
}
}Auto-inicializacao
Para inicializacao automatica via data-attributes:
<div data-resumo
data-resumo-config="window.meuConfig"
data-resumo-readonly="true"
data-resumo-variant="full-width"
data-resumo-show-separator="true">
</div>
<script>
window.meuConfig = {
data: [
{
id: 'secao1',
title: 'Dados',
items: [
{ label: 'Campo', value: 'Valor' }
]
}
]
};
</script>Dark Mode
O componente suporta dark mode automaticamente atraves de:
- Classe
dark-modenobody - Atributo
data-theme="dark"em qualquer ancestral
/* Aplicado automaticamente */
body.dark-mode .resumo__title { color: var(--color-text-primary-dark); }
[data-theme="dark"] .resumo__item-value { color: var(--color-text-primary-dark); }Responsividade
Em telas menores que 768px:
- Max-width removido
- Separador vertical escondido
- Items empilham verticalmente (label acima do valor)
- Botao "Editar etapa" mostra apenas icone
- Footer em coluna com botoes full-width
- Lista de erros empilhada
Impressao
Para impressao (@media print):
- Botoes de acao escondidos
- Links "Corrigir" escondidos
- Rodape escondido
- Quebras de pagina evitadas em secoes
Acessibilidade
- Labels e valores tem contraste adequado
- Estrutura semantica com
<h4>para titulos - Botoes de acao sao focaveis via teclado
- Valores vazios exibem "-" para leitores de tela
- Foco visivel com outline nos botoes
- Mensagens de erro associadas visualmente aos campos
Boas Praticas
- Use IDs unicos para cada secao
- Formate valores no Mapper, nao no template
- Trate valores nulos - o componente exibe "-" automaticamente
- Agrupe campos relacionados em secoes logicas
- Use highlight com moderacao - apenas para campos importantes
- Prefira acoes especificas (ex: "Editar Endereco") a genericas ("Editar")
- Sempre forneca fieldKey para items que podem ter erros
- Use stepIndex para navegacao precisa no stepper
- Configure readonly: true para telas de detalhar
Arquivos Relacionados
- CSS:
/css/design-system/components/resumo.css - JS:
/css/design-system/components/resumo.js - Exemplo:
/css/design-system/exemplos-resumo.html
Changelog
v2.0.0
- Inline validation com area de erros
- Suporte a compartilhamento (data-share-*)
- Modo readonly para detalhar
- Metodos
setErrors(),clearErrors(),hasErrors(),getErrors() - Callback
onFieldClick(fieldKey, stepIndex) - Atributos de entidade para share
- Dark mode completo
- Responsividade mobile
- Animacao de erro
- Suporte a impressao
v1.0.0
- Versao inicial
- Secoes com titulo e acao
- Variantes de layout
- Dark mode basico