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`.
|
||||
@@ -138,7 +138,8 @@ fn caixa_modal<'a>(
|
||||
col = col.push(botoes);
|
||||
|
||||
container(col.align_x(Alignment::Start))
|
||||
.width(Length::Fixed(420.0))
|
||||
.max_width(420)
|
||||
.width(Length::Fill)
|
||||
.padding(24)
|
||||
.style(t::card)
|
||||
.into()
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn tabela_preview(linhas: &[Vec<String>]) -> Element<'_, Message> {
|
||||
.size(12)
|
||||
.color(t::TEXT_SECONDARY),
|
||||
)
|
||||
.width(Length::Fixed(120.0))
|
||||
.width(Length::Fixed(100.0))
|
||||
.padding([4, 6])
|
||||
.into()
|
||||
})
|
||||
@@ -63,7 +63,7 @@ pub fn tabela_preview(linhas: &[Vec<String>]) -> Element<'_, Message> {
|
||||
celula.to_string()
|
||||
};
|
||||
container(text(truncado).font(Font::MONOSPACE).size(11).color(t::TEXT))
|
||||
.width(Length::Fixed(120.0))
|
||||
.width(Length::Fixed(100.0))
|
||||
.padding([3, 6])
|
||||
.into()
|
||||
})
|
||||
|
||||
@@ -59,7 +59,7 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
text("Layout:")
|
||||
.size(13)
|
||||
.color(t::TEXT_SECONDARY)
|
||||
.width(Length::Fixed(80.0)),
|
||||
.width(Length::Shrink),
|
||||
pick_list(opcoes_layout, nome_layout_sel, {
|
||||
let layouts = app.layouts_salvos.clone();
|
||||
move |nome_selecionado: String| {
|
||||
@@ -76,7 +76,7 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
})
|
||||
.placeholder("— Selecionar layout —")
|
||||
.style(input_style_pick)
|
||||
.width(Length::Fixed(260.0)),
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center),
|
||||
@@ -251,7 +251,7 @@ fn view_csv(app: &App) -> Element<'_, Message> {
|
||||
Message::DelimitadorAlterado(c)
|
||||
})
|
||||
.style(input_style_pick)
|
||||
.width(Length::Fixed(200.0))
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
),
|
||||
campo_row(
|
||||
@@ -262,7 +262,7 @@ fn view_csv(app: &App) -> Element<'_, Message> {
|
||||
Message::EncodingAlterado
|
||||
)
|
||||
.style(input_style_pick)
|
||||
.width(Length::Fixed(200.0))
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
),
|
||||
campo_row(
|
||||
@@ -274,7 +274,7 @@ fn view_csv(app: &App) -> Element<'_, Message> {
|
||||
.unwrap_or(Message::Noop)
|
||||
})
|
||||
.style(t::input_dark)
|
||||
.width(Length::Fixed(80.0))
|
||||
.width(Length::Fixed(90.0))
|
||||
.into(),
|
||||
),
|
||||
secao_subtitulo("Mapeamento de colunas (índice base 0)"),
|
||||
@@ -287,7 +287,7 @@ fn view_csv(app: &App) -> Element<'_, Message> {
|
||||
.unwrap_or(Message::Noop)
|
||||
})
|
||||
.style(t::input_dark)
|
||||
.width(Length::Fixed(80.0))
|
||||
.width(Length::Fixed(90.0))
|
||||
.into(),
|
||||
),
|
||||
campo_row(
|
||||
@@ -299,7 +299,7 @@ fn view_csv(app: &App) -> Element<'_, Message> {
|
||||
.unwrap_or(Message::Noop)
|
||||
})
|
||||
.style(t::input_dark)
|
||||
.width(Length::Fixed(80.0))
|
||||
.width(Length::Fixed(90.0))
|
||||
.into(),
|
||||
),
|
||||
campo_indice_opcional_csv(
|
||||
@@ -341,7 +341,7 @@ fn view_xlsx(app: &App) -> Element<'_, Message> {
|
||||
text_input("Nome da aba", &c.aba)
|
||||
.on_input(Message::AbaXlsxAlterada)
|
||||
.style(t::input_dark)
|
||||
.width(Length::Fixed(200.0))
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
@@ -354,7 +354,7 @@ fn view_xlsx(app: &App) -> Element<'_, Message> {
|
||||
"Aba",
|
||||
pick_list(app.abas_xlsx.clone(), aba_sel, Message::AbaXlsxAlterada)
|
||||
.style(input_style_pick)
|
||||
.width(Length::Fixed(200.0))
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
)
|
||||
};
|
||||
@@ -416,7 +416,7 @@ fn campo_row<'a>(label: &'a str, input: Element<'a, Message>) -> Element<'a, Mes
|
||||
text(label)
|
||||
.size(13)
|
||||
.color(t::TEXT_SECONDARY)
|
||||
.width(Length::Fixed(220.0)),
|
||||
.width(Length::FillPortion(3)),
|
||||
input,
|
||||
]
|
||||
.spacing(10)
|
||||
@@ -437,11 +437,11 @@ fn campo_indice_opcional_csv(
|
||||
|
||||
if ativo {
|
||||
row![
|
||||
cb.width(Length::Fixed(220.0)),
|
||||
cb.width(Length::FillPortion(3)),
|
||||
text_input("0", &val_str)
|
||||
.on_input(move |s| { s.parse::<usize>().map(&msg_valor).unwrap_or(Message::Noop) })
|
||||
.style(t::input_dark)
|
||||
.width(Length::Fixed(80.0)),
|
||||
.width(Length::Fixed(90.0)),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center)
|
||||
@@ -461,7 +461,7 @@ fn campo_letra_linha<'a>(
|
||||
text_input("ex: B3", valor)
|
||||
.on_input(msg)
|
||||
.style(t::input_dark)
|
||||
.width(Length::Fixed(100.0))
|
||||
.width(Length::Fixed(110.0))
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
@@ -479,11 +479,11 @@ fn campo_letra_linha_opcional<'a>(
|
||||
|
||||
if ativo {
|
||||
row![
|
||||
cb.width(Length::Fixed(220.0)),
|
||||
cb.width(Length::FillPortion(3)),
|
||||
text_input("ex: B3", &val_str)
|
||||
.on_input(msg_valor)
|
||||
.style(t::input_dark)
|
||||
.width(Length::Fixed(100.0)),
|
||||
.width(Length::Fixed(110.0)),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::ui::app::App;
|
||||
use crate::ui::message::Message;
|
||||
use crate::ui::theme as t;
|
||||
use iced::widget::{button, column, container, pick_list, row, text};
|
||||
use iced::widget::{button, column, container, pick_list, row, scrollable, text};
|
||||
use iced::{Alignment, Element, Length};
|
||||
|
||||
/// Tela de importação de arquivos.
|
||||
@@ -110,7 +110,7 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
text("Layout salvo:")
|
||||
.size(13)
|
||||
.color(t::TEXT_SECONDARY)
|
||||
.width(Length::Fixed(110.0)),
|
||||
.width(Length::Shrink),
|
||||
pick_list(opcoes_layout, nome_layout_sel, {
|
||||
let layouts = app.layouts_salvos.clone();
|
||||
move |nome_selecionado: String| {
|
||||
@@ -173,7 +173,7 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
.width(Length::Fill);
|
||||
|
||||
// ── Layout geral ──────────────────────────────────────────────────────────
|
||||
container(
|
||||
container(scrollable(
|
||||
column![
|
||||
text("Importar Arquivo").size(20).color(t::TEXT),
|
||||
text("Selecione e configure sua planilha para análise")
|
||||
@@ -185,7 +185,7 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
.align_x(Alignment::Center)
|
||||
.padding([32, 20])
|
||||
.width(Length::Fill),
|
||||
)
|
||||
))
|
||||
.style(t::fundo)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
|
||||
@@ -31,22 +31,27 @@ pub fn view<'a>(app: &'a App, resultado: &'a ResultadoAnalise) -> Element<'a, Me
|
||||
let botoes_topo = row![
|
||||
button("< Nova Análise")
|
||||
.on_press(Message::NovaAnalise)
|
||||
.style(t::btn_secondary),
|
||||
button("Reconfigurar Colunas")
|
||||
.style(t::btn_secondary)
|
||||
.width(Length::Shrink),
|
||||
button("Reconfigurar")
|
||||
.on_press(Message::IrParaConfiguracaoColunas)
|
||||
.style(t::btn_ghost),
|
||||
button("Reanalisar Arquivo")
|
||||
.style(t::btn_ghost)
|
||||
.width(Length::Shrink),
|
||||
button("Reanalisar")
|
||||
.on_press_maybe(
|
||||
app.caminho_arquivo
|
||||
.as_ref()
|
||||
.map(|_| Message::ReanalisarArquivo)
|
||||
)
|
||||
.style(t::btn_ghost),
|
||||
.style(t::btn_ghost)
|
||||
.width(Length::Shrink),
|
||||
button("Exportar PDF")
|
||||
.on_press(Message::ExportarPdf)
|
||||
.style(t::btn_primary),
|
||||
.style(t::btn_primary)
|
||||
.width(Length::Shrink),
|
||||
]
|
||||
.spacing(8);
|
||||
.spacing(8)
|
||||
.wrap();
|
||||
|
||||
// ── Controle de itens por página ──────────────────────────────────────────
|
||||
let opcoes_por_pagina = row(OPCOES_PAGINA
|
||||
|
||||
Reference in New Issue
Block a user