Pular para conteúdo

0011 — Gaveta de navegação do mobile conforme o design system 1.5.0

Status CONCLUÍDA
Branch spec/0011-gaveta-de-navegacao-mobile
Criada em 2026-08-02
Apps afetados api (sem impacto) · web · e2e

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

1. Problema

Quem usa o Garantora no celular navega por um menu que não foi dimensionado para o polegar nem para o tamanho que o produto alcançou. São 16 destinos em quatro grupos, e três coisas atrapalham quem está em pé, com uma mão:

  • O rodapé de conta compete com a lista. Cabeçalho e rodapé da gaveta não são declarados como blocos fixos; num aparelho baixo, com a lista inteira, eles encolhem e o rodapé (avatar, tema, sair) sobe por cima dos últimos destinos e encosta na barra de abas. É o erro que o design system nomeia como o mais fácil de cometer nesta peça.
  • Os alvos de toque erram. A linha de destino tem ~40px e os botões de ícone do rodapé ~34px. O toque escorrega para o vizinho, e o item ativo não se distingue de relance porque a pílula usa um tingimento ad-hoc em vez do tom da marca.
  • No tema escuro o logo do cabeçalho é o claro. O arquivo logo-dark.svg está no projeto desde 2026-08-01 e nenhuma tela o usa — o logo colorido para fundo claro aparece sobre o slate escuro.

Nada disso é opinião nossa: a versão 1.5.0 do @garantora/design-system (2026-08-02) adicionou o MobileNavDrawer justamente para fixar essas decisões, e o app está pinado na 1.4.3, uma versão anterior à existência da peça.

2. Objetivo

A gaveta de navegação do mobile passa a ser a peça que o design system define: três regiões independentes (só o miolo rola), alvos de toque de 48px, item ativo com o tom da marca e logo que acompanha o tema — com o menu inteiro alcançável em aparelho pequeno.

3. Fora de escopo

  • Barra de abas inferior e top bar. A 1.5.0 não alterou TabBar nem MobileAppBar; as duas seguem como estão. A única mudança que encosta na top bar é consequência da RN8 (o avatar deixa de abrir a gaveta).
  • Sider do desktop. Continua com o layout atual. O módulo logo fica ciente do tema (RN7) e o desktop herda isso, mas nenhuma outra decisão de navegação desktop entra aqui.
  • Reorganizar os destinos. Quais itens existem, em que grupos e em que ordem segue definido por buildNavGroups — esta spec muda como a linha se apresenta e se comporta, não o mapa de navegação. O modelo-alvo de 10 papéis area-scoped do navigation-brief.md é assunto de outra spec.
  • Trocar os ícones por Material Symbols Rounded. O design system de referência usa ligaduras (description, task_alt); o app usa @mui/icons-material, que é o equivalente em produção. Trocar a fonte de ícones é mudança transversal ao app.
  • Adotar o MobileNavDrawer.jsx do pacote. Ele é referência visual — em produção vale MUI + o tema, como está escrito no theme/README.md do próprio pacote.
  • API. Nenhuma mudança de contrato.

4. Regras de negócio

  • RN1. A gaveta tem três regiões independentes: cabeçalho e rodapé fixos (flex: 0 0 auto) e miolo rolável (flex: 1; min-height: 0; overflow-y: auto). Com os 16 destinos abertos num viewport de 640px de altura, cabeçalho e rodapé permanecem visíveis e íntegros, e apenas a lista rola.
  • RN2. Linha de destino com altura mínima de 48px; botões de ícone do cabeçalho e do rodapé com 44×44px.
  • RN3. O item ativo recebe pílula tingida com theme.garantora.surfaceNavActive, ícone e rótulo em primary.main (azul structure) e peso 600.
  • RN4. O toque em qualquer linha tinge o fundo com theme.garantora.surfaceRowHover enquanto o dedo está pressionado.
  • RN5. Item comingSoon fica a 50% de opacidade, sem toque (não navega, não fecha a gaveta) e com o distintivo "Em breve" no padrão SoonBadge: altura 20px, raio radius.sm, fundo divider, texto text.secondary em 11px/500. O produto mostra o mapa inteiro em vez de esconder o que ainda não entregou.
  • RN6. O contador de fila usa o padrão CountBadge: pílula de 20px de altura, fundo palette.highlight.main (âmbar) e texto highlight.contrastText (quase-preto), 11px/700. Substitui o warning.main usado hoje — o âmbar é a cor de "aja agora" do design system, e é ela que o pacote aplica em contador de trabalho na fila.
  • RN7. O logo do cabeçalho acompanha o tema: logo.svg no claro, logo-dark.svg no escuro. A escolha vive no módulo components/logo, que é a fonte única dos ~10 lugares que exibem o logo — não no cabeçalho da gaveta.
  • RN8. A gaveta abre somente pela aba "Mais". O avatar da top bar deixa de abri-la; conta, tema e sair passam a ter um caminho único, o rodapé da gaveta. O avatar sai da top bar em vez de virar enfeite não clicável — botão que não faz nada é pior que botão ausente.
  • RN9. Escolher um destino fecha a gaveta. Tocar no véu também fecha. Tocar dentro da gaveta não fecha.
  • RN10. A gaveta é camada acima do app e cobre a barra de abas.
  • RN11. Nenhum papel muda de visibilidade: a gaveta segue exibindo exatamente os grupos e itens que buildNavGroups devolve para o usuário logado, os mesmos da sider desktop.
  • RN12. O package.json do web passa a pinar #v1.5.0. O diff 1.4.3→1.5.0 não toca theme/, único ponto que o app importa do pacote — o pino existe para que a versão fixada seja a mesma cuja definição o código segue.

5. Impacto por app

apps/api

Sem impacto. Nenhum modelo, endpoint, migration ou regra de service muda. A navegação é montada no cliente a partir das permissões que /auth/me já devolve.

apps/web

  • Páginas/componentes:
  • src/components/mobile-shell/index.tsx — a gaveta é reescrita conforme RN1–RN10. Vale extrair a linha para um DrawerItem local, espelhando o DrawerItem do pacote: é ele que concentra alvo de toque, pílula ativa, estado de toque, soon e badge.
  • src/components/logo/index.tsx — LOGO_SRC deixa de ser constante e passa a depender do modo (RN7). Como é const exportada e consumida em ~10 lugares, a forma menos invasiva é manter o nome para o claro e expor um hook/componente ciente do tema, ajustando os consumidores que renderizam sobre fundo escuro.
  • package.json / pnpm-lock.yaml — pino em #v1.5.0 (RN12).
  • data-testid: os atributos atuais são preservados, porque o e2e existente depende deles: mobile-drawer, mobile-drawer-close, mobile-nav-<path>, mobile-logout, mobile-tab-mais, mobile-topbar. Sai mobile-drawer-open (o avatar deixa de abrir a gaveta, RN8) — nenhum spec o usa hoje. Entra mobile-drawer-theme-toggle no botão de tema do rodapé, que hoje não tem testid.
  • Sider: sem módulo novo — App.tsx e custom-sider não mudam.
  • Responsividade & PWA: a mudança é exclusivamente mobile (< 600px, o useIsMobile). O desktop só recebe o logo ciente do tema. PWA: o bundle é servido pelo service worker no dev-remote, então validar a mudança exige rebuild do container web e reabrir a tela instalada para furar o cache — não há mudança de manifest nem de estratégia de cache.

apps/e2e

  • Specs: estender tests/e2e/mobile-shell.spec.ts, que já cobre abrir a gaveta pela aba "Mais" e navegar por ela. Cenários novos:
  • com a gaveta aberta num viewport baixo, cabeçalho e rodapé seguem visíveis e a lista rola (RN1);
  • item "Em breve" não navega nem fecha a gaveta (RN5);
  • o avatar da top bar não abre mais a gaveta (RN8);
  • tocar no véu fecha (RN9).
  • Seeds: sem dados novos — o seed seed_e2e já cria os usuários e os destinos aparecem por permissão.

6. Contrato de API

Sem mudança de contrato. Nenhum endpoint criado, alterado ou removido.

7. Critérios de aceite

  • [x] CA1. Dado um viewport de 375×640 com a gaveta aberta, quando eu rolo a lista de destinos até o fim, então o cabeçalho (logo + fechar) e o rodapé (avatar, tema, sair) permanecem visíveis e sem sobreposição, e nenhum destino fica inacessível.
  • [x] CA2. Toda linha de destino mede ao menos 48px de altura, e os botões de ícone do cabeçalho e do rodapé medem ao menos 44×44px.
  • [x] CA3. O destino da rota atual aparece com pílula tingida, ícone e rótulo em azul structure e peso 600 — nos temas claro e escuro.
  • [x] CA4. Tocar em "Cobranças" ou "Sinistros" (itens "Em breve") não navega e não fecha a gaveta; os dois aparecem a meia opacidade com o distintivo.
  • [x] CA5. Com análises pendentes, "Análise de Crédito" mostra o contador em âmbar com texto escuro.
  • [x] CA6. No tema escuro, o logo do cabeçalho é o logo-dark.svg; no claro, o logo.svg.
  • [x] CA7. A aba "Mais" abre a gaveta; o avatar da top bar não a abre (e não está mais lá). Escolher um destino fecha a gaveta; tocar no véu fecha; tocar dentro não fecha.
  • [x] CA8. A gaveta aberta cobre a barra de abas inferior.
  • [x] CA9. O package.json pina #v1.5.0 e o tsc do web passa limpo.
  • [x] CA10. Em 375px, nenhuma tela ganha rolagem horizontal (scrollWidth - clientWidth == 0) com a gaveta aberta e fechada.

8. Plano de implementação

  • [x] apps/api — sem impacto (nenhum model, migration, service ou router)
  • [x] apps/web — pino do @garantora/design-system em #v1.5.0
  • [x] apps/web — components/logo ciente do tema, e consumidores sobre fundo escuro ajustados
  • [x] apps/web — gaveta em mobile-shell: três regiões, alvos de toque, pílula ativa, estado de toque, soon, contador (RN1–RN6, RN9, RN10)
  • [x] apps/web — avatar deixa de abrir a gaveta e sai da top bar (RN8)
  • [x] apps/web — data-testid preservados + mobile-drawer-theme-toggle
  • [x] apps/e2e — cenários novos em mobile-shell.spec.ts
  • [x] make test (segue igual — a mudança é só de web) e make lint sem apontamento novo nos arquivos tocados
  • [x] Validado no browser em 375×640 e 375×812, nos temas claro e escuro, com script de screenshot
  • [x] Reabrir a tela instalada depois do rebuild do web, para furar o cache do service worker

9. Decisões em aberto

Nenhuma. As quatro que existiam foram decididas na entrevista de abertura:

  • Escopo restrito à gaveta (não à casca mobile inteira).
  • Pino sobe para #v1.5.0.
  • Gaveta abre somente pela aba "Mais" (RN8).
  • Logo ciente do tema vive no módulo logo, com efeito também no desktop (RN7).

10. Registro

Implementada em 2026-08-02. O que divergiu do planejado, e por quê:

O módulo logo ganhou um hook, não um LOGO_SRC variável. A spec falava em "LOGO_SRC deixa de ser constante". Não dá: é uma const de módulo lida em tempo de import por 7 telas de acesso, que forçam o tema claro — torná-la dependente do modo exigiria um ThemeProvider que ali não governa o fundo. A forma que resolve sem quebrar nada: LOGO_SRC passa a significar explicitamente "assinatura para fundo claro", entra LOGO_DARK_SRC, e a escolha vive em useLogoSrc(). Só o SidebarTitle consome o hook — e ele é justamente o componente compartilhado entre a sider do desktop e o cabeçalho da gaveta, os dois lugares do app com tema alternável. As telas de acesso não foram tocadas.

O painel de marca das telas de acesso continua com o truque do filtro. login e register exibem o logo claro sobre fundo escuro com filter: brightness(0) invert(1). Agora que existe LOGO_DARK_SRC, o truque é substituível — mas é superfície de tela de acesso, que a RN7 não cobre, e trocar sem olhar o resultado nas quatro telas seria mudança cega. Fica anotado como dívida.

A linha da gaveta virou um DrawerItem local, com onPointer*. O estado de toque da RN4 não existe no MUI (:hover não serve em touch, e :active não cobre o arrasto). onPointerDown/Up/Leave/Cancel cobrem dedo e mouse com o mesmo código. O soon usa disabled no <button>, o que dá de graça o "não navega" da RN5 — o spec e2e precisa de click({ force: true }) para provar isso, e o comentário no teste explica.

CA5 foi verificado com a fila vazia. O tenant de e2e não tem análise pendente, e o NavBadgesProvider começa em zero e só é preenchido pelo Painel. Para medir o que a gaveta renderiza, o script de screenshot interceptou /operations-center/overview e injetou pending_analysis: 3 — sem tocar no app. O contador saiu #FFA94D sobre #1A1A1A no claro e #FFB866 sobre #1A1A1A no escuro, 20px de altura: exatamente os tokens highlight e a geometria do CountBadge.

Medições da validação (375×640 e 375×812, claro e escuro, com a lista rolada até o fim): gaveta de 300px, 15 destinos, menor linha 48,0px, botões 44×44, miolo rolável com cabeçalho e rodapé imóveis, último destino dentro do miolo, z-index 1200 contra 0 da barra de abas, zero rolagem horizontal. Nenhum erro de console além do websocket de HMR.

Fora do que a spec previa: Divider e alpha saíram dos imports do mobile-shell (ficaram órfãos com a reescrita do rodapé e da pílula).

Pendência conhecida: o logo-dark.svg só chega ao dev-remote depois do rebuild da imagem do web — o service worker serve o bundle cacheado. Último item do plano.

e2e executado a pedido, e a primeira rodada pegou uma regressão que nada mais pegaria. Duas falhas, ambas de teste:

  • mobile-shell.spec.ts da spec 0004 clicava em mobile-nav-billings sem force. O item "Em breve" agora é <button disabled> — é assim que "sem toque" (RN5) se escreve em HTML — e o Playwright esperava 30s por um botão que nunca habilita. A garantia do teste não mudou; ficou mais forte, porque antes o clique acontecia e nada ocorria, e agora o clique não é possível. Corrigido com click({ force: true }) e o porquê no comentário.
  • O CA7 novo clicava no centro do véu, que cai dentro da gaveta de 300px. Passou a clicar na faixa descoberta (x: 350).

Nem o tsc nem os screenshots pegariam a primeira: é comportamento de teste contra um atributo HTML novo. Vale como argumento a favor de rodar a suíte no fechamento.

Segunda rodada: 211 passaram, 6 skipped, 0 falhas (9,6 min, chromium + firefox + webkit), contra o dev-remote já com a imagem do web reconstruída — o que também fecha o último item do plano, o cache do service worker.