Files
comparador-notas/src/ui/AGENTS.md
T
2026-03-04 12:35:36 -03:00

12 KiB
Raw Blame History

AGENTS.md — Guia de desenvolvimento da UI

Este documento descreve como a interface do Comparador de Notas é estruturada, quais padrões devem ser seguidos e o que não fazer. Leia antes de criar ou modificar qualquer arquivo em src/ui/.


1. Arquitetura da UI

O projeto usa iced 0.13 com arquitetura Elm (Model / Update / View).

src/ui/
├── app.rs                  Estado global (App), update, view raiz, breadcrumb
├── message.rs              Enum Message — todos os eventos da UI
├── mod.rs                  Re-exporta submódulos
├── theme.rs                Paleta de cores, estilos de widgets (sem lógica)
├── components/
│   ├── modal.rs            Overlay de modal bloqueante
│   ├── paginacao.rs        Controles de paginação (◀ / ▶)
│   └── tabela_preview.rs   Tabela de pré-visualização do arquivo importado
└── screens/
    ├── import.rs           Tela 1 — seleção de arquivo e layout
    ├── selecionar_aba.rs   Tela 1.5 — seleção de aba XLSX
    ├── configuracao_colunas.rs  Tela 2 — mapeamento de colunas
    ├── resultado.rs        Tela 3 — resultado da análise
    └── layouts.rs          Tela lateral — gerenciamento de layouts

Responsabilidades por camada

Camada Responsabilidade
app.rs Estado global, update(), view() raiz, roteamento entre telas, tarefas assíncronas
screens/ Renderização de cada tela: coleta inputs, monta widgets, emite Message
components/ Widgets reutilizáveis sem estado próprio (recebem dados por parâmetro)
theme.rs Apenas estilos visuais. Sem lógica de negócio. Sem Message.
message.rs Todos os eventos possíveis da UI. Nenhuma lógica aqui.

Regra: nenhuma lógica de domínio (parsing, validação de sequência, cálculos) pode estar em src/ui/. A UI apenas chama use cases de src/application/.


2. Tema e paleta de cores

Todas as cores estão em theme.rs como constantes Color. Nunca use valores RGB literais fora de theme.rs.

Paleta

Constante Hex Uso
BG #0F172A Fundo geral da janela
SURFACE #1E293B Cards primários
SURFACE_2 #334155 Cards secundários, cabeçalho de tabela
BORDER #334155 Bordas de inputs e cards
TEXT #F1F5F9 Texto principal
TEXT_SECONDARY #94A3B8 Labels, placeholders
TEXT_MUTED #64748B Texto desabilitado
PRIMARY #3B82F6 Botões primários, step ativo
PRIMARY_HOVER #2563EB Hover em botões primários
SUCCESS #22C55E Badge OK, barra ≥ 90%
WARNING #F59E0B Badge faltante, barra 6089%
DANGER #EF4444 Badge duplicada, barra < 60%
TRACK_BG #111827 Trilha da progress bar

Helpers de estilo disponíveis em theme.rs

Containers:

  • t::fundo — fundo geral da janela
  • t::card — card principal (SURFACE + borda + radius 8)
  • t::card_secondary — card secundário (SURFACE_2 + borda + radius 6)
  • t::cabecalho_tabela — cabeçalho de tabela sem borda
  • t::badge_sucesso / t::badge_aviso / t::badge_perigo — badges coloridos
  • t::area_erro — área de validação com fundo vermelho sutil
  • t::stat_card — card de estatística (igual a t::card)
  • t::separador — linha divisória fina
  • t::breadcrumb_bg — fundo da barra de breadcrumb

Botões:

  • t::btn_primary — azul sólido, ação principal
  • t::btn_secondary — SURFACE_2, ação secundária
  • t::btn_ghost — transparente com borda, ação terciária
  • t::btn_danger — vermelho semitransparente, exclusão
  • t::btn_aba_ativa / t::btn_aba_inativa — seleção de aba XLSX
  • t::btn_pagina_ativo / t::btn_pagina_inativo — paginação

Inputs:

  • t::input_dark — text_input com fundo BG, borda BORDER, focus PRIMARY

Progress bar:

  • t::progress_bar_por_percentual(f32) — retorna closure com cor por threshold:
    • ≥ 0.90 → SUCCESS, 0.600.89 → WARNING, < 0.60 → DANGER

3. Regras de layout e responsividade

Princípio geral

A janela tem tamanho mínimo de 800×600. Todo layout deve funcionar bem nessa dimensão e escalar corretamente ao aumentar.

O que usar

Situação Valor correto
Widget que deve preencher o espaço disponível Length::Fill
Label ao lado de input em linha Length::FillPortion(3) (label) + input com tamanho fixo pequeno ou Fill
Input numérico curto (índice, posição) Length::Fixed(90.0) ou Length::Fixed(110.0)
Input de texto longo (nome, aba) Length::Fill
Pick list de opções Length::Fill
Modal/card centralizado com largura máxima .max_width(N) + Length::Fill
Botões em linha que podem quebrar .wrap() no row![]
Elemento que deve ter tamanho mínimo sem crescer Length::Shrink
Células de tabela com scroll horizontal Length::Fixed(100.0) mínimo

O que não fazer

  • Não use Length::Fixed em labels de formulário. Labels devem usar FillPortion para se adaptar ao espaço disponível.
  • Não use Length::Fixed em pick lists ou text inputs de texto livre. Use Fill para que se adaptem à largura do container pai.
  • Não coloque valores maiores que max_width em modais. Use .max_width(N) em vez de Fixed(N) para que o modal encolha em janelas menores.
  • Não deixe telas sem scrollable. Toda tela com conteúdo vertical deve ser envolvida em scrollable() para evitar clipping em janelas pequenas.
  • Não use row![] com muitos itens sem .wrap(). Botões de ação e grupos de controles devem usar .wrap() para quebrar linha quando não couberem.

Padrão de campo de formulário

// Linha de campo: label proporcional + input
fn campo_row<'a>(label: &'a str, input: Element<'a, Message>) -> Element<'a, Message> {
    row![
        text(label)
            .size(13)
            .color(t::TEXT_SECONDARY)
            .width(Length::FillPortion(3)),  // proporcional, não fixo
        input,                               // input define seu próprio tamanho
    ]
    .spacing(10)
    .align_y(Alignment::Center)
    .into()
}

Padrão de campo opcional com checkbox

// Quando ativo: checkbox (FillPortion) + input
// Quando inativo: só o checkbox
if ativo {
    row![
        cb.width(Length::FillPortion(3)),
        text_input("...", &val)
            .width(Length::Fixed(90.0)),   // input numérico curto
    ]
    .spacing(10)
    .align_y(Alignment::Center)
    .into()
} else {
    row![cb].into()
}

4. Estrutura de uma tela (screen)

Toda tela segue o mesmo padrão de função pública view:

pub fn view(app: &App) -> Element<'_, Message> {
    // 1. Montar seções/componentes individuais
    let secao_x = ...;
    let secao_y = ...;

    // 2. Combinar em coluna principal
    let conteudo = column![secao_x, secao_y]
        .spacing(14)
        .padding([20, 24])
        .width(Length::Fill);

    // 3. Envolver em scrollable + container de fundo
    container(scrollable(conteudo))
        .style(t::fundo)
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
}

Toda tela deve ter scrollable e container com t::fundo na raiz.


5. Componentes reutilizáveis

modal::view_com_modal(conteudo, modal)

Envolve qualquer Element com um overlay de modal bloqueante. Chamado em app.rs quando self.modal.is_some().

  • Modal tem max_width(420) + Length::Fill para ser responsivo.
  • Tipos disponíveis: Informacao, Aviso, Erro, Confirmacao, InputTexto.
  • Disparar modal: usar os helpers em app.rs (exibir_erro, exibir_aviso, exibir_confirmacao).

tabela_preview::tabela_preview(linhas)

Renderiza as primeiras N linhas do arquivo com cabeçalho estilo Excel (A, B, C...).

  • Células com Fixed(100.0) — tamanho fixo mínimo com scroll horizontal.
  • A altura da área de scroll está fixada em Fixed(160.0) — intencional.
  • Scroll horizontal via scrollable::Direction::Horizontal.

paginacao::controles_paginacao(pagina, total, msg_anterior, msg_proxima)

Row de botões ◀ / "Página X / Y" / ▶. Emite as mensagens passadas como parâmetro.


6. Roteamento entre telas

O roteamento é feito pelo enum EstadoApp em app.rs:

Estado Tela renderizada
Importando screens/import.rs
SelecionandoAba screens/selecionar_aba.rs
ConfigurandoColunas screens/configuracao_colunas.rs
ConfirmandoIntervalo screens/configuracao_colunas.rs (mesmo view)
ExibindoResultado(r) screens/resultado.rs
GerenciandoLayouts screens/layouts.rs
Analisando Spinner inline em app.rs

Transições são sempre via Messageupdate(). Nunca altere self.estado diretamente de dentro de uma tela.

O breadcrumb é renderizado automaticamente por app.rs para todos os estados exceto GerenciandoLayouts e Analisando.


7. Adicionando uma nova tela

  1. Crie src/ui/screens/minha_tela.rs com função pub fn view(app: &App) -> Element<'_, Message>.
  2. Adicione pub mod minha_tela; em src/ui/screens/mod.rs.
  3. Adicione a variante correspondente em EstadoApp (app.rs).
  4. Adicione o arm no match &self.estado em app.view() (app.rs).
  5. Adicione as mensagens necessárias em message.rs.
  6. Trate as mensagens no update() de app.rs.

8. Adicionando um novo componente

  1. Crie src/ui/components/meu_componente.rs.
  2. Adicione pub mod meu_componente; em src/ui/components/mod.rs.
  3. O componente deve ser uma função pura: recebe dados por parâmetro, retorna Element<'_, Message>.
  4. Sem estado interno, sem self, sem acesso ao banco.

9. Adicionando novos estilos ao tema

  • Sempre adicione em theme.rs.
  • Siga o padrão dos helpers existentes: função que recebe &Theme e retorna o Style do widget.
  • Para cores com alpha: use Color { a: 0.N, ..CONSTANTE } em vez de valores RGB manuais.
  • Nomeie helpers de container como nome_do_contexto, botões como btn_nome, inputs como input_nome.

10. Mensagens e estado assíncrono

  • Operações bloqueantes (I/O, análise) são sempre executadas em Task::perform com tokio::task::spawn_blocking.
  • O resultado retorna para update() via Message.
  • O estado EstadoApp::Analisando é usado enquanto a operação está em background.
  • ResultadoPendente é o tipo intermediário entre a thread de análise e a UI.

Não bloquear a thread principal da UI. Qualquer operação lenta deve usar Task.


11. Drag-and-drop de arquivos

O iced 0.13 expõe eventos de janela para drag-and-drop via iced::window::Event. O projeto os captura através de App::subscription() registrado em main.rs.

Eventos capturados

Evento iced Mensagem emitida Efeito
window::Event::FileDropped(path) Message::ArquivoSolto(path) Processa o arquivo como se fosse selecionado via botão
window::Event::FileHovered(_) Message::ArquivoEmHover Liga App.arquivo_em_hover = true
window::Event::FilesHoveredLeft Message::ArquivoHoverSaiu Liga App.arquivo_em_hover = false

Subscription

App::subscription() em app.rs usa iced::event::listen_with para filtrar apenas os três eventos acima. Está registrado em main.rs via .subscription(App::subscription).

Estado de hover

O campo App.arquivo_em_hover: bool é true enquanto um arquivo está sendo arrastado sobre a janela. A tela screens/import.rs usa esse campo para alterar visualmente a drop_zone: borda mais brilhante (a: 0.9) e mais espessa (width: 2.0) durante o hover.

Fluxo de processamento

Message::ArquivoSolto chama self.processar_arquivo_selecionado(caminho) — o mesmo método chamado pelo botão de seleção de arquivo. O comportamento é idêntico: validação de extensão, leitura de abas (XLSX) ou preview (CSV), e transição de estado.

Como estender

Para adicionar suporte a drag-and-drop em outras telas (ex: importar layout JSON por drag), basta verificar App.estado dentro do arm Message::ArquivoSolto antes de chamar processar_arquivo_selecionado, e desviar conforme necessário.