From 4c4f573cd1a71c5e4d06f79fd6eba1a62b45c576 Mon Sep 17 00:00:00 2001 From: Felipe Canin Novaes Date: Wed, 4 Mar 2026 22:46:27 -0300 Subject: [PATCH] Fix button colors. --- docs/FEATURES_BACKLOG.md | 76 ----------------------------- src/ui/AGENTS.md | 20 ++++++-- src/ui/app.rs | 22 +++++++-- src/ui/components/tabela_preview.rs | 2 +- src/ui/theme.rs | 56 +++++++++++++++++++-- 5 files changed, 88 insertions(+), 88 deletions(-) diff --git a/docs/FEATURES_BACKLOG.md b/docs/FEATURES_BACKLOG.md index 2579fd0..3495965 100644 --- a/docs/FEATURES_BACKLOG.md +++ b/docs/FEATURES_BACKLOG.md @@ -2,40 +2,6 @@ > Features avaliadas em 03/03/2026. Organizadas por categoria e esforço estimado. -## Alta Prioridade - -### F-01 — Exportar Resultado em CSV. - -**Problema:** O único formato de exportação é PDF. Para processar os resultados em ferramentas externas (Excel, Power BI, sistemas ERP), o usuário precisa redigitar dados do PDF. - -**Solução:** Botão "Exportar CSV" na tela de resultado, gerando dois arquivos (ou um com duas seções): -- `faltantes.csv`: `serie,documento_tipo,numero` -- `duplicatas.csv`: `serie,documento_tipo,numero,ocorrencias` - -**Escopo técnico:** -- `use case` `exportar_csv(resultado: &ResultadoAnalise, caminho: &Path)` -- Trait `CsvExporter` análoga à `PdfGenerator` (opcional, para testabilidade) -- Botão na barra de ações da tela `resultado.rs` -- Dependência `csv` já está no `Cargo.toml` - -**Esforço estimado:** Médio (2–3h) -**Impacto:** Alto — elimina retrabalho manual - -**Observações:** -Se não for muito custoso para implementar, pode ser uma boa ideia adicionar uma essa função. - ---- - -### F-02 — Validação de Tamanho de Arquivo (PRD RF01.3) (Implementado) - -**Status:** Já implementado. Ambos `csv_reader.rs` e `xlsx_reader.rs` verificam o tamanho do arquivo -antes de qualquer leitura via constante `LIMITE_BYTES = 50 MB`, retornando `ErroArquivo::TamanhoExcedido` -se excedido — exibido como modal de erro pela UI. - -**Esforço estimado:** Baixo (30min) — *concluído* -**Impacto:** Médio — evita travamentos inesperados - ---- ## Média Prioridade @@ -59,43 +25,6 @@ Verificar se já existe no codigo, pois na tela de configuração do Layout ele --- -### F-04 — Busca por Número na Tela de Resultado (Util) - -**Problema:** Com listas longas, o usuário precisa navegar páginas para verificar se um número específico está faltante ou duplicado. - -**Solução:** Campo de busca no topo da tela de resultado. Ao digitar `1234`, destaca se a nota: -- está **faltante** (aparece na lista de faltantes) -- está **duplicada** (aparece na lista de duplicatas) -- está **presente** (está nas notas importadas) -- **não encontrada** (fora do intervalo conhecido) - -**Escopo técnico:** -- Campo `filtro_numero: String` no `App` -- Busca em `resultado.faltantes_por_serie` e `resultado.duplicadas_por_serie` -- Para "presente": busca em `app.notas_importadas` (já mantido em memória) -- Banner de resultado no topo da tela - -**Esforço estimado:** Médio (2–3h) -**Impacto:** Médio — uso diário em auditorias pontuais - ---- - -### F-05 — Recarregar Arquivo Sem Reconfigurar (Implementado) - -**Problema:** Quando o usuário corrige o arquivo fonte e quer re-verificar, precisa navegar todo o fluxo novamente (selecionar arquivo → configurar colunas → analisar). - -**Solução:** Botão "🔄 Reanalisar Arquivo" na tela de resultado que reimporta o mesmo caminho com o mesmo layout atual, sem nenhuma interação adicional. - -**Escopo técnico:** -- Persistir `caminho_arquivo_atual: Option` no `App` (já existe parcialmente como `nome_arquivo`) -- Reaproveitar o fluxo de `executar_importacao()` com os parâmetros atuais -- Botão na barra de ações da tela `resultado.rs` - -**Esforço estimado:** Baixo (1h) -**Impacto:** Médio — elimina atrito no ciclo corrigir → verificar - ---- - ### F-06 — Auto-detecção de Delimitador CSV (Util) **Problema:** O usuário precisa saber antecipadamente qual delimitador o arquivo usa (`,`, `;`, `\t`). Arquivos gerados por diferentes sistemas variam. @@ -152,11 +81,6 @@ Verificar se já existe no codigo, pois na tela de configuração do Layout ele | ID | Feature | Esforço | Impacto | Prioridade | Status | |---|---|---|---|---|---| -| F-01 | Exportar resultado em CSV | Médio | Alto | Alta | -| F-02 | Validação de tamanho de arquivo | Baixo | Médio | Alta | | F-03 | Preview de colunas antes da análise | Alto | Alto | Média | -| F-04 | Busca por número no resultado | Médio | Médio | Média | -| F-06 | Recarregar arquivo sem reconfigurar | Baixo | Médio | Média | | F-07 | Auto-detecção de delimitador CSV | Médio | Baixo-Médio | Média | | F-08 | Auto-detecção de encoding CSV | Médio | Baixo | Baixa | -| F-09 | Agrupamento de faltantes no PDF | Baixo | Baixo-Médio | Baixa | ✅ Implementado | diff --git a/src/ui/AGENTS.md b/src/ui/AGENTS.md index 0936078..c409a11 100644 --- a/src/ui/AGENTS.md +++ b/src/ui/AGENTS.md @@ -81,11 +81,19 @@ literais fora de `theme.rs`.** **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_secondary` — SURFACE_2 com borda `TEXT_SECONDARY` 55%, ação secundária +- `t::btn_ghost` — transparente com borda `TEXT_SECONDARY` 55%, 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 +- `t::btn_breadcrumb_visitado` — botão sem fundo para passos anteriores do breadcrumb + +### Contraste de estados de botão + +`btn_secondary` e `btn_ghost` usam borda visível (`TEXT_SECONDARY` a 55% de opacidade) +no estado normal para se destacar de containers com fundo `SURFACE_2`. O estado +`Disabled` reduz a borda para 20% de opacidade, criando contraste claro entre ativo e +inativo. **Nunca remova a borda desses estilos** — sem ela os botões se perdem no fundo. **Inputs:** - `t::input_dark` — text_input com fundo BG, borda BORDER, focus PRIMARY @@ -214,7 +222,7 @@ Chamado em `app.rs` quando `self.modal.is_some()`. 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. +- A altura da área de scroll está fixada em `Fixed(200.0)` — acomoda 5 linhas + cabeçalho + scrollbar horizontal. - Scroll horizontal via `scrollable::Direction::Horizontal`. ### `paginacao::controles_paginacao(pagina, total, msg_anterior, msg_proxima)` @@ -244,6 +252,12 @@ diretamente de dentro de uma tela.** O breadcrumb é renderizado automaticamente por `app.rs` para todos os estados exceto `GerenciandoLayouts` e `Analisando`. +### Comportamento de navegação do breadcrumb + +- **Passo ativo** — destaque azul com fundo semitransparente (`PRIMARY` a 15%). +- **Passos anteriores** — `button` com `t::btn_breadcrumb_visitado` e `on_press` com a mensagem de navegação correspondente (`Message::IrParaImportacao` para passo 1, `Message::IrParaConfiguracaoColunas` para passo 2). O hover revela fundo sutil + borda, sinalizando interatividade. +- **Passos futuros** — `container` com texto `TEXT_MUTED`, sem interação (não é possível pular à frente). + --- ## 7. Adicionando uma nova tela diff --git a/src/ui/app.rs b/src/ui/app.rs index fdc2096..be886e0 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -14,7 +14,7 @@ use crate::domain::{ use crate::infrastructure::sqlite::{connection::abrir_banco, migrations::aplicar_migrations}; use crate::ui::message::{Message, ResultadoPendente}; use crate::ui::theme as t; -use iced::widget::{column, container, text, Row}; +use iced::widget::{button, column, container, text, Row}; use iced::{Alignment, Element, Length, Task}; use rusqlite::Connection; use std::path::PathBuf; @@ -788,6 +788,12 @@ impl App { let mut items: Vec> = Vec::new(); for (i, (label, n)) in passos.iter().enumerate() { + let msg_nav = match n { + 1 => Some(Message::IrParaImportacao), + 2 => Some(Message::IrParaConfiguracaoColunas), + _ => None, + }; + let elem: Element<'_, Message> = if *n == passo_ativo { container(text(*label).size(13).color(t::PRIMARY)) .padding([4, 10]) @@ -798,9 +804,19 @@ impl App { }) .into() } else if *n < passo_ativo { - text(*label).size(13).color(t::TEXT_SECONDARY).into() + // Passo já visitado — botão clicável + let mut btn = button(text(*label).size(13)) + .padding([4, 10]) + .style(t::btn_breadcrumb_visitado); + if let Some(msg) = msg_nav { + btn = btn.on_press(msg); + } + btn.into() } else { - text(*label).size(13).color(t::TEXT_MUTED).into() + // Passo futuro — ainda não disponível + container(text(*label).size(13).color(t::TEXT_MUTED)) + .padding([4, 10]) + .into() }; items.push(elem); if i < passos.len() - 1 { diff --git a/src/ui/components/tabela_preview.rs b/src/ui/components/tabela_preview.rs index 6f42838..a2eb50d 100644 --- a/src/ui/components/tabela_preview.rs +++ b/src/ui/components/tabela_preview.rs @@ -82,7 +82,7 @@ pub fn tabela_preview(linhas: &[Vec]) -> Element<'_, Message> { .direction(scrollable::Direction::Horizontal( scrollable::Scrollbar::default(), )) - .height(Length::Fixed(160.0)), + .height(Length::Fixed(200.0)), ) .style(|_theme| iced::widget::container::Style { background: Some(t::SURFACE.into()), diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 21258df..95a1b64 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -288,8 +288,12 @@ pub fn btn_secondary(theme: &Theme, status: button::Status) -> button::Style { background: Some(SURFACE_2.into()), text_color: TEXT, border: Border { + color: Color { + a: 0.55, + ..TEXT_SECONDARY + }, + width: 1.0, radius: 6.0.into(), - ..Default::default() }, ..Default::default() }; @@ -304,6 +308,10 @@ pub fn btn_secondary(theme: &Theme, status: button::Status) -> button::Style { } .into(), ), + border: Border { + color: TEXT_SECONDARY, + ..base.border + }, ..base }, button::Status::Disabled => button::Style { @@ -315,6 +323,10 @@ pub fn btn_secondary(theme: &Theme, status: button::Status) -> button::Style { .into(), ), text_color: Color { a: 0.4, ..TEXT }, + border: Border { + color: Color { a: 0.2, ..BORDER }, + ..base.border + }, ..base }, _ => { @@ -324,13 +336,16 @@ pub fn btn_secondary(theme: &Theme, status: button::Status) -> button::Style { } } -/// Botão ghost (sem fundo, borda sutil) +/// Botão ghost (sem fundo, borda visível) pub fn btn_ghost(theme: &Theme, status: button::Status) -> button::Style { let base = button::Style { background: Some(Color::TRANSPARENT.into()), - text_color: TEXT_SECONDARY, + text_color: TEXT, border: Border { - color: BORDER, + color: Color { + a: 0.55, + ..TEXT_SECONDARY + }, width: 1.0, radius: 6.0.into(), }, @@ -340,12 +355,16 @@ pub fn btn_ghost(theme: &Theme, status: button::Status) -> button::Style { button::Status::Hovered => button::Style { background: Some(Color { a: 0.08, ..PRIMARY }.into()), text_color: TEXT, + border: Border { + color: PRIMARY, + ..base.border + }, ..base }, button::Status::Disabled => button::Style { text_color: TEXT_MUTED, border: Border { - color: Color { a: 0.3, ..BORDER }, + color: Color { a: 0.2, ..BORDER }, ..base.border }, ..base @@ -381,6 +400,33 @@ pub fn btn_danger(theme: &Theme, status: button::Status) -> button::Style { } } +/// Botão de passo anterior no breadcrumb (clicável, mas sem destaque) +pub fn btn_breadcrumb_visitado(_theme: &Theme, status: button::Status) -> button::Style { + let base = button::Style { + background: None, + text_color: TEXT_SECONDARY, + border: Border { + color: Color::TRANSPARENT, + width: 0.0, + radius: 4.0.into(), + }, + ..Default::default() + }; + match status { + button::Status::Hovered => button::Style { + background: Some(Color { a: 0.08, ..TEXT }.into()), + text_color: TEXT, + border: Border { + color: Color { a: 0.15, ..TEXT }, + width: 1.0, + radius: 4.0.into(), + }, + ..base + }, + _ => base, + } +} + /// Botão de aba selecionada (tela selecionar_aba) pub fn btn_aba_ativa(theme: &Theme, status: button::Status) -> button::Style { let base = button::Style {