Repository navigation
Description
O problema
docs/UI-AAA-OVERHAUL.md §VII.4 trata prefers-reduced-motion como métrica binária de "AAA pronto":
prefers-reduced-motionhonrado em cada efeito; sem flash > 3Hz.
Hoje o projeto tem um fallback global em globals.css:49-56:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
...
}
}Essa regra cobre só CSS (animation e transition). Framer Motion não passa por nenhum dos dois: ele roda um laço de requestAnimationFrame e escreve style.transform / style.opacity inline a cada quadro. Zerar transition-duration não tem efeito sobre um valor que já chega interpolado no estilo inline. Ou seja: o usuário liga "reduzir movimento" no sistema, o CSS obedece, e todo o movimento de framer-motion continua exatamente igual.
Por isso vários arquivos comentam confiando na regra global — ex. CodeEditor.tsx:85 ("sob prefers-reduced-motion a regra global zera a duração"). Para aquele caso o comentário está certo, porque o shake é uma animation CSS. Para os <motion.*> abaixo, não está.
Inventário (medido em origin/main @ 82f7817)
10 componentes importam framer-motion e não importam useReducedMotion:
| Componente | Movimento | Gravidade |
|---|---|---|
Countdown.tsx:11-18 |
dígito de 160–220px em tela cheia: scale 0.4→1 + rotate -8°→0, saída scale 1.6, 1× por segundo, 3×seguidas |
🔴 pior caso |
RaceTrack.tsx:142-156 |
2 springs por jogador (barra + racer) animando continuamente durante a corrida | 🔴 área sagrada |
Modal.tsx:30-41 · Toast.tsx:25-29 |
slide + scale de entrada/saída | 🟠 |
HomeView.tsx (11) · Lobby.tsx (13) · RoomView · Chat · FloatingChat · Logo |
reveals de entrada | 🟡 |
O Countdown é o mais sério e a spec já o marca nominalmente (§ linha 552): "O flash branco respeita reduced-motion (vira mudança de cor suave) — importante para fotossensibilidade". Hoje não respeita nada: é o maior elemento da tela pulsando 3× seguidas, sem escapatória, e é inevitável — passa em toda partida multiplayer.
O RaceTrack é o segundo porque não é um reveal que passa: são springs vivos ao lado da textarea durante a corrida inteira. Com o limite de 30 jogadores (room.ts:282, #69) são até 60 springs simultâneos.
Que o padrão certo já existe no repo é o que torna isto barato: Results, TypingCore, SpectatorView, AuroraBackground, ClickSpark, DecryptedText e o BannerCarousel novo (:100) já usam useReducedMotion() corretamente. Isto é propagar um padrão da casa, não inventá-lo.
Escopo desta fatia
Só os dois de gravidade 🔴 + a primitiva compartilhada. Os 8 restantes são mecânicos e viram fatia 2 (declarada abaixo) — a intenção é estabelecer o padrão com revisão cuidadosa antes de espalhá-lo por 8 arquivos.
Acceptance criteria
- Existe uma primitiva única (sugestão:
src/lib/motion.ts, que a §VII.1 Fase 0 já prevê) exportando os presets a serem reusados — sem repetirconst reduced = useReducedMotion()+ ternário solto em cada arquivo. -
Countdown: comprefers-reduced-motion: reduce, o dígito não escala nem rotaciona. Troca permitida: cross-fade de opacidade, ou nenhuma transição. Okey={n}e oAnimatePresencecontinuam trocando o número a cada segundo (a informação não pode sumir). -
RaceTrack: com reduced-motion, barra e racer vão para a posição nova sem spring (transition={{ duration: 0 }}ou equivalente). Sem salto de layout e sem perder a posição correta. - Nenhum dos dois perde informação sob reduced-motion: o número do countdown continua legível a cada tick e o progresso de cada jogador continua correto.
- O comentário enganoso de
CodeEditor.tsx:85fica; mas não adicione comentários novos afirmando que a regra global do CSS cobre framer-motion. - Gate:
pnpm typecheck+pnpm build+vitest+validate-metrics(oRaceTracké vizinho da área sagrada).
Verificação sugerida (leia antes de tentar screenshot)
.claude/skills/cr-fleet-ops §11 e a memória da frota registram que no Browser pane headless o rAF congela — framer-motion trava em opacity: 0 e o visual mente. Não tente provar isto por screenshot.
O que funciona: emular a media query e instrumentar a lógica — ler o valor que o componente passa em transition sob cada preferência, ou um teste de render (jsdom + matchMedia mockado) afirmando que transition.duration === 0 / ausência de scale no initial quando reduce está ligado. Um teste assim também protege contra regressão, que é o que a §VII.4 exige de uma métrica binária.
Fora de escopo (fatia 2 — abrir depois desta mergear)
Os 8 componentes 🟠/🟡 (Modal, Toast, HomeView, Lobby, RoomView, Chat, FloatingChat, Logo), reusando a primitiva criada aqui.
Não confundir com a #59
A #59 fatia 2 também toca o RaceTrack, mas por performance (tirar a pista do caminho do re-render + coalescing). Reduced-motion foi deixado explicitamente de fora dela em 28/07 e é o que esta issue cobre. Se as duas forem pegas juntas, atenção ao conflito no mesmo arquivo.
Activity
caioross commentedon Jul 31, 2026
🗂️ Correção pontual do inventário (Curador · 2026-07-31)
O inventário desta issue lista o
TypingCoreentre os componentes que "já usamuseReducedMotion()corretamente". Usa — mas só noIdleWarning. A barra de progresso do card de estatísticas (TypingCore.tsx:251-256) anima num spring sem consultar oreducedque já existe na linha 60.Não há nada a fazer aqui: essas 5 linhas serão reescritas pela #114 (a barra anima
width— mesmo defeito da #59), e o guard de reduced-motion entra junto, como AC dela. Registro só para que a fatia 2 não conte com um inventário incompleto nem volte a este arquivo por engano.