@workflow/web-shared 5.0.0-beta.6 → 5.0.0-beta.8

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 (200) hide show
  1. package/dist/components/error-boundary.js +1 -1
  2. package/dist/components/event-list-view.d.ts +12 -0
  3. package/dist/components/event-list-view.d.ts.map +1 -1
  4. package/dist/components/event-list-view.js +26 -13
  5. package/dist/components/hook-actions.js +1 -1
  6. package/dist/components/index.js +1 -1
  7. package/dist/components/new-trace-viewer/components/copy-button.js +2 -2
  8. package/dist/components/new-trace-viewer/components/event-list.js +3 -3
  9. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +1 -1
  10. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +1 -1
  11. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +1 -1
  12. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +1 -1
  13. package/dist/components/new-trace-viewer/components/split-pane.js +1 -1
  14. package/dist/components/new-trace-viewer/components/timeline.d.ts +1 -1
  15. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
  16. package/dist/components/new-trace-viewer/components/timeline.js +187 -103
  17. package/dist/components/new-trace-viewer/context.js +1 -1
  18. package/dist/components/new-trace-viewer/detail-panel.js +2 -2
  19. package/dist/components/new-trace-viewer/icons.js +1 -1
  20. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
  21. package/dist/components/new-trace-viewer/trace-viewer.js +53 -26
  22. package/dist/components/new-trace-viewer/types.js +1 -1
  23. package/dist/components/new-trace-viewer/util/constants.js +1 -1
  24. package/dist/components/new-trace-viewer/utils.d.ts +8 -4
  25. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
  26. package/dist/components/new-trace-viewer/utils.js +31 -6
  27. package/dist/components/run-trace-view.js +1 -1
  28. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  29. package/dist/components/sidebar/attribute-panel.js +99 -73
  30. package/dist/components/sidebar/conversation-view.js +1 -1
  31. package/dist/components/sidebar/copyable-data-block.d.ts +1 -0
  32. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
  33. package/dist/components/sidebar/copyable-data-block.js +16 -15
  34. package/dist/components/sidebar/detail-card.d.ts +5 -6
  35. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  36. package/dist/components/sidebar/detail-card.js +32 -16
  37. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  38. package/dist/components/sidebar/entity-detail-panel.js +6 -3
  39. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  40. package/dist/components/sidebar/events-list.js +21 -15
  41. package/dist/components/sidebar/resolve-hook-modal.js +1 -1
  42. package/dist/components/sidebar/sidebar-data-context.js +1 -1
  43. package/dist/components/stream-viewer.js +1 -1
  44. package/dist/components/trace-viewer/components/map.js +1 -1
  45. package/dist/components/trace-viewer/components/markers.js +1 -1
  46. package/dist/components/trace-viewer/components/node.js +1 -1
  47. package/dist/components/trace-viewer/components/search-input.js +1 -1
  48. package/dist/components/trace-viewer/components/search.js +1 -1
  49. package/dist/components/trace-viewer/components/span-content.js +1 -1
  50. package/dist/components/trace-viewer/components/span-detail-panel.js +1 -1
  51. package/dist/components/trace-viewer/components/span-segments.js +1 -1
  52. package/dist/components/trace-viewer/components/span-strategies.js +1 -1
  53. package/dist/components/trace-viewer/components/ui.js +1 -1
  54. package/dist/components/trace-viewer/components/zoom-button.js +1 -1
  55. package/dist/components/trace-viewer/components/zoom-icons.js +1 -1
  56. package/dist/components/trace-viewer/context.js +1 -1
  57. package/dist/components/trace-viewer/index.js +1 -1
  58. package/dist/components/trace-viewer/trace-viewer.js +1 -1
  59. package/dist/components/trace-viewer/types.js +1 -1
  60. package/dist/components/trace-viewer/util/constants.js +1 -1
  61. package/dist/components/trace-viewer/util/scrollbar-width.js +1 -1
  62. package/dist/components/trace-viewer/util/timing.js +1 -1
  63. package/dist/components/trace-viewer/util/tree.js +1 -1
  64. package/dist/components/trace-viewer/util/use-immediate-style.js +1 -1
  65. package/dist/components/trace-viewer/util/use-streaming-spans.js +1 -1
  66. package/dist/components/trace-viewer/util/use-trackpad-zoom.js +1 -1
  67. package/dist/components/trace-viewer/worker.js +1 -1
  68. package/dist/components/trace-viewer-new.js +1 -1
  69. package/dist/components/ui/alert.js +1 -1
  70. package/dist/components/ui/button.d.ts +10 -0
  71. package/dist/components/ui/button.d.ts.map +1 -0
  72. package/dist/components/ui/button.js +39 -0
  73. package/dist/components/ui/card.js +1 -1
  74. package/dist/components/ui/data-inspector.d.ts +1 -0
  75. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  76. package/dist/components/ui/data-inspector.js +4 -9
  77. package/dist/components/ui/decrypt-button.js +1 -1
  78. package/dist/components/ui/error-card.js +1 -1
  79. package/dist/components/ui/error-stack-block.d.ts.map +1 -1
  80. package/dist/components/ui/error-stack-block.js +4 -18
  81. package/dist/components/ui/inspector-theme.js +1 -1
  82. package/dist/components/ui/load-more-button.js +1 -1
  83. package/dist/components/ui/menu-dropdown.js +1 -1
  84. package/dist/components/ui/skeleton.js +1 -1
  85. package/dist/components/ui/spinner.js +1 -1
  86. package/dist/components/ui/timestamp-tooltip.js +1 -1
  87. package/dist/components/workflow-trace-view.js +1 -1
  88. package/dist/components/workflow-traces/event-colors.js +1 -1
  89. package/dist/components/workflow-traces/trace-colors.js +1 -1
  90. package/dist/components/workflow-traces/trace-span-construction.js +1 -1
  91. package/dist/components/workflow-traces/trace-time-utils.js +1 -1
  92. package/dist/hooks/use-dark-mode.js +1 -1
  93. package/dist/hooks/use-reduced-motion.d.ts +10 -0
  94. package/dist/hooks/use-reduced-motion.d.ts.map +1 -0
  95. package/dist/hooks/use-reduced-motion.js +29 -0
  96. package/dist/index.js +1 -1
  97. package/dist/lib/event-analysis.js +1 -1
  98. package/dist/lib/event-materialization.js +1 -1
  99. package/dist/lib/hydration.js +1 -1
  100. package/dist/lib/stream-display.js +1 -1
  101. package/dist/lib/toast.js +1 -1
  102. package/dist/lib/trace-builder.js +1 -1
  103. package/dist/lib/utils.js +1 -1
  104. package/package.json +5 -5
  105. package/src/components/event-list-view.tsx +29 -13
  106. package/src/components/new-trace-viewer/components/copy-button.tsx +1 -1
  107. package/src/components/new-trace-viewer/components/event-list.tsx +2 -2
  108. package/src/components/new-trace-viewer/components/timeline.tsx +353 -192
  109. package/src/components/new-trace-viewer/detail-panel.tsx +1 -1
  110. package/src/components/new-trace-viewer/trace-viewer.tsx +93 -59
  111. package/src/components/new-trace-viewer/utils.ts +40 -8
  112. package/src/components/sidebar/attribute-panel.tsx +212 -235
  113. package/src/components/sidebar/copyable-data-block.tsx +49 -27
  114. package/src/components/sidebar/detail-card.tsx +119 -41
  115. package/src/components/sidebar/entity-detail-panel.tsx +28 -28
  116. package/src/components/sidebar/events-list.tsx +51 -79
  117. package/src/components/ui/button.tsx +61 -0
  118. package/src/components/ui/data-inspector.tsx +9 -18
  119. package/src/components/ui/error-stack-block.tsx +7 -26
  120. package/src/hooks/use-reduced-motion.ts +34 -0
  121. package/dist/components/error-boundary.js.map +0 -1
  122. package/dist/components/event-list-view.js.map +0 -1
  123. package/dist/components/hook-actions.js.map +0 -1
  124. package/dist/components/index.js.map +0 -1
  125. package/dist/components/new-trace-viewer/components/copy-button.js.map +0 -1
  126. package/dist/components/new-trace-viewer/components/event-list.js.map +0 -1
  127. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js.map +0 -1
  128. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js.map +0 -1
  129. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js.map +0 -1
  130. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js.map +0 -1
  131. package/dist/components/new-trace-viewer/components/split-pane.js.map +0 -1
  132. package/dist/components/new-trace-viewer/components/timeline.js.map +0 -1
  133. package/dist/components/new-trace-viewer/context.js.map +0 -1
  134. package/dist/components/new-trace-viewer/detail-panel.js.map +0 -1
  135. package/dist/components/new-trace-viewer/icons.js.map +0 -1
  136. package/dist/components/new-trace-viewer/trace-viewer.js.map +0 -1
  137. package/dist/components/new-trace-viewer/types.js.map +0 -1
  138. package/dist/components/new-trace-viewer/util/constants.js.map +0 -1
  139. package/dist/components/new-trace-viewer/utils.js.map +0 -1
  140. package/dist/components/run-trace-view.js.map +0 -1
  141. package/dist/components/sidebar/attribute-panel.js.map +0 -1
  142. package/dist/components/sidebar/conversation-view.js.map +0 -1
  143. package/dist/components/sidebar/copyable-data-block.js.map +0 -1
  144. package/dist/components/sidebar/detail-card.js.map +0 -1
  145. package/dist/components/sidebar/entity-detail-panel.js.map +0 -1
  146. package/dist/components/sidebar/events-list.js.map +0 -1
  147. package/dist/components/sidebar/resolve-hook-modal.js.map +0 -1
  148. package/dist/components/sidebar/sidebar-data-context.js.map +0 -1
  149. package/dist/components/stream-viewer.js.map +0 -1
  150. package/dist/components/trace-viewer/components/map.js.map +0 -1
  151. package/dist/components/trace-viewer/components/markers.js.map +0 -1
  152. package/dist/components/trace-viewer/components/node.js.map +0 -1
  153. package/dist/components/trace-viewer/components/search-input.js.map +0 -1
  154. package/dist/components/trace-viewer/components/search.js.map +0 -1
  155. package/dist/components/trace-viewer/components/span-content.js.map +0 -1
  156. package/dist/components/trace-viewer/components/span-detail-panel.js.map +0 -1
  157. package/dist/components/trace-viewer/components/span-segments.js.map +0 -1
  158. package/dist/components/trace-viewer/components/span-strategies.js.map +0 -1
  159. package/dist/components/trace-viewer/components/ui.js.map +0 -1
  160. package/dist/components/trace-viewer/components/zoom-button.js.map +0 -1
  161. package/dist/components/trace-viewer/components/zoom-icons.js.map +0 -1
  162. package/dist/components/trace-viewer/context.js.map +0 -1
  163. package/dist/components/trace-viewer/index.js.map +0 -1
  164. package/dist/components/trace-viewer/trace-viewer.js.map +0 -1
  165. package/dist/components/trace-viewer/types.js.map +0 -1
  166. package/dist/components/trace-viewer/util/constants.js.map +0 -1
  167. package/dist/components/trace-viewer/util/scrollbar-width.js.map +0 -1
  168. package/dist/components/trace-viewer/util/timing.js.map +0 -1
  169. package/dist/components/trace-viewer/util/tree.js.map +0 -1
  170. package/dist/components/trace-viewer/util/use-immediate-style.js.map +0 -1
  171. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +0 -1
  172. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +0 -1
  173. package/dist/components/trace-viewer/worker.js.map +0 -1
  174. package/dist/components/trace-viewer-new.js.map +0 -1
  175. package/dist/components/ui/alert.js.map +0 -1
  176. package/dist/components/ui/card.js.map +0 -1
  177. package/dist/components/ui/data-inspector.js.map +0 -1
  178. package/dist/components/ui/decrypt-button.js.map +0 -1
  179. package/dist/components/ui/error-card.js.map +0 -1
  180. package/dist/components/ui/error-stack-block.js.map +0 -1
  181. package/dist/components/ui/inspector-theme.js.map +0 -1
  182. package/dist/components/ui/load-more-button.js.map +0 -1
  183. package/dist/components/ui/menu-dropdown.js.map +0 -1
  184. package/dist/components/ui/skeleton.js.map +0 -1
  185. package/dist/components/ui/spinner.js.map +0 -1
  186. package/dist/components/ui/timestamp-tooltip.js.map +0 -1
  187. package/dist/components/workflow-trace-view.js.map +0 -1
  188. package/dist/components/workflow-traces/event-colors.js.map +0 -1
  189. package/dist/components/workflow-traces/trace-colors.js.map +0 -1
  190. package/dist/components/workflow-traces/trace-span-construction.js.map +0 -1
  191. package/dist/components/workflow-traces/trace-time-utils.js.map +0 -1
  192. package/dist/hooks/use-dark-mode.js.map +0 -1
  193. package/dist/index.js.map +0 -1
  194. package/dist/lib/event-analysis.js.map +0 -1
  195. package/dist/lib/event-materialization.js.map +0 -1
  196. package/dist/lib/hydration.js.map +0 -1
  197. package/dist/lib/stream-display.js.map +0 -1
  198. package/dist/lib/toast.js.map +0 -1
  199. package/dist/lib/trace-builder.js.map +0 -1
  200. package/dist/lib/utils.js.map +0 -1
@@ -1,200 +1,321 @@
1
1
  'use client';
2
2
 
3
3
  import { ArrowLeft, ArrowRight } from 'lucide-react';
4
- import type { ReactNode } from 'react';
5
- import { memo, useEffect, useMemo, useRef, useState } from 'react';
4
+ import type { CSSProperties, ReactNode } from 'react';
5
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { cn } from '../../../lib/utils';
7
7
  import type { Span } from '../../trace-viewer/types';
8
8
  import { formatDuration, getHighResInMs } from '../../trace-viewer/util/timing';
9
- import type { SegmentStatus, TimeMarker } from '../utils';
9
+ import type { Segment, SegmentStatus, TimeMarker } from '../utils';
10
10
  import {
11
11
  computeSpanGaps,
12
12
  computeSpanSegments,
13
- getSpanDurationMs,
14
13
  getResourceColor,
14
+ getSpanDurationMs,
15
15
  } from '../utils';
16
16
 
17
- const SEGMENT_CONFIG: Record<
18
- SegmentStatus,
19
- { className?: string; style?: React.CSSProperties }
20
- > = {
21
- queued: { className: 'bg-gray-500' },
22
- retrying: {
23
- className: 'box-border bg-gray-500',
24
- },
25
- waiting: { className: 'bg-gray-500' },
26
- running: { className: 'bg-blue-700' },
27
- failed: { className: 'bg-red-700' },
28
- succeeded: { className: 'bg-green-700' },
29
- sleeping: { className: 'bg-gray-500' },
30
- received: { className: 'bg-blue-700' },
31
- };
17
+ // ---------------------------------------------------------------------------
18
+ // Constants
19
+ // ---------------------------------------------------------------------------
32
20
 
33
- const BAR_HEIGHT_PX = 24;
34
21
  const TINY_BAR_BOX_SIZE_PX = 24;
35
22
  const TINY_BAR_WIDTH_PX = 4;
36
- const SEGMENT_GAP_PX = 1;
37
- // Keep this in sync with the rendered row height in the timeline/event list.
38
- const ROW_HEIGHT = 40;
39
- const CONTAINER_PAD_Y = 8;
40
- const END_CAP_HEIGHT = 8;
41
23
  export const TIMELINE_PADDING_PX = 16;
42
- const ORIGIN_MARKER_EPSILON = 0.000001;
43
24
 
44
- const DeltaIndicator = memo(function DeltaIndicator({
45
- leftFrac,
46
- rightFrac,
47
- label,
48
- rowIndex,
49
- }: {
50
- leftFrac: number;
51
- rightFrac: number;
52
- label: string;
53
- rowIndex: number;
54
- }) {
55
- const centerY = CONTAINER_PAD_Y + (rowIndex + 1) * ROW_HEIGHT;
25
+ const SEGMENT_CLASSES: Record<SegmentStatus, string> = {
26
+ queued: 'bg-gray-400 border border-gray-500',
27
+ retrying: 'bg-gray-400 border border-gray-500',
28
+ waiting: 'bg-gray-400 border border-gray-500',
29
+ running: 'bg-blue-200 border border-blue-500',
30
+ failed: 'bg-red-200 border border-red-500',
31
+ succeeded: 'bg-green-200 border border-green-500',
32
+ sleeping: 'bg-gray-400 border border-gray-500',
33
+ received: 'bg-blue-200 border border-blue-500',
34
+ };
56
35
 
57
- return (
58
- <div
59
- className="absolute pointer-events-none"
60
- style={{
61
- left: `${leftFrac * 100}%`,
62
- width: `${(rightFrac - leftFrac) * 100}%`,
63
- top: centerY - END_CAP_HEIGHT / 2,
64
- height: END_CAP_HEIGHT,
65
- }}
66
- >
67
- <div className="absolute left-0 top-0 w-px h-full bg-amber-800" />
68
- <div className="absolute left-0 right-0 top-1/2 h-px bg-amber-800" />
69
- <div className="absolute right-0 top-0 w-px h-full bg-amber-800" />
70
- <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">
71
- {label}
72
- </span>
73
- </div>
74
- );
75
- });
36
+ const TIMELINE_INSET_STYLE: CSSProperties = {
37
+ left: TIMELINE_PADDING_PX,
38
+ right: TIMELINE_PADDING_PX,
39
+ };
76
40
 
77
- const TimelineBar = memo(function TimelineBar({
78
- span,
79
- viewStart,
80
- viewDuration,
81
- containerWidth,
82
- isSelected,
83
- onClick,
84
- }: {
85
- span: Span;
86
- viewStart: number;
87
- viewDuration: number;
88
- containerWidth: number;
89
- isSelected: boolean;
90
- onClick: () => void;
91
- }): ReactNode {
92
- const startTime = getHighResInMs(span.startTime);
93
- const endTime = getHighResInMs(span.endTime);
94
- const totalDurationMs = getSpanDurationMs(span);
41
+ // ---------------------------------------------------------------------------
42
+ // Bar geometry
43
+ // ---------------------------------------------------------------------------
95
44
 
96
- const leftFracRaw =
97
- viewDuration > 0 ? (startTime - viewStart) / viewDuration : 0;
98
- const rightFracRaw =
99
- viewDuration > 0 ? (endTime - viewStart) / viewDuration : 0;
100
- const widthFrac = rightFracRaw - leftFracRaw;
45
+ type BarMode =
46
+ | { kind: 'arrow'; direction: 'left' | 'right' }
47
+ | { kind: 'tiny' }
48
+ | { kind: 'full' };
101
49
 
102
- const leftPct = leftFracRaw * 100;
103
- const widthPct = widthFrac * 100;
50
+ interface BarGeometry {
51
+ mode: BarMode;
52
+ leftPct: number;
53
+ widthPct: number;
54
+ visibleStartMs: number;
55
+ visibleEndMs: number;
56
+ visiblePixelWidth: number;
57
+ }
104
58
 
105
- const pixelWidth = widthFrac * containerWidth;
106
- const visibleLeftFrac = Math.max(0, Math.min(1, leftFracRaw));
107
- const visibleRightFrac = Math.max(0, Math.min(1, rightFracRaw));
108
- const visiblePixelWidth =
109
- Math.max(0, visibleRightFrac - visibleLeftFrac) * containerWidth;
110
- const isTinyBar =
111
- containerWidth > 0 && visiblePixelWidth < TINY_BAR_BOX_SIZE_PX;
112
- const [isRowHovered, setIsRowHovered] = useState(false);
59
+ /**
60
+ * Compute the bar's geometry inside the timeline viewport. Percentages are
61
+ * always in [0, 100] so we never emit CSS values that exceed browser layout
62
+ * limits at extreme zoom.
63
+ */
64
+ function computeBarGeometry(
65
+ startMs: number,
66
+ endMs: number,
67
+ viewStart: number,
68
+ viewEnd: number,
69
+ containerWidth: number
70
+ ): BarGeometry {
71
+ const viewDuration = viewEnd - viewStart;
72
+ const visibleStartMs = Math.max(startMs, viewStart);
73
+ const visibleEndMs = Math.min(endMs, viewEnd);
74
+ const visibleDurationMs = Math.max(0, visibleEndMs - visibleStartMs);
75
+ const widthFrac = viewDuration > 0 ? visibleDurationMs / viewDuration : 0;
76
+ const visiblePixelWidth = widthFrac * containerWidth;
113
77
 
114
- const segments = useMemo(() => computeSpanSegments(span), [span]);
78
+ const isTiny = containerWidth > 0 && visiblePixelWidth < TINY_BAR_BOX_SIZE_PX;
79
+ const extendsOffLeft = startMs < viewStart;
80
+ const extendsOffRight = endMs > viewEnd;
115
81
 
116
- const workflowStatus = (span.attributes.data as Record<string, unknown>)
117
- ?.status as string | undefined;
118
- const isErrored = span.status.code === 2 || workflowStatus === 'failed';
119
- const colors = getResourceColor(span.resource);
120
- const fallbackColor = isErrored
121
- ? (colors.errorBar ?? 'var(--ds-red-700)')
122
- : colors.bar;
123
- const renderDurationLabel = (label: string) => (
124
- <span
125
- 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-white tabular-nums"
126
- style={{ textShadow: '0 1px 1px rgba(0, 0, 0, 0.45)' }}
127
- >
82
+ const mode: BarMode =
83
+ isTiny && (extendsOffLeft || extendsOffRight)
84
+ ? { kind: 'arrow', direction: extendsOffRight ? 'right' : 'left' }
85
+ : isTiny
86
+ ? { kind: 'tiny' }
87
+ : { kind: 'full' };
88
+
89
+ return {
90
+ mode,
91
+ leftPct:
92
+ viewDuration > 0
93
+ ? ((visibleStartMs - viewStart) / viewDuration) * 100
94
+ : 0,
95
+ widthPct: widthFrac * 100,
96
+ visibleStartMs,
97
+ visibleEndMs,
98
+ visiblePixelWidth,
99
+ };
100
+ }
101
+
102
+ function getBarPositionStyle(geometry: BarGeometry): {
103
+ left: string;
104
+ width: string;
105
+ } {
106
+ switch (geometry.mode.kind) {
107
+ case 'arrow':
108
+ return {
109
+ left:
110
+ geometry.mode.direction === 'right'
111
+ ? `calc(100% - ${TINY_BAR_BOX_SIZE_PX}px)`
112
+ : '0px',
113
+ width: `${TINY_BAR_BOX_SIZE_PX}px`,
114
+ };
115
+ case 'tiny':
116
+ return {
117
+ left: `min(${geometry.leftPct}%, calc(100% - ${TINY_BAR_WIDTH_PX}px))`,
118
+ width: `${TINY_BAR_WIDTH_PX}px`,
119
+ };
120
+ case 'full':
121
+ return {
122
+ left: `${geometry.leftPct}%`,
123
+ width: `max(${geometry.widthPct}%, 4px)`,
124
+ };
125
+ }
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Segment projection
130
+ // ---------------------------------------------------------------------------
131
+
132
+ interface VisibleSegment {
133
+ status: SegmentStatus;
134
+ leftPct: number;
135
+ widthPct: number;
136
+ pixelWidth: number;
137
+ fullDurationMs: number;
138
+ }
139
+
140
+ /**
141
+ * Project status segments onto the visible portion of a bar. Segments fully
142
+ * outside the visible window are dropped; segments crossing the edge are
143
+ * clipped to [0%, 100%] of the visible bar.
144
+ */
145
+ function projectSegments(
146
+ segments: Segment[],
147
+ spanStartMs: number,
148
+ spanDurationMs: number,
149
+ geometry: BarGeometry
150
+ ): VisibleSegment[] {
151
+ const visibleDurationMs = geometry.visibleEndMs - geometry.visibleStartMs;
152
+ if (visibleDurationMs <= 0) return [];
153
+
154
+ return segments.flatMap((seg) => {
155
+ const segStartMs = spanStartMs + seg.startFraction * spanDurationMs;
156
+ const segEndMs = spanStartMs + seg.endFraction * spanDurationMs;
157
+ const startMs = Math.max(segStartMs, geometry.visibleStartMs);
158
+ const endMs = Math.min(segEndMs, geometry.visibleEndMs);
159
+ if (endMs <= startMs) return [];
160
+
161
+ const widthFrac = (endMs - startMs) / visibleDurationMs;
162
+ return [
163
+ {
164
+ status: seg.status,
165
+ leftPct:
166
+ ((startMs - geometry.visibleStartMs) / visibleDurationMs) * 100,
167
+ widthPct: widthFrac * 100,
168
+ pixelWidth: widthFrac * geometry.visiblePixelWidth,
169
+ fullDurationMs: (seg.endFraction - seg.startFraction) * spanDurationMs,
170
+ },
171
+ ];
172
+ });
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Small render helpers
177
+ // ---------------------------------------------------------------------------
178
+
179
+ function DurationLabel({ label }: { label: string }): ReactNode {
180
+ return (
181
+ <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">
128
182
  {label}
129
183
  </span>
130
184
  );
131
- const getMinDurationLabelWidthPx = (label: string) =>
132
- Math.max(40, label.length * 6 + 12);
133
- const totalDurationLabel = formatDuration(totalDurationMs);
134
- const showBarDurationLabel =
135
- isRowHovered &&
136
- pixelWidth >= getMinDurationLabelWidthPx(totalDurationLabel);
137
-
138
- const showBoundaryArrow = isTinyBar && (leftFracRaw < 0 || rightFracRaw > 1);
139
- const BoundaryArrow = leftFracRaw < 0.5 ? ArrowLeft : ArrowRight;
140
- const barContent = showBoundaryArrow ? (
185
+ }
186
+
187
+ function BoundaryArrow({
188
+ direction,
189
+ }: {
190
+ direction: 'left' | 'right';
191
+ }): ReactNode {
192
+ const Icon = direction === 'right' ? ArrowRight : ArrowLeft;
193
+ return (
141
194
  <div className="flex h-6 w-6 items-center justify-center rounded-[0.25rem]">
142
- <BoundaryArrow className="size-3 text-gray-900" />
195
+ <Icon className="size-3 text-gray-900" />
143
196
  </div>
144
- ) : isTinyBar ? (
197
+ );
198
+ }
199
+
200
+ function PlainBar({
201
+ bg,
202
+ border,
203
+ label,
204
+ }: {
205
+ bg: string;
206
+ border: string;
207
+ label: string | null;
208
+ }): ReactNode {
209
+ return (
145
210
  <div
146
- className="h-6 rounded-[0.25rem]"
147
- style={{ background: fallbackColor }}
148
- />
149
- ) : segments.length > 0 ? (
211
+ className="relative h-6 w-full min-w-1 rounded-[0.25rem] border"
212
+ style={{ background: bg, borderColor: border }}
213
+ >
214
+ {label ? <DurationLabel label={label} /> : null}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ function SegmentBar({ segments }: { segments: VisibleSegment[] }): ReactNode {
220
+ return (
150
221
  <div className="relative h-6 w-full">
151
222
  {segments.map((seg, i) => {
152
- const segPixelWidth =
153
- (seg.endFraction - seg.startFraction) * pixelWidth;
154
- const segDurationLabel = formatDuration(
155
- (seg.endFraction - seg.startFraction) * totalDurationMs
156
- );
157
- const showSegmentDurationLabel =
158
- isRowHovered &&
159
- segPixelWidth >= getMinDurationLabelWidthPx(segDurationLabel);
160
- const segStyle =
161
- seg.status === 'queued' && segPixelWidth < 20
162
- ? { background: 'var(--ds-gray-500)' }
163
- : SEGMENT_CONFIG[seg.status].style;
223
+ const label = formatDuration(seg.fullDurationMs);
224
+ // Only render the label when there's enough room for it without clipping.
225
+ const showLabel = seg.pixelWidth >= Math.max(40, label.length * 6 + 12);
226
+ // Beef up the queued segment when it's too narrow to read.
227
+ const isNarrowQueued = seg.status === 'queued' && seg.pixelWidth < 20;
228
+ const overrideBg = isNarrowQueued ? 'var(--ds-gray-400)' : undefined;
229
+ const overrideBorder = isNarrowQueued
230
+ ? 'var(--ds-gray-500)'
231
+ : undefined;
164
232
 
165
233
  return (
166
234
  <div
167
235
  key={`${seg.status}-${i}`}
168
236
  className={cn(
169
237
  'absolute h-full rounded-[0.25rem]',
170
- SEGMENT_CONFIG[seg.status].className
238
+ SEGMENT_CLASSES[seg.status]
171
239
  )}
172
240
  style={{
173
- left: `calc(${seg.startFraction * 100}% + ${SEGMENT_GAP_PX / 2}px)`,
174
- width: `calc(${(seg.endFraction - seg.startFraction) * 100}% - ${SEGMENT_GAP_PX}px)`,
241
+ // 1px gap between adjacent segments, distributed equally.
242
+ left: `calc(${seg.leftPct}% + 0.5px)`,
243
+ width: `calc(${seg.widthPct}% - 1px)`,
175
244
  minWidth: 1,
176
- ...segStyle,
245
+ background: overrideBg,
246
+ borderColor: overrideBorder,
177
247
  }}
178
248
  >
179
- {showSegmentDurationLabel
180
- ? renderDurationLabel(segDurationLabel)
181
- : null}
249
+ {showLabel ? <DurationLabel label={label} /> : null}
182
250
  </div>
183
251
  );
184
252
  })}
185
253
  </div>
186
- ) : (
187
- <div
188
- className="relative h-6 rounded-[0.25rem]"
189
- style={{
190
- width: '100%',
191
- minWidth: 4,
192
- background: fallbackColor,
193
- }}
194
- >
195
- {showBarDurationLabel ? renderDurationLabel(totalDurationLabel) : null}
196
- </div>
197
254
  );
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // TimelineBar
259
+ // ---------------------------------------------------------------------------
260
+
261
+ const TimelineBar = memo(function TimelineBar({
262
+ span,
263
+ viewStart,
264
+ viewDuration,
265
+ containerWidth,
266
+ isSelected,
267
+ onSelect,
268
+ }: {
269
+ span: Span;
270
+ viewStart: number;
271
+ viewDuration: number;
272
+ containerWidth: number;
273
+ isSelected: boolean;
274
+ onSelect: (spanId: string) => void;
275
+ }): ReactNode {
276
+ const startMs = getHighResInMs(span.startTime);
277
+ const endMs = getHighResInMs(span.endTime);
278
+ const totalDurationMs = getSpanDurationMs(span);
279
+
280
+ const geometry = useMemo(
281
+ () =>
282
+ computeBarGeometry(
283
+ startMs,
284
+ endMs,
285
+ viewStart,
286
+ viewStart + viewDuration,
287
+ containerWidth
288
+ ),
289
+ [startMs, endMs, viewStart, viewDuration, containerWidth]
290
+ );
291
+
292
+ const baseSegments = useMemo(() => computeSpanSegments(span), [span]);
293
+ const segments = useMemo(
294
+ () =>
295
+ geometry.mode.kind === 'full'
296
+ ? projectSegments(baseSegments, startMs, totalDurationMs, geometry)
297
+ : [],
298
+ [geometry, baseSegments, startMs, totalDurationMs]
299
+ );
300
+
301
+ const workflowStatus = (span.attributes.data as Record<string, unknown>)
302
+ ?.status as string | undefined;
303
+ const isErrored = span.status.code === 2 || workflowStatus === 'failed';
304
+ const colors = getResourceColor(span.resource);
305
+ const fallbackBg = isErrored
306
+ ? (colors.errorBg ?? 'var(--ds-red-200)')
307
+ : colors.bg;
308
+ const fallbackBorder = isErrored
309
+ ? (colors.errorBorder ?? 'var(--ds-red-500)')
310
+ : colors.border;
311
+
312
+ const totalLabel = formatDuration(totalDurationMs);
313
+ const showTotalLabel =
314
+ geometry.visiblePixelWidth >= Math.max(40, totalLabel.length * 6 + 12);
315
+
316
+ const handleClick = useCallback(() => {
317
+ onSelect(span.spanId);
318
+ }, [onSelect, span.spanId]);
198
319
 
199
320
  return (
200
321
  <div
@@ -202,37 +323,30 @@ const TimelineBar = memo(function TimelineBar({
202
323
  aria-selected={isSelected}
203
324
  aria-expanded={isSelected}
204
325
  aria-level={1}
205
- className={cn(
206
- 'h-10 relative flex items-center hover:bg-gray-100 aria-selected:bg-gray-100 aria-selected:hover:bg-gray-200'
207
- )}
208
- onMouseEnter={() => setIsRowHovered(true)}
209
- onMouseLeave={() => setIsRowHovered(false)}
210
- onClick={onClick}
326
+ className="group/timeline-row h-10 relative flex items-center hover:bg-gray-100 aria-selected:bg-gray-100 aria-selected:hover:bg-gray-200"
327
+ onClick={handleClick}
211
328
  >
212
- <div
213
- className="absolute inset-y-0"
214
- style={{
215
- left: TIMELINE_PADDING_PX,
216
- right: TIMELINE_PADDING_PX,
217
- }}
218
- >
329
+ <div className="absolute inset-y-0" style={TIMELINE_INSET_STYLE}>
219
330
  <div
220
- className="absolute top-1/2 -translate-y-1/2"
221
- style={{
222
- left: showBoundaryArrow
223
- ? `min(max(${leftPct}%, 0px), calc(100% - ${TINY_BAR_BOX_SIZE_PX}px))`
224
- : isTinyBar
225
- ? `min(${leftPct}%, calc(100% - ${TINY_BAR_WIDTH_PX}px))`
226
- : `${leftPct}%`,
227
- width: showBoundaryArrow
228
- ? `${TINY_BAR_BOX_SIZE_PX}px`
229
- : isTinyBar
230
- ? `${TINY_BAR_WIDTH_PX}px`
231
- : `max(${widthPct}%, 4px)`,
232
- height: BAR_HEIGHT_PX,
233
- }}
331
+ className="absolute top-1/2 h-6 -translate-y-1/2"
332
+ style={getBarPositionStyle(geometry)}
234
333
  >
235
- {barContent}
334
+ {geometry.mode.kind === 'arrow' ? (
335
+ <BoundaryArrow direction={geometry.mode.direction} />
336
+ ) : geometry.mode.kind === 'tiny' ? (
337
+ <div
338
+ className="h-6 rounded-[0.25rem] border"
339
+ style={{ background: fallbackBg, borderColor: fallbackBorder }}
340
+ />
341
+ ) : segments.length > 0 ? (
342
+ <SegmentBar segments={segments} />
343
+ ) : (
344
+ <PlainBar
345
+ bg={fallbackBg}
346
+ border={fallbackBorder}
347
+ label={showTotalLabel ? totalLabel : null}
348
+ />
349
+ )}
236
350
  </div>
237
351
  </div>
238
352
  </div>
@@ -241,6 +355,57 @@ const TimelineBar = memo(function TimelineBar({
241
355
 
242
356
  export { TimelineBar };
243
357
 
358
+ // ---------------------------------------------------------------------------
359
+ // DeltaIndicator (Alt-key gap overlay)
360
+ // ---------------------------------------------------------------------------
361
+
362
+ // Row and indicator sizes are local to DeltaIndicator since it's the only
363
+ // place that needs to compute Y positions from a row index. ROW_HEIGHT must
364
+ // match the `h-10` (40px) row used in TimelineBar.
365
+ const DELTA_ROW_HEIGHT_PX = 40;
366
+ const DELTA_CAP_HEIGHT_PX = 8;
367
+ // Vertical offset to sit the indicator inside the gap between row N and N+1,
368
+ // aligned with where the bar starts in the next row (rows center a 24px bar
369
+ // inside 40px, so bars start ~8px from the top of the row).
370
+ const DELTA_ROW_OFFSET_PX = 8;
371
+
372
+ const DeltaIndicator = memo(function DeltaIndicator({
373
+ leftFrac,
374
+ rightFrac,
375
+ label,
376
+ rowIndex,
377
+ }: {
378
+ leftFrac: number;
379
+ rightFrac: number;
380
+ label: string;
381
+ rowIndex: number;
382
+ }) {
383
+ const centerY = DELTA_ROW_OFFSET_PX + (rowIndex + 1) * DELTA_ROW_HEIGHT_PX;
384
+
385
+ return (
386
+ <div
387
+ className="absolute pointer-events-none"
388
+ style={{
389
+ left: `${leftFrac * 100}%`,
390
+ width: `${(rightFrac - leftFrac) * 100}%`,
391
+ top: centerY - DELTA_CAP_HEIGHT_PX / 2,
392
+ height: DELTA_CAP_HEIGHT_PX,
393
+ }}
394
+ >
395
+ <div className="absolute left-0 top-0 w-px h-full bg-amber-800" />
396
+ <div className="absolute left-0 right-0 top-1/2 h-px bg-amber-800" />
397
+ <div className="absolute right-0 top-0 w-px h-full bg-amber-800" />
398
+ <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">
399
+ {label}
400
+ </span>
401
+ </div>
402
+ );
403
+ });
404
+
405
+ // ---------------------------------------------------------------------------
406
+ // TimelineHeader
407
+ // ---------------------------------------------------------------------------
408
+
244
409
  export function TimelineHeader({
245
410
  markers,
246
411
  hoverInfo,
@@ -273,6 +438,10 @@ export function TimelineHeader({
273
438
  );
274
439
  }
275
440
 
441
+ // ---------------------------------------------------------------------------
442
+ // Timeline
443
+ // ---------------------------------------------------------------------------
444
+
276
445
  export function Timeline({
277
446
  spans,
278
447
  viewStart,
@@ -317,13 +486,11 @@ export function Timeline({
317
486
  <div
318
487
  aria-hidden
319
488
  className="absolute inset-y-0 pointer-events-none"
320
- style={{
321
- left: TIMELINE_PADDING_PX,
322
- right: TIMELINE_PADDING_PX,
323
- }}
489
+ style={TIMELINE_INSET_STYLE}
324
490
  >
325
491
  {markers.map((marker) =>
326
- Math.abs(marker.value) > ORIGIN_MARKER_EPSILON ? (
492
+ // Skip the "0s" origin marker since the left edge already implies it.
493
+ Math.abs(marker.value) > 0.000001 ? (
327
494
  <div
328
495
  key={`${marker.position}-${marker.label}`}
329
496
  className="absolute top-0 bottom-0 w-px bg-gray-alpha-300"
@@ -335,13 +502,10 @@ export function Timeline({
335
502
  {hoverFraction != null && (
336
503
  <div
337
504
  className="absolute inset-y-0 pointer-events-none z-10"
338
- style={{
339
- left: TIMELINE_PADDING_PX,
340
- right: TIMELINE_PADDING_PX,
341
- }}
505
+ style={TIMELINE_INSET_STYLE}
342
506
  >
343
507
  <div
344
- className="absolute top-0 bottom-0 w-px bg-gray-alpha-400"
508
+ className="absolute top-0 bottom-0 w-px bg-gray-alpha-500"
345
509
  style={{ left: `${hoverFraction * 100}%` }}
346
510
  />
347
511
  </div>
@@ -354,17 +518,14 @@ export function Timeline({
354
518
  viewDuration={viewDuration}
355
519
  containerWidth={timelineWidth}
356
520
  isSelected={selectedId === span.spanId}
357
- onClick={() => onSelect(span.spanId)}
521
+ onSelect={onSelect}
358
522
  />
359
523
  ))}
360
524
  {altHeld && (
361
525
  <div
362
526
  aria-hidden
363
527
  className="absolute inset-y-0 pointer-events-none"
364
- style={{
365
- left: TIMELINE_PADDING_PX,
366
- right: TIMELINE_PADDING_PX,
367
- }}
528
+ style={TIMELINE_INSET_STYLE}
368
529
  >
369
530
  {gaps.map((gap) => (
370
531
  <DeltaIndicator
@@ -29,7 +29,7 @@ export function DetailPanel({
29
29
  </span>
30
30
  <button
31
31
  type="button"
32
- className="p-1 rounded-md text-gray-900 hover:text-gray-1000 hover:bg-gray-alpha-200 transition-colors"
32
+ className="p-1 rounded-md text-gray-1000 hover:bg-gray-alpha-100 transition-colors"
33
33
  onClick={onClose}
34
34
  >
35
35
  <X className="w-4 h-4" />