# 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 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.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::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 ```rust // 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 ```rust // 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`: ```rust 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 `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 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.