Pular para conteúdo

0024 — Ações do formulário no topo

Status CONCLUÍDA
Branch spec/0024-acoes-do-formulario-no-topo
Criada em 2026-08-08
Apps afetados web · e2e (api: sem impacto)

Status: RASCUNHO → APROVADA → EM IMPLEMENTAÇÃO → CONCLUÍDA

1. Problema

Quem edita um registro no celular encontra as ações em dois lugares diferentes, dependendo de estar vendo ou editando.

Nas telas de visualização as ações estão no topo, como botões de ícone na barra do app — foi o que as specs 0020 e os dois ajustes de 2026-08-08 firmaram (Imobiliárias e Análise de Crédito). Nas telas de edição elas estão numa barra fixa no rodapé, com rótulo. A mesma pessoa, no mesmo fluxo — abre o registro, toca em Editar — vê a ação mudar de lugar, de forma e de tamanho no meio do caminho.

O padrão do rodapé não é errado em si; o problema é haver dois padrões para a mesma coisa. Hoje são quatro formulários com rodapé fixo:

Tela Ações no rodapé
Imobiliária Cancelar · Salvar
Imóveis Cancelar · Salvar
Inquilinos Cancelar · Salvar
Análise de Crédito três por etapa, mais um texto de dica

2. Objetivo

Que a ação de uma tela esteja sempre no mesmo lugar — o topo —, esteja a pessoa vendo ou editando, em qualquer largura. Um formulário deixa de ter barra de ações própria.

3. Fora de escopo

  • Formulários de autenticação e onboarding (login, registro, verificação, redefinição de senha). Eles não têm barra do app para portalar a ação — são telas de cartão centralizado, sem sider nem topo. O padrão do topo não se aplica.
  • users/form e roles/form. Não estavam no pedido, e ainda usam painel próprio em vez de FormPanel — mexer nas ações antes disso seria consertar a moldura de uma tela que ainda precisa da estrutura. Fica registrado como pendência.
  • Diálogos e modais. As ações de um Dialog pertencem ao próprio diálogo; o topo da página está atrás dele.
  • Rever quais ações cada formulário oferece. A spec move o que existe; não acrescenta "Duplicar", não remove "Cancelar".
  • A ordem das etapas do formulário de Análise de Crédito. Continua com as duas etapas e a mesma navegação entre elas.

4. Regras de negócio

Onde a ação vive

  • RN1 — Formulário não tem barra de ações. As ações vão para as slots tipadas do PageHeader (primaryAction, secondaryAction, tertiaryAction), nos dois viewports. A barra fixa do rodapé sai dos quatro formulários.

  • RN2 — Quem decide a forma é o PageHeader. Rótulo no desktop, botão de ícone de 44×44 no mobile. A página passa o que a ação faz; nunca <Button> cru. É a RN5 da spec 0020, e é a regra que o rodapé destes formulários vinha contornando.

  • RN3 — No mobile a ação é ícone puro, inclusive "Salvar". Sem exceção para a ação principal: o aria-label sai do label, então leitor de tela anuncia "Salvar". O que se perde é o rótulo visível, e o ganho é um topo que se comporta igual em toda tela.

  • RN4 — Teto de três, e a principal por último na fila. O teto e a ordem são os do PageHeader (ver docs/ui-patterns.md): a terceira ação é a mais à esquerda, a principal encosta nos controles do usuário. Os quatro formulários cabem — três ações é o máximo que qualquer um deles mostra.

Comportamento

  • RN5 — O envio continua funcionando pelo teclado. O botão passa a chamar handleSubmit(onSubmit) no onClick — obrigatoriamente, porque o PageHeader portala a ação para fora do <form> e type="submit" deixa de encontrar o formulário.

Correção durante a implementação. Esta regra dizia que "o onSubmit do <form> permanece: Enter num campo salva, como hoje". Está errado, e o e2e mostrou: a submissão implícita do HTML exige um botão submit dentro do formulário, e sem nenhum — com mais de um campo — o Enter não faz nada. Os três formulários passaram a carregar um <button type="submit" hidden> que devolve o comportamento nativo. Ver o Registro.

  • RN6 — Estado de envio preservado. Enquanto salva, a ação fica desabilitada e o rótulo vira "Salvando…" (no mobile, o aria-label). É o que impede o duplo envio hoje.

  • RN7 — Ícone por ação, nomeado pelo verbo. floppy-disk salvar, x cancelar, arrow-right avançar de etapa, caret-left voltar, paper-plane-tilt enviar para análise. Vêm do registry de components/icon.

  • RN8 — A dica do formulário de Análise de Crédito sobe com as ações, e passa a aparecer também no mobile, onde hoje é escondida (display: { xs: "none" }). Saber que faltam dois documentos obrigatórios muda a decisão de enviar, e quem preenche no celular não tinha essa informação.

  • RN9 — Nenhum data-testid de ação muda. São 26 usos na suíte e2e; renomear seria custo sem retorno. agency-form-submit, renter-form-cancel, credit-analysis-form-save-continue e os demais seguem respondendo.

  • RN10 — Nenhuma tela rola na horizontal em 375px, e o título não colapsa. É a medição que a fix 0005 ensinou a fazer: o título é o que perde espaço quando as ações crescem.

5. Impacto por app

apps/api

Sem impacto. Nada de modelo, endpoint ou permissão muda — é composição de tela.

apps/web

  • pages/agencies/form.tsx, pages/renters/form.tsx, pages/properties/form.tsx — as três têm o mesmo par: primaryAction = Salvar (floppy-disk, chamando handleSubmit(onSubmit)), secondaryAction = Cancelar (x). Sai o bloco sticky do rodapé e, com ele, o alpha se ficar sem uso.
  • pages/credit-analyses/CreditAnalysisForm.tsx — três ações por etapa:
  • etapa 1: principal "Gravar e anexar documentos" / "Ir para documentos" (arrow-right), secundária "Salvar rascunho" / "Salvar alterações" (floppy-disk), terceira "Cancelar" (x)
  • etapa 2: principal "Enviar para análise" (paper-plane-tilt), secundária "Concluir depois" (floppy-disk), terceira "Voltar aos dados" (caret-left)
  • a dica de documentos pendentes sai do rodapé e vira uma linha abaixo do topo, visível nos dois viewports (RN8)
  • components/shared/PageHeader.tsx — sem mudança prevista. As três slots e o comportamento por viewport já existem desde 2026-08-08. Se algo faltar, é sinal de que a spec entendeu errado o componente, não de que ele precisa crescer.
  • data-testid: nenhum novo para as ações (RN9). A dica da Análise de Crédito ganha credit-analysis-form-hint, que hoje não é alcançável por teste no mobile porque não existe lá.
  • Responsividade & PWA: é o assunto da spec — os dois viewports no mesmo passo. Sem impacto no service worker além do rebuild.

apps/e2e

  • Specs: as ações mudam de lugar mas não de data-testid, então os 26 usos atuais seguem válidos — e são a rede de segurança desta spec. O cenário novo afere o que os testids não pegam: que os quatro formulários não têm mais barra de rodapé, que a ação está no page-header-actions nos dois viewports, que o título não colapsa em 375px e que a dica da Análise de Crédito aparece no mobile.
  • Seeds: nenhum novo. O setupDraftCreditAnalysis de credit-analyses-mobile.spec.ts já monta o mundo necessário.

6. Contrato de API

Sem mudança. Nenhum endpoint, parâmetro ou payload é tocado.

7. Critérios de aceite

  • [x] CA1. Nos quatro formulários, editando, as ações aparecem no topo — no mobile e no desktop.
  • [x] CA2. Nenhum dos quatro tem barra de ações no rodapé, em nenhum viewport.
  • [x] CA3. No mobile as ações são botões de ícone de 44×44; no desktop, botões com rótulo.
  • [x] CA4. Salvar pelo botão do topo grava — nos quatro.
  • [x] CA5. Enter num campo de texto continua salvando (RN5).
  • [x] CA6. Durante o envio a ação fica desabilitada e não é possível salvar duas vezes.
  • [x] CA7. Cancelar volta para a lista (ou para o detalhe, no caso da Análise de Crédito), sem gravar.
  • [x] CA8. No formulário de Análise de Crédito, cada etapa mostra as três ações da etapa, e a navegação entre etapas funciona pelo topo.
  • [x] CA9. A dica de documentos pendentes aparece no mobile.
  • [x] CA10. Nenhum dos quatro rola na horizontal em 375px, e o título do topo tem mais de 40px (RN10).
  • [x] CA11. Os 26 usos de data-testid de ação da suíte continuam respondendo.
  • [x] CA12. tsc limpo, sem apontamento novo.

8. Plano de implementação

Ordem obrigatória no monorepo: api → web → e2e. Aqui a api não entra.

  • [x] apps/web — agencies/form.tsx: ações no topo, rodapé fora
  • [x] apps/web — renters/form.tsx: idem
  • [x] apps/web — properties/form.tsx: idem
  • [x] apps/web — CreditAnalysisForm.tsx: três ações por etapa e a dica acima do conteúdo
  • [x] docs/ui-patterns.md — a ação do formulário como padrão do topo, e a nota de que type="submit" não sobrevive ao portal
  • [x] apps/e2e — spec dos quatro formulários nos dois viewports
  • [x] tsc e make test (a api não muda, mas a suíte é o piso)
  • [x] /devremote-rebuild + medição nos dois viewports

9. Decisões em aberto

Nenhuma. As três que abriam a spec foram resolvidas antes da escrita:

  • Os dois viewports, não só o mobile. Manter o rodapé no desktop deixaria dois desenhos para a mesma ação — que é exatamente a dor da seção 1, movida de lugar.
  • Ícone puro no mobile, inclusive "Salvar". A alternativa era abrir exceção no PageHeader para a principal de formulário; foi recusada por quebrar a uniformidade do topo em troca de um rótulo.
  • O formulário de Análise de Crédito entra, com a dica subindo junto e ganhando visibilidade no mobile.

10. Registro

O que entrou

Quatro formulários sem barra de rodapé. agencies, renters e properties levaram Salvar (floppy-disk) e Cancelar (x) para as slots do topo. O CreditAnalysisForm levou três ações por etapa — etapa 1: gravar-e-avançar, salvar rascunho, cancelar; etapa 2: enviar para análise, concluir depois, voltar aos dados — e a dica de documentos subiu junto, ganhando credit-analysis-form-hint e visibilidade no mobile, onde era display: { xs: "none" }.

O PageHeader não mudou, como a spec previa: as três slots e a decisão de forma por viewport já existiam. Button ficou órfão nos quatro arquivos e saiu, junto com alpha na agencies/form.

O que divergiu

A RN5 estava errada, e o e2e é que disse. Escrevi que manter o onSubmit do <form> preservaria o Enter. Não preserva: a submissão implícita do HTML exige um botão submit dentro do formulário, e ao mover o Salvar para a barra do app o <form> ficou sem nenhum — com mais de um campo, o Enter deixa de fazer qualquer coisa. Seis cenários passaram e o CA5 falhou, apontando exatamente isso.

A correção é um <button type="submit" aria-hidden="true" tabIndex={-1} style={{ display: "none" }} /> em cada um dos três formulários. O de Análise de Crédito não precisa: ele nunca foi um elemento <form>, e o Enter nunca submeteu lá.

Vale registrar o padrão, porque é o que o docs/ui-patterns.md passou a documentar: ao tirar um botão de dentro do <form>, dois comportamentos que pareciam do React eram do HTML — o type="submit" e a submissão implícita. Os dois se perdem, e só o primeiro é óbvio.

Um corte errado, pego pelo guarda antes de escrever. A primeira tentativa de remover o rodapé usou regex sobre estrutura aninhada e engoliu o corpo inteiro do return em duas telas. O tsc acusou; refiz por número de linha, com asserção de que o bloco começa em <Box, termina em </Box> e tem <Box/</Box> balanceados. A segunda tentativa errou o limite por uma linha e a asserção parou antes de gravar.

Uma linha contraditória no ui-patterns.md. Ele dizia, em outra seção, que "formulários não passam actions ao PageHeader — usam o footer fixo próprio". Corrigida no mesmo passo; documentação que contradiz o código é pior que documentação ausente.

Verificação

  • form-actions.spec.ts — 7 cenários, todos verdes, nos dois viewports
  • 46 cenários dos specs que dirigem esses formulários pelos data-testid (agencies, renters, properties, credit-analyses-form-steps, credit-analyses-mobile, renter-form-mobile, page-header) — a rede de segurança da RN9, intacta
  • tsc limpo
  • pytest: 211 passaram, 1 falhou — a intermitente conhecida (test_partner_agency_search::test_busca_alcanca_quem_esta_fora_da_primeira_pagina, registrada no CLAUDE.md como falhando em ~2 de 3 execuções por empate de created_at no SQLite). A api não foi tocada por esta spec.
  • Medição contra o dev-remote, nos quatro formulários e nos dois viewports:
Título Botão Rodapé fixo Overflow
mobile 375px 96–169px 44px 0 0
desktop 96–177px 32px com rótulo 0 0

CA6 (ação desabilitada durante o envio) é o único critério não coberto por teste: o disabled: isSubmitting está ligado nas quatro telas, e era o comportamento anterior — preservado por construção, não medido.