12 KiB
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 60–89% |
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 janelat::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 bordat::badge_sucesso/t::badge_aviso/t::badge_perigo— badges coloridost::area_erro— área de validação com fundo vermelho sutilt::stat_card— card de estatística (igual at::card)t::separador— linha divisória finat::breadcrumb_bg— fundo da barra de breadcrumb
Botões:
t::btn_primary— azul sólido, ação principalt::btn_secondary— SURFACE_2, ação secundáriat::btn_ghost— transparente com borda, ação terciáriat::btn_danger— vermelho semitransparente, exclusãot::btn_aba_ativa/t::btn_aba_inativa— seleção de aba XLSXt::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.60–0.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::Fixedem labels de formulário. Labels devem usarFillPortionpara se adaptar ao espaço disponível. - Não use
Length::Fixedem pick lists ou text inputs de texto livre. UseFillpara que se adaptem à largura do container pai. - Não coloque valores maiores que
max_widthem modais. Use.max_width(N)em vez deFixed(N)para que o modal encolha em janelas menores. - Não deixe telas sem
scrollable. Toda tela com conteúdo vertical deve ser envolvida emscrollable()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::Fillpara 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 Message → update(). 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
- Crie
src/ui/screens/minha_tela.rscom funçãopub fn view(app: &App) -> Element<'_, Message>. - Adicione
pub mod minha_tela;emsrc/ui/screens/mod.rs. - Adicione a variante correspondente em
EstadoApp(app.rs). - Adicione o arm no
match &self.estadoemapp.view()(app.rs). - Adicione as mensagens necessárias em
message.rs. - Trate as mensagens no
update()deapp.rs.
8. Adicionando um novo componente
- Crie
src/ui/components/meu_componente.rs. - Adicione
pub mod meu_componente;emsrc/ui/components/mod.rs. - O componente deve ser uma função pura: recebe dados por parâmetro, retorna
Element<'_, Message>. - 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
&Themee retorna oStyledo 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 comobtn_nome, inputs comoinput_nome.
10. Mensagens e estado assíncrono
- Operações bloqueantes (I/O, análise) são sempre executadas em
Task::performcomtokio::task::spawn_blocking. - O resultado retorna para
update()viaMessage. - 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.