@@ -19,10 +19,9 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
1919 const [ expandedSpans , setExpandedSpans ] = useState < Set < string > > ( new Set ( ) )
2020 const [ typeFilters , setTypeFilters ] = useState < Record < string , boolean > > ( { } )
2121 const containerRef = useRef < HTMLDivElement | null > ( null )
22- const timelineHitboxRef = useRef < HTMLDivElement | null > ( null )
23- const [ hoveredPercent , setHoveredPercent ] = useState < number | null > ( null )
2422 const [ hoveredWorkflowMs , setHoveredWorkflowMs ] = useState < number | null > ( null )
2523 const [ hoveredX , setHoveredX ] = useState < number | null > ( null )
24+ const [ hoveredY , setHoveredY ] = useState < number | null > ( null )
2625 const [ containerWidth , setContainerWidth ] = useState < number > ( 0 )
2726
2827 type ChipVisibility = {
@@ -151,39 +150,24 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
151150 return traceSpans ? filterTree ( traceSpans ) : [ ]
152151 } , [ traceSpans , effectiveTypeFilters ] )
153152
154- const forwardHover = useCallback (
155- ( clientX : number , clientY : number ) => {
156- if ( ! timelineHitboxRef . current || ! containerRef . current ) return
157-
158- const railRect = timelineHitboxRef . current . getBoundingClientRect ( )
153+ const handleTimelineHover = useCallback (
154+ ( clientX : number , clientY : number , timelineRect : DOMRect ) => {
155+ if ( ! containerRef . current ) return
159156 const containerRect = containerRef . current . getBoundingClientRect ( )
157+ const clamped = Math . max ( 0 , Math . min ( 1 , ( clientX - timelineRect . left ) / timelineRect . width ) )
160158
161- const withinX = clientX >= railRect . left && clientX <= railRect . right
162- const withinY = clientY >= railRect . top && clientY <= railRect . bottom
163-
164- if ( ! withinX || ! withinY ) {
165- setHoveredPercent ( null )
166- setHoveredWorkflowMs ( null )
167- setHoveredX ( null )
168- return
169- }
170-
171- const clamped = Math . max ( 0 , Math . min ( 1 , ( clientX - railRect . left ) / railRect . width ) )
172- setHoveredPercent ( clamped * 100 )
173159 setHoveredWorkflowMs ( workflowStartTime + clamped * actualTotalDuration )
174- setHoveredX ( railRect . left + clamped * railRect . width - containerRect . left )
160+ setHoveredX ( timelineRect . left + clamped * timelineRect . width - containerRect . left )
161+ setHoveredY ( timelineRect . top - containerRect . top )
175162 } ,
176163 [ actualTotalDuration , workflowStartTime ]
177164 )
178165
179- useEffect ( ( ) => {
180- const handleMove = ( event : MouseEvent ) => {
181- forwardHover ( event . clientX , event . clientY )
182- }
183-
184- window . addEventListener ( 'pointermove' , handleMove )
185- return ( ) => window . removeEventListener ( 'pointermove' , handleMove )
186- } , [ forwardHover ] )
166+ const handleTimelineLeave = useCallback ( ( ) => {
167+ setHoveredWorkflowMs ( null )
168+ setHoveredX ( null )
169+ setHoveredY ( null )
170+ } , [ ] )
187171
188172 useEffect ( ( ) => {
189173 if ( ! containerRef . current ) return
@@ -203,7 +187,7 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
203187 }
204188
205189 return (
206- < div className = 'w-full' >
190+ < div className = 'relative w-full' >
207191 < div className = 'mb-2 flex items-center justify-between' >
208192 < div className = 'flex items-center gap-2' >
209193 < div className = 'font-medium text-muted-foreground text-xs' > Workflow Execution</ div >
@@ -234,11 +218,6 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
234218 < div
235219 ref = { containerRef }
236220 className = 'relative w-full overflow-hidden rounded-md border shadow-sm'
237- onMouseLeave = { ( ) => {
238- setHoveredPercent ( null )
239- setHoveredWorkflowMs ( null )
240- setHoveredX ( null )
241- } }
242221 >
243222 { filtered . map ( ( span , index ) => {
244223 const normalizedSpan = normalizeChildWorkflowSpan ( span )
@@ -276,39 +255,26 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
276255 onToggle = { handleSpanToggle }
277256 expandedSpans = { expandedSpans }
278257 hasSubItems = { hasSubItems }
279- hoveredPercent = { hoveredPercent }
280- hoveredWorkflowMs = { hoveredWorkflowMs }
281- forwardHover = { forwardHover }
258+ onTimelineHover = { handleTimelineHover }
259+ onTimelineLeave = { handleTimelineLeave }
282260 gapBeforeMs = { gapMs }
283261 gapBeforePercent = { gapPercent }
284262 showRelativeChip = { chipVisibility . relative }
285263 chipVisibility = { chipVisibility }
286264 />
287265 )
288266 } ) }
267+ </ div >
289268
290- { /* Time label for hover (keep top label, row lines render per-row) */ }
291- { hoveredPercent !== null && hoveredX !== null && (
292- < div
293- className = '-translate-x-1/2 pointer-events-none absolute top-1 rounded bg-popover px-1.5 py-0.5 text-[10px] text-foreground shadow'
294- style = { { left : hoveredX , zIndex : 20 } }
295- >
296- { formatDurationDisplay ( Math . max ( 0 , ( hoveredWorkflowMs || 0 ) - workflowStartTime ) ) }
297- </ div >
298- ) }
299-
300- { /* Hover capture area - aligned to timeline bars, not extending to edge */ }
269+ { /* Time label for hover (positioned at top of timeline) */ }
270+ { hoveredWorkflowMs !== null && hoveredX !== null && hoveredY !== null && (
301271 < div
302- ref = { timelineHitboxRef }
303- className = 'pointer-events-auto absolute inset-y-0 right-[73px] w-[calc(45%-73px)]'
304- onPointerMove = { ( e ) => forwardHover ( e . clientX , e . clientY ) }
305- onPointerLeave = { ( ) => {
306- setHoveredPercent ( null )
307- setHoveredWorkflowMs ( null )
308- setHoveredX ( null )
309- } }
310- />
311- </ div >
272+ className = '-translate-x-1/2 pointer-events-none absolute rounded border bg-popover px-1.5 py-0.5 font-mono text-[10px] text-foreground shadow-lg'
273+ style = { { left : hoveredX , top : hoveredY , zIndex : 20 } }
274+ >
275+ { formatDurationDisplay ( Math . max ( 0 , ( hoveredWorkflowMs || 0 ) - workflowStartTime ) ) }
276+ </ div >
277+ ) }
312278 </ div >
313279 )
314280}
0 commit comments