@workflow/web-shared 4.1.0-beta.52 → 4.1.0-beta.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/components/error-boundary.d.ts +15 -20
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/error-boundary.js +17 -31
  4. package/dist/components/error-boundary.js.map +1 -1
  5. package/dist/components/event-list-view.d.ts +7 -6
  6. package/dist/components/event-list-view.d.ts.map +1 -1
  7. package/dist/components/event-list-view.js +492 -109
  8. package/dist/components/event-list-view.js.map +1 -1
  9. package/dist/components/index.d.ts +1 -0
  10. package/dist/components/index.d.ts.map +1 -1
  11. package/dist/components/index.js +1 -0
  12. package/dist/components/index.js.map +1 -1
  13. package/dist/components/run-trace-view.d.ts +2 -1
  14. package/dist/components/run-trace-view.d.ts.map +1 -1
  15. package/dist/components/run-trace-view.js +2 -2
  16. package/dist/components/run-trace-view.js.map +1 -1
  17. package/dist/components/sidebar/attribute-panel.d.ts +2 -1
  18. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  19. package/dist/components/sidebar/attribute-panel.js +53 -142
  20. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  21. package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
  22. package/dist/components/sidebar/conversation-view.js +3 -17
  23. package/dist/components/sidebar/conversation-view.js.map +1 -1
  24. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  25. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  26. package/dist/components/sidebar/entity-detail-panel.js +63 -10
  27. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  28. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  29. package/dist/components/sidebar/events-list.js +4 -8
  30. package/dist/components/sidebar/events-list.js.map +1 -1
  31. package/dist/components/sidebar/resolve-hook-modal.d.ts +3 -0
  32. package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
  33. package/dist/components/sidebar/resolve-hook-modal.js +152 -3
  34. package/dist/components/sidebar/resolve-hook-modal.js.map +1 -1
  35. package/dist/components/stream-viewer.d.ts +7 -5
  36. package/dist/components/stream-viewer.d.ts.map +1 -1
  37. package/dist/components/stream-viewer.js +54 -22
  38. package/dist/components/stream-viewer.js.map +1 -1
  39. package/dist/components/trace-viewer/components/markers.d.ts +2 -1
  40. package/dist/components/trace-viewer/components/markers.d.ts.map +1 -1
  41. package/dist/components/trace-viewer/components/markers.js +59 -20
  42. package/dist/components/trace-viewer/components/markers.js.map +1 -1
  43. package/dist/components/trace-viewer/components/node.d.ts +5 -1
  44. package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
  45. package/dist/components/trace-viewer/components/node.js +250 -68
  46. package/dist/components/trace-viewer/components/node.js.map +1 -1
  47. package/dist/components/trace-viewer/components/span-content.d.ts +19 -0
  48. package/dist/components/trace-viewer/components/span-content.d.ts.map +1 -0
  49. package/dist/components/trace-viewer/components/span-content.js +137 -0
  50. package/dist/components/trace-viewer/components/span-content.js.map +1 -0
  51. package/dist/components/trace-viewer/components/span-detail-panel.d.ts.map +1 -1
  52. package/dist/components/trace-viewer/components/span-detail-panel.js +3 -2
  53. package/dist/components/trace-viewer/components/span-detail-panel.js.map +1 -1
  54. package/dist/components/trace-viewer/components/span-segments.d.ts +50 -0
  55. package/dist/components/trace-viewer/components/span-segments.d.ts.map +1 -0
  56. package/dist/components/trace-viewer/components/span-segments.js +392 -0
  57. package/dist/components/trace-viewer/components/span-segments.js.map +1 -0
  58. package/dist/components/trace-viewer/components/span-strategies.d.ts +46 -0
  59. package/dist/components/trace-viewer/components/span-strategies.d.ts.map +1 -0
  60. package/dist/components/trace-viewer/components/span-strategies.js +108 -0
  61. package/dist/components/trace-viewer/components/span-strategies.js.map +1 -0
  62. package/dist/components/trace-viewer/context.d.ts +7 -6
  63. package/dist/components/trace-viewer/context.d.ts.map +1 -1
  64. package/dist/components/trace-viewer/context.js +47 -18
  65. package/dist/components/trace-viewer/context.js.map +1 -1
  66. package/dist/components/trace-viewer/trace-viewer.d.ts +5 -1
  67. package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
  68. package/dist/components/trace-viewer/trace-viewer.js +87 -11
  69. package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
  70. package/dist/components/trace-viewer/trace-viewer.module.css +179 -6
  71. package/dist/components/trace-viewer/util/timing.d.ts +5 -0
  72. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  73. package/dist/components/trace-viewer/util/timing.js +12 -0
  74. package/dist/components/trace-viewer/util/timing.js.map +1 -1
  75. package/dist/components/trace-viewer/util/use-streaming-spans.d.ts +1 -1
  76. package/dist/components/trace-viewer/util/use-streaming-spans.d.ts.map +1 -1
  77. package/dist/components/trace-viewer/util/use-streaming-spans.js +29 -17
  78. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +1 -1
  79. package/dist/components/trace-viewer/worker.js +3 -1
  80. package/dist/components/trace-viewer/worker.js.map +1 -1
  81. package/dist/components/ui/alert.js +3 -3
  82. package/dist/components/ui/alert.js.map +1 -1
  83. package/dist/components/ui/card.d.ts.map +1 -1
  84. package/dist/components/ui/card.js +2 -2
  85. package/dist/components/ui/card.js.map +1 -1
  86. package/dist/components/ui/data-inspector.d.ts +17 -0
  87. package/dist/components/ui/data-inspector.d.ts.map +1 -0
  88. package/dist/components/ui/data-inspector.js +184 -0
  89. package/dist/components/ui/data-inspector.js.map +1 -0
  90. package/dist/components/ui/error-card.d.ts.map +1 -1
  91. package/dist/components/ui/error-card.js +4 -1
  92. package/dist/components/ui/error-card.js.map +1 -1
  93. package/dist/components/ui/inspector-theme.d.ts +39 -24
  94. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  95. package/dist/components/ui/inspector-theme.js +90 -38
  96. package/dist/components/ui/inspector-theme.js.map +1 -1
  97. package/dist/components/ui/skeleton.d.ts +1 -1
  98. package/dist/components/ui/skeleton.d.ts.map +1 -1
  99. package/dist/components/ui/skeleton.js +2 -2
  100. package/dist/components/ui/skeleton.js.map +1 -1
  101. package/dist/components/workflow-trace-view.d.ts +3 -1
  102. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  103. package/dist/components/workflow-trace-view.js +435 -21
  104. package/dist/components/workflow-trace-view.js.map +1 -1
  105. package/dist/components/workflow-traces/trace-span-construction.d.ts +1 -1
  106. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  107. package/dist/components/workflow-traces/trace-span-construction.js +2 -2
  108. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  109. package/dist/lib/hydration.d.ts.map +1 -1
  110. package/dist/lib/hydration.js +17 -3
  111. package/dist/lib/hydration.js.map +1 -1
  112. package/dist/styles.css +186 -0
  113. package/package.json +8 -7
  114. package/src/components/error-boundary.tsx +29 -40
  115. package/src/components/event-list-view.tsx +1000 -287
  116. package/src/components/index.ts +1 -0
  117. package/src/components/run-trace-view.tsx +3 -0
  118. package/src/components/sidebar/attribute-panel.tsx +58 -258
  119. package/src/components/sidebar/conversation-view.tsx +30 -27
  120. package/src/components/sidebar/entity-detail-panel.tsx +86 -20
  121. package/src/components/sidebar/events-list.tsx +4 -11
  122. package/src/components/sidebar/resolve-hook-modal.tsx +206 -47
  123. package/src/components/stream-viewer.tsx +138 -61
  124. package/src/components/trace-viewer/components/markers.tsx +69 -21
  125. package/src/components/trace-viewer/components/node.tsx +346 -100
  126. package/src/components/trace-viewer/components/span-content.tsx +247 -0
  127. package/src/components/trace-viewer/components/span-detail-panel.tsx +7 -2
  128. package/src/components/trace-viewer/components/span-segments.ts +516 -0
  129. package/src/components/trace-viewer/components/span-strategies.ts +205 -0
  130. package/src/components/trace-viewer/context.tsx +92 -40
  131. package/src/components/trace-viewer/trace-viewer.module.css +179 -6
  132. package/src/components/trace-viewer/trace-viewer.tsx +115 -11
  133. package/src/components/trace-viewer/util/timing.ts +13 -0
  134. package/src/components/trace-viewer/util/use-streaming-spans.ts +28 -17
  135. package/src/components/trace-viewer/worker.ts +4 -1
  136. package/src/components/ui/alert.tsx +3 -3
  137. package/src/components/ui/card.tsx +3 -5
  138. package/src/components/ui/data-inspector.tsx +318 -0
  139. package/src/components/ui/error-card.tsx +17 -6
  140. package/src/components/ui/inspector-theme.ts +127 -39
  141. package/src/components/ui/skeleton.tsx +3 -1
  142. package/src/components/workflow-trace-view.tsx +625 -26
  143. package/src/components/workflow-traces/trace-span-construction.ts +3 -2
  144. package/src/lib/hydration.ts +17 -8
  145. package/src/styles.css +186 -0
@@ -1,27 +1,116 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from 'react';
4
- import { ObjectInspector } from 'react-inspector';
5
- import { useDarkMode } from '../hooks/use-dark-mode';
3
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { DataInspector } from './ui/data-inspector';
5
+ import { Skeleton } from './ui/skeleton';
6
+
7
+ // ──────────────────────────────────────────────────────────────────────────
8
+ // Helpers
9
+ // ──────────────────────────────────────────────────────────────────────────
10
+
11
+ function deserializeChunkText(text: string): string {
12
+ try {
13
+ const parsed = JSON.parse(text);
14
+ if (typeof parsed === 'string') {
15
+ return parsed;
16
+ }
17
+ return JSON.stringify(parsed, null, 2);
18
+ } catch {
19
+ return text;
20
+ }
21
+ }
22
+
23
+ function parseChunkData(text: string): unknown {
24
+ try {
25
+ return JSON.parse(text);
26
+ } catch {
27
+ return text;
28
+ }
29
+ }
30
+
31
+ // ──────────────────────────────────────────────────────────────────────────
32
+ // Types
33
+ // ──────────────────────────────────────────────────────────────────────────
6
34
 
7
35
  export interface StreamChunk {
8
36
  id: number;
9
- /** The raw hydrated value from the stream */
10
- data: unknown;
37
+ text: string;
11
38
  }
12
39
 
40
+ type Chunk = StreamChunk;
41
+
13
42
  interface StreamViewerProps {
14
43
  streamId: string;
15
- chunks: StreamChunk[];
44
+ chunks: Chunk[];
16
45
  isLive: boolean;
17
46
  error?: string | null;
47
+ /** True while the initial stream connection is being established */
48
+ isLoading?: boolean;
49
+ }
50
+
51
+ // ──────────────────────────────────────────────────────────────────────────
52
+ // Chunk row — memoized to prevent remounts during polling
53
+ // ──────────────────────────────────────────────────────────────────────────
54
+
55
+ const ChunkRow = React.memo(function ChunkRow({
56
+ chunk,
57
+ index,
58
+ }: {
59
+ chunk: Chunk;
60
+ index: number;
61
+ }) {
62
+ const parsed = parseChunkData(chunk.text);
63
+
64
+ return (
65
+ <div
66
+ className="text-[11px] rounded-md border p-3"
67
+ style={{
68
+ borderColor: 'var(--ds-gray-300)',
69
+ backgroundColor: 'var(--ds-gray-100)',
70
+ }}
71
+ >
72
+ <span
73
+ className="select-none mr-2"
74
+ style={{ color: 'var(--ds-gray-500)' }}
75
+ >
76
+ [{index}]
77
+ </span>
78
+ {typeof parsed === 'string' ? (
79
+ <span
80
+ className="whitespace-pre-wrap break-words"
81
+ style={{ color: 'var(--ds-gray-1000)' }}
82
+ >
83
+ {deserializeChunkText(parsed)}
84
+ </span>
85
+ ) : (
86
+ <DataInspector data={parsed} expandLevel={1} />
87
+ )}
88
+ </div>
89
+ );
90
+ });
91
+
92
+ // ──────────────────────────────────────────────────────────────────────────
93
+ // Skeleton loading
94
+ // ──────────────────────────────────────────────────────────────────────────
95
+
96
+ function StreamSkeleton() {
97
+ return (
98
+ <div className="flex flex-col gap-3 animate-in fade-in">
99
+ <Skeleton style={{ width: 120, height: 16, borderRadius: 4 }} />
100
+ {[1, 2, 3, 4].map((i) => (
101
+ <Skeleton key={i} style={{ height: 56, borderRadius: 6 }} />
102
+ ))}
103
+ </div>
104
+ );
18
105
  }
19
106
 
20
- import { inspectorThemeDark, inspectorThemeLight } from './ui/inspector-theme';
107
+ // ──────────────────────────────────────────────────────────────────────────
108
+ // Main component
109
+ // ──────────────────────────────────────────────────────────────────────────
21
110
 
22
111
  /**
23
112
  * StreamViewer component that displays real-time stream data.
24
- * Each chunk is rendered with ObjectInspector for proper display
113
+ * Each chunk is rendered with DataInspector for proper display
25
114
  * of complex types (Map, Set, Date, custom classes, etc.).
26
115
  */
27
116
  export function StreamViewer({
@@ -29,8 +118,8 @@ export function StreamViewer({
29
118
  chunks,
30
119
  isLive,
31
120
  error,
121
+ isLoading,
32
122
  }: StreamViewerProps) {
33
- const isDark = useDarkMode();
34
123
  const [hasMoreBelow, setHasMoreBelow] = useState(false);
35
124
  const scrollRef = useRef<HTMLDivElement>(null);
36
125
 
@@ -50,36 +139,49 @@ export function StreamViewer({
50
139
  checkScrollPosition();
51
140
  }, [chunks.length, checkScrollPosition]);
52
141
 
53
- const theme = isDark ? inspectorThemeDark : inspectorThemeLight;
142
+ // Show skeleton when loading and no chunks have arrived yet
143
+ if (isLoading && chunks.length === 0) {
144
+ return (
145
+ <div className="flex flex-col h-full pb-4">
146
+ <StreamSkeleton />
147
+ </div>
148
+ );
149
+ }
54
150
 
55
151
  return (
56
152
  <div className="flex flex-col h-full pb-4">
57
- <div className="flex items-center justify-between mb-3 px-1">
58
- <code
59
- className="text-xs font-mono truncate max-w-[80%]"
60
- style={{ color: 'var(--ds-gray-900)' }}
61
- title={streamId}
62
- >
63
- {streamId}
64
- </code>
65
- <span
66
- className="text-xs flex items-center gap-1.5"
67
- style={{
68
- color: isLive ? 'var(--ds-green-700)' : 'var(--ds-gray-600)',
69
- }}
70
- >
153
+ {/* Live indicator */}
154
+ {isLive && (
155
+ <div className="flex items-center gap-1.5 mb-3 px-1">
71
156
  <span
72
157
  className="inline-block w-2 h-2 rounded-full"
73
- style={{
74
- backgroundColor: isLive
75
- ? 'var(--ds-green-600)'
76
- : 'var(--ds-gray-500)',
77
- }}
158
+ style={{ backgroundColor: 'var(--ds-green-600)' }}
78
159
  />
79
- {isLive ? 'Live' : 'Closed'}
80
- </span>
81
- </div>
160
+ <span className="text-xs" style={{ color: 'var(--ds-green-700)' }}>
161
+ Live
162
+ </span>
163
+ </div>
164
+ )}
165
+
166
+ {/* Header */}
167
+ {chunks.length > 0 && (
168
+ <div className="flex items-center gap-2 mb-2 px-1">
169
+ <span
170
+ className="text-[13px] font-medium"
171
+ style={{ color: 'var(--ds-gray-900)' }}
172
+ >
173
+ Stream Chunks
174
+ </span>
175
+ <span
176
+ className="text-xs tabular-nums"
177
+ style={{ color: 'var(--ds-gray-600)' }}
178
+ >
179
+ ({chunks.length})
180
+ </span>
181
+ </div>
182
+ )}
82
183
 
184
+ {/* Content */}
83
185
  <div className="relative flex-1 min-h-[200px]">
84
186
  <div
85
187
  ref={scrollRef}
@@ -111,36 +213,11 @@ export function StreamViewer({
111
213
  </div>
112
214
  ) : (
113
215
  chunks.map((chunk, index) => (
114
- <div
216
+ <ChunkRow
115
217
  key={`${streamId}-chunk-${chunk.id}`}
116
- className="rounded-md border p-3"
117
- style={{
118
- borderColor: 'var(--ds-gray-300)',
119
- }}
120
- >
121
- <span
122
- className="select-none mr-2 text-[11px] font-mono"
123
- style={{ color: 'var(--ds-gray-500)' }}
124
- >
125
- [{index}]
126
- </span>
127
- {typeof chunk.data === 'string' ? (
128
- <span
129
- className="text-[11px] font-mono"
130
- style={{ color: 'var(--ds-gray-1000)' }}
131
- >
132
- {chunk.data}
133
- </span>
134
- ) : (
135
- <ObjectInspector
136
- data={chunk.data}
137
- // @ts-expect-error react-inspector accepts theme objects at runtime despite
138
- // types declaring string only — see https://github.com/storybookjs/react-inspector/blob/main/README.md#theme
139
- theme={theme}
140
- expandLevel={1}
141
- />
142
- )}
143
- </div>
218
+ chunk={chunk}
219
+ index={index}
220
+ />
144
221
  ))
145
222
  )}
146
223
  </div>
@@ -7,7 +7,7 @@ import type {
7
7
  MutableRefObject,
8
8
  ReactNode,
9
9
  } from 'react';
10
- import { useEffect, useMemo, useRef } from 'react';
10
+ import { useEffect, useMemo, useRef, useState } from 'react';
11
11
  import { type TraceViewerAction, useTraceViewer } from '../context';
12
12
  import styles from '../trace-viewer.module.css';
13
13
  import type {
@@ -23,32 +23,70 @@ import {
23
23
  ROW_PADDING,
24
24
  TIMELINE_PADDING,
25
25
  } from '../util/constants';
26
- import { formatDurationForTimeline, formatTimeSelection } from '../util/timing';
26
+ import {
27
+ formatDurationForTimeline,
28
+ formatTimeSelection,
29
+ formatWallClockTime,
30
+ } from '../util/timing';
27
31
  import { useImmediateStyle } from '../util/use-immediate-style';
28
32
  import { useTrackpadZoom } from '../util/use-trackpad-zoom';
29
33
 
30
- const DIVISORS = [1, 2, 4, 5, 8, 10];
34
+ /**
35
+ * Snap a raw duration to the nearest "nice" number in the 1-2-5 × 10^n
36
+ * sequence (e.g. …, 0.5, 1, 2, 5, 10, 20, 50, 100, …).
37
+ */
38
+ function snapToNice(raw: number): number {
39
+ if (raw <= 0) return 1;
40
+ const log10 = Math.floor(Math.log10(raw));
41
+ const pow = 10 ** log10;
42
+ const normalized = raw / pow;
43
+
44
+ if (normalized <= 1.5) return pow;
45
+ if (normalized <= 3.5) return 2 * pow;
46
+ if (normalized <= 7.5) return 5 * pow;
47
+ return 10 * pow;
48
+ }
31
49
 
32
- export function Markers({ scale }: { scale: number }): ReactNode {
50
+ export function Markers({
51
+ scale,
52
+ isLive = false,
53
+ }: {
54
+ scale: number;
55
+ isLive?: boolean;
56
+ }): ReactNode {
33
57
  const {
34
58
  state: { root },
35
59
  } = useTraceViewer();
60
+ // Force a re-render every second when live to pick up new tick marks.
61
+ // The markers container width is grown at 60fps by useLiveTick;
62
+ // this interval only ensures the marker *labels* stay current.
63
+ const [, forceMarkerUpdate] = useState(0);
64
+ useEffect(() => {
65
+ if (!isLive) return;
66
+ const id = setInterval(() => forceMarkerUpdate((v) => v + 1), 1000);
67
+ return () => clearInterval(id);
68
+ }, [isLive]);
36
69
 
37
70
  const fullDuration = root.duration;
38
- const logFull = Math.floor(Math.log10(fullDuration));
39
- const logHalf = Math.floor(Math.log10(fullDuration * 0.5));
40
- let markerDuration = Math.max(
41
- 2,
42
- 10 ** logFull * (logHalf === logFull ? 1 : 0.5)
43
- );
44
- let markerWidth = markerDuration * scale;
45
- let divisor = 1;
46
- for (const d of DIVISORS) {
47
- if (markerDuration / d < 24) break;
48
- divisor = d;
71
+
72
+ // Calculate a marker interval that gives reasonable spacing at any zoom level.
73
+ // We target ~50px per notch mark; labels appear on every Nth notch via labelSpacing.
74
+ // When scale is <= 0 (e.g. the initial -1 sentinel), fall back to a safe default
75
+ // to avoid generating an absurd number of markers.
76
+ const effectiveScale =
77
+ scale > 0 ? scale : fullDuration > 0 ? 1 / fullDuration : 1;
78
+ const targetNotchPx = 50;
79
+ let markerDuration = snapToNice(targetNotchPx / effectiveScale);
80
+ markerDuration = Math.max(1, markerDuration);
81
+ let markerWidth = markerDuration * effectiveScale;
82
+
83
+ // Cap marker count to avoid creating too many DOM elements at extreme zoom
84
+ // on very long traces. Only the visible portion is shown on screen anyway.
85
+ const MAX_MARKERS = 1000;
86
+ if (fullDuration / markerDuration > MAX_MARKERS) {
87
+ markerDuration = snapToNice(fullDuration / MAX_MARKERS);
88
+ markerWidth = markerDuration * effectiveScale;
49
89
  }
50
- markerDuration /= divisor;
51
- markerWidth /= divisor;
52
90
  const markerCount = Math.ceil(fullDuration / markerDuration);
53
91
 
54
92
  // How often labels should appear for markers, e.g. 3 === one label for every third marker
@@ -77,6 +115,9 @@ export function Markers({ scale }: { scale: number }): ReactNode {
77
115
  {hasLabel ? (
78
116
  <span className={styles.markerLabel}>
79
117
  {formatDurationForTimeline(markerDuration * i)}
118
+ <span className={styles.markerClockTime}>
119
+ {formatWallClockTime(root.startTime + markerDuration * i)}
120
+ </span>
80
121
  </span>
81
122
  ) : null}
82
123
  </span>
@@ -327,13 +368,20 @@ export function CursorMarker({
327
368
  cache.set(span.span.spanId, {});
328
369
  }
329
370
 
330
- // Event Hover
371
+ // Event Hover — only show the nearest event when multiple overlap
331
372
  const eventSpreadPx = 12;
332
373
  const eventSpreadMs = eventSpreadPx / scale;
374
+ let closestEvent: (typeof eventsRef.current)[number] | null = null;
375
+ let closestDist = Infinity;
333
376
  for (const event of eventsRef.current) {
334
- const isHovered =
335
- t >= event.timestamp - eventSpreadMs &&
336
- t <= event.timestamp + eventSpreadMs;
377
+ const dist = Math.abs(event.timestamp - t);
378
+ if (dist <= eventSpreadMs && dist < closestDist) {
379
+ closestDist = dist;
380
+ closestEvent = event;
381
+ }
382
+ }
383
+ for (const event of eventsRef.current) {
384
+ const isHovered = event === closestEvent;
337
385
  if (event.isHovered === isHovered) continue;
338
386
  event.isHovered = isHovered;
339
387
  const $event = event.ref?.current;