Pular para conteúdo

0021 — Borda do topo e tom das linhas, iguais nos dois viewports

Status CONCLUÍDA
Branch spec/0021-linhas-e-borda-do-header
Criada em 2026-08-06
Apps afetados web · e2e (api: sem impacto)

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

1. Problema

O mesmo app parece dois produtos conforme o viewport, em dois detalhes que se repetem em toda tela.

1.1 O topo do desktop tem borda em volta e cantos arredondados

No mobile a barra do topo é um filete embaixo e nada mais. No desktop ela tem borda nos quatro lados e raio de 6px, o que produz cantos arredondados e filetes laterais numa faixa que atravessa a tela inteira — moldura onde deveria haver só a divisão entre o topo e o conteúdo.

Não é o AppLayout que pede isso. Ele declara exatamente o que o mobile declara:

sx={{ borderBottom: "1px solid", borderColor: "divider", … }}

A borda extra vem do tema: AppBar do MUI é um Paper por baixo, e o override MuiPaper.root aplica border: 1px solid ${divider} + borderRadius: 6 a todo Paper. O mobile escapa porque a barra dele é um Box puro.

Medido no dev-remote:

Elemento Borda Raio
Desktop header.MuiPaper-root 1px nos quatro lados 6px
Mobile div.MuiBox-root só bottom 0

1.2 O vazamento não para no topo

MuiPaper.root alcança todo componente do MUI que é Paper por dentro. Medido na mesma tela, com o menu do usuário aberto: AppBar e Popover carregam a borda de painel, e o Popover ainda soma isso à própria sombra — moldura dupla num elemento que já se destaca por flutuar.

Dialog, Menu, Drawer, Accordion e Snackbar são Paper pela mesma via. A regra descreve painel de conteúdo e está sendo aplicada a casca, a sobreposição e a barra.

1.3 As linhas de lista têm tons diferentes entre os viewports

Componente Cor Razão
Desktop .MuiDataGrid-cell #C9D4E0 1,50:1 (dividerSubtle)
Mobile RecordCard / MobileListCard #ADB8C8 2,01:1 (divider)

Cada lado escolheu um token, e o resultado é que a mesma lista tem peso visual diferente conforme o aparelho. A escolha do mobile não foi arbitrária — pelo fix 0004, cartão é painel e painel é 2:1. Mas o cartão de uma lista é a linha daquela lista: ele repete por registro, não delimita uma seção de conteúdo. Tratá-lo como painel foi ler a estrutura do DOM em vez da função na tela.

2. Objetivo

Que a divisão entre topo e conteúdo, e o peso das linhas de lista, sejam os mesmos no desktop e no mobile — e que a regra de borda do tema deixe de alcançar componente que não é painel.

3. Fora de escopo

  • Mudar a altura das barras. 64px no mobile e 56px no desktop continuam (specs 0016 e 0020).
  • Rever as réguas de contraste. Os três níveis do fix 0004 — campo 3:1, painel 2:1, linha 1,5:1 — continuam valendo. Esta spec corrige qual token cada elemento usa, não o valor dos tokens.
  • Unificar DataGrid e cartões num componente só. A lista muda de forma entre viewports por decisão das specs 0013 e 0016; aqui só o tom do separador se alinha.
  • Sombra e elevação. Dialog, Popover e Menu mantêm as sombras que já têm; o que sai é a borda que o tema empurrava por cima delas.
  • Telas fora da casca (login, register, onboarding, …). Não têm barra de app.

4. Regras de negócio

  • RN1 — A barra do topo tem borda só embaixo, sem raio, nos dois viewports. Mesma cor (divider), mesma espessura, mesmo desenho. É divisão entre duas áreas, não moldura de um objeto.

  • RN2 — A borda de painel do tema vale para painel de conteúdo, não para casca. Card e Paper de conteúdo continuam com borda e raio; AppBar, Popover, Menu, Dialog e Drawer deixam de recebê-los. Cada um desses já se separa do fundo por outro meio — a barra por posição, os demais por sombra —, e somar borda a isso é dizer a mesma coisa duas vezes.

  • RN3 — Linha de lista usa dividerSubtle (1,5:1) nos dois viewports. Vale para a célula do DataGrid e para a borda do cartão de lista no mobile. O cartão que se repete por registro é a linha daquela lista, não um painel: a função na tela decide o token, não o componente que o DOM usa.

  • RN4 — Painel de conteúdo continua em divider (2:1). FormPanel, cartões de detalhe e blocos de seção não mudam. A RN3 fala do cartão de lista; um painel continua sendo painel.

  • RN5 — A varredura é por rota, não por arquivo. A verificação percorre as rotas internas do App.tsx e afere o topo em cada uma. Foi por globar nome de arquivo que a spec 0020 deixou sete telas para trás.

  • RN6 — A regra vale nos dois temas. Claro e escuro têm valores diferentes e o mesmo desenho.

5. Impacto por app

apps/api

Sem impacto. Nenhum modelo, endpoint, schema ou migration.

apps/web

design-system/theme/components.ts — o centro da correção:

  • MuiPaper.root deixa de aplicar borda e raio incondicionalmente. A borda de painel passa a valer para Card/Paper de conteúdo, e AppBar, Popover, Menu, Dialog e Drawer ficam sem ela (RN2).
  • MuiAppBar ganha o desenho da RN1 explicitamente, para não depender de ausência.

design-system/theme/dataGrid.ts — já usa dividerSubtle na célula; sem mudança.

Componentes de lista do mobile (RN3):

Arquivo Hoje
components/shared/RecordCard.tsx borderColor: "divider" no cartão e no rodapé de ações
components/shared/MobileListCard.tsx idem

components/app-layout/index.tsx — confere que o borderBottom que já existe é o único, agora que o tema não empurra os outros três.

data-testid: nenhum novo. O topo já publica page-header, e a barra do mobile, mobile-topbar; falta um no desktop para a varredura da RN5 — entra app-topbar no AppBar.

Responsividade & PWA: é o assunto da spec — as duas cascas passam a desenhar igual. Sem impacto no service worker além do rebuild.

apps/e2e

  • Spec: cenário que percorre as rotas internas (RN5) e afere, nos dois viewports, que a barra do topo tem borda só embaixo e raio 0 (RN1), que a cor do filete é a mesma nos dois (RN1) e que a linha de lista mede o mesmo tom (RN3). Cobre também que Popover e Dialog não têm borda (RN2).
  • Seeds: nenhum. As telas existentes bastam.

6. Contrato de API

Sem alteração. Nenhum endpoint criado, removido ou modificado.

7. Critérios de aceite

  • [x] CA1. Em toda rota interna, nos dois viewports, a barra do topo tem border-radius: 0 e borda apenas em bottom.
  • [x] CA2. A cor do filete do topo é idêntica no desktop e no mobile.
  • [x] CA3. AppBar, Popover, Menu, Dialog e Drawer não têm borda vinda do tema.
  • [x] CA4. Card/Paper de conteúdo — FormPanel, painéis de detalhe — continuam com borda e raio.
  • [x] CA5. A linha de lista mede o mesmo tom nos dois viewports: célula do DataGrid e cartão de lista do mobile, ambos em dividerSubtle.
  • [x] CA6. Nenhuma tela rola na horizontal em 375px.
  • [x] CA7. A varredura do e2e percorre as rotas do App.tsx, não uma lista de arquivos.
  • [x] CA8. tsc limpo, sem apontamento novo nos arquivos tocados.

8. Plano de implementação

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

  • [x] apps/api — sem impacto, nada a fazer
  • [x] apps/web — MuiPaper restrito a painel; MuiAppBar com o desenho da RN1
  • [x] apps/web — RecordCard e MobileListCard em dividerSubtle (RN3)
  • [x] apps/web — data-testid="app-topbar" no AppBar do desktop
  • [x] docs/design-tokens.md — a nota dos três níveis ganha "onde cada um se aplica"
  • [x] apps/e2e — spec varrendo as rotas nos dois viewports
  • [x] make test e tsc
  • [x] /devremote-rebuild + medição nos dois temas e nos dois viewports

9. Decisões em aberto

Nenhuma. As duas que abriam a spec foram resolvidas antes da escrita:

  • Tom da linha de lista: o do desktop (1,50:1) vira o padrão, e o cartão do mobile se alinha a ele — mantendo a decisão do fix 0004 em vez de desfazê-la.
  • Vazamento do MuiPaper: corrigir na origem, restringindo a regra ao que é painel, em vez de neutralizar só no header.

10. Registro

O que mudou, medido

Antes Depois
Topo do desktop borda nos 4 lados, raio 6px filete só embaixo, raio 0
Topo do mobile filete embaixo, raio 0 inalterado
Cor do filete igual nos dois igual nos dois (confirmado)
Linha de lista desktop #C9D4E0 · mobile #ADB8C8 #C9D4E0 nos dois
Popover borda + sombra só sombra

O que divergiu do planejado

A primeira tentativa introduziu um defeito silencioso — de especificidade.

Para excluir as cascas, envolvi a regra do MuiPaper num &:not(.MuiAppBar-root):not(.MuiPopover-paper)… com sete classes. Funcionou para o header, e quebrou tudo o mais: cada :not() de classe soma especificidade, então a regra saiu de 0,1,0 para 0,8,0 e passou a vencer o sx de qualquer componente que definisse a própria borda.

O sintoma foi o RecordCard, que eu tinha acabado de mudar para dividerSubtle e continuava saindo em divider — sem erro, sem aviso, com o código-fonte dizendo uma coisa e o pixel outra. Só o cenário de comparação entre viewports pegou.

A forma certa é manter a regra do Paper rasa e excluir cada casca no próprio slot (MuiAppBar, MuiPopover, MuiMenu, MuiDialog, MuiDrawer): mesma especificidade, e a ordem de composição do MUI resolve. Fica registrado porque é uma armadilha reaproveitável: elevar especificidade num override de tema é sequestrar o sx de todo consumidor.

O AppLayout perdeu o borderBottom local. Com o tema declarando o desenho da barra, manter a mesma regra nos dois lugares é convite a divergirem — a spec 0020 já mostrou o que acontece quando a decisão mora em dois lugares.

Verificação

  • make test: 206 passaram — nenhum arquivo da API tocado.
  • tsc: limpo.
  • chrome-borders.spec.ts: 6/6 em chromium, incluindo a comparação entre viewports, que é o cenário que pegou o defeito de especificidade.
  • Medido no dev-remote: topo sem raio e sem bordas laterais nas 16 rotas internas, nos dois viewports; sem rolagem horizontal em 390px.
  • Suíte e2e completa: a executar antes do merge.