@workflow/web-shared 5.0.0-beta.1 → 5.0.0-beta.11

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 (298) hide show
  1. package/README.md +1 -1
  2. package/dist/components/error-boundary.js +1 -1
  3. package/dist/components/event-list-view.d.ts +18 -1
  4. package/dist/components/event-list-view.d.ts.map +1 -1
  5. package/dist/components/event-list-view.js +219 -125
  6. package/dist/components/hook-actions.js +1 -1
  7. package/dist/components/index.d.ts +5 -1
  8. package/dist/components/index.d.ts.map +1 -1
  9. package/dist/components/index.js +6 -2
  10. package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
  11. package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
  12. package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
  13. package/dist/components/new-trace-viewer/components/event-list.d.ts +10 -0
  14. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
  15. package/dist/components/new-trace-viewer/components/event-list.js +44 -0
  16. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
  17. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
  18. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
  19. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
  20. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
  21. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
  22. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
  23. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
  24. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
  25. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
  26. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
  27. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
  28. package/dist/components/new-trace-viewer/components/split-pane.d.ts +15 -0
  29. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
  30. package/dist/components/new-trace-viewer/components/split-pane.js +105 -0
  31. package/dist/components/new-trace-viewer/components/timeline.d.ts +34 -0
  32. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
  33. package/dist/components/new-trace-viewer/components/timeline.js +221 -0
  34. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts +2 -0
  35. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -0
  36. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.js +19 -0
  37. package/dist/components/new-trace-viewer/components/use-row-window.d.ts +20 -0
  38. package/dist/components/new-trace-viewer/components/use-row-window.d.ts.map +1 -0
  39. package/dist/components/new-trace-viewer/components/use-row-window.js +69 -0
  40. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  41. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  42. package/dist/components/new-trace-viewer/context.js +47 -0
  43. package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
  44. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
  45. package/dist/components/new-trace-viewer/detail-panel.js +13 -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 +400 -0
  55. package/dist/components/new-trace-viewer/types.d.ts +1 -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/util/constants.d.ts +1 -0
  59. package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
  60. package/dist/components/new-trace-viewer/util/constants.js +2 -0
  61. package/dist/components/new-trace-viewer/utils.d.ts +46 -0
  62. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  63. package/dist/components/new-trace-viewer/utils.js +330 -0
  64. package/dist/components/run-trace-view.d.ts +2 -1
  65. package/dist/components/run-trace-view.d.ts.map +1 -1
  66. package/dist/components/run-trace-view.js +3 -3
  67. package/dist/components/sidebar/attribute-panel.d.ts +7 -1
  68. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  69. package/dist/components/sidebar/attribute-panel.js +131 -70
  70. package/dist/components/sidebar/conversation-view.js +1 -1
  71. package/dist/components/sidebar/copyable-data-block.d.ts +1 -0
  72. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
  73. package/dist/components/sidebar/copyable-data-block.js +16 -15
  74. package/dist/components/sidebar/detail-card.d.ts +5 -6
  75. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  76. package/dist/components/sidebar/detail-card.js +32 -16
  77. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  78. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  79. package/dist/components/sidebar/entity-detail-panel.js +20 -37
  80. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  81. package/dist/components/sidebar/events-list.js +21 -15
  82. package/dist/components/sidebar/resolve-hook-modal.js +1 -1
  83. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  84. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  85. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  86. package/dist/components/stream-viewer.d.ts +1 -1
  87. package/dist/components/stream-viewer.d.ts.map +1 -1
  88. package/dist/components/stream-viewer.js +3 -27
  89. package/dist/components/trace-viewer/components/map.js +1 -1
  90. package/dist/components/trace-viewer/components/markers.js +1 -1
  91. package/dist/components/trace-viewer/components/node.js +1 -1
  92. package/dist/components/trace-viewer/components/search-input.js +1 -1
  93. package/dist/components/trace-viewer/components/search.js +1 -1
  94. package/dist/components/trace-viewer/components/span-content.js +1 -1
  95. package/dist/components/trace-viewer/components/span-detail-panel.js +1 -1
  96. package/dist/components/trace-viewer/components/span-segments.js +1 -1
  97. package/dist/components/trace-viewer/components/span-strategies.js +1 -1
  98. package/dist/components/trace-viewer/components/ui.js +1 -1
  99. package/dist/components/trace-viewer/components/zoom-button.js +1 -1
  100. package/dist/components/trace-viewer/components/zoom-icons.js +1 -1
  101. package/dist/components/trace-viewer/context.js +1 -1
  102. package/dist/components/trace-viewer/index.js +1 -1
  103. package/dist/components/trace-viewer/trace-viewer.js +1 -1
  104. package/dist/components/trace-viewer/types.js +1 -1
  105. package/dist/components/trace-viewer/util/constants.js +1 -1
  106. package/dist/components/trace-viewer/util/scrollbar-width.js +1 -1
  107. package/dist/components/trace-viewer/util/timing.d.ts +1 -1
  108. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  109. package/dist/components/trace-viewer/util/timing.js +6 -9
  110. package/dist/components/trace-viewer/util/tree.js +1 -1
  111. package/dist/components/trace-viewer/util/use-immediate-style.js +1 -1
  112. package/dist/components/trace-viewer/util/use-streaming-spans.js +1 -1
  113. package/dist/components/trace-viewer/util/use-trackpad-zoom.js +1 -1
  114. package/dist/components/trace-viewer/worker.js +1 -1
  115. package/dist/components/trace-viewer-new.d.ts +14 -0
  116. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  117. package/dist/components/trace-viewer-new.js +22 -0
  118. package/dist/components/ui/alert.js +1 -1
  119. package/dist/components/ui/button.d.ts +10 -0
  120. package/dist/components/ui/button.d.ts.map +1 -0
  121. package/dist/components/ui/button.js +46 -0
  122. package/dist/components/ui/card.js +1 -1
  123. package/dist/components/ui/data-inspector.d.ts +39 -1
  124. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  125. package/dist/components/ui/data-inspector.js +207 -22
  126. package/dist/components/ui/decrypt-button.js +1 -1
  127. package/dist/components/ui/error-card.js +1 -1
  128. package/dist/components/ui/error-stack-block.d.ts.map +1 -1
  129. package/dist/components/ui/error-stack-block.js +27 -19
  130. package/dist/components/ui/icon-button.d.ts +19 -0
  131. package/dist/components/ui/icon-button.d.ts.map +1 -0
  132. package/dist/components/ui/icon-button.js +29 -0
  133. package/dist/components/ui/inspector-theme.d.ts +9 -10
  134. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  135. package/dist/components/ui/inspector-theme.js +39 -72
  136. package/dist/components/ui/kbd.d.ts +6 -0
  137. package/dist/components/ui/kbd.d.ts.map +1 -0
  138. package/dist/components/ui/kbd.js +6 -0
  139. package/dist/components/ui/load-more-button.js +1 -1
  140. package/dist/components/ui/menu-dropdown.js +1 -1
  141. package/dist/components/ui/skeleton.js +1 -1
  142. package/dist/components/ui/spinner.js +1 -1
  143. package/dist/components/ui/timestamp-tooltip.js +1 -1
  144. package/dist/components/ui/tooltip.d.ts +10 -0
  145. package/dist/components/ui/tooltip.d.ts.map +1 -0
  146. package/dist/components/ui/tooltip.js +12 -0
  147. package/dist/components/workflow-trace-view.d.ts +3 -1
  148. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  149. package/dist/components/workflow-trace-view.js +10 -5
  150. package/dist/components/workflow-traces/event-colors.js +1 -1
  151. package/dist/components/workflow-traces/trace-colors.js +1 -1
  152. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  153. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  154. package/dist/components/workflow-traces/trace-span-construction.js +19 -6
  155. package/dist/components/workflow-traces/trace-time-utils.js +1 -1
  156. package/dist/hooks/use-dark-mode.js +1 -1
  157. package/dist/hooks/use-intersection-observer.d.ts +17 -0
  158. package/dist/hooks/use-intersection-observer.d.ts.map +1 -0
  159. package/dist/hooks/use-intersection-observer.js +29 -0
  160. package/dist/hooks/use-load-more-on-scroll.d.ts +21 -0
  161. package/dist/hooks/use-load-more-on-scroll.d.ts.map +1 -0
  162. package/dist/hooks/use-load-more-on-scroll.js +28 -0
  163. package/dist/hooks/use-reduced-motion.d.ts +10 -0
  164. package/dist/hooks/use-reduced-motion.d.ts.map +1 -0
  165. package/dist/hooks/use-reduced-motion.js +29 -0
  166. package/dist/index.d.ts +3 -1
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +3 -2
  169. package/dist/lib/event-analysis.js +1 -1
  170. package/dist/lib/event-materialization.js +1 -1
  171. package/dist/lib/exact-event-search-id.d.ts +25 -0
  172. package/dist/lib/exact-event-search-id.d.ts.map +1 -0
  173. package/dist/lib/exact-event-search-id.js +39 -0
  174. package/dist/lib/hydration.d.ts +14 -1
  175. package/dist/lib/hydration.d.ts.map +1 -1
  176. package/dist/lib/hydration.js +157 -4
  177. package/dist/lib/stream-display.d.ts +18 -0
  178. package/dist/lib/stream-display.d.ts.map +1 -0
  179. package/dist/lib/stream-display.js +30 -0
  180. package/dist/lib/toast.js +1 -1
  181. package/dist/lib/trace-builder.d.ts.map +1 -1
  182. package/dist/lib/trace-builder.js +3 -2
  183. package/dist/lib/utils.d.ts +9 -9
  184. package/dist/lib/utils.d.ts.map +1 -1
  185. package/dist/lib/utils.js +27 -32
  186. package/dist/styles.css +475 -1
  187. package/package.json +6 -4
  188. package/src/components/event-list-view.tsx +421 -262
  189. package/src/components/index.ts +13 -1
  190. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  191. package/src/components/new-trace-viewer/components/event-list.tsx +135 -0
  192. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  193. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  194. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  195. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  196. package/src/components/new-trace-viewer/components/split-pane.tsx +198 -0
  197. package/src/components/new-trace-viewer/components/timeline.tsx +560 -0
  198. package/src/components/new-trace-viewer/components/trace-viewer-skeleton.tsx +94 -0
  199. package/src/components/new-trace-viewer/components/use-row-window.ts +93 -0
  200. package/src/components/new-trace-viewer/context.tsx +90 -0
  201. package/src/components/new-trace-viewer/detail-panel.tsx +55 -0
  202. package/src/components/new-trace-viewer/icons.tsx +79 -0
  203. package/src/components/new-trace-viewer/search.ts +178 -0
  204. package/src/components/new-trace-viewer/trace-viewer.tsx +762 -0
  205. package/src/components/new-trace-viewer/types.ts +0 -0
  206. package/src/components/new-trace-viewer/util/constants.ts +0 -0
  207. package/src/components/new-trace-viewer/utils.ts +482 -0
  208. package/src/components/run-trace-view.tsx +3 -0
  209. package/src/components/sidebar/attribute-panel.tsx +240 -196
  210. package/src/components/sidebar/copyable-data-block.tsx +49 -27
  211. package/src/components/sidebar/detail-card.tsx +119 -59
  212. package/src/components/sidebar/entity-detail-panel.tsx +92 -148
  213. package/src/components/sidebar/events-list.tsx +51 -76
  214. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  215. package/src/components/stream-viewer.tsx +18 -40
  216. package/src/components/trace-viewer/util/timing.ts +5 -9
  217. package/src/components/trace-viewer-new.tsx +57 -0
  218. package/src/components/ui/button.tsx +68 -0
  219. package/src/components/ui/data-inspector.tsx +427 -31
  220. package/src/components/ui/error-stack-block.tsx +39 -30
  221. package/src/components/ui/icon-button.tsx +53 -0
  222. package/src/components/ui/inspector-theme.ts +39 -81
  223. package/src/components/ui/kbd.tsx +21 -0
  224. package/src/components/ui/tooltip.tsx +47 -0
  225. package/src/components/workflow-trace-view.tsx +12 -2
  226. package/src/components/workflow-traces/trace-span-construction.ts +24 -6
  227. package/src/hooks/use-intersection-observer.ts +57 -0
  228. package/src/hooks/use-load-more-on-scroll.ts +49 -0
  229. package/src/hooks/use-reduced-motion.ts +34 -0
  230. package/src/index.ts +9 -0
  231. package/src/lib/exact-event-search-id.ts +67 -0
  232. package/src/lib/hydration.ts +169 -1
  233. package/src/lib/stream-display.ts +48 -0
  234. package/src/lib/trace-builder.ts +2 -1
  235. package/src/lib/utils.ts +27 -32
  236. package/src/styles.css +475 -1
  237. package/dist/components/error-boundary.js.map +0 -1
  238. package/dist/components/event-list-view.js.map +0 -1
  239. package/dist/components/hook-actions.js.map +0 -1
  240. package/dist/components/index.js.map +0 -1
  241. package/dist/components/run-trace-view.js.map +0 -1
  242. package/dist/components/sidebar/attribute-panel.js.map +0 -1
  243. package/dist/components/sidebar/conversation-view.js.map +0 -1
  244. package/dist/components/sidebar/copyable-data-block.js.map +0 -1
  245. package/dist/components/sidebar/detail-card.js.map +0 -1
  246. package/dist/components/sidebar/entity-detail-panel.js.map +0 -1
  247. package/dist/components/sidebar/events-list.js.map +0 -1
  248. package/dist/components/sidebar/resolve-hook-modal.js.map +0 -1
  249. package/dist/components/stream-viewer.js.map +0 -1
  250. package/dist/components/trace-viewer/components/map.js.map +0 -1
  251. package/dist/components/trace-viewer/components/markers.js.map +0 -1
  252. package/dist/components/trace-viewer/components/node.js.map +0 -1
  253. package/dist/components/trace-viewer/components/search-input.js.map +0 -1
  254. package/dist/components/trace-viewer/components/search.js.map +0 -1
  255. package/dist/components/trace-viewer/components/span-content.js.map +0 -1
  256. package/dist/components/trace-viewer/components/span-detail-panel.js.map +0 -1
  257. package/dist/components/trace-viewer/components/span-segments.js.map +0 -1
  258. package/dist/components/trace-viewer/components/span-strategies.js.map +0 -1
  259. package/dist/components/trace-viewer/components/ui.js.map +0 -1
  260. package/dist/components/trace-viewer/components/zoom-button.js.map +0 -1
  261. package/dist/components/trace-viewer/components/zoom-icons.js.map +0 -1
  262. package/dist/components/trace-viewer/context.js.map +0 -1
  263. package/dist/components/trace-viewer/index.js.map +0 -1
  264. package/dist/components/trace-viewer/trace-viewer.js.map +0 -1
  265. package/dist/components/trace-viewer/types.js.map +0 -1
  266. package/dist/components/trace-viewer/util/constants.js.map +0 -1
  267. package/dist/components/trace-viewer/util/scrollbar-width.js.map +0 -1
  268. package/dist/components/trace-viewer/util/timing.js.map +0 -1
  269. package/dist/components/trace-viewer/util/tree.js.map +0 -1
  270. package/dist/components/trace-viewer/util/use-immediate-style.js.map +0 -1
  271. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +0 -1
  272. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +0 -1
  273. package/dist/components/trace-viewer/worker.js.map +0 -1
  274. package/dist/components/ui/alert.js.map +0 -1
  275. package/dist/components/ui/card.js.map +0 -1
  276. package/dist/components/ui/data-inspector.js.map +0 -1
  277. package/dist/components/ui/decrypt-button.js.map +0 -1
  278. package/dist/components/ui/error-card.js.map +0 -1
  279. package/dist/components/ui/error-stack-block.js.map +0 -1
  280. package/dist/components/ui/inspector-theme.js.map +0 -1
  281. package/dist/components/ui/load-more-button.js.map +0 -1
  282. package/dist/components/ui/menu-dropdown.js.map +0 -1
  283. package/dist/components/ui/skeleton.js.map +0 -1
  284. package/dist/components/ui/spinner.js.map +0 -1
  285. package/dist/components/ui/timestamp-tooltip.js.map +0 -1
  286. package/dist/components/workflow-trace-view.js.map +0 -1
  287. package/dist/components/workflow-traces/event-colors.js.map +0 -1
  288. package/dist/components/workflow-traces/trace-colors.js.map +0 -1
  289. package/dist/components/workflow-traces/trace-span-construction.js.map +0 -1
  290. package/dist/components/workflow-traces/trace-time-utils.js.map +0 -1
  291. package/dist/hooks/use-dark-mode.js.map +0 -1
  292. package/dist/index.js.map +0 -1
  293. package/dist/lib/event-analysis.js.map +0 -1
  294. package/dist/lib/event-materialization.js.map +0 -1
  295. package/dist/lib/hydration.js.map +0 -1
  296. package/dist/lib/toast.js.map +0 -1
  297. package/dist/lib/trace-builder.js.map +0 -1
  298. package/dist/lib/utils.js.map +0 -1
@@ -0,0 +1,560 @@
1
+ 'use client';
2
+
3
+ import { ArrowLeft, ArrowRight } from 'lucide-react';
4
+ import type { CSSProperties, ReactNode } from 'react';
5
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { cn } from '../../../lib/utils';
7
+ import type { Span } from '../../trace-viewer/types';
8
+ import { formatDuration, getHighResInMs } from '../../trace-viewer/util/timing';
9
+ import { isSpanDimmedBySearch, type SpanSearchResult } from '../search';
10
+ import type { Segment, SegmentStatus, TimeMarker } from '../utils';
11
+ import {
12
+ computeSpanGaps,
13
+ computeSpanSegments,
14
+ getResourceColor,
15
+ getSpanDurationMs,
16
+ } from '../utils';
17
+ import { ROW_HEIGHT_PX, useRowWindow } from './use-row-window';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Constants
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const TINY_BAR_BOX_SIZE_PX = 24;
24
+ const TINY_BAR_WIDTH_PX = 4;
25
+ export const TIMELINE_PADDING_PX = 16;
26
+
27
+ const SEGMENT_CLASSES: Record<SegmentStatus, string> = {
28
+ queued: 'bg-gray-400 border border-gray-500',
29
+ retrying: 'bg-gray-400 border border-gray-500',
30
+ waiting: 'bg-gray-400 border border-gray-500',
31
+ running: 'bg-blue-200 border border-blue-500',
32
+ failed: 'bg-red-200 border border-red-500',
33
+ succeeded: 'bg-green-200 border border-green-500',
34
+ sleeping: 'bg-gray-400 border border-gray-500',
35
+ received: 'bg-blue-200 border border-blue-500',
36
+ };
37
+
38
+ const TIMELINE_INSET_STYLE: CSSProperties = {
39
+ left: TIMELINE_PADDING_PX,
40
+ right: TIMELINE_PADDING_PX,
41
+ };
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Bar geometry
45
+ // ---------------------------------------------------------------------------
46
+
47
+ type BarMode =
48
+ | { kind: 'arrow'; direction: 'left' | 'right' }
49
+ | { kind: 'tiny' }
50
+ | { kind: 'full' };
51
+
52
+ interface BarGeometry {
53
+ mode: BarMode;
54
+ leftPct: number;
55
+ widthPct: number;
56
+ visibleStartMs: number;
57
+ visibleEndMs: number;
58
+ visiblePixelWidth: number;
59
+ }
60
+
61
+ /**
62
+ * Compute the bar's geometry inside the timeline viewport. Percentages are
63
+ * always in [0, 100] so we never emit CSS values that exceed browser layout
64
+ * limits at extreme zoom.
65
+ */
66
+ function computeBarGeometry(
67
+ startMs: number,
68
+ endMs: number,
69
+ viewStart: number,
70
+ viewEnd: number,
71
+ containerWidth: number
72
+ ): BarGeometry {
73
+ const viewDuration = viewEnd - viewStart;
74
+ const visibleStartMs = Math.max(startMs, viewStart);
75
+ const visibleEndMs = Math.min(endMs, viewEnd);
76
+ const visibleDurationMs = Math.max(0, visibleEndMs - visibleStartMs);
77
+ const widthFrac = viewDuration > 0 ? visibleDurationMs / viewDuration : 0;
78
+ const visiblePixelWidth = widthFrac * containerWidth;
79
+
80
+ const isTiny = containerWidth > 0 && visiblePixelWidth < TINY_BAR_BOX_SIZE_PX;
81
+ const extendsOffLeft = startMs < viewStart;
82
+ const extendsOffRight = endMs > viewEnd;
83
+
84
+ const mode: BarMode =
85
+ isTiny && (extendsOffLeft || extendsOffRight)
86
+ ? { kind: 'arrow', direction: extendsOffRight ? 'right' : 'left' }
87
+ : isTiny
88
+ ? { kind: 'tiny' }
89
+ : { kind: 'full' };
90
+
91
+ return {
92
+ mode,
93
+ leftPct:
94
+ viewDuration > 0
95
+ ? ((visibleStartMs - viewStart) / viewDuration) * 100
96
+ : 0,
97
+ widthPct: widthFrac * 100,
98
+ visibleStartMs,
99
+ visibleEndMs,
100
+ visiblePixelWidth,
101
+ };
102
+ }
103
+
104
+ function getBarPositionStyle(geometry: BarGeometry): {
105
+ left: string;
106
+ width: string;
107
+ } {
108
+ switch (geometry.mode.kind) {
109
+ case 'arrow':
110
+ return {
111
+ left:
112
+ geometry.mode.direction === 'right'
113
+ ? `calc(100% - ${TINY_BAR_BOX_SIZE_PX}px)`
114
+ : '0px',
115
+ width: `${TINY_BAR_BOX_SIZE_PX}px`,
116
+ };
117
+ case 'tiny':
118
+ return {
119
+ left: `min(${geometry.leftPct}%, calc(100% - ${TINY_BAR_WIDTH_PX}px))`,
120
+ width: `${TINY_BAR_WIDTH_PX}px`,
121
+ };
122
+ case 'full':
123
+ return {
124
+ left: `${geometry.leftPct}%`,
125
+ width: `max(${geometry.widthPct}%, 4px)`,
126
+ };
127
+ }
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Segment projection
132
+ // ---------------------------------------------------------------------------
133
+
134
+ interface VisibleSegment {
135
+ status: SegmentStatus;
136
+ leftPct: number;
137
+ widthPct: number;
138
+ pixelWidth: number;
139
+ fullDurationMs: number;
140
+ }
141
+
142
+ /**
143
+ * Project status segments onto the visible portion of a bar. Segments fully
144
+ * outside the visible window are dropped; segments crossing the edge are
145
+ * clipped to [0%, 100%] of the visible bar.
146
+ */
147
+ function projectSegments(
148
+ segments: Segment[],
149
+ spanStartMs: number,
150
+ spanDurationMs: number,
151
+ geometry: BarGeometry
152
+ ): VisibleSegment[] {
153
+ const visibleDurationMs = geometry.visibleEndMs - geometry.visibleStartMs;
154
+ if (visibleDurationMs <= 0) return [];
155
+
156
+ return segments.flatMap((seg) => {
157
+ const segStartMs = spanStartMs + seg.startFraction * spanDurationMs;
158
+ const segEndMs = spanStartMs + seg.endFraction * spanDurationMs;
159
+ const startMs = Math.max(segStartMs, geometry.visibleStartMs);
160
+ const endMs = Math.min(segEndMs, geometry.visibleEndMs);
161
+ if (endMs <= startMs) return [];
162
+
163
+ const widthFrac = (endMs - startMs) / visibleDurationMs;
164
+ return [
165
+ {
166
+ status: seg.status,
167
+ leftPct:
168
+ ((startMs - geometry.visibleStartMs) / visibleDurationMs) * 100,
169
+ widthPct: widthFrac * 100,
170
+ pixelWidth: widthFrac * geometry.visiblePixelWidth,
171
+ fullDurationMs: (seg.endFraction - seg.startFraction) * spanDurationMs,
172
+ },
173
+ ];
174
+ });
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Small render helpers
179
+ // ---------------------------------------------------------------------------
180
+
181
+ function DurationLabel({ label }: { label: string }): ReactNode {
182
+ return (
183
+ <span className="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">
184
+ {label}
185
+ </span>
186
+ );
187
+ }
188
+
189
+ function BoundaryArrow({
190
+ direction,
191
+ }: {
192
+ direction: 'left' | 'right';
193
+ }): ReactNode {
194
+ const Icon = direction === 'right' ? ArrowRight : ArrowLeft;
195
+ return (
196
+ <div className="flex h-6 w-6 items-center justify-center rounded-[0.25rem]">
197
+ <Icon className="size-3 text-gray-900" />
198
+ </div>
199
+ );
200
+ }
201
+
202
+ function PlainBar({
203
+ bg,
204
+ border,
205
+ label,
206
+ }: {
207
+ bg: string;
208
+ border: string;
209
+ label: string | null;
210
+ }): ReactNode {
211
+ return (
212
+ <div
213
+ className="relative h-6 w-full min-w-1 rounded-[0.25rem] border"
214
+ style={{ background: bg, borderColor: border }}
215
+ >
216
+ {label ? <DurationLabel label={label} /> : null}
217
+ </div>
218
+ );
219
+ }
220
+
221
+ function SegmentBar({ segments }: { segments: VisibleSegment[] }): ReactNode {
222
+ return (
223
+ <div className="relative h-6 w-full">
224
+ {segments.map((seg, i) => {
225
+ const label = formatDuration(seg.fullDurationMs);
226
+ // Only render the label when there's enough room for it without clipping.
227
+ const showLabel = seg.pixelWidth >= Math.max(40, label.length * 6 + 12);
228
+ // Beef up the queued segment when it's too narrow to read.
229
+ const isNarrowQueued = seg.status === 'queued' && seg.pixelWidth < 20;
230
+ const overrideBg = isNarrowQueued ? 'var(--ds-gray-400)' : undefined;
231
+ const overrideBorder = isNarrowQueued
232
+ ? 'var(--ds-gray-500)'
233
+ : undefined;
234
+
235
+ return (
236
+ <div
237
+ key={`${seg.status}-${i}`}
238
+ className={cn(
239
+ 'absolute h-full rounded-[0.25rem]',
240
+ SEGMENT_CLASSES[seg.status]
241
+ )}
242
+ style={{
243
+ // 1px gap between adjacent segments, distributed equally.
244
+ left: `calc(${seg.leftPct}% + 0.5px)`,
245
+ width: `calc(${seg.widthPct}% - 1px)`,
246
+ minWidth: 1,
247
+ background: overrideBg,
248
+ borderColor: overrideBorder,
249
+ }}
250
+ >
251
+ {showLabel ? <DurationLabel label={label} /> : null}
252
+ </div>
253
+ );
254
+ })}
255
+ </div>
256
+ );
257
+ }
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // TimelineBar
261
+ // ---------------------------------------------------------------------------
262
+
263
+ const TimelineBar = memo(function TimelineBar({
264
+ span,
265
+ viewStart,
266
+ viewDuration,
267
+ containerWidth,
268
+ isSelected,
269
+ isDimmed,
270
+ onSelect,
271
+ }: {
272
+ span: Span;
273
+ viewStart: number;
274
+ viewDuration: number;
275
+ containerWidth: number;
276
+ isSelected: boolean;
277
+ isDimmed?: boolean;
278
+ onSelect: (spanId: string) => void;
279
+ }): ReactNode {
280
+ const startMs = getHighResInMs(span.startTime);
281
+ const endMs = getHighResInMs(span.endTime);
282
+ const totalDurationMs = getSpanDurationMs(span);
283
+
284
+ const geometry = useMemo(
285
+ () =>
286
+ computeBarGeometry(
287
+ startMs,
288
+ endMs,
289
+ viewStart,
290
+ viewStart + viewDuration,
291
+ containerWidth
292
+ ),
293
+ [startMs, endMs, viewStart, viewDuration, containerWidth]
294
+ );
295
+
296
+ const baseSegments = useMemo(() => computeSpanSegments(span), [span]);
297
+ const segments = useMemo(
298
+ () =>
299
+ geometry.mode.kind === 'full'
300
+ ? projectSegments(baseSegments, startMs, totalDurationMs, geometry)
301
+ : [],
302
+ [geometry, baseSegments, startMs, totalDurationMs]
303
+ );
304
+
305
+ const workflowStatus = (span.attributes.data as Record<string, unknown>)
306
+ ?.status as string | undefined;
307
+ const isErrored = span.status.code === 2 || workflowStatus === 'failed';
308
+ const colors = getResourceColor(span.resource);
309
+ const fallbackBg = isErrored
310
+ ? (colors.errorBg ?? 'var(--ds-red-200)')
311
+ : colors.bg;
312
+ const fallbackBorder = isErrored
313
+ ? (colors.errorBorder ?? 'var(--ds-red-500)')
314
+ : colors.border;
315
+
316
+ const totalLabel = formatDuration(totalDurationMs);
317
+ const showTotalLabel =
318
+ geometry.visiblePixelWidth >= Math.max(40, totalLabel.length * 6 + 12);
319
+
320
+ const handleClick = useCallback(() => {
321
+ onSelect(span.spanId);
322
+ }, [onSelect, span.spanId]);
323
+
324
+ return (
325
+ <div
326
+ role="treeitem"
327
+ aria-selected={isSelected}
328
+ aria-expanded={isSelected}
329
+ aria-level={1}
330
+ className={cn(
331
+ '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',
332
+ isDimmed && 'opacity-35'
333
+ )}
334
+ onClick={handleClick}
335
+ >
336
+ <div className="absolute inset-y-0" style={TIMELINE_INSET_STYLE}>
337
+ <div
338
+ className="absolute top-1/2 h-6 -translate-y-1/2 rounded-[0.25rem]"
339
+ style={getBarPositionStyle(geometry)}
340
+ >
341
+ {geometry.mode.kind === 'arrow' ? (
342
+ <BoundaryArrow direction={geometry.mode.direction} />
343
+ ) : geometry.mode.kind === 'tiny' ? (
344
+ <div
345
+ className="h-6 rounded-[0.25rem] border"
346
+ style={{ background: fallbackBg, borderColor: fallbackBorder }}
347
+ />
348
+ ) : segments.length > 0 ? (
349
+ <SegmentBar segments={segments} />
350
+ ) : (
351
+ <PlainBar
352
+ bg={fallbackBg}
353
+ border={fallbackBorder}
354
+ label={showTotalLabel ? totalLabel : null}
355
+ />
356
+ )}
357
+ </div>
358
+ </div>
359
+ </div>
360
+ );
361
+ });
362
+
363
+ export { TimelineBar };
364
+
365
+ // ---------------------------------------------------------------------------
366
+ // DeltaIndicator (Alt-key gap overlay)
367
+ // ---------------------------------------------------------------------------
368
+
369
+ const DELTA_CAP_HEIGHT_PX = 8;
370
+ // Vertical offset to sit the indicator inside the gap between row N and N+1,
371
+ // aligned with where the bar starts in the next row (rows center a 24px bar
372
+ // inside 40px, so bars start ~8px from the top of the row).
373
+ const DELTA_ROW_OFFSET_PX = 8;
374
+
375
+ const DeltaIndicator = memo(function DeltaIndicator({
376
+ leftFrac,
377
+ rightFrac,
378
+ label,
379
+ rowIndex,
380
+ }: {
381
+ leftFrac: number;
382
+ rightFrac: number;
383
+ label: string;
384
+ rowIndex: number;
385
+ }) {
386
+ const centerY = DELTA_ROW_OFFSET_PX + (rowIndex + 1) * ROW_HEIGHT_PX;
387
+
388
+ return (
389
+ <div
390
+ className="absolute pointer-events-none"
391
+ style={{
392
+ left: `${leftFrac * 100}%`,
393
+ width: `${(rightFrac - leftFrac) * 100}%`,
394
+ top: centerY - DELTA_CAP_HEIGHT_PX / 2,
395
+ height: DELTA_CAP_HEIGHT_PX,
396
+ }}
397
+ >
398
+ <div className="absolute left-0 top-0 w-px h-full bg-amber-800" />
399
+ <div className="absolute left-0 right-0 top-1/2 h-px bg-amber-800" />
400
+ <div className="absolute right-0 top-0 w-px h-full bg-amber-800" />
401
+ <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">
402
+ {label}
403
+ </span>
404
+ </div>
405
+ );
406
+ });
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // TimelineHeader
410
+ // ---------------------------------------------------------------------------
411
+
412
+ export function TimelineHeader({
413
+ markers,
414
+ hoverInfo,
415
+ }: {
416
+ markers: TimeMarker[];
417
+ hoverInfo?: { fraction: number; label: string } | null;
418
+ }): ReactNode {
419
+ return (
420
+ <div className="relative bg-background-100 border-b border-gray-alpha-400 h-10 min-h-10 flex items-end px-4 pb-1">
421
+ <div className="relative h-full flex-1">
422
+ {markers.map((m) => (
423
+ <span
424
+ key={`${m.position}-${m.label}`}
425
+ className="absolute bottom-1 font-mono text-xs font-normal leading-4 text-gray-900 whitespace-nowrap"
426
+ style={{ left: `${m.position * 100}%` }}
427
+ >
428
+ {m.label}
429
+ </span>
430
+ ))}
431
+ {hoverInfo && (
432
+ <span
433
+ 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"
434
+ style={{ left: `${hoverInfo.fraction * 100}%` }}
435
+ >
436
+ {hoverInfo.label}
437
+ </span>
438
+ )}
439
+ </div>
440
+ </div>
441
+ );
442
+ }
443
+
444
+ // ---------------------------------------------------------------------------
445
+ // Timeline
446
+ // ---------------------------------------------------------------------------
447
+
448
+ export function Timeline({
449
+ spans,
450
+ viewStart,
451
+ viewEnd,
452
+ markers,
453
+ selectedId,
454
+ searchResult,
455
+ onSelect,
456
+ hoverFraction,
457
+ altHeld = false,
458
+ }: {
459
+ spans: Span[];
460
+ viewStart: number;
461
+ viewEnd: number;
462
+ markers: TimeMarker[];
463
+ selectedId: string | null;
464
+ searchResult: SpanSearchResult;
465
+ onSelect: (spanId: string) => void;
466
+ hoverFraction?: number | null;
467
+ altHeld?: boolean;
468
+ }): ReactNode {
469
+ const containerRef = useRef<HTMLDivElement>(null);
470
+ const [containerWidth, setContainerWidth] = useState(0);
471
+ const viewDuration = viewEnd - viewStart;
472
+ const timelineWidth = Math.max(0, containerWidth - TIMELINE_PADDING_PX * 2);
473
+ const { start, end } = useRowWindow(
474
+ containerRef,
475
+ spans.length,
476
+ ROW_HEIGHT_PX
477
+ );
478
+
479
+ useEffect(() => {
480
+ const el = containerRef.current;
481
+ if (!el) return;
482
+ const ro = new ResizeObserver(([entry]) => {
483
+ setContainerWidth(entry.contentRect.width);
484
+ });
485
+ ro.observe(el);
486
+ return () => ro.disconnect();
487
+ }, []);
488
+
489
+ const gaps = useMemo(
490
+ () => computeSpanGaps(spans, viewStart, viewEnd),
491
+ [spans, viewStart, viewEnd]
492
+ );
493
+
494
+ return (
495
+ <div
496
+ ref={containerRef}
497
+ className="relative h-full overflow-hidden"
498
+ style={{ minHeight: spans.length * ROW_HEIGHT_PX }}
499
+ >
500
+ <div
501
+ aria-hidden
502
+ className="absolute inset-y-0 pointer-events-none"
503
+ style={TIMELINE_INSET_STYLE}
504
+ >
505
+ {markers.map((marker) =>
506
+ // Skip the "0s" origin marker since the left edge already implies it.
507
+ Math.abs(marker.value) > 0.000001 ? (
508
+ <div
509
+ key={`${marker.position}-${marker.label}`}
510
+ className="absolute top-0 bottom-0 w-px bg-gray-alpha-300"
511
+ style={{ left: `${marker.position * 100}%` }}
512
+ />
513
+ ) : null
514
+ )}
515
+ </div>
516
+ {hoverFraction != null && (
517
+ <div
518
+ className="absolute inset-y-0 pointer-events-none z-10"
519
+ style={TIMELINE_INSET_STYLE}
520
+ >
521
+ <div
522
+ className="absolute top-0 bottom-0 w-px bg-gray-alpha-500"
523
+ style={{ left: `${hoverFraction * 100}%` }}
524
+ />
525
+ </div>
526
+ )}
527
+ <div style={{ transform: `translateY(${start * ROW_HEIGHT_PX}px)` }}>
528
+ {spans.slice(start, end).map((span) => (
529
+ <TimelineBar
530
+ key={span.spanId}
531
+ span={span}
532
+ viewStart={viewStart}
533
+ viewDuration={viewDuration}
534
+ containerWidth={timelineWidth}
535
+ isSelected={selectedId === span.spanId}
536
+ isDimmed={isSpanDimmedBySearch(span.spanId, searchResult)}
537
+ onSelect={onSelect}
538
+ />
539
+ ))}
540
+ </div>
541
+ {altHeld && (
542
+ <div
543
+ aria-hidden
544
+ className="absolute inset-y-0 pointer-events-none"
545
+ style={TIMELINE_INSET_STYLE}
546
+ >
547
+ {gaps.map((gap) => (
548
+ <DeltaIndicator
549
+ key={gap.rowIndex}
550
+ leftFrac={gap.leftFrac}
551
+ rightFrac={gap.rightFrac}
552
+ label={formatDuration(gap.gapMs, true)}
553
+ rowIndex={gap.rowIndex}
554
+ />
555
+ ))}
556
+ </div>
557
+ )}
558
+ </div>
559
+ );
560
+ }
@@ -0,0 +1,94 @@
1
+ import { Skeleton } from '../../ui/skeleton';
2
+
3
+ const COL_TEMPLATE = '340px 1px minmax(50px, 1fr)';
4
+
5
+ const ROWS: { id: string; name: number; off: number; bar: number }[] = [
6
+ { id: 'r0', name: 62, off: 0, bar: 72 },
7
+ { id: 'r1', name: 78, off: 6, bar: 48 },
8
+ { id: 'r2', name: 50, off: 10, bar: 55 },
9
+ { id: 'r3', name: 84, off: 18, bar: 30 },
10
+ { id: 'r4', name: 45, off: 18, bar: 42 },
11
+ { id: 'r5', name: 66, off: 34, bar: 38 },
12
+ { id: 'r6', name: 55, off: 41, bar: 25 },
13
+ { id: 'r7', name: 40, off: 50, bar: 30 },
14
+ ];
15
+
16
+ const HEADER_MARKERS = ['m0', 'm1', 'm2', 'm3'];
17
+
18
+ const HeaderDivider = () => (
19
+ <div className="flex justify-center">
20
+ <span aria-hidden className="h-full w-px bg-gray-alpha-400" />
21
+ </div>
22
+ );
23
+
24
+ export function TraceViewerSkeleton() {
25
+ return (
26
+ <output
27
+ aria-busy="true"
28
+ aria-label="Loading trace"
29
+ className="flex flex-col w-full h-full min-h-0 bg-background-100"
30
+ >
31
+ <span className="sr-only">Loading trace…</span>
32
+
33
+ {/* Header row: search header | divider | timeline header */}
34
+ <div
35
+ className="shrink-0 grid"
36
+ style={{ gridTemplateColumns: COL_TEMPLATE }}
37
+ >
38
+ <div className="h-10 min-h-10 flex items-center border-b border-gray-alpha-400 pl-4 pr-2 gap-1.5">
39
+ <Skeleton className="w-3.5 h-3.5 shrink-0 rounded-sm" />
40
+ <Skeleton className="h-3.5 w-40" />
41
+ </div>
42
+ <HeaderDivider />
43
+ <div className="h-10 min-h-10 flex items-end border-b border-gray-alpha-400 px-4 pb-1 gap-2">
44
+ <div className="relative flex-1 flex items-end justify-between">
45
+ {HEADER_MARKERS.map((id) => (
46
+ <Skeleton key={id} className="h-3.5 w-9" />
47
+ ))}
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ {/* Content row: event list | gutter | timeline */}
53
+ <div
54
+ className="grid flex-1 min-h-0 overflow-hidden"
55
+ style={{ gridTemplateColumns: COL_TEMPLATE }}
56
+ >
57
+ {/* Sidebar event rows */}
58
+ <div className="block overflow-visible">
59
+ <ul className="block divide-y divide-gray-alpha-400 border-b border-gray-alpha-400">
60
+ {ROWS.map((row) => (
61
+ <li key={row.id} className="h-10 flex items-center pl-4 pr-2">
62
+ <div className="flex min-w-0 flex-1 items-center gap-2">
63
+ <Skeleton className="w-4 h-4 shrink-0 rounded-sm" />
64
+ <Skeleton
65
+ className="h-3.5"
66
+ style={{ width: `${row.name}%` }}
67
+ />
68
+ </div>
69
+ <Skeleton className="ml-2 h-3.5 w-10 shrink-0" />
70
+ </li>
71
+ ))}
72
+ </ul>
73
+ </div>
74
+
75
+ {/* Gutter */}
76
+ <span aria-hidden className="h-full w-px bg-gray-alpha-400" />
77
+
78
+ {/* Timeline bars */}
79
+ <div className="relative min-h-0">
80
+ {ROWS.map((row) => (
81
+ <div key={row.id} className="relative h-10">
82
+ <div className="absolute inset-x-4 inset-y-0">
83
+ <Skeleton
84
+ className="absolute top-1/2 -translate-y-1/2 h-6 rounded-[0.25rem]"
85
+ style={{ left: `${row.off}%`, width: `${row.bar}%` }}
86
+ />
87
+ </div>
88
+ </div>
89
+ ))}
90
+ </div>
91
+ </div>
92
+ </output>
93
+ );
94
+ }