fix(voice): apply waveform review findings

Three review agents (no regressions found). Applied:
- Keyboard arrow-seek now reads the live media currentTime, not the throttled
  ~500ms state, so rapid presses accumulate instead of dropping steps.
- Scrubbing the waveform (or the fallback seek bar) BEFORE first play now loads
  the media and plays from the clicked position (was a silent no-op).
- Unplayed bars use a dimmed accent (color-mix 32%) instead of a faint surface
  token, for consistent contrast across TDS-dark/light + normal themes.
- Fixed first-bar always-lit off-by-one ((i+1)/len), and added overflow:hidden so
  the strip clips rather than overflows in a very narrow drawer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 22:01:41 -04:00
co-authored by Claude Opus 4.8
parent 56863d2649
commit 7d02f4e538
@@ -62,17 +62,22 @@ function WaveformSeek({
currentTime,
duration,
onSeek,
getCurrentTime,
}: {
waveform: number[];
currentTime: number;
duration: number;
onSeek: (time: number) => void;
/** Reads the live media time (the `currentTime` prop is throttled ~500ms). */
getCurrentTime: () => number;
}) {
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
const containerRef = useRef<HTMLDivElement>(null);
const bars = useMemo(() => downsampleWaveform(waveform, WAVEFORM_DISPLAY_BARS), [waveform]);
const barMax = useMemo(() => Math.max(...bars, 1), [bars]);
const progress = duration > 0 ? Math.min(1, Math.max(0, currentTime / duration)) : 0;
const accent = lotusTerminal ? 'var(--lt-accent-green)' : color.Primary.Main;
const unplayedColor = `color-mix(in srgb, ${accent} 32%, transparent)`;
const seekFromClientX = useCallback(
(clientX: number) => {
@@ -94,12 +99,14 @@ function WaveformSeek({
};
const handleKeyDown = (evt: React.KeyboardEvent<HTMLDivElement>) => {
if (duration <= 0) return;
// Base off the LIVE time so rapid presses accumulate (the prop is throttled).
const base = getCurrentTime();
if (evt.key === 'ArrowRight' || evt.key === 'ArrowUp') {
evt.preventDefault();
onSeek(Math.min(duration, currentTime + 5));
onSeek(Math.min(duration, base + 5));
} else if (evt.key === 'ArrowLeft' || evt.key === 'ArrowDown') {
evt.preventDefault();
onSeek(Math.max(0, currentTime - 5));
onSeek(Math.max(0, base - 5));
} else if (evt.key === 'Home') {
evt.preventDefault();
onSeek(0);
@@ -132,10 +139,11 @@ function WaveformSeek({
height: toRem(24),
cursor: 'pointer',
touchAction: 'none',
overflow: 'hidden',
}}
>
{bars.map((v, i) => {
const played = bars.length > 0 && i / bars.length <= progress;
const played = bars.length > 0 && (i + 1) / bars.length <= progress;
return (
<div
key={i}
@@ -144,11 +152,7 @@ function WaveformSeek({
minWidth: toRem(2),
height: toRem(2 + (v / barMax) * 16),
borderRadius: toRem(1),
background: played
? lotusTerminal
? 'var(--lt-accent-green)'
: color.Primary.Main
: color.SurfaceVariant.ContainerActive,
background: played ? accent : unplayedColor,
transition: 'background 0.1s',
}}
/>
@@ -196,6 +200,8 @@ export function AudioContent({
);
const audioRef = useRef<HTMLAudioElement | null>(null);
// A seek requested before the media has loaded; applied once metadata arrives.
const pendingSeekRef = useRef<number | null>(null);
useEffect(
() => () => {
@@ -236,6 +242,11 @@ export function AudioContent({
if (!audio) return undefined;
const applyRate = () => {
audio.playbackRate = playbackSpeed;
// Apply a seek that was requested before the source loaded.
if (pendingSeekRef.current != null && audio.readyState >= 1) {
audio.currentTime = pendingSeekRef.current;
pendingSeekRef.current = null;
}
};
// Apply immediately, and re-apply whenever the media element (re)loads a new
// source — e.g. after async decrypt swaps in the blob URL — since the browser
@@ -265,6 +276,20 @@ export function AudioContent({
}
};
// Seeking before the media has loaded (e.g. clicking the waveform first) loads it
// and applies the position once metadata arrives (the <audio> autoPlays).
const handleSeek = useCallback(
(time: number) => {
if (srcState.status === AsyncStatus.Success) {
seek(time);
} else if (srcState.status !== AsyncStatus.Loading) {
pendingSeekRef.current = time;
loadSrc();
}
},
[srcState.status, seek, loadSrc],
);
const hasWaveform = !!waveform && waveform.length > 0 && duration > 0;
return renderMediaControl({
@@ -273,7 +298,8 @@ export function AudioContent({
waveform={waveform ?? []}
currentTime={currentTime}
duration={duration}
onSeek={seek}
onSeek={handleSeek}
getCurrentTime={() => audioRef.current?.currentTime ?? currentTime}
/>
) : (
<Range
@@ -281,7 +307,7 @@ export function AudioContent({
min={0}
max={duration || 1}
values={[currentTime]}
onChange={(values) => seek(values[0])}
onChange={(values) => handleSeek(values[0])}
renderTrack={(params) => (
<div {...params.props}>
{params.children}