Skip to content

@audit frontend — Hierarquia, Animações e Comportamento Visual

Invocar com @audit frontend — última atualização: 2026-04-26 · v1.0

SSOT 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árioUse 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

  1. Inspeção antes de reescrita. O DOM real (DevTools) tem mais informação que o código-fonte. Verificar primeiro, codar depois.
  2. Estado visível no DOM. Estado interno crítico (phase, state, progress) DEVE ser exposto via data-* para permitir diagnóstico sem ler código.
  3. Acessibilidade é caminho de código. prefers-reduced-motion não é detalhe — é um caminho de execução completo que precisa ser explicitamente documentado e testado.
  4. 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 class final (após cn() / Tailwind)
  • [ ] Verificar style inline (transforms calculados em runtime)

Pergunta a responder: "O elemento está no estado que eu espero?"

Etapa 2 — Computed Styles

  • [ ] transform está sendo aplicado? Valor numérico real?
  • [ ] opacity, visibility, display permitem renderização?
  • [ ] position e z-index reais (não os esperados)
  • [ ] overflow do pai está cortando?
  • [ ] pointer-events permite 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 reduce E com no-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: reduce ativo no SO. Componente entrava em early-return invisível. Atributo data-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 / transitionend no 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.memo invalidado?

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ão setTimeout/setInterval em loop)
  • [ ] 🔴 Transições pontuais (hover, mount, dropdown): CSS transition (não rAF)
  • [ ] 🔴 NUNCA combinar CSS transition: transform com transform calculado por rAF na mesma propriedade — race condition garantida
  • [ ] 🔴 Cleanup obrigatório no unmount: cancelAnimationFrame(id), clearTimeout(id), removeEventListener
  • [ ] 🟡 will-change: transform em 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 transform está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:

tsx
// ✅ 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

  • [ ] 🟡 isolate no container quando há z-index interno (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-none em 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/flex com filhos que podem expandir: min-w-0 no filho
  • [ ] 🔴 truncate exige overflow-hidden + min-w-0 no 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/matchMedia guardam:
    tsx
    if (typeof window === 'undefined' || !window.matchMedia) return defaultValue;
  • [ ] 🔴 Estado inicial estável entre SSR e CSR — sem Math.random() ou Date.now() no estado inicial
  • [ ] 🟡 useEffect para subscrever matchMedia (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 via batch_init ou setQueryData) — 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

  • [ ] 🔴 setVisitedTabs no MESMO handler síncrono que setActiveTab (não em useEffect) — 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: useCallback obrigatório (ver Caso 5)
  • [ ] 🔴 Sem useEffect calculando campo derivado via setState/handleChange (ver Caso 9) — usar useMemo ou inline no save

6. Como buscar (patterns)

bash
# 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)

CasoTemaLição
Caso 1Modal/Select sem forwardRefWarning de ref = bug estrutural, não cosmético
Caso 5Loop infinito de re-rendersuseCallback obrigatório em funções de hook
Caso 8Flash de skeleton em tabsAnti-flash com isLoading && !data + lazy mount síncrono
Caso 9useEffect derivado quebra React.memoCampos derivados via useMemo, não useEffect
Caso 10TooltipTrigger em SelectItemSem composição entre primitivas Radix
Caso 11Flicker pós-drop em DnDOptimistic update obrigatório em interação contínua
Caso 12Shimmer URGENTE travadoInspeçã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)" para foo@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.comgmail.c e 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:

  1. Existe um parse* extraído como helper SSOT (não regex inline em useMemo)?
  2. Há teste unitário cobrindo cada classe de inválido que o backend rejeita?
  3. A regex/validador é equivalente ou mais estrita que o Zod do edge function?

8. Referências cruzadas