@workflow/web-shared 5.0.0-beta.41 → 5.0.0-beta.42

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 (86) hide show
  1. package/dist/components/event-list-view.d.ts +10 -2
  2. package/dist/components/event-list-view.d.ts.map +1 -1
  3. package/dist/components/event-list-view.js +52 -26
  4. package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
  5. package/dist/components/sidebar/attributes-block.js +3 -6
  6. package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
  7. package/dist/components/sidebar/conversation-view.js +19 -36
  8. package/dist/components/sidebar/entity-detail-panel.d.ts +5 -0
  9. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  10. package/dist/components/sidebar/entity-detail-panel.js +2 -2
  11. package/dist/components/sidebar/events-list.d.ts +7 -1
  12. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  13. package/dist/components/sidebar/events-list.js +6 -5
  14. package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
  15. package/dist/components/sidebar/resolve-hook-modal.js +4 -145
  16. package/dist/components/sidebar/sidebar-data-context.d.ts +6 -0
  17. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
  18. package/dist/components/sidebar/sidebar-data-context.js +1 -1
  19. package/dist/components/stream-viewer-skeleton.js +6 -6
  20. package/dist/components/stream-viewer.js +2 -2
  21. package/dist/components/trace-viewer/components/detail-panel.d.ts.map +1 -1
  22. package/dist/components/trace-viewer/components/detail-panel.js +2 -1
  23. package/dist/components/trace-viewer/components/timeline.d.ts.map +1 -1
  24. package/dist/components/trace-viewer/components/timeline.js +13 -22
  25. package/dist/components/trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
  26. package/dist/components/trace-viewer/components/trace-viewer-skeleton.js +58 -18
  27. package/dist/components/trace-viewer/icons.js +5 -5
  28. package/dist/components/trace-viewer/utils.d.ts +6 -10
  29. package/dist/components/trace-viewer/utils.d.ts.map +1 -1
  30. package/dist/components/trace-viewer/utils.js +14 -24
  31. package/dist/components/trace-viewer.d.ts.map +1 -1
  32. package/dist/components/trace-viewer.js +12 -4
  33. package/dist/components/ui/context-card.d.ts.map +1 -1
  34. package/dist/components/ui/context-card.js +9 -21
  35. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  36. package/dist/components/ui/data-inspector.js +8 -35
  37. package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -0
  38. package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -0
  39. package/dist/components/ui/duplicate-event-tooltip.js +19 -0
  40. package/dist/components/ui/menu-dropdown.d.ts.map +1 -1
  41. package/dist/components/ui/menu-dropdown.js +3 -19
  42. package/dist/components/ui/skeleton.js +2 -2
  43. package/dist/components/ui/spinner.d.ts.map +1 -1
  44. package/dist/components/ui/spinner.js +5 -11
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2 -1
  48. package/dist/lib/duplicate-events.d.ts +27 -0
  49. package/dist/lib/duplicate-events.d.ts.map +1 -0
  50. package/dist/lib/duplicate-events.js +166 -0
  51. package/dist/lib/event-materialization.d.ts +15 -2
  52. package/dist/lib/event-materialization.d.ts.map +1 -1
  53. package/dist/lib/event-materialization.js +19 -4
  54. package/dist/lib/trace-builder.d.ts +16 -1
  55. package/dist/lib/trace-builder.d.ts.map +1 -1
  56. package/dist/lib/trace-builder.js +23 -4
  57. package/package.json +4 -4
  58. package/src/components/event-list-view.tsx +70 -32
  59. package/src/components/sidebar/attributes-block.tsx +4 -11
  60. package/src/components/sidebar/conversation-view.tsx +25 -79
  61. package/src/components/sidebar/entity-detail-panel.tsx +6 -0
  62. package/src/components/sidebar/events-list.tsx +21 -3
  63. package/src/components/sidebar/resolve-hook-modal.tsx +23 -186
  64. package/src/components/sidebar/sidebar-data-context.tsx +6 -0
  65. package/src/components/stream-viewer-skeleton.tsx +5 -5
  66. package/src/components/stream-viewer.tsx +1 -1
  67. package/src/components/trace-viewer/components/detail-panel.tsx +1 -0
  68. package/src/components/trace-viewer/components/timeline.tsx +21 -39
  69. package/src/components/trace-viewer/components/trace-viewer-skeleton.tsx +63 -35
  70. package/src/components/trace-viewer/icons.tsx +4 -4
  71. package/src/components/trace-viewer/utils.test.ts +5 -7
  72. package/src/components/trace-viewer/utils.ts +17 -31
  73. package/src/components/trace-viewer.tsx +15 -3
  74. package/src/components/ui/context-card.tsx +16 -26
  75. package/src/components/ui/data-inspector.tsx +20 -65
  76. package/src/components/ui/duplicate-event-tooltip.tsx +44 -0
  77. package/src/components/ui/menu-dropdown.tsx +8 -22
  78. package/src/components/ui/skeleton.tsx +2 -2
  79. package/src/components/ui/spinner.tsx +4 -8
  80. package/src/index.ts +4 -0
  81. package/src/lib/duplicate-events.test.ts +339 -0
  82. package/src/lib/duplicate-events.ts +188 -0
  83. package/src/lib/event-materialization.test.ts +117 -0
  84. package/src/lib/event-materialization.ts +23 -3
  85. package/src/lib/trace-builder.test.ts +74 -0
  86. package/src/lib/trace-builder.ts +30 -3
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { Send, X } from 'lucide-react';
4
4
  import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import { cn } from '../../lib/cn';
5
6
 
6
7
  interface ResolveHookModalProps {
7
8
  /** Whether the modal is open */
@@ -110,60 +111,24 @@ export function ResolveHookModal({
110
111
 
111
112
  return (
112
113
  <div
113
- style={{
114
- position: 'fixed',
115
- inset: 0,
116
- zIndex: 50,
117
- display: 'flex',
118
- alignItems: 'center',
119
- justifyContent: 'center',
120
- }}
114
+ className="fixed inset-0 z-50 flex items-center justify-center"
121
115
  role="dialog"
122
116
  aria-modal="true"
123
117
  aria-labelledby="resolve-hook-modal-title"
124
118
  >
125
119
  {/* Backdrop — matches Geist dialog ::backdrop */}
126
120
  <div
127
- style={{
128
- position: 'absolute',
129
- inset: 0,
130
- background: 'rgba(0, 0, 0, 0.7)',
131
- }}
121
+ className="absolute inset-0 bg-black/70"
132
122
  onClick={isSubmitting ? undefined : onClose}
133
123
  />
134
124
 
135
125
  {/* Modal card — matches Geist dialog.geist-dialog */}
136
- <div
137
- style={{
138
- position: 'relative',
139
- zIndex: 10,
140
- width: 480,
141
- maxWidth: 'calc(100% - 32px)',
142
- borderRadius: 12,
143
- border: 'none',
144
- boxShadow: 'var(--ds-shadow-menu)',
145
- background: 'var(--ds-background-100)',
146
- color: 'var(--ds-gray-1000)',
147
- overflow: 'hidden',
148
- }}
149
- >
126
+ <div className="relative z-10 w-[480px] max-w-[calc(100%_-_32px)] overflow-hidden !border-none bg-background-100 text-gray-1000 material-menu">
150
127
  {/* Header */}
151
- <div
152
- style={{
153
- display: 'flex',
154
- alignItems: 'center',
155
- justifyContent: 'space-between',
156
- padding: '16px 24px',
157
- }}
158
- >
128
+ <div className="flex items-center justify-between px-6 py-4">
159
129
  <h2
160
130
  id="resolve-hook-modal-title"
161
- style={{
162
- margin: 0,
163
- fontSize: 16,
164
- fontWeight: 600,
165
- color: 'var(--ds-gray-1000)',
166
- }}
131
+ className="m-0 font-semibold text-base text-gray-1000"
167
132
  >
168
133
  Resolve Hook
169
134
  </h2>
@@ -172,66 +137,24 @@ export function ResolveHookModal({
172
137
  onClick={onClose}
173
138
  disabled={isSubmitting}
174
139
  aria-label="Close modal"
175
- style={{
176
- display: 'flex',
177
- alignItems: 'center',
178
- justifyContent: 'center',
179
- padding: 4,
180
- borderRadius: 6,
181
- border: 'none',
182
- background: 'transparent',
183
- color: 'var(--ds-gray-900)',
184
- cursor: isSubmitting ? 'not-allowed' : 'pointer',
185
- opacity: isSubmitting ? 0.5 : 1,
186
- transition: 'background 0.15s',
187
- }}
188
- onMouseEnter={(e) => {
189
- e.currentTarget.style.background = 'var(--ds-gray-alpha-200)';
190
- }}
191
- onMouseLeave={(e) => {
192
- e.currentTarget.style.background = 'transparent';
193
- }}
140
+ className="flex cursor-pointer items-center justify-center rounded-md !border-none !bg-transparent p-1 text-gray-900 transition-colors hover:!bg-gray-alpha-200 disabled:cursor-not-allowed disabled:opacity-50"
194
141
  >
195
- <X style={{ width: 16, height: 16 }} />
142
+ <X className="size-4" />
196
143
  </button>
197
144
  </div>
198
145
 
199
146
  {/* Body */}
200
147
  <form onSubmit={handleSubmit}>
201
- <div style={{ padding: '0 24px 16px' }}>
148
+ <div className="px-6 pb-4">
202
149
  <label
203
150
  htmlFor="json-payload"
204
- style={{
205
- display: 'block',
206
- fontSize: 14,
207
- fontWeight: 500,
208
- marginBottom: 6,
209
- color: 'var(--ds-gray-1000)',
210
- }}
151
+ className="mb-1.5 block font-medium text-gray-1000 text-sm"
211
152
  >
212
153
  JSON Payload
213
154
  </label>
214
- <p
215
- style={{
216
- fontSize: 13,
217
- marginBottom: 12,
218
- marginTop: 0,
219
- color: 'var(--ds-gray-900)',
220
- lineHeight: 1.5,
221
- }}
222
- >
155
+ <p className="mt-0 mb-3 text-[13px] text-gray-900 leading-[1.5]">
223
156
  Enter a JSON value to send to the hook. Leave empty to send{' '}
224
- <code
225
- style={{
226
- padding: '2px 6px',
227
- borderRadius: 4,
228
- fontSize: 12,
229
- fontFamily:
230
- 'var(--font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace)',
231
- background: 'var(--ds-gray-alpha-200)',
232
- color: 'var(--ds-gray-1000)',
233
- }}
234
- >
157
+ <code className="rounded bg-gray-alpha-200 px-1.5 py-0.5 font-mono text-gray-1000 text-xs">
235
158
  null
236
159
  </code>
237
160
  .
@@ -247,57 +170,17 @@ export function ResolveHookModal({
247
170
  onKeyDown={handleKeyDown}
248
171
  disabled={isSubmitting}
249
172
  placeholder='{"key": "value"}'
250
- style={{
251
- width: '100%',
252
- height: 160,
253
- padding: '8px 12px',
254
- fontFamily:
255
- 'var(--font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace)',
256
- fontSize: 13,
257
- lineHeight: 1.5,
258
- borderRadius: 8,
259
- border: `1px solid ${parseError ? 'var(--ds-red-700)' : 'var(--ds-gray-alpha-400)'}`,
260
- color: 'var(--ds-gray-1000)',
261
- background: 'var(--ds-background-100)',
262
- outline: 'none',
263
- resize: 'none',
264
- opacity: isSubmitting ? 0.5 : 1,
265
- cursor: isSubmitting ? 'not-allowed' : 'text',
266
- boxSizing: 'border-box',
267
- }}
173
+ className={cn(
174
+ 'box-border h-40 w-full resize-none rounded-lg border bg-background-100 px-3 py-2 font-mono text-[13px] text-gray-1000 leading-[1.5] outline-none disabled:cursor-not-allowed disabled:opacity-50',
175
+ parseError ? 'border-red-700' : 'border-gray-alpha-400'
176
+ )}
268
177
  />
269
178
  {parseError && (
270
- <p
271
- style={{
272
- marginTop: 8,
273
- fontSize: 13,
274
- color: 'var(--ds-red-900)',
275
- margin: '8px 0 0',
276
- }}
277
- >
278
- {parseError}
279
- </p>
179
+ <p className="mt-2 mb-0 text-[13px] text-red-900">{parseError}</p>
280
180
  )}
281
- <p
282
- style={{
283
- marginTop: 8,
284
- fontSize: 12,
285
- color: 'var(--ds-gray-800)',
286
- margin: '8px 0 0',
287
- }}
288
- >
181
+ <p className="mt-2 mb-0 text-gray-800 text-xs">
289
182
  Press{' '}
290
- <kbd
291
- style={{
292
- padding: '2px 5px',
293
- borderRadius: 4,
294
- fontSize: 11,
295
- fontFamily:
296
- 'var(--font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace)',
297
- background: 'var(--ds-gray-alpha-200)',
298
- border: '1px solid var(--ds-gray-alpha-400)',
299
- }}
300
- >
183
+ <kbd className="rounded border border-gray-alpha-400 bg-gray-alpha-200 px-[5px] py-0.5 font-mono text-[11px]">
301
184
  {isMacPlatform ? '⌘' : 'Ctrl'}
302
185
  +Enter
303
186
  </kbd>{' '}
@@ -306,38 +189,12 @@ export function ResolveHookModal({
306
189
  </div>
307
190
 
308
191
  {/* Footer */}
309
- <div
310
- style={{
311
- display: 'flex',
312
- alignItems: 'center',
313
- justifyContent: 'flex-end',
314
- gap: 8,
315
- padding: '12px 24px',
316
- borderTop: '1px solid var(--ds-gray-alpha-400)',
317
- }}
318
- >
192
+ <div className="flex items-center justify-end gap-2 border-gray-alpha-400 border-t px-6 py-3">
319
193
  <button
320
194
  type="button"
321
195
  onClick={onClose}
322
196
  disabled={isSubmitting}
323
- style={{
324
- padding: '8px 16px',
325
- fontSize: 14,
326
- fontWeight: 500,
327
- borderRadius: 8,
328
- border: '1px solid var(--ds-gray-alpha-400)',
329
- background: 'var(--ds-background-100)',
330
- color: 'var(--ds-gray-1000)',
331
- cursor: isSubmitting ? 'not-allowed' : 'pointer',
332
- opacity: isSubmitting ? 0.5 : 1,
333
- transition: 'background 0.15s',
334
- }}
335
- onMouseEnter={(e) => {
336
- e.currentTarget.style.background = 'var(--ds-gray-alpha-100)';
337
- }}
338
- onMouseLeave={(e) => {
339
- e.currentTarget.style.background = 'var(--ds-background-100)';
340
- }}
197
+ className="cursor-pointer rounded-lg border border-gray-alpha-400 bg-background-100 px-4 py-2 font-medium text-gray-1000 text-sm transition-colors hover:bg-gray-alpha-100 disabled:cursor-not-allowed disabled:opacity-50"
341
198
  >
342
199
  Cancel
343
200
  </button>
@@ -345,29 +202,9 @@ export function ResolveHookModal({
345
202
  type="button"
346
203
  onClick={() => void submitPayload()}
347
204
  disabled={isSubmitting}
348
- style={{
349
- display: 'flex',
350
- alignItems: 'center',
351
- gap: 6,
352
- padding: '8px 16px',
353
- fontSize: 14,
354
- fontWeight: 500,
355
- borderRadius: 8,
356
- border: 'none',
357
- background: 'var(--ds-gray-1000)',
358
- color: 'var(--ds-background-100)',
359
- cursor: isSubmitting ? 'not-allowed' : 'pointer',
360
- opacity: isSubmitting ? 0.5 : 1,
361
- transition: 'opacity 0.15s',
362
- }}
363
- onMouseEnter={(e) => {
364
- if (!isSubmitting) e.currentTarget.style.opacity = '0.9';
365
- }}
366
- onMouseLeave={(e) => {
367
- if (!isSubmitting) e.currentTarget.style.opacity = '1';
368
- }}
205
+ className="flex cursor-pointer items-center gap-1.5 rounded-lg !border-none bg-gray-1000 px-4 py-2 font-medium text-background-100 text-sm transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:opacity-50"
369
206
  >
370
- <Send style={{ width: 14, height: 14 }} />
207
+ <Send className="size-3.5" />
371
208
  {isSubmitting ? 'Sending...' : 'Send Payload'}
372
209
  </button>
373
210
  </div>
@@ -7,6 +7,12 @@ import type { FetchSpanDetail } from './use-selected-span-detail';
7
7
  export interface SidebarDataContextValue {
8
8
  run: WorkflowRun;
9
9
  events: Event[];
10
+ /**
11
+ * Events every replay reads past as repeats, computed once from the whole
12
+ * of `events` by whoever knows the list is complete. Absent when nobody
13
+ * could vouch for that, in which case nothing is marked.
14
+ */
15
+ duplicateEventIds?: ReadonlySet<string>;
10
16
  fetchSpanDetail: FetchSpanDetail;
11
17
  onStreamClick?: (streamId: string) => void;
12
18
  onRunClick?: (runId: string) => void;
@@ -1,10 +1,10 @@
1
1
  import { Skeleton } from './ui/skeleton';
2
2
 
3
3
  const STREAM_SKELETON_ROWS = [
4
- { id: 'stream-skeleton-row-1', width: '72%' },
5
- { id: 'stream-skeleton-row-2', width: '58%' },
6
- { id: 'stream-skeleton-row-3', width: '80%' },
7
- { id: 'stream-skeleton-row-4', width: '64%' },
4
+ { id: 'stream-skeleton-row-1', widthClassName: 'w-[72%]' },
5
+ { id: 'stream-skeleton-row-2', widthClassName: 'w-[58%]' },
6
+ { id: 'stream-skeleton-row-3', widthClassName: 'w-4/5' },
7
+ { id: 'stream-skeleton-row-4', widthClassName: 'w-[64%]' },
8
8
  ];
9
9
 
10
10
  export function StreamViewerSkeleton() {
@@ -24,7 +24,7 @@ export function StreamViewerSkeleton() {
24
24
  key={row.id}
25
25
  className="flex h-10 items-center gap-2 border-b border-gray-alpha-400 px-3"
26
26
  >
27
- <Skeleton className="h-3" style={{ width: row.width }} />
27
+ <Skeleton className={`h-3 ${row.widthClassName}`} />
28
28
  <Skeleton className="h-4 w-4 shrink-0 rounded-sm" />
29
29
  </div>
30
30
  ))}
@@ -122,7 +122,7 @@ export function StreamViewer({
122
122
  <ChunkRow chunk={chunks[index]} />
123
123
  </div>
124
124
  )}
125
- style={{ flex: 1, minHeight: 0 }}
125
+ className="min-h-0 flex-1"
126
126
  />
127
127
  )}
128
128
  </div>
@@ -55,6 +55,7 @@ function useSelectedSpanInfo(): SelectedSpanInfo | null {
55
55
  resource,
56
56
  spanId: activeSpan.spanId,
57
57
  rawEvents,
58
+ duplicateEventIds: sidebar.duplicateEventIds,
58
59
  };
59
60
  }, [activeSpan, sidebar]);
60
61
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { ArrowLeft, ArrowRight } from 'lucide-react';
4
- import type { CSSProperties, ReactNode } from 'react';
4
+ import type { ReactNode } from 'react';
5
5
  import {
6
6
  Fragment,
7
7
  memo,
@@ -28,7 +28,7 @@ import {
28
28
  computeSpanGaps,
29
29
  computeSpanMarkers,
30
30
  computeSpanSegments,
31
- getResourceColor,
31
+ getResourceClassNames,
32
32
  getSpanDurationMs,
33
33
  isSpanErrored,
34
34
  } from '../utils';
@@ -62,11 +62,6 @@ const SEGMENT_CLASSES: Record<SegmentStatus, string> = {
62
62
  received: 'bg-blue-200 border border-blue-500',
63
63
  };
64
64
 
65
- const TIMELINE_INSET_STYLE: CSSProperties = {
66
- left: TIMELINE_PADDING_PX,
67
- right: TIMELINE_PADDING_PX,
68
- };
69
-
70
65
  const STRIPED_SEGMENT_STATUSES: ReadonlySet<SegmentStatus> = new Set([
71
66
  'pending',
72
67
  'running',
@@ -260,18 +255,18 @@ function BoundaryArrow({
260
255
  }
261
256
 
262
257
  function PlainBar({
263
- bg,
264
- border,
258
+ className,
265
259
  label,
266
260
  }: {
267
- bg: string;
268
- border: string;
261
+ className: string;
269
262
  label: string | null;
270
263
  }): ReactNode {
271
264
  return (
272
265
  <div
273
- className="relative h-6 w-full min-w-1 rounded-[0.25rem] border"
274
- style={{ background: bg, borderColor: border }}
266
+ className={cn(
267
+ 'relative h-6 w-full min-w-1 rounded-[0.25rem] border',
268
+ className
269
+ )}
275
270
  >
276
271
  {label ? <DurationLabel label={label} /> : null}
277
272
  </div>
@@ -352,14 +347,13 @@ function SegmentBar({
352
347
  <div
353
348
  key={i}
354
349
  className={cn(
355
- 'absolute h-full overflow-hidden rounded-[0.25rem]',
350
+ 'absolute h-full min-w-px overflow-hidden rounded-[0.25rem]',
356
351
  SEGMENT_CLASSES[seg.status]
357
352
  )}
358
353
  style={{
359
354
  // 1px gap between adjacent segments, distributed equally.
360
355
  left: `calc(${seg.leftPct}% + 0.5px)`,
361
356
  width: `calc(${seg.widthPct}% - 1px)`,
362
- minWidth: 1,
363
357
  }}
364
358
  >
365
359
  {STRIPED_SEGMENT_STATUSES.has(seg.status) ? (
@@ -457,13 +451,10 @@ const TimelineBar = memo(function TimelineBar({
457
451
  markers.length > 0 || offscreen.left !== null || offscreen.right !== null;
458
452
 
459
453
  const isErrored = isSpanErrored(span);
460
- const colors = getResourceColor(span.resource);
461
- const fallbackBg = isErrored
462
- ? (colors.errorBg ?? 'var(--ds-red-200)')
463
- : colors.bg;
464
- const fallbackBorder = isErrored
465
- ? (colors.errorBorder ?? 'var(--ds-red-500)')
466
- : colors.border;
454
+ const colors = getResourceClassNames(span.resource);
455
+ const fallbackClassName = isErrored
456
+ ? (colors.errorClassName ?? 'border-red-500 bg-red-200')
457
+ : colors.className;
467
458
 
468
459
  const totalLabel = formatDurationPrecise(totalDurationMs);
469
460
  const showTotalLabel =
@@ -486,7 +477,7 @@ const TimelineBar = memo(function TimelineBar({
486
477
  )}
487
478
  onClick={handleClick}
488
479
  >
489
- <div className="absolute inset-y-0" style={TIMELINE_INSET_STYLE}>
480
+ <div className="absolute inset-x-4 inset-y-0">
490
481
  <div
491
482
  className="absolute top-1/2 h-6 -translate-y-1/2 overflow-hidden rounded-[0.25rem]"
492
483
  style={getBarPositionStyle(geometry)}
@@ -495,15 +486,13 @@ const TimelineBar = memo(function TimelineBar({
495
486
  <BoundaryArrow direction={geometry.mode.direction} />
496
487
  ) : geometry.mode.kind === 'tiny' ? (
497
488
  <div
498
- className="h-6 rounded-[0.25rem] border"
499
- style={{ background: fallbackBg, borderColor: fallbackBorder }}
489
+ className={cn('h-6 rounded-[0.25rem] border', fallbackClassName)}
500
490
  />
501
491
  ) : segments.length > 0 ? (
502
492
  <SegmentBar segments={segments} showLabels={!hasMarkers} />
503
493
  ) : (
504
494
  <PlainBar
505
- bg={fallbackBg}
506
- border={fallbackBorder}
495
+ className={fallbackClassName}
507
496
  label={showTotalLabel ? totalLabel : null}
508
497
  />
509
498
  )}
@@ -612,10 +601,9 @@ const DeltaMeasureLine = memo(function DeltaMeasureLine({
612
601
  return (
613
602
  <>
614
603
  <div
615
- className="absolute h-px bg-amber-800"
604
+ className="absolute h-px w-1 bg-amber-800"
616
605
  style={{
617
606
  left: Math.min(anchorX, guideX),
618
- width: MEASURE_GUIDE_OUTSET_PX,
619
607
  top: anchorCenterY,
620
608
  }}
621
609
  />
@@ -781,8 +769,7 @@ export function Timeline({
781
769
  >
782
770
  <div
783
771
  aria-hidden
784
- className="absolute inset-y-0 pointer-events-none"
785
- style={TIMELINE_INSET_STYLE}
772
+ className="pointer-events-none absolute inset-x-4 inset-y-0"
786
773
  >
787
774
  {markers.map((marker) =>
788
775
  // Skip the "0s" origin marker since the left edge already implies it.
@@ -796,10 +783,7 @@ export function Timeline({
796
783
  )}
797
784
  </div>
798
785
  {hover != null && (
799
- <div
800
- className="absolute inset-y-0 pointer-events-none z-10"
801
- style={TIMELINE_INSET_STYLE}
802
- >
786
+ <div className="pointer-events-none absolute inset-x-4 inset-y-0 z-10">
803
787
  <div
804
788
  className="absolute top-0 bottom-0 w-px bg-gray-alpha-500"
805
789
  style={{ left: `${hover.fraction * 100}%` }}
@@ -824,8 +808,7 @@ export function Timeline({
824
808
  {altHeld && anchorIndex < 0 && (
825
809
  <div
826
810
  aria-hidden
827
- className="absolute inset-y-0 pointer-events-none"
828
- style={TIMELINE_INSET_STYLE}
811
+ className="pointer-events-none absolute inset-x-4 inset-y-0"
829
812
  >
830
813
  {gapMeasurements.map((gap) => (
831
814
  <DeltaMeasureLine
@@ -839,8 +822,7 @@ export function Timeline({
839
822
  {measurement && (
840
823
  <div
841
824
  aria-hidden
842
- className="absolute inset-y-0 pointer-events-none z-20"
843
- style={TIMELINE_INSET_STYLE}
825
+ className="pointer-events-none absolute inset-x-4 inset-y-0 z-20"
844
826
  >
845
827
  <DeltaMeasureLine {...measurement} timelineWidth={timelineWidth} />
846
828
  </div>
@@ -1,19 +1,62 @@
1
1
  import { Skeleton } from '../../ui/skeleton';
2
- import { DEFAULT_START_PX, paneColTemplate } from './pane-constants';
3
2
 
4
- // Mirrors SplitPane's initial column template so the skeleton lines up with
5
- // the real viewer's first paint.
6
- const COL_TEMPLATE = paneColTemplate(DEFAULT_START_PX);
7
-
8
- const ROWS: { id: string; name: number; off: number; bar: number }[] = [
9
- { id: 'r0', name: 62, off: 0, bar: 72 },
10
- { id: 'r1', name: 78, off: 6, bar: 48 },
11
- { id: 'r2', name: 50, off: 10, bar: 55 },
12
- { id: 'r3', name: 84, off: 18, bar: 30 },
13
- { id: 'r4', name: 45, off: 18, bar: 42 },
14
- { id: 'r5', name: 66, off: 34, bar: 38 },
15
- { id: 'r6', name: 55, off: 41, bar: 25 },
16
- { id: 'r7', name: 40, off: 50, bar: 30 },
3
+ const ROWS = [
4
+ {
5
+ id: 'r0',
6
+ nameClassName: 'w-[62%]',
7
+ offsetClassName: 'left-0',
8
+ barClassName: 'w-[72%]',
9
+ topClassName: 'top-0',
10
+ },
11
+ {
12
+ id: 'r1',
13
+ nameClassName: 'w-[78%]',
14
+ offsetClassName: 'left-[6%]',
15
+ barClassName: 'w-[48%]',
16
+ topClassName: 'top-[3.5px]',
17
+ },
18
+ {
19
+ id: 'r2',
20
+ nameClassName: 'w-1/2',
21
+ offsetClassName: 'left-[10%]',
22
+ barClassName: 'w-[55%]',
23
+ topClassName: 'top-[7px]',
24
+ },
25
+ {
26
+ id: 'r3',
27
+ nameClassName: 'w-[84%]',
28
+ offsetClassName: 'left-[18%]',
29
+ barClassName: 'w-[30%]',
30
+ topClassName: 'top-[10.5px]',
31
+ },
32
+ {
33
+ id: 'r4',
34
+ nameClassName: 'w-[45%]',
35
+ offsetClassName: 'left-[18%]',
36
+ barClassName: 'w-[42%]',
37
+ topClassName: 'top-[14px]',
38
+ },
39
+ {
40
+ id: 'r5',
41
+ nameClassName: 'w-[66%]',
42
+ offsetClassName: 'left-[34%]',
43
+ barClassName: 'w-[38%]',
44
+ topClassName: 'top-[17.5px]',
45
+ },
46
+ {
47
+ id: 'r6',
48
+ nameClassName: 'w-[55%]',
49
+ offsetClassName: 'left-[41%]',
50
+ barClassName: 'w-1/4',
51
+ topClassName: 'top-[21px]',
52
+ },
53
+ {
54
+ id: 'r7',
55
+ nameClassName: 'w-2/5',
56
+ offsetClassName: 'left-1/2',
57
+ barClassName: 'w-[30%]',
58
+ topClassName: 'top-[24.5px]',
59
+ },
17
60
  ];
18
61
 
19
62
  const HEADER_MARKERS = ['m0', 'm1', 'm2', 'm3'];
@@ -36,25 +79,17 @@ export function TraceViewerSkeleton() {
36
79
  {/* Minimap strip: thin density lines tracing the same shape as the bars */}
37
80
  <div className="relative h-10 min-h-10 shrink-0 border-b border-gray-alpha-400">
38
81
  <div className="absolute inset-x-4 top-[6px]">
39
- {ROWS.map((row, index) => (
82
+ {ROWS.map((row) => (
40
83
  <Skeleton
41
84
  key={row.id}
42
- className="absolute h-[3px] rounded-full"
43
- style={{
44
- left: `${row.off}%`,
45
- width: `${row.bar}%`,
46
- top: index * 3.5,
47
- }}
85
+ className={`absolute h-[3px] rounded-full ${row.offsetClassName} ${row.barClassName} ${row.topClassName}`}
48
86
  />
49
87
  ))}
50
88
  </div>
51
89
  </div>
52
90
 
53
91
  {/* Header row: search header | divider | timeline header */}
54
- <div
55
- className="shrink-0 grid"
56
- style={{ gridTemplateColumns: COL_TEMPLATE }}
57
- >
92
+ <div className="grid shrink-0 grid-cols-[340px_1px_minmax(50px,1fr)]">
58
93
  <div className="h-10 min-h-10 flex items-center border-b border-gray-alpha-400 pl-4 pr-2 gap-1.5">
59
94
  <Skeleton className="w-3.5 h-3.5 shrink-0 rounded-sm" />
60
95
  <Skeleton className="h-3.5 w-40" />
@@ -70,10 +105,7 @@ export function TraceViewerSkeleton() {
70
105
  </div>
71
106
 
72
107
  {/* Content row: event list | gutter | timeline */}
73
- <div
74
- className="grid flex-1 min-h-0 overflow-hidden"
75
- style={{ gridTemplateColumns: COL_TEMPLATE }}
76
- >
108
+ <div className="grid min-h-0 flex-1 grid-cols-[340px_1px_minmax(50px,1fr)] overflow-hidden">
77
109
  {/* Sidebar event rows */}
78
110
  <div className="block overflow-visible">
79
111
  <ul className="block divide-y divide-gray-alpha-400 border-b border-gray-alpha-400">
@@ -81,10 +113,7 @@ export function TraceViewerSkeleton() {
81
113
  <li key={row.id} className="h-10 flex items-center pl-4 pr-2">
82
114
  <div className="flex min-w-0 flex-1 items-center gap-2">
83
115
  <Skeleton className="w-4 h-4 shrink-0 rounded-sm" />
84
- <Skeleton
85
- className="h-3.5"
86
- style={{ width: `${row.name}%` }}
87
- />
116
+ <Skeleton className={`h-3.5 ${row.nameClassName}`} />
88
117
  </div>
89
118
  <Skeleton className="ml-2 h-3.5 w-10 shrink-0" />
90
119
  </li>
@@ -101,8 +130,7 @@ export function TraceViewerSkeleton() {
101
130
  <div key={row.id} className="relative h-10">
102
131
  <div className="absolute inset-x-4 inset-y-0">
103
132
  <Skeleton
104
- className="absolute top-1/2 -translate-y-1/2 h-6 rounded-[0.25rem]"
105
- style={{ left: `${row.off}%`, width: `${row.bar}%` }}
133
+ className={`absolute top-1/2 h-6 -translate-y-1/2 rounded-[0.25rem] ${row.offsetClassName} ${row.barClassName}`}
106
134
  />
107
135
  </div>
108
136
  </div>