feat(ui): playhead cursor on the trace tree synced to audio#83
Open
basilebong wants to merge 2 commits into
Open
feat(ui): playhead cursor on the trace tree synced to audio#83basilebong wants to merge 2 commits into
basilebong wants to merge 2 commits into
Conversation
Render a vertical cursor on the trace tree that tracks the audio playback position, so it's clear which span/turn corresponds to the sound being played. The audio player already drew a white cursor; this mirrors it on the tree. Clicking a span seeks the audio, so the trace cursor jumps there too — closing the loop in both directions. - player-provider: a ref + listener playhead channel mirroring the existing controlsRef imperative-handle pattern. usePlayhead (consumer) + usePublishPlayhead (producer). High-frequency position routes through a ref, so only the cursor leaf re-renders, never the rows. - stereo-turn-player: an effect subscribing to wavesurfer's own clock events publishes the position (external-system subscription, not a derived-state effect). - trace-tree: pure fractionOf(sec, scale) + playheadLeft(fraction), shared with TimeBar so a bar and the cursor over it can't drift; plus a TracePlayhead leaf (white glow line + clock pill). Positioned so it lands on the matching bar at any zoom; gated on player readiness; decorative (aria-hidden + pointer-events-none). - format: extract formatClockSeconds, shared by the player readout and the pill (fixes a float-truncation quirk, e.g. 5.3 no longer -> .2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The turn/span count floated above the "Span tree" title — CardTitle sets leading-none (line-box == font size) while the adjacent text-[10px] count span inherits a taller line-box, so flex items-center centered two mismatched boxes. Switch the title+count group to items-baseline so both sit on a shared text baseline. Also picks up a pre-existing biome text reflow in the AudioSection empty-turns message (same file). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
As audio plays in the replay inspector, nothing on the trace tree showed where playback currently was — so it was hard to tell which span/turn produced the sound you were hearing. The two views were coupled in one direction only (clicking a span seeks the audio); there was no feedback the other way.
This adds a vertical playhead cursor on the trace tree, synced to the audio playback position, plus a small floating clock pill (e.g.
0:05.3) that follows it. The audio player already draws a white cursor; the trace line mirrors it so the two read as one shared cursor. Because clicking a span already seeks the audio, the trace cursor also jumps to a clicked span — closing the loop visually.Scope is intentionally just the cursor. Physically aligning the spans under the waveform (shared zoom + scroll between the two views) was considered and deferred — it's a much larger redesign with real UX tradeoffs.
How it works
player-provider— a ref + listener playhead channel mirroring the existingcontrolsRefimperative-handle pattern.usePlayhead()(consumer) +usePublishPlayhead()(producer). The high-frequency position routes through a ref, so only the single cursor leaf re-renders on each frame — never the trace rows or the provider.stereo-turn-player— an effect subscribing to wavesurfer's own clock events (timeupdate/seeking/play/…) publishes the position. A genuine external-system subscription, not a derived-state effect.trace-tree— purefractionOf(sec, scale)+playheadLeft(fraction), shared withTimeBarso a bar and the cursor over it can't drift. The cursor is positioned withcalc(280px + f·(100% − 280px)), which lands on the matching bar at any zoom. Gated on player readiness (no audio → no cursor); decorative (aria-hidden+pointer-events-none, so it never steals a seek click).format— extractedformatClockSeconds, now shared by the player's clock readout and the cursor pill (also fixes a latent float-truncation quirk, e.g.5.3no longer rendered as.2).How to verify
snapshot/replay works) and press play → a white glowing line sweeps the trace tree in lockstep with the waveform cursor; the clock pill at the top tracks it.Verification done
inspector.test.tsxand a cross-file leak incompare, both confirmed on a clean baseline). Server suite: 407 pass.tsc, biome, andpnpm buildall clean.🤖 Generated with Claude Code