@principal-ai/principal-view-react 0.7.9 → 0.7.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.
@@ -0,0 +1,287 @@
1
+ import React, { useState } from 'react';
2
+ import { useTheme } from '@principal-ade/industry-theme';
3
+ import { HelpCircle } from 'lucide-react';
4
+
5
+ interface SpanEvent {
6
+ time: number;
7
+ name: string;
8
+ attributes: Record<string, string | number | boolean>;
9
+ }
10
+
11
+ interface TestSpan {
12
+ id: string;
13
+ name: string;
14
+ startTime: number;
15
+ endTime?: number;
16
+ duration?: number;
17
+ attributes: Record<string, string | number | boolean>;
18
+ events: SpanEvent[];
19
+ status: 'OK' | 'ERROR';
20
+ errorMessage?: string;
21
+ }
22
+
23
+ export interface TestEventPanelProps {
24
+ spans: TestSpan[];
25
+ currentSpanIndex: number;
26
+ currentEventIndex: number;
27
+ highlightedPhase?: string; // 'setup' | 'execution' | 'assertion'
28
+ }
29
+
30
+ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
31
+ spans,
32
+ currentSpanIndex,
33
+ currentEventIndex,
34
+ highlightedPhase,
35
+ }) => {
36
+ const { theme } = useTheme();
37
+ const [showHelp, setShowHelp] = useState(false);
38
+ const currentSpan = spans[currentSpanIndex];
39
+ const eventsUpToNow = currentSpan?.events.slice(0, currentEventIndex + 1) || [];
40
+
41
+ return (
42
+ <div
43
+ style={{
44
+ width: '100%',
45
+ height: '100%',
46
+ backgroundColor: theme.colors.background,
47
+ color: theme.colors.text,
48
+ padding: '20px',
49
+ fontFamily: theme.fonts.monospace,
50
+ fontSize: '14px',
51
+ overflow: 'auto',
52
+ boxSizing: 'border-box',
53
+ }}
54
+ >
55
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '15px' }}>
56
+ <div style={{ fontWeight: 'bold', fontSize: '18px' }}>
57
+ Wide Event Pattern - Code Journey
58
+ </div>
59
+ <button
60
+ onClick={() => setShowHelp(true)}
61
+ style={{
62
+ background: 'transparent',
63
+ border: 'none',
64
+ cursor: 'pointer',
65
+ padding: '4px',
66
+ display: 'flex',
67
+ alignItems: 'center',
68
+ color: theme.colors.textMuted,
69
+ }}
70
+ onMouseEnter={(e) => {
71
+ e.currentTarget.style.color = theme.colors.text;
72
+ }}
73
+ onMouseLeave={(e) => {
74
+ e.currentTarget.style.color = theme.colors.textMuted;
75
+ }}
76
+ >
77
+ <HelpCircle size={20} />
78
+ </button>
79
+ </div>
80
+ <div style={{ fontSize: '13px', color: theme.colors.textMuted, marginBottom: '15px' }}>
81
+ Test: {currentSpan?.name || 'Loading...'}
82
+ </div>
83
+
84
+ {/* Help Modal */}
85
+ {showHelp && (
86
+ <div
87
+ style={{
88
+ position: 'fixed',
89
+ top: 0,
90
+ left: 0,
91
+ right: 0,
92
+ bottom: 0,
93
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
94
+ display: 'flex',
95
+ alignItems: 'center',
96
+ justifyContent: 'center',
97
+ zIndex: 9999,
98
+ }}
99
+ onClick={() => setShowHelp(false)}
100
+ >
101
+ <div
102
+ style={{
103
+ backgroundColor: theme.colors.background,
104
+ color: theme.colors.text,
105
+ padding: '24px',
106
+ borderRadius: '8px',
107
+ maxWidth: '600px',
108
+ border: `1px solid ${theme.colors.border}`,
109
+ }}
110
+ onClick={(e) => e.stopPropagation()}
111
+ >
112
+ <div style={{ fontWeight: 'bold', fontSize: '18px', marginBottom: '16px' }}>
113
+ How to Read This Panel
114
+ </div>
115
+ <div style={{ fontSize: '14px', marginBottom: '16px', lineHeight: '1.6' }}>
116
+ <p style={{ marginBottom: '12px' }}>
117
+ <strong>Watch how execution flows through files:</strong>
118
+ </p>
119
+ <ul style={{ marginLeft: '20px', marginBottom: '16px' }}>
120
+ <li style={{ marginBottom: '8px' }}>
121
+ <span style={{ color: '#60a5fa' }}>Blue = Test file</span>
122
+ </li>
123
+ <li>
124
+ <span style={{ color: '#4ade80' }}>Green → Code under test</span>
125
+ </li>
126
+ </ul>
127
+ <p style={{ marginBottom: '12px' }}>
128
+ <strong>Span Context (Static)</strong>
129
+ </p>
130
+ <pre
131
+ style={{
132
+ background: theme.colors.surface,
133
+ padding: '12px',
134
+ borderRadius: '4px',
135
+ fontSize: '13px',
136
+ overflow: 'auto',
137
+ }}
138
+ >
139
+ {`{
140
+ "test.file": "GraphConverter.test.ts",
141
+ "test.suite": "GraphConverter",
142
+ "test.result": "pass"
143
+ }`}
144
+ </pre>
145
+ </div>
146
+ <button
147
+ onClick={() => setShowHelp(false)}
148
+ style={{
149
+ padding: '8px 16px',
150
+ backgroundColor: theme.colors.primary,
151
+ color: theme.colors.background,
152
+ border: 'none',
153
+ borderRadius: '4px',
154
+ cursor: 'pointer',
155
+ fontSize: '14px',
156
+ fontWeight: 500,
157
+ }}
158
+ >
159
+ Got it
160
+ </button>
161
+ </div>
162
+ </div>
163
+ )}
164
+
165
+ {currentSpan && (
166
+ <>
167
+ {/* Event Timeline (context mutations) */}
168
+ <div>
169
+ <div
170
+ style={{
171
+ color: '#4ade80',
172
+ fontWeight: 'bold',
173
+ marginBottom: '8px',
174
+ fontSize: '15px',
175
+ }}
176
+ >
177
+ Event Timeline (Context Mutations)
178
+ </div>
179
+ {eventsUpToNow.map((event, idx) => {
180
+ const filepath = event.attributes['code.filepath'] as string;
181
+ const lineno = event.attributes['code.lineno'] as number;
182
+ const isCodeUnderTest = filepath && filepath !== 'GraphConverter.test.ts';
183
+
184
+ // Determine which phase this event belongs to
185
+ const eventPhase = event.name.split('.')[0]; // 'setup', 'execution', 'assertion'
186
+ const isHighlighted = highlightedPhase === eventPhase;
187
+
188
+ return (
189
+ <div
190
+ key={idx}
191
+ style={{
192
+ marginBottom: '12px',
193
+ paddingBottom: '12px',
194
+ borderBottom: idx < eventsUpToNow.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
195
+ opacity: highlightedPhase && !isHighlighted ? 0.4 : 1,
196
+ transition: 'opacity 0.2s ease',
197
+ transform: isHighlighted ? 'scale(1.02)' : 'scale(1)',
198
+ backgroundColor: isHighlighted ? theme.colors.surface : 'transparent',
199
+ padding: isHighlighted ? '8px' : '0',
200
+ borderRadius: '4px',
201
+ }}
202
+ >
203
+ <div
204
+ style={{
205
+ display: 'flex',
206
+ justifyContent: 'space-between',
207
+ alignItems: 'center',
208
+ marginBottom: '4px',
209
+ gap: '8px',
210
+ }}
211
+ >
212
+ <div style={{ color: '#f59e0b', fontSize: '13px', flexShrink: 0 }}>
213
+ {idx + 1}. {event.name}
214
+ </div>
215
+ {filepath && (
216
+ <div
217
+ style={{
218
+ fontSize: '12px',
219
+ color: isCodeUnderTest ? '#4ade80' : '#60a5fa',
220
+ fontFamily: 'monospace',
221
+ background: isCodeUnderTest ? '#064e3b' : '#1e3a8a',
222
+ padding: '2px 6px',
223
+ borderRadius: '3px',
224
+ flexShrink: 1,
225
+ minWidth: 0,
226
+ overflow: 'hidden',
227
+ textOverflow: 'ellipsis',
228
+ whiteSpace: 'nowrap',
229
+ }}
230
+ >
231
+ {isCodeUnderTest && '→ '}
232
+ {filepath}:{lineno}
233
+ </div>
234
+ )}
235
+ </div>
236
+ <pre
237
+ style={{
238
+ background: theme.colors.surface,
239
+ padding: '8px',
240
+ borderRadius: '4px',
241
+ margin: 0,
242
+ fontSize: '12px',
243
+ lineHeight: '1.4',
244
+ overflow: 'auto',
245
+ maxWidth: '100%',
246
+ }}
247
+ >
248
+ {JSON.stringify(
249
+ Object.fromEntries(
250
+ Object.entries(event.attributes).filter(
251
+ ([key]) => key !== 'code.filepath' && key !== 'code.lineno'
252
+ )
253
+ ),
254
+ null,
255
+ 2
256
+ )}
257
+ </pre>
258
+ </div>
259
+ );
260
+ })}
261
+ </div>
262
+ </>
263
+ )}
264
+
265
+ <div
266
+ style={{
267
+ marginTop: '20px',
268
+ paddingTop: '15px',
269
+ borderTop: `1px solid ${theme.colors.border}`,
270
+ fontSize: '13px',
271
+ color: theme.colors.textMuted,
272
+ }}
273
+ >
274
+ <div style={{ marginBottom: '8px' }}>
275
+ <strong>Total tests:</strong> {spans.length}
276
+ </div>
277
+ <div style={{ marginBottom: '8px' }}>
278
+ <strong>Pattern:</strong> One span per test + event timeline
279
+ </div>
280
+ <div>
281
+ <strong>Status:</strong>{' '}
282
+ <span style={{ color: '#4ade80' }}>All Passed ✓</span>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ );
287
+ };
package/src/index.ts CHANGED
@@ -52,6 +52,9 @@ export type { NodeInfoPanelProps } from './components/NodeInfoPanel';
52
52
  export { ConfigurationSelector } from './components/ConfigurationSelector';
53
53
  export type { ConfigurationSelectorProps } from './components/ConfigurationSelector';
54
54
 
55
+ export { TestEventPanel } from './components/TestEventPanel';
56
+ export type { TestEventPanelProps } from './components/TestEventPanel';
57
+
55
58
  // Export node/edge renderers
56
59
  export { GenericNode } from './nodes/GenericNode';
57
60
  export type { GenericNodeProps } from './nodes/GenericNode';
@@ -6,6 +6,32 @@ import { resolveIcon } from '../utils/iconResolver';
6
6
  import { NodeTooltip } from '../components/NodeTooltip';
7
7
  import type { OtelInfo } from '../components/NodeTooltip';
8
8
 
9
+ /**
10
+ * Converts a hex color to a lighter/tinted version (opaque, not transparent)
11
+ * @param hexColor - Hex color string (e.g., "#FF5733" or "#888")
12
+ * @param lightness - How much to lighten (0-1), defaults to 0.88 (88% white mixed in)
13
+ * @returns hex color string
14
+ */
15
+ function hexToLightColor(hexColor: string, lightness = 0.88): string {
16
+ // Remove # if present
17
+ const hex = hexColor.replace('#', '');
18
+
19
+ // Parse hex to RGB
20
+ const r = parseInt(hex.substring(0, 2), 16);
21
+ const g = parseInt(hex.substring(2, 4), 16);
22
+ const b = parseInt(hex.substring(4, 6), 16);
23
+
24
+ // Mix with white based on lightness factor
25
+ // lightness of 0.88 means 88% white + 12% original color
26
+ const newR = Math.round(r + (255 - r) * lightness);
27
+ const newG = Math.round(g + (255 - g) * lightness);
28
+ const newB = Math.round(b + (255 - b) * lightness);
29
+
30
+ // Convert back to hex
31
+ const toHex = (n: number) => n.toString(16).padStart(2, '0');
32
+ return `#${toHex(newR)}${toHex(newG)}${toHex(newB)}`;
33
+ }
34
+
9
35
  export interface CustomNodeData extends Record<string, unknown> {
10
36
  name: string;
11
37
  typeDefinition: NodeTypeDefinition;
@@ -19,6 +45,8 @@ export interface CustomNodeData extends Record<string, unknown> {
19
45
  editable?: boolean;
20
46
  // Whether tooltips are enabled (defaults to true)
21
47
  tooltipsEnabled?: boolean;
48
+ // Whether this node is highlighted (e.g., during execution playback)
49
+ isHighlighted?: boolean;
22
50
  }
23
51
 
24
52
  /**
@@ -41,6 +69,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
41
69
  animationDuration = 1000,
42
70
  editable = false,
43
71
  tooltipsEnabled = true,
72
+ isHighlighted = false,
44
73
  } = nodeProps;
45
74
 
46
75
  // Extract OTEL info and description for tooltip
@@ -155,7 +184,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
155
184
  const getShapeStyles = () => {
156
185
  const baseStyles = {
157
186
  padding: '12px 16px',
158
- backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' : 'white',
187
+ backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' : hexToLightColor(fillColor),
159
188
  color: '#000',
160
189
  border: `2px solid ${hasViolations ? '#D0021B' : strokeColor}`,
161
190
  fontSize: '12px',
@@ -171,7 +200,11 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
171
200
  alignItems: 'center',
172
201
  justifyContent: isGroup ? 'flex-start' : 'center',
173
202
  gap: '4px',
174
- boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
203
+ boxShadow: isHighlighted
204
+ ? `0 0 0 3px #3b82f6, 0 0 20px rgba(59, 130, 246, 0.5)`
205
+ : selected
206
+ ? `0 0 0 2px ${strokeColor}`
207
+ : '0 2px 4px rgba(0,0,0,0.1)',
175
208
  transition: 'box-shadow 0.2s ease',
176
209
  animationDuration: animationType ? `${animationDuration}ms` : undefined,
177
210
  boxSizing: 'border-box' as const,
@@ -253,7 +286,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
253
286
  }
254
287
  : {};
255
288
 
256
- // Hexagon inner fill styles (white background inset from border)
289
+ // Hexagon inner fill styles (light color background inset from border)
257
290
  const hexagonInnerStyle: React.CSSProperties = isHexagon
258
291
  ? {
259
292
  position: 'absolute',
@@ -262,7 +295,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
262
295
  right: hexagonBorderWidth,
263
296
  bottom: hexagonBorderWidth,
264
297
  clipPath: hexagonClipPath,
265
- backgroundColor: 'white',
298
+ backgroundColor: hexToLightColor(fillColor),
266
299
  color: '#000',
267
300
  display: 'flex',
268
301
  flexDirection: 'column',
@@ -295,7 +328,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
295
328
  }
296
329
  : {};
297
330
 
298
- // Diamond inner fill styles (white background inset from border)
331
+ // Diamond inner fill styles (light color background inset from border)
299
332
  const diamondInnerStyle: React.CSSProperties = isDiamond
300
333
  ? {
301
334
  position: 'absolute',
@@ -304,7 +337,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
304
337
  right: diamondBorderWidth,
305
338
  bottom: diamondBorderWidth,
306
339
  clipPath: diamondClipPath,
307
- backgroundColor: 'white',
340
+ backgroundColor: hexToLightColor(fillColor),
308
341
  color: '#000',
309
342
  display: 'flex',
310
343
  flexDirection: 'column',
@@ -411,7 +411,7 @@ function MultiConfigDemo() {
411
411
 
412
412
  {/* Graph visualization */}
413
413
  <div style={{ flex: 1 }}>
414
- <GraphRenderer canvas={selectedConfig.canvas} showMinimap showControls showBackground />
414
+ <GraphRenderer canvas={selectedConfig.canvas} showControls showBackground />
415
415
  </div>
416
416
  </div>
417
417
  );
@@ -310,7 +310,6 @@ export const Default: Story = {
310
310
  canvas: multiDirectionalCanvas,
311
311
  width: '100%',
312
312
  height: '800px',
313
- showMinimap: true,
314
313
  showControls: true,
315
314
  showBackground: true,
316
315
  },
@@ -0,0 +1,280 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { GraphRenderer } from '../components/GraphRenderer';
4
+ import { TestEventPanel } from '../components/TestEventPanel';
5
+ import type { ExtendedCanvas, GraphEvent } from '@principal-ai/principal-view-core';
6
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
7
+ import testSpans from './data/graph-converter-test-execution.json';
8
+
9
+ const meta = {
10
+ title: 'Features/Real Test Execution',
11
+ component: GraphRenderer,
12
+ parameters: {
13
+ layout: 'fullscreen',
14
+ docs: {
15
+ description: {
16
+ component:
17
+ 'Visualizes REAL test execution data from instrumented Bun tests using the "wide event" pattern. Shows actual spans with file/line information collected from running GraphConverter.test.ts. Hover over graph nodes to highlight related events in the panel.',
18
+ },
19
+ },
20
+ },
21
+ tags: ['autodocs'],
22
+ decorators: [
23
+ (Story) => (
24
+ <ThemeProvider theme={defaultEditorTheme}>
25
+ <div style={{ width: '100vw', height: '100vh', background: '#0a0a0a' }}>
26
+ <Story />
27
+ </div>
28
+ </ThemeProvider>
29
+ ),
30
+ ],
31
+ } satisfies Meta<typeof GraphRenderer>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ // ============================================================================
37
+ // Test Execution Flow Canvas
38
+ // ============================================================================
39
+
40
+ const testExecutionCanvas: ExtendedCanvas = {
41
+ nodes: [
42
+ // Test Suite
43
+ {
44
+ id: 'test-suite',
45
+ type: 'text',
46
+ text: 'GraphConverter Test Suite',
47
+ x: -100,
48
+ y: -100,
49
+ width: 240,
50
+ height: 80,
51
+ pv: {
52
+ nodeType: 'test-suite',
53
+ name: 'Test Suite',
54
+ description: 'Collection of GraphConverter tests',
55
+ shape: 'rectangle',
56
+ fill: '#3b82f6',
57
+ },
58
+ },
59
+
60
+ // Test Phase Nodes
61
+ {
62
+ id: 'setup-phase',
63
+ type: 'text',
64
+ text: 'Setup',
65
+ x: -250,
66
+ y: 50,
67
+ width: 120,
68
+ height: 80,
69
+ pv: {
70
+ nodeType: 'test-phase',
71
+ name: 'Setup Phase',
72
+ description: 'Test data preparation',
73
+ shape: 'hexagon',
74
+ fill: '#10b981',
75
+ },
76
+ },
77
+ {
78
+ id: 'execution-phase',
79
+ type: 'text',
80
+ text: 'Execution',
81
+ x: -80,
82
+ y: 50,
83
+ width: 120,
84
+ height: 80,
85
+ pv: {
86
+ nodeType: 'test-phase',
87
+ name: 'Execution Phase',
88
+ description: 'Code under test runs',
89
+ shape: 'hexagon',
90
+ fill: '#f59e0b',
91
+ },
92
+ },
93
+ {
94
+ id: 'assertion-phase',
95
+ type: 'text',
96
+ text: 'Assertion',
97
+ x: 90,
98
+ y: 50,
99
+ width: 120,
100
+ height: 80,
101
+ pv: {
102
+ nodeType: 'test-phase',
103
+ name: 'Assertion Phase',
104
+ description: 'Verify results',
105
+ shape: 'hexagon',
106
+ fill: '#8b5cf6',
107
+ },
108
+ },
109
+
110
+ // Result Node
111
+ {
112
+ id: 'test-result',
113
+ type: 'text',
114
+ text: 'Test Result',
115
+ x: -100,
116
+ y: 200,
117
+ width: 240,
118
+ height: 80,
119
+ pv: {
120
+ nodeType: 'result',
121
+ name: 'Test Result',
122
+ description: 'Pass/Fail outcome',
123
+ shape: 'rectangle',
124
+ fill: '#10b981',
125
+ },
126
+ },
127
+ ],
128
+ edges: [
129
+ {
130
+ id: 'suite-to-setup',
131
+ fromNode: 'test-suite',
132
+ toNode: 'setup-phase',
133
+ fromSide: 'bottom',
134
+ toSide: 'top',
135
+ label: 'start test',
136
+ pv: {
137
+ edgeType: 'flow',
138
+ style: 'solid',
139
+ },
140
+ },
141
+ {
142
+ id: 'setup-to-execution',
143
+ fromNode: 'setup-phase',
144
+ toNode: 'execution-phase',
145
+ fromSide: 'right',
146
+ toSide: 'left',
147
+ label: 'data ready',
148
+ pv: {
149
+ edgeType: 'flow',
150
+ style: 'solid',
151
+ },
152
+ },
153
+ {
154
+ id: 'execution-to-assertion',
155
+ fromNode: 'execution-phase',
156
+ toNode: 'assertion-phase',
157
+ fromSide: 'right',
158
+ toSide: 'left',
159
+ label: 'got result',
160
+ pv: {
161
+ edgeType: 'flow',
162
+ style: 'solid',
163
+ },
164
+ },
165
+ {
166
+ id: 'assertion-to-result',
167
+ fromNode: 'assertion-phase',
168
+ toNode: 'test-result',
169
+ fromSide: 'bottom',
170
+ toSide: 'top',
171
+ label: 'complete',
172
+ pv: {
173
+ edgeType: 'flow',
174
+ style: 'solid',
175
+ },
176
+ },
177
+ ],
178
+ pv: {
179
+ version: '1.0.0',
180
+ name: 'Test Execution Flow',
181
+ description: 'Visualizes the flow of test execution through phases',
182
+ },
183
+ };
184
+
185
+ // ============================================================================
186
+ // Interactive Story (No Animation)
187
+ // ============================================================================
188
+
189
+ const AnimatedTestExecution = () => {
190
+ const [events] = useState<GraphEvent[]>([]);
191
+ const [currentSpanIndex] = useState(0);
192
+ // Show all events by default - set to a large number
193
+ const [currentEventIndex] = useState(999);
194
+ const [highlightedPhase, setHighlightedPhase] = useState<string | undefined>();
195
+
196
+ return (
197
+ <div style={{ display: 'flex', width: '100vw', height: '100vh' }}>
198
+ {/* Graph Visualization - Left Side */}
199
+ <div
200
+ style={{ flex: '0 0 60%', height: '100%', position: 'relative' }}
201
+ onMouseLeave={() => setHighlightedPhase(undefined)}
202
+ >
203
+ <div
204
+ style={{ width: '100%', height: '100%' }}
205
+ onMouseOver={(e) => {
206
+ // Check if hovering over a phase node
207
+ const target = e.target as HTMLElement;
208
+ const textContent = target.textContent;
209
+ if (textContent === 'Setup') setHighlightedPhase('setup');
210
+ else if (textContent === 'Execution') setHighlightedPhase('execution');
211
+ else if (textContent === 'Assertion') setHighlightedPhase('assertion');
212
+ }}
213
+ >
214
+ <GraphRenderer
215
+ canvas={testExecutionCanvas}
216
+ showControls={true}
217
+ events={events}
218
+ />
219
+ </div>
220
+ </div>
221
+
222
+ {/* Event Panel - Right Side */}
223
+ <div style={{ flex: '0 0 40%', height: '100%', borderLeft: `1px solid #333`, overflow: 'hidden' }}>
224
+ <TestEventPanel
225
+ spans={testSpans as any}
226
+ currentSpanIndex={currentSpanIndex}
227
+ currentEventIndex={currentEventIndex}
228
+ highlightedPhase={highlightedPhase}
229
+ />
230
+ </div>
231
+ </div>
232
+ );
233
+ };
234
+
235
+ /**
236
+ * Interactive visualization of real test execution data using the "wide event" pattern.
237
+ *
238
+ * This demonstrates the key concept from loggingsucks.com:
239
+ * - ONE comprehensive span per test (not multiple child spans)
240
+ * - Events show the narrative of what happened during execution
241
+ * - Context accumulates through event attributes with file/line information
242
+ * - Easy to search by test.name to get full execution story
243
+ *
244
+ * **Interaction:**
245
+ * - Hover over graph nodes (Setup, Execution, Assertion) to highlight related events
246
+ * - Watch the code journey: blue = test file, green = code under test
247
+ * - All events are shown immediately for easy review
248
+ */
249
+ export const Animated: Story = {
250
+ render: () => <AnimatedTestExecution />,
251
+ };
252
+
253
+ /**
254
+ * Static view of the test execution flow showing phases.
255
+ */
256
+ export const StaticView: Story = {
257
+ args: {
258
+ canvas: testExecutionCanvas,
259
+ showControls: true,
260
+ },
261
+ };
262
+
263
+ /**
264
+ * Event panel component showing test execution narrative with file/line information.
265
+ *
266
+ * Shows how events accumulate context as tests execute, with automatic file/line
267
+ * capture from stack traces and manual override for code under test.
268
+ */
269
+ export const EventPanelOnly: StoryObj = {
270
+ render: () => (
271
+ <div style={{ width: '600px', height: '100vh' }}>
272
+ <TestEventPanel
273
+ spans={testSpans as any}
274
+ currentSpanIndex={0}
275
+ currentEventIndex={999} // Show all events
276
+ highlightedPhase={undefined}
277
+ />
278
+ </div>
279
+ ),
280
+ };