Corrected: Responsive design for low-resolution monitors.
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user