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.svgestá 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
TabBarnemMobileAppBar; 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
logofica 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 donavigation-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.jsxdo pacote. Ele é referência visual — em produção vale MUI + o tema, como está escrito notheme/README.mddo 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 emprimary.main(azul structure) e peso 600. - RN4. O toque em qualquer linha tinge o fundo com
theme.garantora.surfaceRowHoverenquanto o dedo está pressionado. - RN5. Item
comingSoonfica a 50% de opacidade, sem toque (não navega, não fecha a gaveta) e com o distintivo "Em breve" no padrãoSoonBadge: altura 20px, raioradius.sm, fundodivider, textotext.secondaryem 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, fundopalette.highlight.main(âmbar) e textohighlight.contrastText(quase-preto), 11px/700. Substitui owarning.mainusado 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.svgno claro,logo-dark.svgno escuro. A escolha vive no módulocomponents/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
buildNavGroupsdevolve para o usuário logado, os mesmos da sider desktop. - RN12. O
package.jsondo web passa a pinar#v1.5.0. O diff 1.4.3→1.5.0 não tocatheme/, ú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 umDrawerItemlocal, espelhando oDrawerItemdo pacote: é ele que concentra alvo de toque, pílula ativa, estado de toque,soone badge.src/components/logo/index.tsx—LOGO_SRCdeixa de ser constante e passa a depender do modo (RN7). Como éconstexportada 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. Saimobile-drawer-open(o avatar deixa de abrir a gaveta, RN8) — nenhum spec o usa hoje. Entramobile-drawer-theme-toggleno botão de tema do rodapé, que hoje não tem testid.- Sider: sem módulo novo —
App.tsxecustom-sidernão mudam. - Responsividade & PWA: a mudança é exclusivamente mobile (
< 600px, ouseIsMobile). 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 containerwebe 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_e2ejá 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, ologo.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.jsonpina#v1.5.0e otscdo 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-systemem#v1.5.0 - [x]
apps/web—components/logociente do tema, e consumidores sobre fundo escuro ajustados - [x]
apps/web— gaveta emmobile-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-testidpreservados +mobile-drawer-theme-toggle - [x]
apps/e2e— cenários novos emmobile-shell.spec.ts - [x]
make test(segue igual — a mudança é só de web) emake lintsem 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.tsda spec 0004 clicava emmobile-nav-billingssemforce. 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 comclick({ 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.