Skip to content
Extensões

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

CenarioRecomendado
Ultima etapa de Wizards (Steppers)Sim - Para revisao antes de salvar
Modais de Detalhes ("Ver Detalhes")Sim - Com readonly: true
Compartilhamento via EmailSim - Com atributos data-share-*
Cards de Informacao em dashboardsSim
Formularios de edicaoNao - Use componentes de input
Tabelas de dados massivosNao - 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 elementos

Importacao

html
<!-- 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

ParametroTipoPadraoDescricao
selectorstring|Element-Seletor CSS ou elemento DOM
options.dataArray[]Array de secoes (ver estrutura abaixo)
options.footerActionsstring|ElementnullHTML ou elemento para o rodape
options.variantstring'default''default', 'compact', 'full-width'
options.readonlybooleanfalseEsconde botoes de acao
options.showSeparatorbooleantrueMostra linha separadora vertical
options.errorsArray[]Array de erros de validacao
options.showErrorSummarybooleantrueMostra area de resumo de erros
options.onFieldClickFunctionnullCallback: function(fieldKey, stepIndex)
options.entityTypestring''Tipo da entidade (para data-share)
options.entityIdstring|number''ID da entidade (para data-share)
options.entityNamestring''Nome da entidade (para data-share)

Retorno

javascript
{
  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)

javascript
{
  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)

javascript
{
  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)

javascript
{
  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

javascript
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)

javascript
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

javascript
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)

javascript
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 }
      ]
    }
  ]
});
javascript
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:

  1. Area de Resumo (topo): Lista de todos os erros com links "Corrigir"
  2. Inline no Item: Destaque visual com borda vermelha e mensagem
javascript
// 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

javascript
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:

html
<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

javascript
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.

javascript
window.createResumo('#container', {
  variant: 'default',
  data: [...]
});

Compact

Gap menor entre secoes, para espacos limitados.

javascript
window.createResumo('#container', {
  variant: 'compact',
  data: [...]
});

Full Width

Sem limite de largura, ocupa todo o container pai.

javascript
window.createResumo('#container', {
  variant: 'full-width',
  data: [...]
});

Sem Separador

Esconde a linha vertical entre labels e valores.

javascript
window.createResumo('#container', {
  showSeparator: false,
  data: [...]
});

Padrao Mapper

Para alimentar o componente, utilize uma funcao transformadora (Mapper) em vez de construir o array manualmente.

javascript
// 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:

javascript
// 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:

html
<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-mode no body
  • Atributo data-theme="dark" em qualquer ancestral
css
/* 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

  1. Use IDs unicos para cada secao
  2. Formate valores no Mapper, nao no template
  3. Trate valores nulos - o componente exibe "-" automaticamente
  4. Agrupe campos relacionados em secoes logicas
  5. Use highlight com moderacao - apenas para campos importantes
  6. Prefira acoes especificas (ex: "Editar Endereco") a genericas ("Editar")
  7. Sempre forneca fieldKey para items que podem ter erros
  8. Use stepIndex para navegacao precisa no stepper
  9. 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

Design System interno do GALES