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

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 (32) hide show
  1. package/dist/components/index.d.ts +1 -0
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +2 -1
  4. package/dist/components/new-trace-viewer/components/event-list.d.ts +3 -1
  5. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -1
  6. package/dist/components/new-trace-viewer/components/event-list.js +8 -7
  7. package/dist/components/new-trace-viewer/components/timeline.d.ts +4 -1
  8. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
  9. package/dist/components/new-trace-viewer/components/timeline.js +6 -5
  10. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -1
  11. package/dist/components/new-trace-viewer/detail-panel.js +3 -2
  12. package/dist/components/new-trace-viewer/search.d.ts +19 -0
  13. package/dist/components/new-trace-viewer/search.d.ts.map +1 -0
  14. package/dist/components/new-trace-viewer/search.js +134 -0
  15. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
  16. package/dist/components/new-trace-viewer/trace-viewer.js +23 -18
  17. package/dist/components/ui/button.d.ts.map +1 -1
  18. package/dist/components/ui/button.js +10 -3
  19. package/dist/components/ui/icon-button.d.ts +13 -0
  20. package/dist/components/ui/icon-button.d.ts.map +1 -0
  21. package/dist/components/ui/icon-button.js +29 -0
  22. package/dist/styles.css +5 -0
  23. package/package.json +2 -2
  24. package/src/components/index.ts +1 -0
  25. package/src/components/new-trace-viewer/components/event-list.tsx +16 -5
  26. package/src/components/new-trace-viewer/components/timeline.tsx +11 -2
  27. package/src/components/new-trace-viewer/detail-panel.tsx +3 -6
  28. package/src/components/new-trace-viewer/search.ts +178 -0
  29. package/src/components/new-trace-viewer/trace-viewer.tsx +53 -46
  30. package/src/components/ui/button.tsx +9 -2
  31. package/src/components/ui/icon-button.tsx +57 -0
  32. package/src/styles.css +5 -0
@@ -13,6 +13,7 @@ import {
13
13
  import {
14
14
  type ReactNode,
15
15
  useCallback,
16
+ useDeferredValue,
16
17
  useEffect,
17
18
  useMemo,
18
19
  useRef,
@@ -27,15 +28,17 @@ import {
27
28
  import { useSidebarDataOptional } from '../sidebar/sidebar-data-context';
28
29
  import type { Trace } from '../trace-viewer/types';
29
30
  import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
31
+ import { IconButton } from '../ui/icon-button';
30
32
  import EventList from './components/event-list';
31
33
  import { SplitPane } from './components/split-pane';
32
34
  import {
35
+ TIMELINE_PADDING_PX,
33
36
  Timeline,
34
37
  TimelineHeader,
35
- TIMELINE_PADDING_PX,
36
38
  } from './components/timeline';
37
39
  import { ActiveSpanProvider, useActiveSpan } from './context';
38
40
  import { DetailPanel } from './detail-panel';
41
+ import { searchSpans } from './search';
39
42
  import { computeRootBounds, computeTimeMarkers } from './utils';
40
43
 
41
44
  interface NewTraceViewerProps {
@@ -142,7 +145,7 @@ function useSelectedSpanInfo(): SelectedSpanInfo | null {
142
145
  spanId: activeSpan.spanId,
143
146
  rawEvents,
144
147
  };
145
- }, [activeSpan, sidebar?.events]);
148
+ }, [activeSpan, sidebar]);
146
149
  }
147
150
 
148
151
  // ---------------------------------------------------------------------------
@@ -165,15 +168,12 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
165
168
  const selectedSpan = useSelectedSpanInfo();
166
169
 
167
170
  const [searchQuery, setSearchQuery] = useState('');
171
+ const deferredSearchQuery = useDeferredValue(searchQuery);
168
172
 
169
- const filteredSpans = useMemo(() => {
170
- const q = searchQuery.trim().toLowerCase();
171
- if (!q) return trace.spans;
172
- return trace.spans.filter(
173
- (s) =>
174
- s.name.toLowerCase().includes(q) || s.resource.toLowerCase().includes(q)
175
- );
176
- }, [trace.spans, searchQuery]);
173
+ const searchResult = useMemo(
174
+ () => searchSpans(trace.spans, deferredSearchQuery),
175
+ [trace.spans, deferredSearchQuery]
176
+ );
177
177
 
178
178
  const root = useMemo(() => computeRootBounds(trace.spans), [trace.spans]);
179
179
 
@@ -204,7 +204,7 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
204
204
  });
205
205
 
206
206
  prevRootRef.current = { start: newStart, end: newEnd };
207
- }, [root.startTime, root.duration]);
207
+ }, [root.startTime, root.duration, setViewport]);
208
208
 
209
209
  const viewDuration = viewport.end - viewport.start;
210
210
 
@@ -420,7 +420,8 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
420
420
  (e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
421
421
  )
422
422
  );
423
- const scaleFactor = Math.pow(2, dy / 200);
423
+ const isMouseWheel = e.deltaMode === 1 || Math.abs(e.deltaY) >= 50;
424
+ const scaleFactor = Math.pow(2, dy / (isMouseWheel ? 200 : 60));
424
425
 
425
426
  setViewport((prev) => {
426
427
  const prevDuration = prev.end - prev.start;
@@ -471,7 +472,7 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
471
472
 
472
473
  el.addEventListener('wheel', onWheel, { passive: false });
473
474
  return () => el.removeEventListener('wheel', onWheel);
474
- }, [root.startTime, root.duration]);
475
+ }, [root.startTime, root.duration, setViewport]);
475
476
 
476
477
  // Derive the selected span name and metadata for the panel header
477
478
  const selectedSpanName = useMemo(() => {
@@ -495,13 +496,13 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
495
496
 
496
497
  const { prevSpanId, nextSpanId } = useMemo(() => {
497
498
  if (!activeSpanId) return { prevSpanId: null, nextSpanId: null };
498
- const i = filteredSpans.findIndex((s) => s.spanId === activeSpanId);
499
+ const i = trace.spans.findIndex((s) => s.spanId === activeSpanId);
499
500
  if (i === -1) return { prevSpanId: null, nextSpanId: null };
500
501
  return {
501
- prevSpanId: filteredSpans[i - 1]?.spanId ?? null,
502
- nextSpanId: filteredSpans[i + 1]?.spanId ?? null,
502
+ prevSpanId: trace.spans[i - 1]?.spanId ?? null,
503
+ nextSpanId: trace.spans[i + 1]?.spanId ?? null,
503
504
  };
504
- }, [activeSpanId, filteredSpans]);
505
+ }, [activeSpanId, trace.spans]);
505
506
 
506
507
  const handleSelectPrevSpan = useCallback(() => {
507
508
  if (prevSpanId) handleSelectSpan(prevSpanId);
@@ -538,6 +539,13 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
538
539
  type="text"
539
540
  value={searchQuery}
540
541
  onChange={(e) => setSearchQuery(e.target.value)}
542
+ onKeyDown={(e) => {
543
+ if (e.key === 'Escape' && searchQuery) {
544
+ e.preventDefault();
545
+ e.stopPropagation();
546
+ setSearchQuery('');
547
+ }
548
+ }}
541
549
  placeholder="Search spans..."
542
550
  aria-label="Search spans"
543
551
  className="flex-1 min-w-0 bg-transparent text-sm text-gray-1000 placeholder:text-gray-800 outline-none"
@@ -547,9 +555,11 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
547
555
  type="button"
548
556
  aria-label="Clear search"
549
557
  onClick={() => setSearchQuery('')}
550
- className="shrink-0 p-0.5 rounded-sm text-gray-800 hover:text-gray-1000 hover:bg-gray-200 transition-colors"
558
+ className="-mr-2 hidden h-full max-w-full shrink-0 cursor-pointer items-center rounded-r-md border-0 bg-transparent px-2.5 font-inherit text-base text-gray-900 no-underline transition-colors duration-150 ease-in hover:text-gray-1000 focus-visible:-outline-offset-1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--ds-focus-color)] min-[961px]:flex"
551
559
  >
552
- <X className="w-3 h-3" />
560
+ <kbd className="inline-flex h-5 min-h-5 min-w-5 items-center justify-center rounded border border-gray-alpha-400 bg-background-100 px-1 font-sans text-[13px] font-medium leading-[1.7em] text-gray-900">
561
+ Esc
562
+ </kbd>
553
563
  </button>
554
564
  )}
555
565
  </div>
@@ -560,11 +570,13 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
560
570
  >
561
571
  <div className="block overflow-visible">
562
572
  <EventList
563
- spans={filteredSpans}
573
+ spans={trace.spans}
564
574
  activeSpanId={activeSpanId}
575
+ searchResult={searchResult}
565
576
  onSelectSpan={handleSelectSpan}
566
577
  />
567
578
  </div>
579
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: timeline hover and wheel gestures are pointer-only annotations */}
568
580
  <div
569
581
  ref={timelineRef}
570
582
  className="block min-h-0 overflow-visible relative"
@@ -573,42 +585,43 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
573
585
  onMouseLeave={handleTimelineMouseLeave}
574
586
  >
575
587
  <Timeline
576
- spans={filteredSpans}
588
+ spans={trace.spans}
577
589
  viewStart={viewport.start}
578
590
  viewEnd={viewport.end}
579
591
  markers={timeMarkers}
580
592
  selectedId={activeSpanId}
593
+ searchResult={searchResult}
581
594
  onSelect={handleSelectSpan}
582
595
  hoverFraction={hoverFraction}
583
596
  altHeld={altHeld}
584
597
  />
585
598
  </div>
586
599
  </SplitPane>
587
- <div className="absolute right-3 bottom-3 z-[5] flex items-center border border-gray-alpha-400 rounded-lg bg-background-100 shadow-sm overflow-hidden divide-x divide-gray-alpha-400">
588
- <button
589
- type="button"
590
- className="flex items-center justify-center w-8 h-8 text-gray-900 cursor-pointer transition-colors duration-[time:120ms] ease-in-out hover:text-gray-1000 hover:bg-gray-alpha-100"
600
+ <div className="absolute right-3 bottom-3 z-[5] flex items-center border border-gray-alpha-400 rounded-md bg-background-100 shadow-sm overflow-hidden divide-x divide-gray-alpha-400">
601
+ <IconButton
602
+ variant="muted"
603
+ size="small"
591
604
  onClick={zoomOut}
592
605
  aria-label="Zoom out"
593
606
  >
594
607
  <ZoomOut className="w-4 h-4" />
595
- </button>
596
- <button
597
- type="button"
598
- className="flex items-center justify-center w-8 h-8 text-gray-900 cursor-pointer transition-colors duration-[time:120ms] ease-in-out hover:text-gray-1000 hover:bg-gray-alpha-100"
608
+ </IconButton>
609
+ <IconButton
610
+ variant="muted"
611
+ size="small"
599
612
  onClick={resetZoom}
600
613
  aria-label="Reset zoom"
601
614
  >
602
615
  <RotateCcw className="w-3.5 h-3.5" />
603
- </button>
604
- <button
605
- type="button"
606
- className="flex items-center justify-center w-8 h-8 text-gray-900 cursor-pointer transition-colors duration-[time:120ms] ease-in-out hover:text-gray-1000 hover:bg-gray-alpha-100"
616
+ </IconButton>
617
+ <IconButton
618
+ variant="muted"
619
+ size="small"
607
620
  onClick={zoomIn}
608
621
  aria-label="Zoom in"
609
622
  >
610
623
  <ZoomIn className="w-4 h-4" />
611
- </button>
624
+ </IconButton>
612
625
  </div>
613
626
  </div>
614
627
 
@@ -621,36 +634,30 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
621
634
  {selectedSpanName}
622
635
  </span>
623
636
  <div className="flex items-center gap-0.5 shrink-0">
624
- <button
625
- type="button"
637
+ <IconButton
626
638
  aria-label="Navigate to previous span"
627
639
  aria-keyshortcuts="K"
628
640
  onClick={handleSelectPrevSpan}
629
641
  disabled={!prevSpanId}
630
- className="p-1 rounded text-gray-1000 transition-colors enabled:hover:bg-gray-alpha-100 disabled:opacity-40 disabled:cursor-not-allowed"
631
642
  >
632
643
  <ChevronUp className="w-4 h-4" />
633
- </button>
634
- <button
635
- type="button"
644
+ </IconButton>
645
+ <IconButton
636
646
  aria-label="Navigate to next span"
637
647
  aria-keyshortcuts="J"
638
648
  onClick={handleSelectNextSpan}
639
649
  disabled={!nextSpanId}
640
- className="p-1 rounded text-gray-1000 transition-colors enabled:hover:bg-gray-alpha-100 disabled:opacity-40 disabled:cursor-not-allowed"
641
650
  >
642
651
  <ChevronDown className="w-4 h-4" />
643
- </button>
652
+ </IconButton>
644
653
  <div aria-hidden className="w-px h-4 bg-gray-alpha-400 mx-1" />
645
- <button
646
- type="button"
654
+ <IconButton
647
655
  aria-label="Close span details"
648
656
  aria-keyshortcuts="Escape"
649
- className="p-1 rounded text-gray-1000 hover:bg-gray-alpha-100 transition-colors"
650
657
  onClick={clearActiveSpan}
651
658
  >
652
659
  <X className="w-4 h-4" />
653
- </button>
660
+ </IconButton>
654
661
  </div>
655
662
  </div>
656
663
  {/* Panel body */}
@@ -10,7 +10,14 @@ const buttonVariants = cva(
10
10
  '!text-gray-100',
11
11
  'bg-gray-1000',
12
12
  'transition-[border-color,background,color,transform,box-shadow] duration-150 ease-in-out',
13
- 'hover:bg-[var(--themed-hover-bg,_hsl(0,_0%,_22%))] dark-theme:hover:bg-[var(--themed-hover-bg,_hsl(0,_0%,_80%))]',
13
+ // Geist invert hover: literal fallbacks (no token dep, so it resolves in any
14
+ // consuming app), darkened via ancestor theme class without a registered variant.
15
+ 'hover:bg-[var(--themed-hover-bg,_hsl(0,_0%,_22%))]',
16
+ '[.dark-theme_&]:hover:bg-[var(--themed-hover-bg,_hsl(0,_0%,_80%))]',
17
+ '[[data-theme=dark]_&]:hover:bg-[var(--themed-hover-bg,_hsl(0,_0%,_80%))]',
18
+ // Geist focus ring as arbitrary properties (no bare `outline`/ambiguous
19
+ // arbitrary-color utilities, which differ between Tailwind v3 and v4).
20
+ 'focus-visible:[outline:2px_solid_var(--ds-focus-color)] focus-visible:[outline-offset:2px]',
14
21
  // disabled styles
15
22
  'disabled:cursor-not-allowed aria-disabled:cursor-not-allowed',
16
23
  'disabled:bg-gray-100 disabled:!text-gray-700 disabled:hover:bg-gray-100',
@@ -28,7 +35,7 @@ const buttonVariants = cva(
28
35
  size: {
29
36
  default: 'h-10 px-4 text-[14px]',
30
37
  sm: 'h-8 px-3 text-[14px]',
31
- xs: 'h-6 px-1.5 py-0.5 text-button-12',
38
+ xs: 'h-6 px-1.5 py-0.5 text-button-12 rounded-[4px]',
32
39
  icon: 'h-8 w-8',
33
40
  },
34
41
  },
@@ -0,0 +1,57 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import type * as React from 'react';
3
+ import { cn } from '../../lib/utils';
4
+
5
+ const iconButtonVariants = cva(
6
+ [
7
+ 'm-0 inline-flex shrink-0 appearance-none items-center justify-center border-0 bg-transparent p-0 align-baseline font-inherit no-underline [background:none] [-webkit-appearance:none] [-webkit-tap-highlight-color:transparent]',
8
+ 'transition-colors duration-150 ease-in-out',
9
+ 'enabled:cursor-pointer disabled:cursor-not-allowed disabled:opacity-40',
10
+ 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--ds-focus-color)] focus-visible:outline-offset-2',
11
+ ],
12
+ {
13
+ variants: {
14
+ variant: {
15
+ tertiary:
16
+ 'text-gray-900 enabled:hover:bg-gray-alpha-200 enabled:hover:text-gray-1000 enabled:active:bg-gray-alpha-200 focus-visible:text-gray-1000',
17
+ muted:
18
+ 'text-gray-900 enabled:hover:bg-gray-alpha-200 enabled:hover:text-gray-1000 enabled:active:bg-gray-alpha-200 focus-visible:text-gray-1000',
19
+ },
20
+ size: {
21
+ tiny: 'h-6 w-6 rounded-[4px] [&_svg]:h-4 [&_svg]:w-4',
22
+ small: 'h-8 w-8 rounded-md [&_svg]:h-4 [&_svg]:w-4',
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ variant: 'tertiary',
27
+ size: 'tiny',
28
+ },
29
+ }
30
+ );
31
+
32
+ export type IconButtonProps = Omit<
33
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
34
+ 'type'
35
+ > &
36
+ VariantProps<typeof iconButtonVariants> & {
37
+ 'aria-label': string;
38
+ type?: 'button' | 'submit' | 'reset';
39
+ };
40
+
41
+ export function IconButton({
42
+ className,
43
+ variant,
44
+ size,
45
+ type = 'button',
46
+ ...props
47
+ }: IconButtonProps) {
48
+ return (
49
+ <button
50
+ type={type}
51
+ className={cn(iconButtonVariants({ variant, size, className }))}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ export { iconButtonVariants };
package/src/styles.css CHANGED
@@ -36,6 +36,9 @@
36
36
  --ds-gray-alpha-700: hsla(0, 0%, 0%, 0.44);
37
37
  --ds-gray-alpha-1000: hsla(0, 0%, 0%, 0.91);
38
38
 
39
+ /* Focus */
40
+ --ds-focus-color: var(--ds-blue-700);
41
+
39
42
  /* Blue */
40
43
  --ds-blue-100: hsl(212, 100%, 97%);
41
44
  --ds-blue-200: hsl(210, 100%, 96%);
@@ -126,6 +129,8 @@
126
129
  --ds-gray-alpha-700: rgba(255, 255, 255, 0.45);
127
130
  --ds-gray-alpha-1000: rgba(255, 255, 255, 0.92);
128
131
 
132
+ --ds-focus-color: var(--ds-blue-900);
133
+
129
134
  --ds-blue-100: hsl(216, 50%, 12%);
130
135
  --ds-blue-200: hsl(214, 59%, 15%);
131
136
  --ds-blue-300: hsl(213, 71%, 20%);