Skip to content

Feat/aaa overhaul - #2

Merged
caioross merged 2 commits into
mainfrom
feat/aaa-overhaul
Jun 21, 2026
Merged

Feat/aaa overhaul#2
caioross merged 2 commits into
mainfrom
feat/aaa-overhaul

Conversation

@caioross

Copy link
Copy Markdown
Owner

No description provided.

caioross added 2 commits June 20, 2026 22:51
Introduce a new CodeEditor component (VSCode-like typing surface) and replace the old textarea in Race. Tweak CodeDisplay wrapping behavior. Enhance Lobby with ready toggle for players, leader-only start gating, ready counts, and a kick button for the leader. Improve RaceTrack to handle finished vs abandoned players, update sorting, visuals, and markers. Add ready and abandoned flags across types and realtime room messages; track ready in presence meta and broadcast a kick event. Update useRoom to manage ready/abandon state, freeze timers on finish, assign places only to actual finishers, and expose setReady and kick actions. Misc: small style/UX adjustments and wiring in RoomView mapping to include new flags.
Introduce a lightweight tokenizer and per-char coloring, add a tiny WebAudio keystroke synth, and wire them into the editor UI. New files: src/lib/highlight.ts (tokenize(), tokColor(), charColors()) and src/lib/sound.ts (playKey(), playError(), mute persistence). CodeDisplay now applies real syntax colors per character and visual states for typed/cursor/error. CodeEditor was refactored to render a colored <pre> mirror from tokenize(), overlay a transparent textarea, sync scrolling, and add a sound toggle that persists mute and triggers playKey on input. Added a FloatingChat component for transient, floating chat messages and swapped Race to use FloatingChat (and adjusted layout). Overall UX: improved editor visuals, typing sounds, and a lightweight live chat experience.
@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
code-racer Building Building Preview, Comment Jun 21, 2026 2:11am

@caioross
caioross merged commit b3e9043 into main Jun 21, 2026
2 of 3 checks passed
@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a major upgrade to the typing interface, replacing the basic input with a custom, VSCode-like CodeEditor featuring live syntax highlighting, keyboard sound effects, line-number gutters, and an options menu. It also implements a FloatingChat component, a player "ready" state system in the Lobby, leader kick capabilities, and improved race track sorting that accounts for finished and abandoned players. Review feedback focuses on improving the robustness of the new editor and chat components, specifically by reading the textarea value directly in syncCaret to avoid state desyncs, utilizing the native onSelect event for reliable cursor tracking, hiding the textarea scrollbar to prevent visual misalignment during line wrapping, and ensuring active timeouts in the floating chat are cleared on unmount to prevent memory leaks.

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.

Comment on lines +61 to +68
function syncCaret() {
const ta = taRef.current;
if (!ta) return;
const pos = ta.selectionStart ?? 0;
const before = value.slice(0, pos);
const nl = before.split("\n");
setCaret({ line: nl.length, col: nl[nl.length - 1].length + 1 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid potential desyncs due to React state batching or asynchronous updates, read the value directly from the DOM element (ta.value) instead of the state value prop inside syncCaret.

Suggested change
function syncCaret() {
const ta = taRef.current;
if (!ta) return;
const pos = ta.selectionStart ?? 0;
const before = value.slice(0, pos);
const nl = before.split("\n");
setCaret({ line: nl.length, col: nl[nl.length - 1].length + 1 });
}
function syncCaret() {
const ta = taRef.current;
if (!ta) return;
const pos = ta.selectionStart ?? 0;
const before = ta.value.slice(0, pos);
const nl = before.split("\n");
setCaret({ line: nl.length, col: nl[nl.length - 1].length + 1 });
}

Comment on lines +222 to +230
onChange={e => {
if (e.target.value.length > value.length) playKey();
onChange(e.target.value);
syncCaret();
}}
onPaste={onPaste}
onScroll={onScroll}
onKeyUp={syncCaret}
onClick={syncCaret}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of manually binding onKeyUp, onClick, and calling syncCaret inside onChange, use React's native onSelect event on the <textarea>. onSelect fires reliably on any cursor position or selection change, including holding down arrow keys, clicking, or typing, which fixes the issue where holding down arrow keys doesn't update the active line highlight or status bar.

Suggested change
onChange={e => {
if (e.target.value.length > value.length) playKey();
onChange(e.target.value);
syncCaret();
}}
onPaste={onPaste}
onScroll={onScroll}
onKeyUp={syncCaret}
onClick={syncCaret}
onChange={e => {
if (e.target.value.length > value.length) playKey();
onChange(e.target.value);
}}
onPaste={onPaste}
onScroll={onScroll}
onSelect={syncCaret}

autoCapitalize="off"
disabled={disabled}
wrap={wrap ? "soft" : "off"}
className="absolute inset-0 resize-none bg-transparent px-4 py-4 text-transparent caret-neon-green outline-none placeholder:text-text-dim disabled:opacity-50"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When wrap is enabled, the presence of a vertical scrollbar in the <textarea> reduces its usable width, whereas the underlying <pre> has overflow-hidden and no scrollbar. This causes lines to wrap at different character positions, leading to a complete visual desync between the highlighted text and the cursor. Hide the scrollbar on the <textarea> using Tailwind arbitrary classes to ensure perfect alignment across all browsers and operating systems.

Suggested change
className="absolute inset-0 resize-none bg-transparent px-4 py-4 text-transparent caret-neon-green outline-none placeholder:text-text-dim disabled:opacity-50"
className="absolute inset-0 resize-none bg-transparent px-4 py-4 text-transparent caret-neon-green outline-none placeholder:text-text-dim disabled:opacity-50 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"

Comment on lines +30 to +44
useEffect(() => {
const fresh = messages.filter(m => !seen.current.has(m.id));
fresh.forEach(m => seen.current.add(m.id));
// Don't replay history on mount — only float messages that arrive live.
if (!mounted.current) {
mounted.current = true;
return;
}
if (!fresh.length) return;
setActive(a => [...a, ...fresh]);
for (const m of fresh) {
const id = m.id;
setTimeout(() => setActive(a => a.filter(x => x.id !== id)), TTL_MS);
}
}, [messages]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Active setTimeout calls are not cleared if the component unmounts, which can lead to memory leaks and unexpected state updates. Maintain a ref to track active timeout IDs and clear them on unmount.

  const timeoutsRef = useRef<NodeJS.Timeout[]>([]);

  useEffect(() => {
    return () => {
      timeoutsRef.current.forEach(clearTimeout);
    };
  }, []);

  useEffect(() => {
    const fresh = messages.filter(m => !seen.current.has(m.id));
    fresh.forEach(m => seen.current.add(m.id));
    // Don't replay history on mount — only float messages that arrive live.
    if (!mounted.current) {
      mounted.current = true;
      return;
    }
    if (!fresh.length) return;
    setActive(a => [...a, ...fresh]);
    for (const m of fresh) {
      const id = m.id;
      const t = setTimeout(() => {
        setActive(a => a.filter(x => x.id !== id));
        timeoutsRef.current = timeoutsRef.current.filter(x => x !== t);
      }, TTL_MS);
      timeoutsRef.current.push(t);
    }
  }, [messages]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant