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:
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
DataGride 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,PopovereMenumantê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.
CardePaperde conteúdo continuam com borda e raio;AppBar,Popover,Menu,DialogeDrawerdeixam 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 doDataGride 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.tsxe 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.rootdeixa de aplicar borda e raio incondicionalmente. A borda de painel passa a valer paraCard/Paperde conteúdo, eAppBar,Popover,Menu,DialogeDrawerficam sem ela (RN2).MuiAppBarganha 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
PopovereDialognã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: 0e borda apenas embottom. - [x] CA2. A cor do filete do topo é idêntica no desktop e no mobile.
- [x] CA3.
AppBar,Popover,Menu,DialogeDrawernão têm borda vinda do tema. - [x] CA4.
Card/Paperde 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
DataGride cartão de lista do mobile, ambos emdividerSubtle. - [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.
tsclimpo, 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—MuiPaperrestrito a painel;MuiAppBarcom o desenho da RN1 - [x]
apps/web—RecordCardeMobileListCardemdividerSubtle(RN3) - [x]
apps/web—data-testid="app-topbar"noAppBardo 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 testetsc - [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.