@workflow/web-shared 4.1.0-beta.63 → 4.1.0-beta.64

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.
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Copy } from 'lucide-react';
3
+ import { AlertCircle, Copy } from 'lucide-react';
4
4
  import { toast } from 'sonner';
5
5
 
6
6
  /**
@@ -19,10 +19,9 @@ export function isStructuredErrorWithStack(
19
19
  }
20
20
 
21
21
  /**
22
- * Renders an error with a `stack` field as readable pre-formatted text,
23
- * styled to match the CopyableDataBlock component. The error message is
24
- * displayed at the top with a visual separator from the stack trace.
25
- * The entire block is copyable via a copy button.
22
+ * Renders an error with a `stack` field as a visually distinct error block.
23
+ * Shows the error message with an alert icon at the top, separated from
24
+ * the stack trace below.
26
25
  */
27
26
  export function ErrorStackBlock({
28
27
  value,
@@ -35,15 +34,22 @@ export function ErrorStackBlock({
35
34
 
36
35
  return (
37
36
  <div
38
- className="relative overflow-x-auto rounded-md border p-3 pt-9"
39
- style={{ borderColor: 'var(--ds-gray-300)' }}
37
+ className="relative overflow-hidden rounded-md border"
38
+ style={{
39
+ borderColor: 'var(--ds-red-400)',
40
+ background: 'var(--ds-red-100)',
41
+ }}
40
42
  >
41
43
  <button
42
44
  type="button"
43
45
  aria-label="Copy error"
44
46
  title="Copy"
45
- className="!absolute !right-2 !top-2 !flex !h-6 !w-6 !items-center !justify-center !rounded-md !border !bg-[var(--ds-background-100)] !text-[var(--ds-gray-800)] transition-transform transition-colors duration-100 hover:!bg-[var(--ds-gray-alpha-200)] active:!scale-95 active:!bg-[var(--ds-gray-alpha-300)]"
46
- style={{ borderColor: 'var(--ds-gray-300)' }}
47
+ className="!absolute !right-2 !top-2 !flex !h-6 !w-6 !items-center !justify-center !rounded-md !border transition-transform transition-colors duration-100 hover:!bg-[var(--ds-red-200)] active:!scale-95"
48
+ style={{
49
+ borderColor: 'var(--ds-red-400)',
50
+ background: 'var(--ds-red-100)',
51
+ color: 'var(--ds-red-900)',
52
+ }}
47
53
  onClick={() => {
48
54
  navigator.clipboard
49
55
  .writeText(copyText)
@@ -59,19 +65,23 @@ export function ErrorStackBlock({
59
65
  </button>
60
66
 
61
67
  {message && (
62
- <p
63
- className="pb-2 mb-2 text-xs font-semibold font-mono"
68
+ <div
69
+ className="flex items-start gap-2 px-3 py-2.5 pr-10"
64
70
  style={{
65
71
  color: 'var(--ds-red-900)',
66
- borderBottom: '1px solid var(--ds-gray-300)',
72
+ borderBottom: '1px solid var(--ds-red-400)',
67
73
  }}
68
74
  >
69
- {message}
70
- </p>
75
+ <AlertCircle className="h-4 w-4 shrink-0" style={{ marginTop: 1 }} />
76
+ <p className="text-xs font-semibold m-0 break-words">{message}</p>
77
+ </div>
71
78
  )}
72
79
  <pre
73
- className="text-xs font-mono whitespace-pre-wrap break-words overflow-auto m-0"
74
- style={{ color: 'var(--ds-gray-1000)' }}
80
+ className="px-3 py-2.5 text-xs font-mono whitespace-pre-wrap break-words overflow-auto m-0"
81
+ style={{
82
+ color: 'var(--ds-red-900)',
83
+ background: 'var(--ds-red-200)',
84
+ }}
75
85
  >
76
86
  {stack}
77
87
  </pre>
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+
5
+ export interface MenuDropdownOption<T extends string = string> {
6
+ value: T;
7
+ label: string;
8
+ }
9
+
10
+ interface MenuDropdownProps<T extends string = string> {
11
+ options: MenuDropdownOption<T>[];
12
+ value: T;
13
+ onChange: (value: T) => void;
14
+ }
15
+
16
+ /**
17
+ * A dropdown menu that matches Geist's MenuButton (secondary) + Menu styling.
18
+ * Uses CSS classes with proper :hover specificity (no inline background).
19
+ */
20
+ export function MenuDropdown<T extends string = string>({
21
+ options,
22
+ value,
23
+ onChange,
24
+ }: MenuDropdownProps<T>) {
25
+ const [open, setOpen] = useState(false);
26
+ const ref = useRef<HTMLDivElement>(null);
27
+ const label =
28
+ options.find((o) => o.value === value)?.label ?? options[0]?.label ?? '';
29
+
30
+ useEffect(() => {
31
+ if (!open) return;
32
+ function handleClickOutside(e: MouseEvent) {
33
+ if (ref.current && !ref.current.contains(e.target as Node)) {
34
+ setOpen(false);
35
+ }
36
+ }
37
+ document.addEventListener('mousedown', handleClickOutside);
38
+ return () => document.removeEventListener('mousedown', handleClickOutside);
39
+ }, [open]);
40
+
41
+ return (
42
+ <div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
43
+ <style>{`
44
+ .wf-menu-btn{appearance:none;-webkit-appearance:none;border:none;display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 12px;border-radius:6px;font-size:14px;font-weight:500;line-height:20px;color:var(--ds-gray-1000);background:var(--ds-background-100);box-shadow:0 0 0 1px var(--ds-gray-400);cursor:pointer;white-space:nowrap;transition:background 150ms}
45
+ .wf-menu-btn:hover{background:var(--ds-gray-alpha-200)}
46
+ .wf-menu-item{appearance:none;-webkit-appearance:none;border:none;display:flex;align-items:center;width:100%;height:40px;padding:0 8px;border-radius:6px;font-size:14px;color:var(--ds-gray-1000);background:transparent;cursor:pointer;transition:background 150ms}
47
+ .wf-menu-item:hover{background:var(--ds-gray-alpha-100)}
48
+ `}</style>
49
+
50
+ <button
51
+ type="button"
52
+ className="wf-menu-btn"
53
+ onClick={() => setOpen(!open)}
54
+ >
55
+ <span>{label}</span>
56
+ <svg
57
+ width={16}
58
+ height={16}
59
+ viewBox="0 0 16 16"
60
+ fill="none"
61
+ style={{
62
+ marginLeft: 16,
63
+ marginRight: -4,
64
+ color: 'var(--ds-gray-900)',
65
+ }}
66
+ >
67
+ <path
68
+ d="M4.5 6L8 9.5L11.5 6"
69
+ stroke="currentColor"
70
+ strokeWidth="1.5"
71
+ strokeLinecap="round"
72
+ strokeLinejoin="round"
73
+ />
74
+ </svg>
75
+ </button>
76
+
77
+ {open && (
78
+ <div
79
+ style={{
80
+ position: 'absolute',
81
+ right: 0,
82
+ top: '100%',
83
+ marginTop: 4,
84
+ minWidth: 140,
85
+ padding: 4,
86
+ borderRadius: 12,
87
+ background: 'var(--ds-background-100)',
88
+ boxShadow: 'var(--ds-shadow-menu, var(--ds-shadow-medium))',
89
+ zIndex: 2001,
90
+ }}
91
+ role="menu"
92
+ >
93
+ {options.map((option) => (
94
+ <button
95
+ key={option.value}
96
+ type="button"
97
+ role="menuitem"
98
+ className="wf-menu-item"
99
+ style={{
100
+ fontWeight: option.value === value ? 500 : 400,
101
+ }}
102
+ onClick={() => {
103
+ onChange(option.value);
104
+ setOpen(false);
105
+ }}
106
+ >
107
+ {option.label}
108
+ </button>
109
+ ))}
110
+ </div>
111
+ )}
112
+ </div>
113
+ );
114
+ }
@@ -706,12 +706,14 @@ function PanelResizeHandle({
706
706
  function TraceViewerFooter({
707
707
  hasMore,
708
708
  isLive,
709
+ isInitialLoading,
709
710
  }: {
710
711
  hasMore: boolean;
711
712
  isLive: boolean;
713
+ isInitialLoading: boolean;
712
714
  }): ReactNode {
713
715
  const style = { color: 'var(--ds-gray-900)' };
714
- if (hasMore) {
716
+ if (hasMore || isInitialLoading) {
715
717
  return (
716
718
  <div
717
719
  className="flex items-center justify-center gap-2 py-3 text-xs"
@@ -997,9 +999,13 @@ export const WorkflowTraceViewer = ({
997
999
  isLive={isLive}
998
1000
  trace={trace}
999
1001
  knownDurationMs={traceWithMeta?.knownDurationMs}
1000
- hasMoreData={hasMoreSpans}
1002
+ hasMoreData={hasMoreSpans || Boolean(isLoading)}
1001
1003
  footer={
1002
- <TraceViewerFooter hasMore={hasMoreSpans} isLive={isLive} />
1004
+ <TraceViewerFooter
1005
+ hasMore={hasMoreSpans}
1006
+ isLive={isLive}
1007
+ isInitialLoading={Boolean(isLoading)}
1008
+ />
1003
1009
  }
1004
1010
  />
1005
1011
  </TraceViewerWithContextMenu>