@audit frontend — Hierarquia, Animações e Comportamento Visual
Invocar com
@audit frontend— última atualização: 2026-04-26 · v1.0SSOT para auditoria de componentes visuais, animações, hierarquia DOM/CSS e estado visual. Extensão obrigatória de AUDIT.md §10.9 sempre que o trabalho envolver UI visual.
Como usar este documento
| Cenário | Use este doc? |
|---|---|
| Implementou ou alterou animação / transição / shimmer / loading visual | ✅ Sim |
| Sintoma é "elemento não aparece / não responde / aparece errado / trava" | ✅ Sim — começar por §2 (protocolo de diagnóstico) |
Mexeu em z-index, overflow, transform, transition, opacity, position | ✅ Sim |
Adicionou media query (prefers-reduced-motion, prefers-color-scheme, viewport) | ✅ Sim |
| Componente com estado visual condicional (loading / error / hover / focus / open) | ✅ Sim |
| Ajustou apenas conteúdo de texto / props / lógica de negócio | ❌ Use AUDIT.md padrão |
| Está investigando bug de backend / RLS / API | ❌ Use PROBLEM_SOLVING.md |
⚠️ Antes de tocar em código de animação ou UI travada, executar §2 (Protocolo de Diagnóstico Visual). Pular esta etapa custou 3 reescritas inúteis no Caso 12 do PROBLEM_SOLVING.md.
1. Princípios
- Inspeção antes de reescrita. O DOM real (DevTools) tem mais informação que o código-fonte. Verificar primeiro, codar depois.
- Estado visível no DOM. Estado interno crítico (
phase,state,progress) DEVE ser exposto viadata-*para permitir diagnóstico sem ler código. - Acessibilidade é caminho de código.
prefers-reduced-motionnão é detalhe — é um caminho de execução completo que precisa ser explicitamente documentado e testado. - Composição respeita primitivas. Cada lib (Radix, Framer Motion, Tailwind) tem invariantes. Compor primitivas de libs diferentes sem entender suas restrições é a maior fonte de bugs visuais.
2. Protocolo de Diagnóstico Visual (OBRIGATÓRIO antes de mexer em código)
Aplica-se quando o sintoma é "algo visual está errado". Cada etapa custa <2 minutos. Pular etapas é o anti-padrão #1 documentado no Caso 12.
Etapa 1 — Inspecionar o DOM real (não o código-fonte)
- [ ] DevTools → Elements → localizar o elemento exato
- [ ] Ler atributos
data-*(estado interno do componente exposto) - [ ] Ler
classfinal (apóscn()/ Tailwind) - [ ] Verificar
styleinline (transforms calculados em runtime)
Pergunta a responder: "O elemento está no estado que eu espero?"
Etapa 2 — Computed Styles
- [ ]
transformestá sendo aplicado? Valor numérico real? - [ ]
opacity,visibility,displaypermitem renderização? - [ ]
positionez-indexreais (não os esperados) - [ ]
overflowdo pai está cortando? - [ ]
pointer-eventspermite interação?
Pergunta a responder: "O CSS final está realmente aplicando o que eu acho?"
Etapa 3 — Media queries ativas (CRÍTICO para animação)
- [ ] DevTools → ⋮ → More tools → Rendering
- [ ] Localizar "Emulate CSS media feature prefers-reduced-motion"
- [ ] Testar com
reduceE comno-preference— comportamento muda? - [ ] Idem para
prefers-color-scheme,forced-colors - [ ] Verificar viewport queries ativas (DevTools → Toggle device toolbar)
Pergunta a responder: "Estou rodando o caminho de código que acho que estou rodando?"
💡 Sintoma do Caso 12: máquina do dev tinha
prefers-reduced-motion: reduceativo no SO. Componente entrava em early-return invisível. Atributodata-shimmer-phase="reduced"no DOM era a pista direta — descoberta em 30s na Etapa 1 economizaria 3 reescritas.
Etapa 4 — Animation/Performance panel
- [ ] DevTools → Animations → confirmar se há frames sendo emitidos
- [ ] Performance → gravar 3s → procurar
requestAnimationFrame/transitionendno timeline - [ ] Se ZERO frames: o caminho de animação NUNCA está sendo executado (early return ou condição falsa)
Pergunta a responder: "A animação está rodando lento, ou simplesmente não está rodando?"
Etapa 5 — Console + React DevTools
- [ ] Console: warnings de ref forwarding, hydration mismatch, key duplicada
- [ ] React DevTools → Components → estado real do hook (não o "esperado")
- [ ] React DevTools → Profiler → re-renders excessivos?
React.memoinvalidado?
Regra de ouro
Nenhuma reescrita de código antes das Etapas 1–3 mostrarem evidência observável da causa. Reescrever sem evidência = chute caro. Cada reescrita errada custa: tempo + complexidade + risco de quebrar outra coisa.
3. Checklist de Implementação de Animações
3.1 Mecanismo
- [ ] 🔴 Animações cíclicas/longas:
requestAnimationFrame(nãosetTimeout/setIntervalem loop) - [ ] 🔴 Transições pontuais (hover, mount, dropdown): CSS
transition(não rAF) - [ ] 🔴 NUNCA combinar CSS
transition: transformcomtransformcalculado por rAF na mesma propriedade — race condition garantida - [ ] 🔴 Cleanup obrigatório no unmount:
cancelAnimationFrame(id),clearTimeout(id),removeEventListener - [ ] 🟡
will-change: transformem animações cíclicas longas (libera composição GPU)
3.2 Acessibilidade — prefers-reduced-motion
[ ] 🔴 Política explícita documentada no JSDoc do componente:
tsx/** * Política de prefers-reduced-motion: * - 'off': animação totalmente desligada (default seguro) * - 'reduced': animação simplificada (cadência mais lenta + opacidade reduzida) * - 'animated-forced': animação mantida (justificar — ex: indicador de URGENTE * é parte semântica essencial da feature) */[ ] 🔴 Atributo
data-motion-policy="off|reduced|animated|animated-forced"no DOM — torna a política visível no DevTools sem ler código[ ] 🔴 NUNCA renderizar JSX com
transformestático intermediário em modo reduced. Ou anima com cadência reduzida, ou não renderiza a faixa/elemento decorativo. Posição "congelada no meio" parece bug travado.
Padrão recomendado:
// ✅ CORRETO — reduced anima com cadência mais lenta
const runMs = reduced ? 4000 : configuredRunMs;
const restMs = reduced ? 6000 : configuredRestMs;
// ❌ ERRADO — JSX estático intermediário
if (reduced) return <div style={{ transform: 'translateX(75%)' }} />;3.3 Camadas e empilhamento
- [ ] 🟡
isolateno container quando ház-indexinterno (cria novo stacking context) - [ ] 🟡 Hierarquia de planos documentada em comentário:tsx
// z-0 → fundo // z-10 → camada de shimmer/decoração // z-20 → conteúdo (sempre acima da decoração) - [ ] 🟡
pointer-events-noneem camadas decorativas (shimmer, overlays) - [ ] 🟡
aria-hidden="true"em camadas puramente visuais
3.4 Observabilidade do componente
- [ ] 🔴 Atributos
data-*expostos para diagnóstico:data-state="open|closed|loading"(estado de UI)data-phase="rest|run|done"(fase de animação)data-progress="0..100"(progresso instantâneo)data-motion-policy(caminho de acessibilidade ativo)
- [ ] Permite responder "em que estado este elemento está?" em 5 segundos no DevTools, sem
console.log
3.5 Configurabilidade
- [ ] Quando duração/intervalo é configurável:
clamp()no componente (sanitiza valores fora de range) - [ ] Backend valida range (fail-close) E frontend valida (UX)
- [ ] Defaults sensatos no componente — não confiar 100% no servidor
4. Checklist de Hierarquia DOM/CSS
4.1 Composição Radix
- [ ] 🔴 Sem composição entre primitivas Radix (ex:
<TooltipTrigger><SelectItem/></TooltipTrigger>) — gera warning de ref forwarding e quebra acessibilidade. Ver Caso 10. - [ ] 🔴 Componentes UI customizados que envolvem primitivas Radix usam
React.forwardRef - [ ] 🟡 Atributos ARIA preservados ao envolver primitivas
- [ ] 🟡 Para tooltip em item Radix: usar
title="..."HTML nativo
4.2 Layout shifts
- [ ] 🔴 Containers
grid/flexcom filhos que podem expandir:min-w-0no filho - [ ] 🔴
truncateexigeoverflow-hidden+min-w-0no pai (caso contrário, texto vaza grid) - [ ] 🟡 Modais densos (6+ campos OU >600px): padrão 3-zonas — ver FRONTEND_MODAL_DENSITY_STANDARD.md
- [ ] 🟡 Skeleton dimensionado igual ao conteúdo final (sem CLS)
4.3 SSR/Hydration
- [ ] 🔴 Componentes que dependem de
window/matchMediaguardam:tsxif (typeof window === 'undefined' || !window.matchMedia) return defaultValue; - [ ] 🔴 Estado inicial estável entre SSR e CSR — sem
Math.random()ouDate.now()no estado inicial - [ ] 🟡
useEffectpara subscrevermatchMedia(não no estado inicial)
5. Checklist de Estado Visual
5.1 Loading e cache pré-populado
- [ ] 🔴
if (isLoading && !data)(anti-flash em telas com cache pré-populado viabatch_initousetQueryData) — ver ATOMIC_RENDERING.md e Caso 8 - [ ] 🟡 Skeleton dimensionado igual ao conteúdo final (sem layout shift)
- [ ] 🟡 Empty state visualmente distinto de loading state
5.2 Lazy mount e tabs
- [ ] 🔴
setVisitedTabsno MESMO handler síncrono quesetActiveTab(não emuseEffect) — ver Caso 8 §Camada 2 - [ ] 🟡 Estado de "já montou" preservado entre trocas de tab
5.3 Memoização
- [ ] 🔴 Funções de hook usadas como deps de
useEffect:useCallbackobrigatório (ver Caso 5) - [ ] 🔴 Sem
useEffectcalculando campo derivado viasetState/handleChange(ver Caso 9) — usaruseMemoou inline no save
6. Como buscar (patterns)
# Animações com setInterval (preferir rAF para ciclos)
search_files: "setInterval\(" em src/components/
→ Cada match: justificar ou migrar para requestAnimationFrame
# prefers-reduced-motion sem política documentada
search_files: "prefers-reduced-motion\|reducedMotion" em src/
→ Cada match deve ter comentário/JSDoc com política (off/reduced/animated-forced)
# Estado de animação não exposto via data-*
search_files: "useState.*phase\|useState.*animation" em src/components/
→ Verificar JSX correspondente — DEVE ter data-phase/data-state
# Transform sem rAF cleanup
search_files: "requestAnimationFrame" em src/components/
→ Mesmo arquivo DEVE ter cancelAnimationFrame no cleanup do useEffect
# CSS transition em elemento com transform via rAF (race condition)
search_files: "transition.*transform" em src/components/
→ Verificar se o mesmo elemento tem transform calculado dinamicamente
# z-index sem isolate
search_files: "z-\[?[0-9]" em src/components/
→ Pai DEVE ter `isolate` ou `position: relative` documentado
# Composição Radix proibida
search_files: "TooltipTrigger.*SelectItem\|TooltipTrigger.*DropdownMenuItem" em src/
→ Deve ser ZERO — usar title="..." nativo
# Loading sem anti-flash
search_files: "if \(isLoading\) return" em src/components/
→ Em telas com cache pré-populado, DEVE ser `isLoading && !data`7. Casos de estudo (PROBLEM_SOLVING.md)
| Caso | Tema | Lição |
|---|---|---|
| Caso 1 | Modal/Select sem forwardRef | Warning de ref = bug estrutural, não cosmético |
| Caso 5 | Loop infinito de re-renders | useCallback obrigatório em funções de hook |
| Caso 8 | Flash de skeleton em tabs | Anti-flash com isLoading && !data + lazy mount síncrono |
| Caso 9 | useEffect derivado quebra React.memo | Campos derivados via useMemo, não useEffect |
| Caso 10 | TooltipTrigger em SelectItem | Sem composição entre primitivas Radix |
| Caso 11 | Flicker pós-drop em DnD | Optimistic update obrigatório em interação contínua |
| Caso 12 | Shimmer URGENTE travado | Inspeção do DOM ANTES de reescrita; prefers-reduced-motion é caminho de código |
7.1 Validação client/server — paridade obrigatória
Toda regex/validador client-side que sinaliza "OK para enviar" (badge verde, contador "X válido(s)", botão habilitado) DEVE ser pelo menos tão estrita quanto o schema Zod equivalente do backend. Caso contrário o usuário vê confirmação visual de sucesso e recebe rejeição apenas no toast pós-envio — UX quebrada e ruído de suporte.
Caso de referência (2026-05-10) — composer de e-mail avulso:
- Frontend:
RE_EMAIL = /^[^@\s]+@[^@\s]+\.[^@\s]+$/(TLD ≥ 1 char) → badge "1 válido(s)" parafoo@gmail.c. - Backend:
z.string().email()(TLD ≥ 2) → 400 "Destinatários: e-mail inválido". - Sintoma reportado: "está rejeitando e-mail válido". Sintoma real: autofill truncou
gmail.com→gmail.ce o frontend não detectou. - Correção: regex alinhada em
src/components/admin-comunicacao/enviar/audiencia/email-parser.ts(SSOT testável). Antes do envio o usuário já vê "1 inválido(s)".
Checklist ao revisar um novo formulário com lista de inputs validados:
- Existe um
parse*extraído como helper SSOT (não regex inline emuseMemo)? - Há teste unitário cobrindo cada classe de inválido que o backend rejeita?
- A regex/validador é equivalente ou mais estrita que o Zod do edge function?
8. Referências cruzadas
- AUDIT.md — checklist principal
@audit(este doc é extensão obrigatória do modo@audit frontend) - PROBLEM_SOLVING.md — protocolo de diagnóstico de bugs (usar ANTES de auditar)
- FRONTEND_UI_STANDARDS.md — padrões de composição visual (shadcn/Radix)
- FRONTEND_MODAL_DENSITY_STANDARD.md — padrão de densidade em modais
- ATOMIC_RENDERING.md — anti-flash e renderização atômica
- STATE_OF_THE_ART.md — arquitetura React/hooks
- OPTIMISTIC_UPDATES.md — interações contínuas (DnD, toggles, sliders)