@principal-ai/principal-view-react 0.14.29 → 0.14.31

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.
@@ -36,15 +36,71 @@ import {
36
36
 
37
37
  /**
38
38
  * Minimal marker node for arrow-centric sequence diagrams
39
- * Invisible - just used for positioning, all rendering done by edges
39
+ * Invisible - just used for positioning, selection is shown on edge labels
40
+ * Can optionally show a label for better clickability
40
41
  */
41
- function SequenceMarkerNode({ data }: NodeProps) {
42
+ function SequenceMarkerNode({ data, selected }: NodeProps) {
43
+ const { theme } = useTheme();
44
+ const showLabel = data.showEventLabels === true; // Only show if explicitly enabled
45
+ const label = data.label as string | undefined;
46
+
47
+ // If labels are shown on nodes, render them
48
+ if (showLabel && label) {
49
+ return (
50
+ <div
51
+ style={{
52
+ width: '100%',
53
+ height: '100%',
54
+ position: 'relative',
55
+ cursor: 'pointer',
56
+ display: 'flex',
57
+ flexDirection: 'column',
58
+ alignItems: 'center',
59
+ justifyContent: 'center',
60
+ }}
61
+ title={data.fullName as string}
62
+ >
63
+ <div
64
+ style={{
65
+ padding: '6px 12px',
66
+ background: selected ? theme.colors.primary : theme.colors.background,
67
+ color: selected ? theme.colors.background : theme.colors.text,
68
+ border: `2px solid ${selected ? theme.colors.primary : theme.colors.border}`,
69
+ borderRadius: 4,
70
+ fontSize: theme.fontSizes[1],
71
+ fontWeight: selected ? theme.fontWeights.bold : theme.fontWeights.medium,
72
+ fontFamily: theme.fonts.body,
73
+ whiteSpace: 'nowrap',
74
+ pointerEvents: 'none',
75
+ userSelect: 'none',
76
+ transition: 'all 0.2s ease',
77
+ boxShadow: selected ? `0 2px 8px ${theme.colors.primary}40` : 'none',
78
+ }}
79
+ >
80
+ {label}
81
+ </div>
82
+ <Handle
83
+ type="target"
84
+ position={Position.Top}
85
+ style={{ visibility: 'hidden' }}
86
+ />
87
+ <Handle
88
+ type="source"
89
+ position={Position.Bottom}
90
+ style={{ visibility: 'hidden' }}
91
+ />
92
+ </div>
93
+ );
94
+ }
95
+
96
+ // Default: invisible node (selection shown on edge labels)
42
97
  return (
43
98
  <div
44
99
  style={{
45
100
  width: '100%',
46
101
  height: '100%',
47
102
  opacity: 0,
103
+ cursor: 'pointer',
48
104
  }}
49
105
  title={data.fullName as string}
50
106
  >
@@ -80,6 +136,7 @@ function SequenceArrowEdge({
80
136
 
81
137
  // Use a straight line for same-lane, bezier for cross-lane
82
138
  const isSameLane = !data?.crossesLanes;
139
+ const isSourceSelected = data?.isSourceSelected === true;
83
140
 
84
141
  const [edgePath, labelX, labelY] = getBezierPath({
85
142
  sourceX,
@@ -108,16 +165,19 @@ function SequenceArrowEdge({
108
165
  style={{
109
166
  position: 'absolute',
110
167
  transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
111
- background: theme.colors.background,
112
- padding: '2px 8px',
168
+ background: isSourceSelected ? theme.colors.primary : theme.colors.background,
169
+ padding: isSourceSelected ? '4px 10px' : '2px 8px',
113
170
  borderRadius: 4,
114
171
  fontSize: theme.fontSizes[0],
115
- fontWeight: theme.fontWeights.medium,
172
+ fontWeight: isSourceSelected ? theme.fontWeights.bold : theme.fontWeights.medium,
116
173
  fontFamily: theme.fonts.body,
117
- color: theme.colors.text,
118
- border: `1px solid ${theme.colors.border}`,
174
+ color: isSourceSelected ? theme.colors.background : theme.colors.text,
175
+ border: `${isSourceSelected ? 2 : 1}px solid ${isSourceSelected ? theme.colors.primary : theme.colors.border}`,
119
176
  pointerEvents: 'all',
120
177
  whiteSpace: 'nowrap',
178
+ cursor: 'pointer',
179
+ transition: 'all 0.2s ease',
180
+ boxShadow: isSourceSelected ? `0 2px 8px ${theme.colors.primary}60` : 'none',
121
181
  }}
122
182
  className="nodrag nopan"
123
183
  >
@@ -154,6 +214,7 @@ function SequenceArrowParticipantEdge({
154
214
 
155
215
  // Style based on whether it's a move event (IPC) or transform event (internal)
156
216
  const isMoveEvent = data?.isMoveEvent === true;
217
+ const isSourceSelected = data?.isSourceSelected === true;
157
218
  const strokeColor = isMoveEvent ? (theme.colors.accent || '#f48771') : theme.colors.primary;
158
219
 
159
220
  // Same lane: render as activation bar
@@ -200,16 +261,19 @@ function SequenceArrowParticipantEdge({
200
261
  style={{
201
262
  position: 'absolute',
202
263
  transform: `translate(0, -50%) translate(${sourceX + 15}px,${barY + barHeight / 2}px)`,
203
- background: theme.colors.background,
204
- padding: '2px 8px',
264
+ background: isSourceSelected ? strokeColor : theme.colors.background,
265
+ padding: isSourceSelected ? '4px 10px' : '2px 8px',
205
266
  borderRadius: 4,
206
267
  fontSize: theme.fontSizes[0],
207
- fontWeight: theme.fontWeights.medium,
268
+ fontWeight: isSourceSelected ? theme.fontWeights.bold : theme.fontWeights.medium,
208
269
  fontFamily: theme.fonts.body,
209
- color: strokeColor,
210
- border: `1px solid ${strokeColor}`,
270
+ color: isSourceSelected ? theme.colors.background : strokeColor,
271
+ border: `${isSourceSelected ? 2 : 1}px solid ${strokeColor}`,
211
272
  pointerEvents: 'all',
212
273
  whiteSpace: 'nowrap',
274
+ cursor: 'pointer',
275
+ transition: 'all 0.2s ease',
276
+ boxShadow: isSourceSelected ? `0 2px 8px ${strokeColor}60` : 'none',
213
277
  }}
214
278
  className="nodrag nopan"
215
279
  >
@@ -248,16 +312,19 @@ function SequenceArrowParticipantEdge({
248
312
  style={{
249
313
  position: 'absolute',
250
314
  transform: `translate(-50%, -50%) translate(${labelX}px,${labelY - 12}px)`,
251
- background: theme.colors.background,
252
- padding: isMoveEvent ? '3px 10px' : '2px 8px',
315
+ background: isSourceSelected ? strokeColor : theme.colors.background,
316
+ padding: isSourceSelected ? '5px 12px' : (isMoveEvent ? '3px 10px' : '2px 8px'),
253
317
  borderRadius: 4,
254
318
  fontSize: theme.fontSizes[0],
255
- fontWeight: isMoveEvent ? theme.fontWeights.bold : theme.fontWeights.medium,
319
+ fontWeight: isSourceSelected ? theme.fontWeights.bold : (isMoveEvent ? theme.fontWeights.bold : theme.fontWeights.medium),
256
320
  fontFamily: theme.fonts.body,
257
- color: strokeColor,
258
- border: isMoveEvent ? `2px solid ${strokeColor}` : `1px solid ${strokeColor}`,
321
+ color: isSourceSelected ? theme.colors.background : strokeColor,
322
+ border: `${isSourceSelected ? 3 : (isMoveEvent ? 2 : 1)}px solid ${strokeColor}`,
259
323
  pointerEvents: 'all',
260
324
  whiteSpace: 'nowrap',
325
+ cursor: 'pointer',
326
+ transition: 'all 0.2s ease',
327
+ boxShadow: isSourceSelected ? `0 4px 12px ${strokeColor}60` : 'none',
261
328
  }}
262
329
  className="nodrag nopan"
263
330
  >
@@ -457,6 +524,9 @@ export interface SequenceDiagramRendererProps {
457
524
  /** Callback when a node is clicked */
458
525
  onNodeClick?: (nodeId: string, event: React.MouseEvent) => void;
459
526
 
527
+ /** ID of the currently selected node (for visual highlighting) */
528
+ selectedNodeId?: string;
529
+
460
530
  /** Optional class name */
461
531
  className?: string;
462
532
 
@@ -474,6 +544,9 @@ export interface SequenceDiagramRendererProps {
474
544
 
475
545
  /** Whether swimlane headers should stick to the top when scrolling vertically (default: true) */
476
546
  stickyHeaders?: boolean;
547
+
548
+ /** Whether to show event labels on nodes (default: false, labels already shown on edges) */
549
+ showEventLabels?: boolean;
477
550
  }
478
551
 
479
552
  /**
@@ -490,6 +563,8 @@ function SequenceDiagramInner({
490
563
  showControls = true,
491
564
  showBackground = false, // Default to false since swimlanes provide visual structure
492
565
  stickyHeaders = true,
566
+ selectedNodeId,
567
+ showEventLabels = false, // Default to false - labels already shown on edges
493
568
  }: SequenceDiagramRendererProps) {
494
569
  const { theme } = useTheme();
495
570
 
@@ -507,12 +582,35 @@ function SequenceDiagramInner({
507
582
  );
508
583
 
509
584
  // Compute layout
510
- const { nodes, edges, swimlanes, totalHeight } = useSequenceLayout(
585
+ const { nodes: layoutNodes, edges, swimlanes, totalHeight } = useSequenceLayout(
511
586
  events,
512
587
  sequenceEdges,
513
588
  layoutOptions
514
589
  );
515
590
 
591
+ // Mark selected node and add showEventLabels to node data
592
+ const nodes = useMemo(() => {
593
+ return layoutNodes.map(node => ({
594
+ ...node,
595
+ selected: node.id === selectedNodeId,
596
+ data: {
597
+ ...node.data,
598
+ showEventLabels,
599
+ },
600
+ }));
601
+ }, [layoutNodes, selectedNodeId, showEventLabels]);
602
+
603
+ // Add selectedNodeId to edge data so edges can highlight their labels
604
+ const edgesWithSelection = useMemo(() => {
605
+ return edges.map(edge => ({
606
+ ...edge,
607
+ data: {
608
+ ...edge.data,
609
+ isSourceSelected: edge.source === selectedNodeId,
610
+ },
611
+ }));
612
+ }, [edges, selectedNodeId]);
613
+
516
614
  // Handle node click
517
615
  const handleNodeClick = useCallback(
518
616
  (_event: React.MouseEvent, node: { id: string }) => {
@@ -521,6 +619,16 @@ function SequenceDiagramInner({
521
619
  [onNodeClick]
522
620
  );
523
621
 
622
+ // Handle edge click - extract source event from edge and trigger node selection
623
+ const handleEdgeClick = useCallback(
624
+ (_event: React.MouseEvent, edge: { id: string; source: string; target: string }) => {
625
+ // When clicking an edge, select the source event (the label describes the source)
626
+ // The edge label comes from the source event, so clicking it should select that event
627
+ onNodeClick?.(edge.source, _event);
628
+ },
629
+ [onNodeClick]
630
+ );
631
+
524
632
  // When sticky headers are enabled, limit upward panning to prevent headers from disconnecting
525
633
  // from swimlane backgrounds. Allow downward panning to see all content.
526
634
  const translateExtent = useMemo(() => {
@@ -562,10 +670,11 @@ function SequenceDiagramInner({
562
670
  return (
563
671
  <ReactFlow
564
672
  nodes={nodes}
565
- edges={edges}
673
+ edges={edgesWithSelection}
566
674
  nodeTypes={nodeTypes}
567
675
  edgeTypes={edgeTypes}
568
676
  onNodeClick={handleNodeClick}
677
+ onEdgeClick={handleEdgeClick}
569
678
  {...viewportConfig}
570
679
  minZoom={0.1}
571
680
  maxZoom={2}
@@ -42,6 +42,12 @@ export interface WorkflowSequenceDiagramProps {
42
42
 
43
43
  /** Callback when an event node is clicked - receives the event index (0-based) */
44
44
  onEventIndexChange?: (eventIndex: number) => void;
45
+
46
+ /** Currently selected event index (0-based) for visual highlighting */
47
+ selectedEventIndex?: number;
48
+
49
+ /** Whether to show event labels on nodes (default: false, labels already shown on edges) */
50
+ showEventLabels?: boolean;
45
51
  }
46
52
 
47
53
  /**
@@ -184,6 +190,8 @@ export function WorkflowSequenceDiagram({
184
190
  showBackground = false,
185
191
  className,
186
192
  onEventIndexChange,
193
+ selectedEventIndex,
194
+ showEventLabels = false, // Default to false - labels already on edges
187
195
  }: WorkflowSequenceDiagramProps) {
188
196
  // Convert workflow to sequence format
189
197
  const { events, edges } = useMemo(
@@ -206,6 +214,12 @@ export function WorkflowSequenceDiagram({
206
214
  [onEventIndexChange]
207
215
  );
208
216
 
217
+ // Convert selected event index to node ID
218
+ const selectedNodeId = useMemo(() => {
219
+ if (selectedEventIndex === undefined || selectedEventIndex === null) return undefined;
220
+ return `event-${selectedEventIndex}`;
221
+ }, [selectedEventIndex]);
222
+
209
223
  // If no events, show empty state
210
224
  if (events.length === 0) {
211
225
  return (
@@ -237,6 +251,8 @@ export function WorkflowSequenceDiagram({
237
251
  showBackground={showBackground}
238
252
  className={className}
239
253
  onNodeClick={onEventIndexChange ? handleNodeClick : undefined}
254
+ selectedNodeId={selectedNodeId}
255
+ showEventLabels={showEventLabels}
240
256
  />
241
257
  );
242
258
  }