feat(nav): arrastar-e-soltar pra reordenar o menu, persistindo entre sessões

Pedido do usuário: "testa arrastando um sub menu pra ver se persiste" —
o acordeão da fase anterior só tinha abrir/fechar, nada de arrastar.
Perguntei o que ele queria dizer com "arrastar" antes de supor, e a
resposta foi: quer a funcionalidade de verdade.

@dnd-kit (core + sortable) — cada grupo do menu pode ser arrastado pra
reordenar entre si; cada sub-item dentro de um grupo pode ser arrastado
pra reordenar DENTRO do próprio grupo (nunca sai pra outro grupo
arrastando, checado explicitamente no onDragEnd). Alça de arrastar
dedicada (GripVertical) em vez do item inteiro, pra não conflitar com o
clique de abrir/fechar acordeão ou navegar pelo link.

Ordem salva em localStorage (nav-order.ts), nunca banco — preferência
pessoal do navegador, mesma convenção já usada pelo ThemeToggle
(claro/escuro/sistema). Uma chave só serve os dois menus (Platform e
Tenant), sem colisão de rótulos entre eles.

Achado real testando: aplicar a ordem salva direto no inicializador do
useState diverge do HTML que o servidor mandou (SSR nunca tem acesso a
localStorage) e disparava "Hydration failed" no React — inofensivo no
resultado final, mas um erro real no console e uma renderização inteira
desperdiçada. Corrigido aplicando a ordem salva só dentro de um
useEffect (roda uma vez, só no client) — o mesmo padrão que o
ThemeToggle já usava e eu não tinha replicado.

Testado com Playwright de verdade em dois cenários: arrastar um grupo
inteiro (ordem mudou e sobreviveu a um F5 completo) e arrastar um
sub-item dentro de "Discador" (reordenou só ali dentro, confirmado que
os outros grupos continuaram intactos, e também sobreviveu ao F5).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-30 20:34:46 -03:00
parent 64ef0892ed
commit a779a7e51f
5 changed files with 462 additions and 111 deletions

38
TODO.md
View File

@@ -2299,6 +2299,44 @@ arcordeon no menu para que eu poder reocler cada sub menu")
`aria-expanded="false"`, clicar abre (`true`), clicar de novo fecha
(`false`)
## PHASE 64 — Arrastar-e-soltar pra reordenar o menu (pedido do usuário:
"testa arrastando um sub menu pra ver se persiste" — a resposta foi que
não existia arrastar nenhum ainda, e depois de perguntar o usuário quis
a funcionalidade de verdade)
- [x] `@dnd-kit/core` + `@dnd-kit/sortable` — cada grupo do menu (nível
raiz) pode ser arrastado pra reordenar entre si; cada sub-item
dentro de um grupo pode ser arrastado pra reordenar DENTRO do
próprio grupo (nunca sai pra outro grupo arrastando — checado
explicitamente no `onDragEnd` comparando o rótulo da seção nos
dois ids envolvidos). Alça de arrastar dedicada (ícone
`GripVertical`) em vez do item inteiro, pra não conflitar com o
clique de abrir/fechar acordeão ou navegar pelo link
- [x] Ordem salva em `localStorage` (`nav-order.ts`), nunca banco —
preferência pessoal do navegador, mesma convenção já usada pelo
`ThemeToggle` (claro/escuro/sistema). Uma chave só serve os dois
menus (Platform e Tenant): os rótulos de cada um nunca colidem, e
cada um vive na sua própria entrada dentro do objeto salvo. Item
novo que não estava salvo ainda (ex.: tela futura) entra no fim,
nunca some por causa de uma ordem antiga
- [x] **Achado real testando**: aplicar a ordem salva direto no
inicializador do `useState` (`useState(() => aplicarOrdem(items))`)
diverge do HTML que o servidor mandou (SSR nunca tem acesso a
`localStorage`) e disparava "Hydration failed" no React —
inofensivo no resultado final (React re-renderiza e corrige
sozinho), mas um erro de verdade no console e uma renderização
inteira desperdiçada. Corrigido aplicando a ordem salva só dentro
de um `useEffect` (roda uma vez, só no client, depois de montar) —
exatamente o mesmo padrão que o `ThemeToggle` já usava e eu não
tinha replicado
- [x] Testado com Playwright de verdade, dois cenários: (1) arrastar um
GRUPO do menu (ex.: "Dashboard" pra depois de "Discador") — a
ordem mudou na tela e sobreviveu a um F5 completo; (2) arrastar um
SUB-ITEM dentro de "Discador" (ex.: "Campanhas" pra depois de
"Leads") — reordenou só dentro do próprio grupo (confirmado que
"Call Center" e os outros grupos continuaram intactos) e também
sobreviveu ao F5. Confirmado ainda que corrigir a Hydration
eliminou o erro do console sem quebrar nenhum dos dois cenários
---
## Riscos conhecidos