feat(ui): feedback de erro no card do editor durante a corrida (#10) - #18
Conversation
O jogador olha para a textarea onde digita, mas o único sinal de erro ficava no painel de código (char-error) e nos números distantes. Agora o card do editor reage no ponto de fixação do olhar: - shake cirúrgico (transform-only, ~90ms, 4–5px) via @Keyframes CSS — composited na GPU, sem layout/reflow competindo com a textarea (área sagrada). Sem transform em repouso: não cria containing block novo para o menu de opções (position:fixed). - flash de borda vermelha (cor) + ícone "erro" na margem (posição) por ~240ms; erros consecutivos apenas renovam a janela → nunca estrobosa >3Hz. - prefers-reduced-motion: a regra global zera a duração da animação → sem movimento, restando o flash de cor + ícone. Dirigido por um errorPulse em Race.handleInput (incrementa quando newErrors>0). Não altera nenhuma métrica (WPM/precisão/erros) nem o char-error de CodeDisplay. Refs #10 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@caioross is attempting to deploy a commit to the caioross' projects team on Vercel, but is not a member of this team. To resolve this issue, you can:
To read more about collaboration on Vercel, click here. |
There was a problem hiding this comment.
Code Review
This pull request implements visual and motion error feedback in the code editor when a user types an incorrect character (Issue #10). This includes a brief border flash, a shake animation, and an error icon in the margin to ensure accessibility. The feedback is driven by an errorPulse prop. The review comments suggest simplifying the React state and effect management in CodeEditor.tsx by combining the two useEffect hooks and returning a cleanup function from the main effect, which eliminates the need for the errorTimer ref.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const pendingCaret = useRef<number | null>(null); | ||
| const errorTimer = useRef<number | null>(null); | ||
| const [errorActive, setErrorActive] = useState(false); |
There was a problem hiding this comment.
Podemos simplificar o código eliminando a referência errorTimer (useRef). Ao utilizar uma função de limpeza (cleanup) diretamente no useEffect do errorPulse, o React gerencia o ciclo de vida do timeout de forma nativa, tornando este useRef desnecessário.
| const pendingCaret = useRef<number | null>(null); | |
| const errorTimer = useRef<number | null>(null); | |
| const [errorActive, setErrorActive] = useState(false); | |
| const pendingCaret = useRef<number | null>(null); | |
| const [errorActive, setErrorActive] = useState(false); |
| useEffect(() => { | ||
| if (errorPulse === 0) return; | ||
| setErrorActive(true); | ||
| if (errorTimer.current) window.clearTimeout(errorTimer.current); | ||
| errorTimer.current = window.setTimeout(() => setErrorActive(false), 240); | ||
|
|
||
| const el = cardRef.current; | ||
| if (el) { | ||
| el.classList.remove("editor-shake"); | ||
| // força reflow para reiniciar a animação em erros consecutivos | ||
| void el.offsetWidth; | ||
| el.classList.add("editor-shake"); | ||
| } | ||
| }, [errorPulse]); | ||
|
|
||
| useEffect( | ||
| () => () => { | ||
| if (errorTimer.current) window.clearTimeout(errorTimer.current); | ||
| }, | ||
| [] | ||
| ); |
There was a problem hiding this comment.
Podemos tornar este fluxo muito mais idiomático e limpo unificando a lógica de feedback e limpeza em um único useEffect.
Ao retornar a função de limpeza () => window.clearTimeout(timer) diretamente do efeito, o React cancelará automaticamente o timeout anterior quando um novo erro ocorrer (evitando sobreposição) e também quando o componente for desmontado. Isso elimina a necessidade de um segundo useEffect e do useRef para rastrear o timer.
useEffect(() => {
if (errorPulse === 0) return;
setErrorActive(true);
const timer = window.setTimeout(() => setErrorActive(false), 240);
const el = cardRef.current;
if (el) {
el.classList.remove("editor-shake");
// força reflow para reiniciar a animação em erros consecutivos
void el.offsetWidth;
el.classList.add("editor-shake");
}
return () => {
window.clearTimeout(timer);
};
}, [errorPulse]);
✅ Parecer PR Doctor — NORMAL (HANDBOOK §7.3), mergeandoDiff lido inteiro (3 arquivos, +81/-3). Classificado NORMAL (UI + engine sem tocar cálculo de métrica).
Gate
Squash-merge. |
Contexto
Feedback da persona O Iniciante (#9, fricção nº2): ao digitar
funzem vez defunc, nada ficava vermelho no ponto onde o olho fixa — atextareadoCodeEditor. O únicochar-error(vermelho) vivia no painel de código (CodeDisplay), longe do olhar; o resto era numérico e distante. Implementa a Parte IV · Spec — erro (docs/UI-AAA-OVERHAUL.md§779).O que mudou (escopo mínimo, 3 arquivos, +81/-3)
Race.tsx— novoerrorPulse(contador) incrementado emhandleInputquandonewErrors > 0. Reusa o sinal já computado; não altera nenhuma métrica (WPM/precisão/erros).CodeEditor.tsx— recebeerrorPulse; a cada pulso:transformonly, ~90ms, 4–5px) via classe CSS;globals.css—@keyframes cr-editor-shake(sótranslateX) +.editor-error(ring/borda vermelha).Por que assim (área sagrada + acessibilidade)
transformpuro (composited na GPU), sem layout/reflow; roda só no erro, não por tecla. Semtransformem repouso → não cria containing block novo para o menu de opçõesposition:fixed.prefers-reduced-motion: a regra global já existente (animation-duration: 0.001ms) zera o shake automaticamente → sem movimento; sobra o flash de cor + ícone. Erro nunca é comunicado só por cor (cor + posição em ambos os modos).char-errordeCodeDisplaynão foi tocado.Gate (verde)
pnpm install --frozen-lockfile✓ ·pnpm typecheck✓ ·pnpm build✓validate-metrics24/0 ✓ ·validate-persistence22/0 ✓ (a mudança não toca cálculo/persistência; rodei por garantia)lint= N/A (repo sem config ESLint; a CI não roda lint)Fora de escopo (outras issues)
Som (
type.errordepende delib/sound.ts), háptico e combo (§IV.4) — não entram aqui.Sem Parecer do Conselho nesta issue; segui os critérios de aceitação e a dica de abordagem da própria issue.
Closes #10