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/formeroles/form. Não estavam no pedido, e ainda usam painel próprio em vez deFormPanel— 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
Dialogpertencem 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-labelsai dolabel, 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(verdocs/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)noonClick— obrigatoriamente, porque oPageHeaderportala a ação para fora do<form>etype="submit"deixa de encontrar o formulário.
Correção durante a implementação. Esta regra dizia que "o
onSubmitdo<form>permanece: Enter num campo salva, como hoje". Está errado, e o e2e mostrou: a submissão implícita do HTML exige um botãosubmitdentro 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-disksalvar,xcancelar,arrow-rightavançar de etapa,caret-leftvoltar,paper-plane-tiltenviar para análise. Vêm do registry decomponents/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-testidde 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-continuee 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, chamandohandleSubmit(onSubmit)),secondaryAction= Cancelar (x). Sai o blocostickydo rodapé e, com ele, oalphase 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 ganhacredit-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á nopage-header-actionsnos 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
setupDraftCreditAnalysisdecredit-analyses-mobile.spec.tsjá 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-testidde ação da suíte continuam respondendo. - [x] CA12.
tsclimpo, 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 quetype="submit"não sobrevive ao portal - [x]
apps/e2e— spec dos quatro formulários nos dois viewports - [x]
tscemake 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
PageHeaderpara 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 tsclimpopytest: 211 passaram, 1 falhou — a intermitente conhecida (test_partner_agency_search::test_busca_alcanca_quem_esta_fora_da_primeira_pagina, registrada noCLAUDE.mdcomo falhando em ~2 de 3 execuções por empate decreated_atno 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.