@workflow/web-shared 5.0.0-beta.2 → 5.0.0-beta.20

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 (321) hide show
  1. package/dist/components/error-boundary.js +1 -1
  2. package/dist/components/event-list-view.d.ts +16 -1
  3. package/dist/components/event-list-view.d.ts.map +1 -1
  4. package/dist/components/event-list-view.js +196 -85
  5. package/dist/components/hook-actions.js +1 -1
  6. package/dist/components/index.d.ts +4 -0
  7. package/dist/components/index.d.ts.map +1 -1
  8. package/dist/components/index.js +5 -1
  9. package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
  10. package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
  11. package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
  12. package/dist/components/new-trace-viewer/components/event-list.d.ts +10 -0
  13. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
  14. package/dist/components/new-trace-viewer/components/event-list.js +46 -0
  15. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
  16. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
  17. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
  18. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
  19. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
  20. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
  21. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
  22. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
  23. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
  24. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
  25. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
  26. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
  27. package/dist/components/new-trace-viewer/components/span-markers.d.ts +46 -0
  28. package/dist/components/new-trace-viewer/components/span-markers.d.ts.map +1 -0
  29. package/dist/components/new-trace-viewer/components/span-markers.js +85 -0
  30. package/dist/components/new-trace-viewer/components/split-pane.d.ts +15 -0
  31. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
  32. package/dist/components/new-trace-viewer/components/split-pane.js +105 -0
  33. package/dist/components/new-trace-viewer/components/timeline.d.ts +36 -0
  34. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
  35. package/dist/components/new-trace-viewer/components/timeline.js +254 -0
  36. package/dist/components/new-trace-viewer/components/timeline.module.css +46 -0
  37. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts +2 -0
  38. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -0
  39. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.js +19 -0
  40. package/dist/components/new-trace-viewer/components/use-row-window.d.ts +34 -0
  41. package/dist/components/new-trace-viewer/components/use-row-window.d.ts.map +1 -0
  42. package/dist/components/new-trace-viewer/components/use-row-window.js +107 -0
  43. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  44. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  45. package/dist/components/new-trace-viewer/context.js +47 -0
  46. package/dist/components/new-trace-viewer/icons.d.ts +6 -0
  47. package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
  48. package/dist/components/new-trace-viewer/icons.js +15 -0
  49. package/dist/components/new-trace-viewer/search.d.ts +19 -0
  50. package/dist/components/new-trace-viewer/search.d.ts.map +1 -0
  51. package/dist/components/new-trace-viewer/search.js +134 -0
  52. package/dist/components/new-trace-viewer/trace-viewer.d.ts +11 -0
  53. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
  54. package/dist/components/new-trace-viewer/trace-viewer.js +462 -0
  55. package/dist/components/new-trace-viewer/types.d.ts +3 -0
  56. package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
  57. package/dist/components/new-trace-viewer/types.js +2 -0
  58. package/dist/components/new-trace-viewer/utils.d.ts +61 -0
  59. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  60. package/dist/components/new-trace-viewer/utils.js +345 -0
  61. package/dist/components/run-trace-view.js +1 -1
  62. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  63. package/dist/components/sidebar/attribute-panel.js +129 -102
  64. package/dist/components/sidebar/attributes-block.d.ts +28 -0
  65. package/dist/components/sidebar/attributes-block.d.ts.map +1 -0
  66. package/dist/components/sidebar/attributes-block.js +81 -0
  67. package/dist/components/sidebar/conversation-view.js +1 -1
  68. package/dist/components/sidebar/copyable-data-block.d.ts +1 -0
  69. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
  70. package/dist/components/sidebar/copyable-data-block.js +16 -15
  71. package/dist/components/sidebar/detail-card.d.ts +5 -6
  72. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  73. package/dist/components/sidebar/detail-card.js +32 -16
  74. package/dist/components/sidebar/entity-detail-panel.d.ts +1 -3
  75. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  76. package/dist/components/sidebar/entity-detail-panel.js +17 -35
  77. package/dist/components/sidebar/events-list.d.ts +3 -1
  78. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  79. package/dist/components/sidebar/events-list.js +36 -22
  80. package/dist/components/sidebar/resolve-hook-modal.js +1 -1
  81. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  82. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  83. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  84. package/dist/components/sidebar/span-detail-merge.d.ts +16 -0
  85. package/dist/components/sidebar/span-detail-merge.d.ts.map +1 -0
  86. package/dist/components/sidebar/span-detail-merge.js +53 -0
  87. package/dist/components/stream-viewer.d.ts +1 -1
  88. package/dist/components/stream-viewer.d.ts.map +1 -1
  89. package/dist/components/stream-viewer.js +3 -27
  90. package/dist/components/trace-viewer/components/map.js +1 -1
  91. package/dist/components/trace-viewer/components/markers.js +1 -1
  92. package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
  93. package/dist/components/trace-viewer/components/node.js +2 -1
  94. package/dist/components/trace-viewer/components/search-input.js +1 -1
  95. package/dist/components/trace-viewer/components/search.js +1 -1
  96. package/dist/components/trace-viewer/components/span-content.js +1 -1
  97. package/dist/components/trace-viewer/components/span-detail-panel.js +1 -1
  98. package/dist/components/trace-viewer/components/span-segments.js +1 -1
  99. package/dist/components/trace-viewer/components/span-strategies.js +1 -1
  100. package/dist/components/trace-viewer/components/ui.js +1 -1
  101. package/dist/components/trace-viewer/components/zoom-button.js +1 -1
  102. package/dist/components/trace-viewer/components/zoom-icons.js +1 -1
  103. package/dist/components/trace-viewer/context.js +1 -1
  104. package/dist/components/trace-viewer/index.js +1 -1
  105. package/dist/components/trace-viewer/trace-viewer.js +1 -1
  106. package/dist/components/trace-viewer/trace-viewer.module.css +4 -4
  107. package/dist/components/trace-viewer/types.js +1 -1
  108. package/dist/components/trace-viewer/util/constants.js +1 -1
  109. package/dist/components/trace-viewer/util/scrollbar-width.js +1 -1
  110. package/dist/components/trace-viewer/util/timing.d.ts +3 -3
  111. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  112. package/dist/components/trace-viewer/util/timing.js +8 -11
  113. package/dist/components/trace-viewer/util/tree.js +1 -1
  114. package/dist/components/trace-viewer/util/use-immediate-style.js +1 -1
  115. package/dist/components/trace-viewer/util/use-streaming-spans.js +1 -1
  116. package/dist/components/trace-viewer/util/use-trackpad-zoom.js +1 -1
  117. package/dist/components/trace-viewer/worker.js +1 -1
  118. package/dist/components/trace-viewer-new.d.ts +14 -0
  119. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  120. package/dist/components/trace-viewer-new.js +21 -0
  121. package/dist/components/ui/alert.js +1 -1
  122. package/dist/components/ui/button.d.ts +10 -0
  123. package/dist/components/ui/button.d.ts.map +1 -0
  124. package/dist/components/ui/button.js +46 -0
  125. package/dist/components/ui/card.js +1 -1
  126. package/dist/components/ui/context-card.d.ts +47 -0
  127. package/dist/components/ui/context-card.d.ts.map +1 -0
  128. package/dist/components/ui/context-card.js +471 -0
  129. package/dist/components/ui/data-inspector.d.ts +22 -0
  130. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  131. package/dist/components/ui/data-inspector.js +103 -13
  132. package/dist/components/ui/decrypt-button.js +1 -1
  133. package/dist/components/ui/error-card.js +1 -1
  134. package/dist/components/ui/error-stack-block.d.ts +17 -10
  135. package/dist/components/ui/error-stack-block.d.ts.map +1 -1
  136. package/dist/components/ui/error-stack-block.js +49 -30
  137. package/dist/components/ui/icon-button.d.ts +19 -0
  138. package/dist/components/ui/icon-button.d.ts.map +1 -0
  139. package/dist/components/ui/icon-button.js +29 -0
  140. package/dist/components/ui/inspector-theme.d.ts +9 -10
  141. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  142. package/dist/components/ui/inspector-theme.js +39 -72
  143. package/dist/components/ui/kbd.d.ts +6 -0
  144. package/dist/components/ui/kbd.d.ts.map +1 -0
  145. package/dist/components/ui/kbd.js +6 -0
  146. package/dist/components/ui/load-more-button.js +1 -1
  147. package/dist/components/ui/menu-dropdown.js +1 -1
  148. package/dist/components/ui/skeleton.js +1 -1
  149. package/dist/components/ui/spinner.js +1 -1
  150. package/dist/components/ui/timestamp-tooltip.d.ts +34 -1
  151. package/dist/components/ui/timestamp-tooltip.d.ts.map +1 -1
  152. package/dist/components/ui/timestamp-tooltip.js +80 -132
  153. package/dist/components/ui/tooltip.d.ts +10 -0
  154. package/dist/components/ui/tooltip.d.ts.map +1 -0
  155. package/dist/components/ui/tooltip.js +12 -0
  156. package/dist/components/workflow-trace-view.d.ts +1 -3
  157. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  158. package/dist/components/workflow-trace-view.js +11 -13
  159. package/dist/components/workflow-traces/event-colors.d.ts +1 -0
  160. package/dist/components/workflow-traces/event-colors.d.ts.map +1 -1
  161. package/dist/components/workflow-traces/event-colors.js +12 -1
  162. package/dist/components/workflow-traces/trace-colors.js +1 -1
  163. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  164. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  165. package/dist/components/workflow-traces/trace-span-construction.js +19 -5
  166. package/dist/components/workflow-traces/trace-time-utils.js +1 -1
  167. package/dist/hooks/use-dark-mode.js +1 -1
  168. package/dist/hooks/use-intersection-observer.d.ts +17 -0
  169. package/dist/hooks/use-intersection-observer.d.ts.map +1 -0
  170. package/dist/hooks/use-intersection-observer.js +29 -0
  171. package/dist/hooks/use-load-more-on-scroll.d.ts +21 -0
  172. package/dist/hooks/use-load-more-on-scroll.d.ts.map +1 -0
  173. package/dist/hooks/use-load-more-on-scroll.js +28 -0
  174. package/dist/hooks/use-reduced-motion.d.ts +10 -0
  175. package/dist/hooks/use-reduced-motion.d.ts.map +1 -0
  176. package/dist/hooks/use-reduced-motion.js +29 -0
  177. package/dist/index.d.ts +3 -1
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +3 -2
  180. package/dist/lib/event-analysis.js +1 -1
  181. package/dist/lib/event-materialization.js +1 -1
  182. package/dist/lib/exact-event-search-id.d.ts +25 -0
  183. package/dist/lib/exact-event-search-id.d.ts.map +1 -0
  184. package/dist/lib/exact-event-search-id.js +39 -0
  185. package/dist/lib/hydration.d.ts +6 -0
  186. package/dist/lib/hydration.d.ts.map +1 -1
  187. package/dist/lib/hydration.js +154 -2
  188. package/dist/lib/stream-display.d.ts +18 -0
  189. package/dist/lib/stream-display.d.ts.map +1 -0
  190. package/dist/lib/stream-display.js +30 -0
  191. package/dist/lib/toast.js +1 -1
  192. package/dist/lib/trace-builder.d.ts +15 -0
  193. package/dist/lib/trace-builder.d.ts.map +1 -1
  194. package/dist/lib/trace-builder.js +29 -3
  195. package/dist/lib/utils.d.ts +20 -9
  196. package/dist/lib/utils.d.ts.map +1 -1
  197. package/dist/lib/utils.js +74 -31
  198. package/dist/lib/zstd-browser-decoder.d.ts +7 -0
  199. package/dist/lib/zstd-browser-decoder.d.ts.map +1 -0
  200. package/dist/lib/zstd-browser-decoder.js +41 -0
  201. package/dist/styles.css +484 -1
  202. package/package.json +10 -5
  203. package/src/components/event-list-view.tsx +279 -104
  204. package/src/components/index.ts +7 -0
  205. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  206. package/src/components/new-trace-viewer/components/event-list.tsx +136 -0
  207. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  208. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  209. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  210. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  211. package/src/components/new-trace-viewer/components/span-markers.tsx +173 -0
  212. package/src/components/new-trace-viewer/components/split-pane.tsx +198 -0
  213. package/src/components/new-trace-viewer/components/timeline.module.css +46 -0
  214. package/src/components/new-trace-viewer/components/timeline.tsx +744 -0
  215. package/src/components/new-trace-viewer/components/trace-viewer-skeleton.tsx +94 -0
  216. package/src/components/new-trace-viewer/components/use-row-window.ts +140 -0
  217. package/src/components/new-trace-viewer/context.tsx +90 -0
  218. package/src/components/new-trace-viewer/icons.tsx +79 -0
  219. package/src/components/new-trace-viewer/search.ts +178 -0
  220. package/src/components/new-trace-viewer/trace-viewer.tsx +843 -0
  221. package/src/components/new-trace-viewer/types.ts +2 -0
  222. package/src/components/new-trace-viewer/utils.test.ts +182 -0
  223. package/src/components/new-trace-viewer/utils.ts +519 -0
  224. package/src/components/sidebar/attribute-panel.tsx +194 -240
  225. package/src/components/sidebar/attributes-block.tsx +173 -0
  226. package/src/components/sidebar/copyable-data-block.tsx +49 -27
  227. package/src/components/sidebar/detail-card.tsx +119 -59
  228. package/src/components/sidebar/entity-detail-panel.tsx +55 -111
  229. package/src/components/sidebar/events-list.tsx +85 -96
  230. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  231. package/src/components/sidebar/span-detail-merge.ts +65 -0
  232. package/src/components/stream-viewer.tsx +18 -40
  233. package/src/components/trace-viewer/components/node.tsx +3 -2
  234. package/src/components/trace-viewer/trace-viewer.module.css +4 -4
  235. package/src/components/trace-viewer/util/timing.ts +7 -11
  236. package/src/components/trace-viewer-new.tsx +55 -0
  237. package/src/components/ui/button.tsx +68 -0
  238. package/src/components/ui/context-card.tsx +784 -0
  239. package/src/components/ui/data-inspector.tsx +244 -22
  240. package/src/components/ui/error-stack-block.tsx +84 -55
  241. package/src/components/ui/icon-button.tsx +53 -0
  242. package/src/components/ui/inspector-theme.ts +39 -81
  243. package/src/components/ui/kbd.tsx +21 -0
  244. package/src/components/ui/timestamp-tooltip.tsx +132 -194
  245. package/src/components/ui/tooltip.tsx +47 -0
  246. package/src/components/workflow-trace-view.tsx +19 -15
  247. package/src/components/workflow-traces/event-colors.ts +12 -0
  248. package/src/components/workflow-traces/trace-span-construction.ts +24 -5
  249. package/src/hooks/use-intersection-observer.ts +57 -0
  250. package/src/hooks/use-load-more-on-scroll.ts +49 -0
  251. package/src/hooks/use-reduced-motion.ts +34 -0
  252. package/src/index.ts +9 -0
  253. package/src/lib/exact-event-search-id.ts +67 -0
  254. package/src/lib/hydration.ts +166 -1
  255. package/src/lib/stream-display.ts +48 -0
  256. package/src/lib/trace-builder.ts +34 -2
  257. package/src/lib/utils.ts +83 -31
  258. package/src/lib/zstd-browser-decoder.ts +42 -0
  259. package/src/styles.css +484 -1
  260. package/dist/components/error-boundary.js.map +0 -1
  261. package/dist/components/event-list-view.js.map +0 -1
  262. package/dist/components/hook-actions.js.map +0 -1
  263. package/dist/components/index.js.map +0 -1
  264. package/dist/components/run-trace-view.js.map +0 -1
  265. package/dist/components/sidebar/attribute-panel.js.map +0 -1
  266. package/dist/components/sidebar/conversation-view.js.map +0 -1
  267. package/dist/components/sidebar/copyable-data-block.js.map +0 -1
  268. package/dist/components/sidebar/detail-card.js.map +0 -1
  269. package/dist/components/sidebar/entity-detail-panel.js.map +0 -1
  270. package/dist/components/sidebar/events-list.js.map +0 -1
  271. package/dist/components/sidebar/resolve-hook-modal.js.map +0 -1
  272. package/dist/components/stream-viewer.js.map +0 -1
  273. package/dist/components/trace-viewer/components/map.js.map +0 -1
  274. package/dist/components/trace-viewer/components/markers.js.map +0 -1
  275. package/dist/components/trace-viewer/components/node.js.map +0 -1
  276. package/dist/components/trace-viewer/components/search-input.js.map +0 -1
  277. package/dist/components/trace-viewer/components/search.js.map +0 -1
  278. package/dist/components/trace-viewer/components/span-content.js.map +0 -1
  279. package/dist/components/trace-viewer/components/span-detail-panel.js.map +0 -1
  280. package/dist/components/trace-viewer/components/span-segments.js.map +0 -1
  281. package/dist/components/trace-viewer/components/span-strategies.js.map +0 -1
  282. package/dist/components/trace-viewer/components/ui.js.map +0 -1
  283. package/dist/components/trace-viewer/components/zoom-button.js.map +0 -1
  284. package/dist/components/trace-viewer/components/zoom-icons.js.map +0 -1
  285. package/dist/components/trace-viewer/context.js.map +0 -1
  286. package/dist/components/trace-viewer/index.js.map +0 -1
  287. package/dist/components/trace-viewer/trace-viewer.js.map +0 -1
  288. package/dist/components/trace-viewer/types.js.map +0 -1
  289. package/dist/components/trace-viewer/util/constants.js.map +0 -1
  290. package/dist/components/trace-viewer/util/scrollbar-width.js.map +0 -1
  291. package/dist/components/trace-viewer/util/timing.js.map +0 -1
  292. package/dist/components/trace-viewer/util/tree.js.map +0 -1
  293. package/dist/components/trace-viewer/util/use-immediate-style.js.map +0 -1
  294. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +0 -1
  295. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +0 -1
  296. package/dist/components/trace-viewer/worker.js.map +0 -1
  297. package/dist/components/ui/alert.js.map +0 -1
  298. package/dist/components/ui/card.js.map +0 -1
  299. package/dist/components/ui/data-inspector.js.map +0 -1
  300. package/dist/components/ui/decrypt-button.js.map +0 -1
  301. package/dist/components/ui/error-card.js.map +0 -1
  302. package/dist/components/ui/error-stack-block.js.map +0 -1
  303. package/dist/components/ui/inspector-theme.js.map +0 -1
  304. package/dist/components/ui/load-more-button.js.map +0 -1
  305. package/dist/components/ui/menu-dropdown.js.map +0 -1
  306. package/dist/components/ui/skeleton.js.map +0 -1
  307. package/dist/components/ui/spinner.js.map +0 -1
  308. package/dist/components/ui/timestamp-tooltip.js.map +0 -1
  309. package/dist/components/workflow-trace-view.js.map +0 -1
  310. package/dist/components/workflow-traces/event-colors.js.map +0 -1
  311. package/dist/components/workflow-traces/trace-colors.js.map +0 -1
  312. package/dist/components/workflow-traces/trace-span-construction.js.map +0 -1
  313. package/dist/components/workflow-traces/trace-time-utils.js.map +0 -1
  314. package/dist/hooks/use-dark-mode.js.map +0 -1
  315. package/dist/index.js.map +0 -1
  316. package/dist/lib/event-analysis.js.map +0 -1
  317. package/dist/lib/event-materialization.js.map +0 -1
  318. package/dist/lib/hydration.js.map +0 -1
  319. package/dist/lib/toast.js.map +0 -1
  320. package/dist/lib/trace-builder.js.map +0 -1
  321. package/dist/lib/utils.js.map +0 -1
@@ -0,0 +1,744 @@
1
+ 'use client';
2
+
3
+ import { ArrowLeft, ArrowRight } from 'lucide-react';
4
+ import type { CSSProperties, ReactNode } from 'react';
5
+ import {
6
+ Fragment,
7
+ memo,
8
+ useCallback,
9
+ useEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from 'react';
14
+ import { cn } from '../../../lib/utils';
15
+ import {
16
+ formatDuration,
17
+ formatDurationPrecise,
18
+ getHighResInMs,
19
+ } from '../../trace-viewer/util/timing';
20
+ import { isSpanDimmedBySearch, type SpanSearchResult } from '../search';
21
+ import type { Span } from '../types';
22
+ import type {
23
+ OffscreenMarkers,
24
+ Segment,
25
+ SegmentStatus,
26
+ TimeMarker,
27
+ } from '../utils';
28
+ import {
29
+ computeOffscreenMarkers,
30
+ computeSpanGaps,
31
+ computeSpanMarkers,
32
+ computeSpanSegments,
33
+ getResourceColor,
34
+ getSpanDurationMs,
35
+ } from '../utils';
36
+ import {
37
+ cullCollidingMarkers,
38
+ MarkerLayer,
39
+ OffscreenMarkerIndicator,
40
+ projectMarkers,
41
+ } from './span-markers';
42
+ import styles from './timeline.module.css';
43
+ import { ROW_HEIGHT_PX, useRowWindow } from './use-row-window';
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Constants
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const TINY_BAR_BOX_SIZE_PX = 24;
50
+ const TINY_BAR_WIDTH_PX = 4;
51
+ export const TIMELINE_PADDING_PX = 16;
52
+
53
+ const SEGMENT_CLASSES: Record<SegmentStatus, string> = {
54
+ queued: 'bg-gray-400 border border-gray-500',
55
+ pending: 'bg-gray-200 border border-gray-500',
56
+ retrying: 'bg-gray-400 border border-gray-500',
57
+ waiting: 'bg-gray-200 border border-gray-500',
58
+ running: 'bg-blue-200 border border-blue-500',
59
+ completed: 'bg-blue-200 border border-blue-500',
60
+ failed: 'bg-red-200 border border-red-500',
61
+ succeeded: 'bg-green-200 border border-green-500',
62
+ sleeping: 'bg-gray-400 border border-gray-500',
63
+ received: 'bg-blue-200 border border-blue-500',
64
+ };
65
+
66
+ const TIMELINE_INSET_STYLE: CSSProperties = {
67
+ left: TIMELINE_PADDING_PX,
68
+ right: TIMELINE_PADDING_PX,
69
+ };
70
+
71
+ const STRIPED_SEGMENT_STATUSES: ReadonlySet<SegmentStatus> = new Set([
72
+ 'pending',
73
+ 'running',
74
+ 'received',
75
+ ]);
76
+
77
+ function AnimatedStripes({ status }: { status: SegmentStatus }): ReactNode {
78
+ return (
79
+ <div
80
+ aria-hidden
81
+ className={
82
+ status === 'pending' ? styles.pendingStripes : styles.runningStripes
83
+ }
84
+ />
85
+ );
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Bar geometry
90
+ // ---------------------------------------------------------------------------
91
+
92
+ type BarMode =
93
+ | { kind: 'arrow'; direction: 'left' | 'right' }
94
+ | { kind: 'tiny' }
95
+ | { kind: 'full' };
96
+
97
+ interface BarGeometry {
98
+ mode: BarMode;
99
+ leftPct: number;
100
+ widthPct: number;
101
+ visibleStartMs: number;
102
+ visibleEndMs: number;
103
+ visiblePixelWidth: number;
104
+ }
105
+
106
+ /**
107
+ * Compute the bar's geometry inside the timeline viewport. Percentages are
108
+ * always in [0, 100] so we never emit CSS values that exceed browser layout
109
+ * limits at extreme zoom.
110
+ */
111
+ function computeBarGeometry(
112
+ startMs: number,
113
+ endMs: number,
114
+ viewStart: number,
115
+ viewEnd: number,
116
+ containerWidth: number
117
+ ): BarGeometry {
118
+ const viewDuration = viewEnd - viewStart;
119
+ const visibleStartMs = Math.max(startMs, viewStart);
120
+ const visibleEndMs = Math.min(endMs, viewEnd);
121
+ const visibleDurationMs = Math.max(0, visibleEndMs - visibleStartMs);
122
+ const widthFrac = viewDuration > 0 ? visibleDurationMs / viewDuration : 0;
123
+ const visiblePixelWidth = widthFrac * containerWidth;
124
+
125
+ const isTiny = containerWidth > 0 && visiblePixelWidth < TINY_BAR_BOX_SIZE_PX;
126
+ const extendsOffLeft = startMs < viewStart;
127
+ const extendsOffRight = endMs > viewEnd;
128
+
129
+ const mode: BarMode =
130
+ isTiny && (extendsOffLeft || extendsOffRight)
131
+ ? { kind: 'arrow', direction: extendsOffRight ? 'right' : 'left' }
132
+ : isTiny
133
+ ? { kind: 'tiny' }
134
+ : { kind: 'full' };
135
+
136
+ return {
137
+ mode,
138
+ leftPct:
139
+ viewDuration > 0
140
+ ? ((visibleStartMs - viewStart) / viewDuration) * 100
141
+ : 0,
142
+ widthPct: widthFrac * 100,
143
+ visibleStartMs,
144
+ visibleEndMs,
145
+ visiblePixelWidth,
146
+ };
147
+ }
148
+
149
+ function getBarPositionStyle(geometry: BarGeometry): {
150
+ left: string;
151
+ width: string;
152
+ } {
153
+ switch (geometry.mode.kind) {
154
+ case 'arrow':
155
+ return {
156
+ left:
157
+ geometry.mode.direction === 'right'
158
+ ? `calc(100% - ${TINY_BAR_BOX_SIZE_PX}px)`
159
+ : '0px',
160
+ width: `${TINY_BAR_BOX_SIZE_PX}px`,
161
+ };
162
+ case 'tiny':
163
+ return {
164
+ left: `min(${geometry.leftPct}%, calc(100% - ${TINY_BAR_WIDTH_PX}px))`,
165
+ width: `${TINY_BAR_WIDTH_PX}px`,
166
+ };
167
+ case 'full':
168
+ return {
169
+ left: `${geometry.leftPct}%`,
170
+ width: `max(${geometry.widthPct}%, 4px)`,
171
+ };
172
+ }
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Segment projection
177
+ // ---------------------------------------------------------------------------
178
+
179
+ interface VisibleSegment {
180
+ status: SegmentStatus;
181
+ leftPct: number;
182
+ widthPct: number;
183
+ pixelWidth: number;
184
+ fullDurationMs: number;
185
+ }
186
+
187
+ /**
188
+ * Project status segments onto the visible portion of a bar. Segments fully
189
+ * outside the visible window are dropped; segments crossing the edge are
190
+ * clipped to [0%, 100%] of the visible bar.
191
+ */
192
+ function projectSegments(
193
+ segments: Segment[],
194
+ spanStartMs: number,
195
+ spanDurationMs: number,
196
+ geometry: BarGeometry
197
+ ): VisibleSegment[] {
198
+ const visibleDurationMs = geometry.visibleEndMs - geometry.visibleStartMs;
199
+ if (visibleDurationMs <= 0) return [];
200
+
201
+ return segments.flatMap((seg) => {
202
+ const segStartMs = spanStartMs + seg.startFraction * spanDurationMs;
203
+ const segEndMs = spanStartMs + seg.endFraction * spanDurationMs;
204
+ const startMs = Math.max(segStartMs, geometry.visibleStartMs);
205
+ const endMs = Math.min(segEndMs, geometry.visibleEndMs);
206
+ if (endMs <= startMs) return [];
207
+
208
+ const widthFrac = (endMs - startMs) / visibleDurationMs;
209
+ return [
210
+ {
211
+ status: seg.status,
212
+ leftPct:
213
+ ((startMs - geometry.visibleStartMs) / visibleDurationMs) * 100,
214
+ widthPct: widthFrac * 100,
215
+ pixelWidth: widthFrac * geometry.visiblePixelWidth,
216
+ fullDurationMs: (seg.endFraction - seg.startFraction) * spanDurationMs,
217
+ },
218
+ ];
219
+ });
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Small render helpers
224
+ // ---------------------------------------------------------------------------
225
+
226
+ function DurationLabel({
227
+ label,
228
+ className,
229
+ }: {
230
+ label: string;
231
+ className?: string;
232
+ }): ReactNode {
233
+ return (
234
+ <span
235
+ className={cn(
236
+ 'pointer-events-none absolute inset-0 flex items-center justify-start overflow-hidden px-1 text-[10px] font-mono font-medium leading-none whitespace-nowrap text-left text-gray-1000 tabular-nums opacity-0 group-hover/timeline-row:opacity-100',
237
+ className
238
+ )}
239
+ >
240
+ {label}
241
+ </span>
242
+ );
243
+ }
244
+
245
+ function BoundaryArrow({
246
+ direction,
247
+ }: {
248
+ direction: 'left' | 'right';
249
+ }): ReactNode {
250
+ const Icon = direction === 'right' ? ArrowRight : ArrowLeft;
251
+ return (
252
+ <div className="flex h-6 w-6 items-center justify-center rounded-[0.25rem]">
253
+ <Icon className="size-3 text-gray-900" />
254
+ </div>
255
+ );
256
+ }
257
+
258
+ function PlainBar({
259
+ bg,
260
+ border,
261
+ label,
262
+ }: {
263
+ bg: string;
264
+ border: string;
265
+ label: string | null;
266
+ }): ReactNode {
267
+ return (
268
+ <div
269
+ className="relative h-6 w-full min-w-1 rounded-[0.25rem] border"
270
+ style={{ background: bg, borderColor: border }}
271
+ >
272
+ {label ? <DurationLabel label={label} /> : null}
273
+ </div>
274
+ );
275
+ }
276
+
277
+ function LeadInConnector({
278
+ leftPct,
279
+ widthPct,
280
+ label,
281
+ }: {
282
+ leftPct: number;
283
+ widthPct: number;
284
+ label: string | null;
285
+ }): ReactNode {
286
+ return (
287
+ <div
288
+ className="absolute top-1/2 h-6 -translate-y-1/2"
289
+ style={{
290
+ left: `calc(${leftPct}% + 0.5px)`,
291
+ width: `calc(${widthPct}% - 1px)`,
292
+ }}
293
+ >
294
+ <div className="absolute left-0 top-1/2 h-4 w-px -translate-y-1/2 bg-gray-500" />
295
+ <div className="absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-gray-500" />
296
+ {label ? <DurationLabel label={label} /> : null}
297
+ </div>
298
+ );
299
+ }
300
+
301
+ function SegmentBar({
302
+ segments,
303
+ showLabels = true,
304
+ }: {
305
+ segments: VisibleSegment[];
306
+ /**
307
+ * When false, segment duration labels are suppressed. Used when a separate
308
+ * top layer (e.g. the resumption-marker duration overlay) renders the label
309
+ * above the markers instead, so we don't draw it twice.
310
+ */
311
+ showLabels?: boolean;
312
+ }): ReactNode {
313
+ return (
314
+ <div className="relative h-6 w-full">
315
+ {segments.map((seg, i) => {
316
+ if (seg.status === 'queued') {
317
+ const leadInLabel = formatDurationPrecise(seg.fullDurationMs);
318
+ const showLeadInLabel =
319
+ showLabels &&
320
+ seg.pixelWidth >= Math.max(40, leadInLabel.length * 6 + 12);
321
+ const isFullWidthQueued = segments.length === 1;
322
+ return (
323
+ <Fragment key={i}>
324
+ <LeadInConnector
325
+ leftPct={seg.leftPct}
326
+ widthPct={seg.widthPct}
327
+ label={showLeadInLabel ? leadInLabel : null}
328
+ />
329
+ {isFullWidthQueued ? (
330
+ <div
331
+ className="absolute top-1/2 h-4 w-px -translate-y-1/2 bg-gray-500"
332
+ style={{
333
+ left: `calc(${seg.leftPct + seg.widthPct}% - 1.5px)`,
334
+ }}
335
+ />
336
+ ) : null}
337
+ </Fragment>
338
+ );
339
+ }
340
+
341
+ const label = formatDurationPrecise(seg.fullDurationMs);
342
+ // Only render the label when there's enough room for it without clipping.
343
+ const showLabel =
344
+ showLabels && seg.pixelWidth >= Math.max(40, label.length * 6 + 12);
345
+
346
+ return (
347
+ <div
348
+ key={i}
349
+ className={cn(
350
+ 'absolute h-full overflow-hidden rounded-[0.25rem]',
351
+ SEGMENT_CLASSES[seg.status]
352
+ )}
353
+ style={{
354
+ // 1px gap between adjacent segments, distributed equally.
355
+ left: `calc(${seg.leftPct}% + 0.5px)`,
356
+ width: `calc(${seg.widthPct}% - 1px)`,
357
+ minWidth: 1,
358
+ }}
359
+ >
360
+ {STRIPED_SEGMENT_STATUSES.has(seg.status) ? (
361
+ <AnimatedStripes status={seg.status} />
362
+ ) : null}
363
+ {showLabel ? <DurationLabel label={label} /> : null}
364
+ </div>
365
+ );
366
+ })}
367
+ </div>
368
+ );
369
+ }
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // TimelineBar
373
+ // ---------------------------------------------------------------------------
374
+
375
+ const TimelineBar = memo(function TimelineBar({
376
+ span,
377
+ viewStart,
378
+ viewDuration,
379
+ containerWidth,
380
+ isSelected,
381
+ isDimmed,
382
+ onSelect,
383
+ onRevealTime,
384
+ }: {
385
+ span: Span;
386
+ viewStart: number;
387
+ viewDuration: number;
388
+ containerWidth: number;
389
+ isSelected: boolean;
390
+ isDimmed?: boolean;
391
+ onSelect: (spanId: string) => void;
392
+ onRevealTime?: (timeMs: number) => void;
393
+ }): ReactNode {
394
+ const startMs = getHighResInMs(span.startTime);
395
+ const endMs = getHighResInMs(span.endTime);
396
+ const totalDurationMs = getSpanDurationMs(span);
397
+
398
+ const geometry = useMemo(
399
+ () =>
400
+ computeBarGeometry(
401
+ startMs,
402
+ endMs,
403
+ viewStart,
404
+ viewStart + viewDuration,
405
+ containerWidth
406
+ ),
407
+ [startMs, endMs, viewStart, viewDuration, containerWidth]
408
+ );
409
+
410
+ const baseSegments = useMemo(() => computeSpanSegments(span), [span]);
411
+ const segments = useMemo(
412
+ () =>
413
+ geometry.mode.kind === 'full'
414
+ ? projectSegments(baseSegments, startMs, totalDurationMs, geometry)
415
+ : [],
416
+ [geometry, baseSegments, startMs, totalDurationMs]
417
+ );
418
+
419
+ const baseMarkers = useMemo(() => computeSpanMarkers(span), [span]);
420
+ const markers = useMemo(
421
+ () =>
422
+ geometry.mode.kind === 'full'
423
+ ? cullCollidingMarkers(
424
+ projectMarkers(
425
+ baseMarkers,
426
+ geometry.visibleStartMs,
427
+ geometry.visibleEndMs
428
+ ),
429
+ geometry.visiblePixelWidth
430
+ )
431
+ : [],
432
+ [geometry, baseMarkers]
433
+ );
434
+
435
+ // Markers that fall outside the visible window (scrolled off while zoomed in)
436
+ // — surfaced as edge indicators so they aren't silently lost.
437
+ const offscreen = useMemo<OffscreenMarkers>(
438
+ () =>
439
+ geometry.mode.kind === 'full'
440
+ ? computeOffscreenMarkers(
441
+ baseMarkers,
442
+ geometry.visibleStartMs,
443
+ geometry.visibleEndMs
444
+ )
445
+ : { left: null, right: null },
446
+ [geometry, baseMarkers]
447
+ );
448
+
449
+ // Markers (visible or off-screen) move the duration label into the overlay,
450
+ // so the in-bar segment label is suppressed.
451
+ const hasMarkers =
452
+ markers.length > 0 || offscreen.left !== null || offscreen.right !== null;
453
+
454
+ const workflowStatus = (span.attributes.data as Record<string, unknown>)
455
+ ?.status as string | undefined;
456
+ const isErrored = span.status.code === 2 || workflowStatus === 'failed';
457
+ const colors = getResourceColor(span.resource);
458
+ const fallbackBg = isErrored
459
+ ? (colors.errorBg ?? 'var(--ds-red-200)')
460
+ : colors.bg;
461
+ const fallbackBorder = isErrored
462
+ ? (colors.errorBorder ?? 'var(--ds-red-500)')
463
+ : colors.border;
464
+
465
+ const totalLabel = formatDurationPrecise(totalDurationMs);
466
+ const showTotalLabel =
467
+ geometry.visiblePixelWidth >= Math.max(40, totalLabel.length * 6 + 12);
468
+
469
+ const handleClick = useCallback(() => {
470
+ onSelect(span.spanId);
471
+ }, [onSelect, span.spanId]);
472
+
473
+ return (
474
+ <div
475
+ role="treeitem"
476
+ aria-selected={isSelected}
477
+ aria-expanded={isSelected}
478
+ aria-level={1}
479
+ className={cn(
480
+ 'group/timeline-row h-10 relative flex items-center hover:bg-gray-100 aria-selected:bg-gray-100 aria-selected:hover:bg-gray-200 transition-opacity',
481
+ isDimmed && 'opacity-35'
482
+ )}
483
+ onClick={handleClick}
484
+ >
485
+ <div className="absolute inset-y-0" style={TIMELINE_INSET_STYLE}>
486
+ <div
487
+ className="absolute top-1/2 h-6 -translate-y-1/2 overflow-hidden rounded-[0.25rem]"
488
+ style={getBarPositionStyle(geometry)}
489
+ >
490
+ {geometry.mode.kind === 'arrow' ? (
491
+ <BoundaryArrow direction={geometry.mode.direction} />
492
+ ) : geometry.mode.kind === 'tiny' ? (
493
+ <div
494
+ className="h-6 rounded-[0.25rem] border"
495
+ style={{ background: fallbackBg, borderColor: fallbackBorder }}
496
+ />
497
+ ) : segments.length > 0 ? (
498
+ <SegmentBar segments={segments} showLabels={!hasMarkers} />
499
+ ) : (
500
+ <PlainBar
501
+ bg={fallbackBg}
502
+ border={fallbackBorder}
503
+ label={showTotalLabel ? totalLabel : null}
504
+ />
505
+ )}
506
+ </div>
507
+ {/* Overlay (not clipped): ticks, off-screen indicators, then the duration label on top. */}
508
+ {hasMarkers ? (
509
+ <div
510
+ className="pointer-events-none absolute top-1/2 h-6 -translate-y-1/2"
511
+ style={getBarPositionStyle(geometry)}
512
+ >
513
+ {markers.length > 0 ? <MarkerLayer markers={markers} /> : null}
514
+ {offscreen.left ? (
515
+ <OffscreenMarkerIndicator
516
+ direction="left"
517
+ count={offscreen.left.count}
518
+ targetMs={offscreen.left.nearestMs}
519
+ onReveal={onRevealTime}
520
+ />
521
+ ) : null}
522
+ {offscreen.right ? (
523
+ <OffscreenMarkerIndicator
524
+ direction="right"
525
+ count={offscreen.right.count}
526
+ targetMs={offscreen.right.nearestMs}
527
+ onReveal={onRevealTime}
528
+ />
529
+ ) : null}
530
+ {showTotalLabel ? (
531
+ <DurationLabel
532
+ label={totalLabel}
533
+ // Shift clear of the left edge indicator so it isn't covered.
534
+ className={offscreen.left ? 'pl-10' : undefined}
535
+ />
536
+ ) : null}
537
+ </div>
538
+ ) : null}
539
+ </div>
540
+ </div>
541
+ );
542
+ });
543
+
544
+ export { TimelineBar };
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // DeltaIndicator (Alt-key gap overlay)
548
+ // ---------------------------------------------------------------------------
549
+
550
+ const DELTA_CAP_HEIGHT_PX = 8;
551
+ // Vertical offset to sit the indicator inside the gap between row N and N+1,
552
+ // aligned with where the bar starts in the next row (rows center a 24px bar
553
+ // inside 40px, so bars start ~8px from the top of the row).
554
+ const DELTA_ROW_OFFSET_PX = 8;
555
+
556
+ const DeltaIndicator = memo(function DeltaIndicator({
557
+ leftFrac,
558
+ rightFrac,
559
+ label,
560
+ rowIndex,
561
+ }: {
562
+ leftFrac: number;
563
+ rightFrac: number;
564
+ label: string;
565
+ rowIndex: number;
566
+ }) {
567
+ const centerY = DELTA_ROW_OFFSET_PX + (rowIndex + 1) * ROW_HEIGHT_PX;
568
+
569
+ return (
570
+ <div
571
+ className="absolute pointer-events-none"
572
+ style={{
573
+ left: `${leftFrac * 100}%`,
574
+ width: `${(rightFrac - leftFrac) * 100}%`,
575
+ top: centerY - DELTA_CAP_HEIGHT_PX / 2,
576
+ height: DELTA_CAP_HEIGHT_PX,
577
+ }}
578
+ >
579
+ <div className="absolute left-0 top-0 w-px h-full bg-amber-800" />
580
+ <div className="absolute left-0 right-0 top-1/2 h-px bg-amber-800" />
581
+ <div className="absolute right-0 top-0 w-px h-full bg-amber-800" />
582
+ <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-label-12 leading-none whitespace-nowrap rounded-xs px-1 py-0.5 text-gray-100 bg-amber-800">
583
+ {label}
584
+ </span>
585
+ </div>
586
+ );
587
+ });
588
+
589
+ // ---------------------------------------------------------------------------
590
+ // TimelineHeader
591
+ // ---------------------------------------------------------------------------
592
+
593
+ export function TimelineHeader({
594
+ markers,
595
+ hoverInfo,
596
+ }: {
597
+ markers: TimeMarker[];
598
+ hoverInfo?: { fraction: number; label: string } | null;
599
+ }): ReactNode {
600
+ return (
601
+ <div className="relative bg-background-100 border-b border-gray-alpha-400 h-10 min-h-10 flex items-end px-4 pb-1">
602
+ <div className="relative h-full flex-1">
603
+ {markers.map((m) => (
604
+ <span
605
+ key={String(m.value)}
606
+ className="absolute bottom-1 font-mono text-xs font-normal leading-4 text-gray-900 whitespace-nowrap"
607
+ style={{ left: `${m.position * 100}%` }}
608
+ >
609
+ {m.label}
610
+ </span>
611
+ ))}
612
+ {hoverInfo && (
613
+ <span
614
+ className="absolute top-1 pointer-events-none z-10 font-mono text-[11px] leading-4 text-gray-1000 whitespace-nowrap bg-background-100 border border-gray-alpha-400 rounded px-1 -translate-x-1/2"
615
+ style={{ left: `${hoverInfo.fraction * 100}%` }}
616
+ >
617
+ {hoverInfo.label}
618
+ </span>
619
+ )}
620
+ </div>
621
+ </div>
622
+ );
623
+ }
624
+
625
+ // ---------------------------------------------------------------------------
626
+ // Timeline
627
+ // ---------------------------------------------------------------------------
628
+
629
+ export function Timeline({
630
+ spans,
631
+ viewStart,
632
+ viewEnd,
633
+ markers,
634
+ selectedId,
635
+ searchResult,
636
+ onSelect,
637
+ onRevealTime,
638
+ hoverFraction,
639
+ altHeld = false,
640
+ }: {
641
+ spans: Span[];
642
+ viewStart: number;
643
+ viewEnd: number;
644
+ markers: TimeMarker[];
645
+ selectedId: string | null;
646
+ searchResult: SpanSearchResult;
647
+ onSelect: (spanId: string) => void;
648
+ onRevealTime?: (timeMs: number) => void;
649
+ hoverFraction?: number | null;
650
+ altHeld?: boolean;
651
+ }): ReactNode {
652
+ const containerRef = useRef<HTMLDivElement>(null);
653
+ const [containerWidth, setContainerWidth] = useState(0);
654
+ const viewDuration = viewEnd - viewStart;
655
+ const timelineWidth = Math.max(0, containerWidth - TIMELINE_PADDING_PX * 2);
656
+ const { start, end } = useRowWindow(
657
+ containerRef,
658
+ spans.length,
659
+ ROW_HEIGHT_PX
660
+ );
661
+
662
+ useEffect(() => {
663
+ const el = containerRef.current;
664
+ if (!el) return;
665
+ const ro = new ResizeObserver(([entry]) => {
666
+ setContainerWidth(entry.contentRect.width);
667
+ });
668
+ ro.observe(el);
669
+ return () => ro.disconnect();
670
+ }, []);
671
+
672
+ const gaps = useMemo(
673
+ () => computeSpanGaps(spans, viewStart, viewEnd),
674
+ [spans, viewStart, viewEnd]
675
+ );
676
+
677
+ return (
678
+ <div
679
+ ref={containerRef}
680
+ className="relative h-full overflow-hidden"
681
+ style={{ minHeight: spans.length * ROW_HEIGHT_PX }}
682
+ >
683
+ <div
684
+ aria-hidden
685
+ className="absolute inset-y-0 pointer-events-none"
686
+ style={TIMELINE_INSET_STYLE}
687
+ >
688
+ {markers.map((marker) =>
689
+ // Skip the "0s" origin marker since the left edge already implies it.
690
+ Math.abs(marker.value) > 0.000001 ? (
691
+ <div
692
+ key={String(marker.value)}
693
+ className="absolute top-0 bottom-0 w-px bg-gray-alpha-300"
694
+ style={{ left: `${marker.position * 100}%` }}
695
+ />
696
+ ) : null
697
+ )}
698
+ </div>
699
+ {hoverFraction != null && (
700
+ <div
701
+ className="absolute inset-y-0 pointer-events-none z-10"
702
+ style={TIMELINE_INSET_STYLE}
703
+ >
704
+ <div
705
+ className="absolute top-0 bottom-0 w-px bg-gray-alpha-500"
706
+ style={{ left: `${hoverFraction * 100}%` }}
707
+ />
708
+ </div>
709
+ )}
710
+ <div style={{ transform: `translateY(${start * ROW_HEIGHT_PX}px)` }}>
711
+ {spans.slice(start, end).map((span) => (
712
+ <TimelineBar
713
+ key={span.spanId}
714
+ span={span}
715
+ viewStart={viewStart}
716
+ viewDuration={viewDuration}
717
+ containerWidth={timelineWidth}
718
+ isSelected={selectedId === span.spanId}
719
+ isDimmed={isSpanDimmedBySearch(span.spanId, searchResult)}
720
+ onSelect={onSelect}
721
+ onRevealTime={onRevealTime}
722
+ />
723
+ ))}
724
+ </div>
725
+ {altHeld && (
726
+ <div
727
+ aria-hidden
728
+ className="absolute inset-y-0 pointer-events-none"
729
+ style={TIMELINE_INSET_STYLE}
730
+ >
731
+ {gaps.map((gap) => (
732
+ <DeltaIndicator
733
+ key={gap.rowIndex}
734
+ leftFrac={gap.leftFrac}
735
+ rightFrac={gap.rightFrac}
736
+ label={formatDuration(gap.gapMs, true)}
737
+ rowIndex={gap.rowIndex}
738
+ />
739
+ ))}
740
+ </div>
741
+ )}
742
+ </div>
743
+ );
744
+ }