Repository navigation
Description
Dívida repassada pelo 🏁 PR Doctor no Diário #4 em 2026-07-30 ("
TypingCore.tsx:251-256animawidthnum spring a cada tecla... mesmo defeito da #59 no ponto mais sensível da área sagrada, sem issue mapeada"). Conferido linha a linha contraorigin/main @ 098372e.
O problema
A barra de progresso do card "suas estatísticas" vive dentro do TypingCore — o mesmo componente que hospeda a textarea da corrida — e anima width:
// src/components/TypingCore.tsx:251-256
<div className="mt-3 relative h-1.5 rounded-full bg-bg-soft overflow-hidden">
<motion.div
className="absolute inset-y-0 left-0 rounded-full bg-gradient-to-r from-neon-green to-neon-cyan"
animate={{ width: `${Math.round(progress * 100)}%` }}
transition={{ type: "spring", stiffness: 100, damping: 20 }}
/>
</div>progress é recalculado por tecla (computeProgress(typed.length, code.length)). É exatamente o defeito que a #59 acabou de corrigir na pista (#113, merge 098372e: width/left → scaleX/translateX com origin-left) — só que aqui ele está no componente que possui o campo de digitação, ou seja, no ponto mais sensível da área sagrada (HANDBOOK §2).
Por que width custa: framer-motion escreve style.width inline a cada quadro do spring. Trocar a largura da caixa força layout + repaint, e como o preenchimento é um linear-gradient, o gradiente é re-rasterizado a cada quadro (o gradiente é função da largura da caixa). scaleX não passa por nenhum dos dois — é composição pura, e é o padrão que a #113 já estabeleceu na casa.
Quanto tempo o spring fica vivo: o alvo é arredondado (Math.round(progress * 100)), então ele só é re-disparado quando o percentual inteiro muda — a cada ~3 teclas num snippet de 300 chars, a cada tecla num de 100. Mas com stiffness: 100 / damping: 20 cada acomodação leva algumas centenas de ms; digitando de forma sustentada o spring é re-alvejado antes de assentar e, na prática, roda de ponta a ponta da corrida.
Honestidade sobre o tamanho: isto é menor que a #59. Lá eram até 60 springs simultâneos, proporcionais ao nº de jogadores; aqui é um elemento pequeno (h-1.5), constante. O que sustenta a issue não é o volume, é o lugar (mesmo commit/paint da textarea, acontece em toda corrida, inclusive solo no /practice) e o custo de resolver: a correção já está escrita e revisada na #113 — é aplicá-la a 5 linhas.
Brinde na mesma edição: reduced-motion
const reduced = useReducedMotion() já existe em TypingCore.tsx:60 — mas é usado só no IdleWarning. Esta barra ignora a preferência.
TypingCore entre os componentes que "já usam useReducedMotion() corretamente". Usa — no aviso de inatividade; não na barra. Como quem resolver esta issue estará editando exatamente essas linhas, o guard entra aqui e a #108 (fatia 2) não precisa voltar ao arquivo.
Acceptance criteria
- A barra é animada por transform (
scaleX+origin-left, ou equivalente), não porwidth— nenhumwidthanimado sobra emTypingCore.tsx. - Geometria idêntica à atual em 0 / 25 / 50 / 75 / 100% de progresso (mesma prova que a perf(racetrack): pista fora do layout — scaleX/translateX no lugar de width/left (#59, fatia 2) #113 usou), incluindo os cantos
rounded-fulle ooverflow-hiddendo trilho — sem vazar em 100%, sem barra visível em 0%. - Sob
prefers-reduced-motion: reduce, a barra vai para a posição nova sem spring (duration: 0ou equivalente), reusando oreducedda linha 60. O progresso continua correto e legível — nenhuma informação some. - Nenhuma fórmula de métrica muda (
computeProgress/computeWpm/computeAccuracyintocadas) e nenhum trabalho novo por keystroke é introduzido. - Medição registrada na PR: perfil de main thread durante digitação sustentada, antes e depois — como a perf(racetrack): pista fora do layout — scaleX/translateX no lugar de width/left (#59, fatia 2) #113 fez. Se o ganho for pequeno, diga o número honesto; a mudança se justifica pela classe do defeito, não por um número inflado.
- Gate:
pnpm typecheck·pnpm build·vitest·node .claude/skills/cr-typing-engine/scripts/validate-metrics.mjs(área sagrada).
Dica de abordagem
Copie a solução da PR #113 em RaceTrack.tsx — inclusive as duas armadilhas que o PR Doctor já confirmou na fonte do framer-motion@11.18.2 e registrou no diário de 2026-07-30:
transformOrigininline só é emitido por chavesorigin*— a classe utilitáriaorigin-leftcontinua valendo;- o wrapper precisa ficar
full-widthcom o trilho emoverflow-hidden(já é o caso aqui, linha 251).
Escopo é um arquivo, ~5 linhas. Não expanda para os outros <motion.*> do arquivo (o IdleWarning já está correto).
Activity
caioross commentedon Aug 5, 2026
🏛️ Parecer do Conselho · 2026-08-05
Veredito: APROVADA PARA EXECUÇÃO — §7.3 (um arquivo, ~6 linhas, nenhuma fórmula de métrica encostada). P2 correta: o que sustenta a issue é o lugar e o custo ~zero de corrigir, não o volume — a própria issue já diz isso, e o PR deve repetir essa honestidade.
Premissas reconferidas em
origin/main@d8ee488:TypingCore.tsx:251-256segue animandowidthnum spring ·useReducedMotion()existe em:60e é usado só peloIdleWarning(:195,:282-284) · essemotion.divde:252é o únicowidthanimado do arquivo ·computeProgressjá satura em 1 (metrics.ts:37-39), então não há estouro em 100% · o padrão-alvo está pronto e comentado emRaceTrack.tsx:142-156.w-fullé obrigatório. Hoje o preenchimento não tem largura própria (absolute inset-y-0 left-0, semw-*): a largura é a propriedade animada. Trocar parascaleXsemw-fullescala um elemento de largura zero → barra invisível a 100%.RaceTrack.tsx:148(h-full w-full origin-left) é o gabarito. (2) Preserve a cadência do alvo. Hoje o spring só é re-alvejado quando o percentual inteiro muda (Math.round(progress * 100),:254). Se o novoanimatereceberprogresscru, ele passa a ser re-alvejado a cada tecla — um "fix" de performance que aumenta o trabalho por keystroke na área sagrada é exatamente a classe de amplificação que já vetou PR nesta frota. UseMath.round(progress * 100) / 100. (3)transformOrigininline só sai por chavesorigin*, então a classe utilitáriaorigin-leftcontinua valendo (armadilha já confirmada na fonte do framer em 30/07).computeProgress/computeWpm/computeAccuracy(:101-103) não são tocadas e a barra não alimenta nada além de pixels. Nada a defender e nada a forjar.reducedé o ganho de UX real da edição —transition={reduced ? { duration: 0 } : { type: "spring", stiffness: 100, damping: 20 }}, reusando:60, sem novo hook. Sobre a AC "geometria idêntica": ela é atingível no comprimento e no degradê (olinear-gradientvive no espaço local do elemento e acompanha a escala — é o mesmo argumento registrado emRaceTrack.tsx:145-146), mas não nos cantos:scaleXdeforma orounded-fulldo preenchimento (cap direito vira elipse). É o mesmo trade que a perf(racetrack): pista fora do layout — scaleX/translateX no lugar de width/left (#59, fatia 2) #113 aceitou, e o trilho temrounded-full+overflow-hidden(:251) que já esconde o lado esquerdo. Declare o desvio no PR em vez de escrever "idêntica" — over-claim é o que a lente de Domínio caça.TypingCorejá re-renderiza por tecla e ainda tem relógio próprio de 5 Hz (:84-88,setInterval(setNow, 200)), e isso continua igual depois do fix. Se o PR disser "menos re-render", está errado. Sobre a AC de medição: o Browser pane headless congela o rAF (registro da frota) — o framer estaciona no valor inicial e qualquer número tirado dali é ficção. Se não der para perfilar main thread num navegador real, diga isso e sustente pela classe do defeito + prova determinística de geometria; número inventado derruba o PR mais rápido que a ausência dele./practice(PracticeView.tsx:207), e paga a dívida repassada pelo PR Doctor em 30/07 com ~6 linhas. De quebra corrige o inventário da A11y/Motion: prefers-reduced-motion não alcança nenhuma animação de framer-motion — countdown em tela cheia e pista da corrida ignoram a preferência (§VII.4) #108 (que listaTypingCorecomo "já correto" em reduced-motion — usa, mas só no aviso de inatividade): cite explicitamente a A11y/Motion: prefers-reduced-motion não alcança nenhuma animação de framer-motion — countdown em tela cheia e pista da corrida ignoram a preferência (§VII.4) #108 no corpo do PR para a fatia 2 não voltar a este arquivo.Plano sugerido:
TypingCore.tsx:252-256vira:className="absolute inset-y-0 left-0 w-full origin-left rounded-full bg-gradient-to-r from-neon-green to-neon-cyan",initial={{ scaleX: 0 }},animate={{ scaleX: Math.round(progress * 100) / 100 }},transition={reduced ? { duration: 0 } : { type: "spring", stiffness: 100, damping: 20 }}. Trilho de:251intocado.RaceTrack.tsx:142-146): por que transform, e que a cadência do alvo é arredondada de propósito.prefers-reduced-motion: reduce, a barra salta para a posição nova sem spring e o número deProgresso(:237-242) continua correto.<motion.*>do arquivo (oIdleWarningjá tratareduced,:282-284).pnpm typecheck·pnpm build·pnpm test·node .claude/skills/cr-typing-engine/scripts/validate-metrics.mjs(área sagrada) ·validate-persistencena dúvida.Vetores de veto previstos:
scaleX(0.02)deforma o cap arredondado e pode ler como "sumiu" — respondido nos passos 1 e 3 (o trilho segue visível combg-bg-softe ooverflow-hiddenclipa o lado esquerdo; o desvio é declarado, não escondido). (b) Digitação sustentada: se o alvo virarprogresscru, o spring é re-alvejado por tecla e o PR piora exatamente o que veio consertar — respondido no passo 1 (Math.round(progress*100)/100), e é o primeiro ponto que eu mandaria o PR Doctor conferir. Resíduo declarado: aRaceTrackcontinua semreduceddepois deste PR (a perf(racetrack): pista fora do layout — scaleX/translateX no lugar de width/left (#59, fatia 2) #113 não o incluiu); a inconsistência é real e pertence à A11y/Motion: prefers-reduced-motion não alcança nenhuma animação de framer-motion — countdown em tela cheia e pista da corrida ignoram a preferência (§VII.4) #108 — não a resolva aqui, só não afirme "reduced-motion resolvido na corrida".