@principal-ai/principal-view-react 0.14.30 → 0.14.32

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,42 +36,74 @@ 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
40
- * Shows a visual indicator when selected
39
+ * Invisible - just used for positioning, selection is shown on edge labels
40
+ * Can optionally show a label for better clickability
41
41
  */
42
42
  function SequenceMarkerNode({ data, selected }: NodeProps) {
43
43
  const { theme } = useTheme();
44
+ const showLabel = data.showEventLabels === true; // Only show if explicitly enabled
45
+ const label = data.label as string | undefined;
44
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)
45
97
  return (
46
98
  <div
47
99
  style={{
48
100
  width: '100%',
49
101
  height: '100%',
50
102
  opacity: 0,
51
- position: 'relative',
103
+ cursor: 'pointer',
52
104
  }}
53
105
  title={data.fullName as string}
54
106
  >
55
- {/* Show indicator when selected */}
56
- {selected && (
57
- <div
58
- style={{
59
- position: 'absolute',
60
- top: '50%',
61
- left: '50%',
62
- transform: 'translate(-50%, -50%)',
63
- width: '12px',
64
- height: '12px',
65
- borderRadius: '50%',
66
- backgroundColor: theme.colors.primary,
67
- border: `2px solid ${theme.colors.background}`,
68
- boxShadow: `0 0 0 2px ${theme.colors.primary}40`,
69
- opacity: 1,
70
- zIndex: 10,
71
- pointerEvents: 'none',
72
- }}
73
- />
74
- )}
75
107
  <Handle
76
108
  type="target"
77
109
  position={Position.Top}
@@ -104,6 +136,7 @@ function SequenceArrowEdge({
104
136
 
105
137
  // Use a straight line for same-lane, bezier for cross-lane
106
138
  const isSameLane = !data?.crossesLanes;
139
+ const isSourceSelected = data?.isSourceSelected === true;
107
140
 
108
141
  const [edgePath, labelX, labelY] = getBezierPath({
109
142
  sourceX,
@@ -132,16 +165,19 @@ function SequenceArrowEdge({
132
165
  style={{
133
166
  position: 'absolute',
134
167
  transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
135
- background: theme.colors.background,
136
- padding: '2px 8px',
168
+ background: isSourceSelected ? theme.colors.primary : theme.colors.background,
169
+ padding: isSourceSelected ? '4px 10px' : '2px 8px',
137
170
  borderRadius: 4,
138
171
  fontSize: theme.fontSizes[0],
139
- fontWeight: theme.fontWeights.medium,
172
+ fontWeight: isSourceSelected ? theme.fontWeights.bold : theme.fontWeights.medium,
140
173
  fontFamily: theme.fonts.body,
141
- color: theme.colors.text,
142
- 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}`,
143
176
  pointerEvents: 'all',
144
177
  whiteSpace: 'nowrap',
178
+ cursor: 'pointer',
179
+ transition: 'all 0.2s ease',
180
+ boxShadow: isSourceSelected ? `0 2px 8px ${theme.colors.primary}60` : 'none',
145
181
  }}
146
182
  className="nodrag nopan"
147
183
  >
@@ -178,6 +214,7 @@ function SequenceArrowParticipantEdge({
178
214
 
179
215
  // Style based on whether it's a move event (IPC) or transform event (internal)
180
216
  const isMoveEvent = data?.isMoveEvent === true;
217
+ const isSourceSelected = data?.isSourceSelected === true;
181
218
  const strokeColor = isMoveEvent ? (theme.colors.accent || '#f48771') : theme.colors.primary;
182
219
 
183
220
  // Same lane: render as activation bar
@@ -224,16 +261,19 @@ function SequenceArrowParticipantEdge({
224
261
  style={{
225
262
  position: 'absolute',
226
263
  transform: `translate(0, -50%) translate(${sourceX + 15}px,${barY + barHeight / 2}px)`,
227
- background: theme.colors.background,
228
- padding: '2px 8px',
264
+ background: isSourceSelected ? strokeColor : theme.colors.background,
265
+ padding: isSourceSelected ? '4px 10px' : '2px 8px',
229
266
  borderRadius: 4,
230
267
  fontSize: theme.fontSizes[0],
231
- fontWeight: theme.fontWeights.medium,
268
+ fontWeight: isSourceSelected ? theme.fontWeights.bold : theme.fontWeights.medium,
232
269
  fontFamily: theme.fonts.body,
233
- color: strokeColor,
234
- border: `1px solid ${strokeColor}`,
270
+ color: isSourceSelected ? theme.colors.background : strokeColor,
271
+ border: `${isSourceSelected ? 2 : 1}px solid ${strokeColor}`,
235
272
  pointerEvents: 'all',
236
273
  whiteSpace: 'nowrap',
274
+ cursor: 'pointer',
275
+ transition: 'all 0.2s ease',
276
+ boxShadow: isSourceSelected ? `0 2px 8px ${strokeColor}60` : 'none',
237
277
  }}
238
278
  className="nodrag nopan"
239
279
  >
@@ -272,16 +312,19 @@ function SequenceArrowParticipantEdge({
272
312
  style={{
273
313
  position: 'absolute',
274
314
  transform: `translate(-50%, -50%) translate(${labelX}px,${labelY - 12}px)`,
275
- background: theme.colors.background,
276
- padding: isMoveEvent ? '3px 10px' : '2px 8px',
315
+ background: isSourceSelected ? strokeColor : theme.colors.background,
316
+ padding: isSourceSelected ? '5px 12px' : (isMoveEvent ? '3px 10px' : '2px 8px'),
277
317
  borderRadius: 4,
278
318
  fontSize: theme.fontSizes[0],
279
- fontWeight: isMoveEvent ? theme.fontWeights.bold : theme.fontWeights.medium,
319
+ fontWeight: isSourceSelected ? theme.fontWeights.bold : (isMoveEvent ? theme.fontWeights.bold : theme.fontWeights.medium),
280
320
  fontFamily: theme.fonts.body,
281
- color: strokeColor,
282
- 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}`,
283
323
  pointerEvents: 'all',
284
324
  whiteSpace: 'nowrap',
325
+ cursor: 'pointer',
326
+ transition: 'all 0.2s ease',
327
+ boxShadow: isSourceSelected ? `0 4px 12px ${strokeColor}60` : 'none',
285
328
  }}
286
329
  className="nodrag nopan"
287
330
  >
@@ -501,6 +544,9 @@ export interface SequenceDiagramRendererProps {
501
544
 
502
545
  /** Whether swimlane headers should stick to the top when scrolling vertically (default: true) */
503
546
  stickyHeaders?: boolean;
547
+
548
+ /** Whether to show event labels on nodes (default: false, labels already shown on edges) */
549
+ showEventLabels?: boolean;
504
550
  }
505
551
 
506
552
  /**
@@ -518,6 +564,7 @@ function SequenceDiagramInner({
518
564
  showBackground = false, // Default to false since swimlanes provide visual structure
519
565
  stickyHeaders = true,
520
566
  selectedNodeId,
567
+ showEventLabels = false, // Default to false - labels already shown on edges
521
568
  }: SequenceDiagramRendererProps) {
522
569
  const { theme } = useTheme();
523
570
 
@@ -541,15 +588,28 @@ function SequenceDiagramInner({
541
588
  layoutOptions
542
589
  );
543
590
 
544
- // Mark selected node
591
+ // Mark selected node and add showEventLabels to node data
545
592
  const nodes = useMemo(() => {
546
- if (!selectedNodeId) return layoutNodes;
547
-
548
593
  return layoutNodes.map(node => ({
549
594
  ...node,
550
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
+ },
551
611
  }));
552
- }, [layoutNodes, selectedNodeId]);
612
+ }, [edges, selectedNodeId]);
553
613
 
554
614
  // Handle node click
555
615
  const handleNodeClick = useCallback(
@@ -559,6 +619,16 @@ function SequenceDiagramInner({
559
619
  [onNodeClick]
560
620
  );
561
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
+
562
632
  // When sticky headers are enabled, limit upward panning to prevent headers from disconnecting
563
633
  // from swimlane backgrounds. Allow downward panning to see all content.
564
634
  const translateExtent = useMemo(() => {
@@ -600,10 +670,11 @@ function SequenceDiagramInner({
600
670
  return (
601
671
  <ReactFlow
602
672
  nodes={nodes}
603
- edges={edges}
673
+ edges={edgesWithSelection}
604
674
  nodeTypes={nodeTypes}
605
675
  edgeTypes={edgeTypes}
606
676
  onNodeClick={handleNodeClick}
677
+ onEdgeClick={handleEdgeClick}
607
678
  {...viewportConfig}
608
679
  minZoom={0.1}
609
680
  maxZoom={2}
@@ -45,6 +45,9 @@ export interface WorkflowSequenceDiagramProps {
45
45
 
46
46
  /** Currently selected event index (0-based) for visual highlighting */
47
47
  selectedEventIndex?: number;
48
+
49
+ /** Whether to show event labels on nodes (default: false, labels already shown on edges) */
50
+ showEventLabels?: boolean;
48
51
  }
49
52
 
50
53
  /**
@@ -188,6 +191,7 @@ export function WorkflowSequenceDiagram({
188
191
  className,
189
192
  onEventIndexChange,
190
193
  selectedEventIndex,
194
+ showEventLabels = false, // Default to false - labels already on edges
191
195
  }: WorkflowSequenceDiagramProps) {
192
196
  // Convert workflow to sequence format
193
197
  const { events, edges } = useMemo(
@@ -248,6 +252,7 @@ export function WorkflowSequenceDiagram({
248
252
  className={className}
249
253
  onNodeClick={onEventIndexChange ? handleNodeClick : undefined}
250
254
  selectedNodeId={selectedNodeId}
255
+ showEventLabels={showEventLabels}
251
256
  />
252
257
  );
253
258
  }
@@ -99,9 +99,11 @@ export const OtelEventNode: React.FC<NodeProps<Node<OtelEventNodeData>>> = ({
99
99
  // Color resolution
100
100
  const scopeColor = nodeData.scopeColor as string | undefined;
101
101
  const spanColor = nodeData.spanColor as string | undefined;
102
- const nodeDataColor = nodeData.color as string | undefined;
103
- // Fill color priority: explicit color > scope color > type definition color > default blue
104
- const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#3b82f6';
102
+
103
+ // OTEL nodes use scope-based coloring exclusively
104
+ // Priority: scopeColor (from library.yaml) typeDefinition.color default
105
+ // Note: node.fill and node.color fields are intentionally ignored (validation enforces this)
106
+ const baseFillColor = scopeColor || typeDefinition.color || '#3b82f6';
105
107
  const fillColor = baseFillColor;
106
108
  // Stroke color priority: explicit stroke > span color (workflow context) > fill color
107
109
  const nodeDataStroke = nodeData.stroke as string | undefined;
@@ -0,0 +1,209 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import '@xyflow/react/dist/style.css';
4
+ import { GraphRenderer } from '../components/GraphRenderer';
5
+ import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
6
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
7
+
8
+ // Import the validation events canvas
9
+ import validationEventsCanvasUrl from '../../../../.principal-views/validation/validation.events.canvas?url';
10
+
11
+ // Helper to load canvas from URL
12
+ async function loadCanvas(url: string): Promise<ExtendedCanvas> {
13
+ const response = await fetch(url);
14
+ return await response.json();
15
+ }
16
+
17
+ // Wrapper component that loads canvas data
18
+ function CanvasLoader({ url, children }: { url: string; children: (canvas: ExtendedCanvas) => React.ReactNode }) {
19
+ const [canvas, setCanvas] = useState<ExtendedCanvas | null>(null);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ useEffect(() => {
23
+ loadCanvas(url)
24
+ .then(setCanvas)
25
+ .catch((err) => setError(err.message));
26
+ }, [url]);
27
+
28
+ if (error) {
29
+ return <div style={{ padding: 20, color: 'red' }}>Error loading canvas: {error}</div>;
30
+ }
31
+
32
+ if (!canvas) {
33
+ return <div style={{ padding: 20 }}>Loading canvas...</div>;
34
+ }
35
+
36
+ return (
37
+ <div style={{ width: '100%', height: '100vh' }}>
38
+ {children(canvas)}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ const meta = {
44
+ title: 'Validation/Event Namespace Canvas',
45
+ component: GraphRenderer,
46
+ parameters: {
47
+ layout: 'fullscreen',
48
+ },
49
+ tags: ['autodocs'],
50
+ decorators: [
51
+ (Story) => (
52
+ <ThemeProvider theme={defaultEditorTheme}>
53
+ <div style={{ width: '100vw', height: '100vh' }}>
54
+ <Story />
55
+ </div>
56
+ </ThemeProvider>
57
+ ),
58
+ ],
59
+ } satisfies Meta<typeof GraphRenderer>;
60
+
61
+ export default meta;
62
+ type Story = StoryObj<typeof meta>;
63
+
64
+ /**
65
+ * Validation Event Namespace Canvas
66
+ *
67
+ * File: .principal-views/validation/validation.events.canvas
68
+ * Type: event-namespace nodes
69
+ * Purpose: Shows the vocabulary of events emitted during validation workflows
70
+ */
71
+ export const EventNamespaceVocabulary: Story = {
72
+ render: () => (
73
+ <CanvasLoader url={validationEventsCanvasUrl}>
74
+ {(canvas) => <GraphRenderer canvas={canvas} showBackground={false} editable={true} />}
75
+ </CanvasLoader>
76
+ ),
77
+ parameters: {
78
+ docs: {
79
+ description: {
80
+ story: `
81
+ Renders the **validation.events.canvas** file - an event namespace vocabulary canvas.
82
+
83
+ ## Event Namespace Canvas Concept
84
+
85
+ Unlike span canvases (which show user workflows) or OTEL event canvases (which show individual events),
86
+ this canvas shows the **vocabulary of event namespaces** and their adjacency relationships.
87
+
88
+ ### Structure
89
+
90
+ **Nodes = Event Namespaces**
91
+ - \`analysis\` - Analysis pipeline lifecycle events
92
+ - \`validation\` - Validation lifecycle events (started, complete, error)
93
+ - \`file\` - File parsing events
94
+ - \`type\` - Type detection events
95
+ - etc.
96
+
97
+ Each namespace node documents all events within that namespace with their attributes.
98
+
99
+ **Edges = Adjacency**
100
+ - Edges represent when events from one namespace appear adjacent to events from another namespace in workflow scenarios
101
+ - Example: \`analysis → filetree\` because \`analysis.started → filetree.built\` in scenarios
102
+ - Example: \`file → validation\` because \`file.parsed → validation.error\` in scenarios
103
+
104
+ ### Purpose
105
+
106
+ This canvas serves as:
107
+ 1. **Event vocabulary documentation** - What events can be emitted
108
+ 2. **Flow visualization** - How events from different namespaces connect
109
+ 3. **Implementation guide** - What attributes each event requires
110
+ 4. **Validation source** - Canvas edges must match actual workflow scenario adjacencies
111
+
112
+ ### Conventions
113
+
114
+ As defined in \`.principal-views/architecture.events.md\`:
115
+ - Events follow \`{namespace}.{action}.{detail}\` naming
116
+ - Edges represent direct adjacency only (not transitive)
117
+ - Canvas is validated against workflow scenario files
118
+ - Namespaces group code-level implementation events (vs user-level workflow spans)
119
+
120
+ **Use this to:**
121
+ - Preview event namespace canvas rendering
122
+ - Understand event flow architecture
123
+ - Document which events your code emits
124
+ - Validate event relationships match actual code
125
+ `,
126
+ },
127
+ },
128
+ },
129
+ };
130
+
131
+ /**
132
+ * Event Namespace Canvas with Annotations
133
+ *
134
+ * Shows the canvas with additional context about what each namespace contains
135
+ */
136
+ export const WithNamespaceDetails: Story = {
137
+ render: () => (
138
+ <CanvasLoader url={validationEventsCanvasUrl}>
139
+ {(canvas) => (
140
+ <div style={{ display: 'flex', height: '100vh' }}>
141
+ <div style={{ flex: 1 }}>
142
+ <GraphRenderer canvas={canvas} showBackground={false} editable={true} />
143
+ </div>
144
+ <div style={{
145
+ width: '350px',
146
+ padding: '20px',
147
+ backgroundColor: '#f5f5f5',
148
+ overflowY: 'auto',
149
+ fontFamily: 'system-ui, -apple-system, sans-serif'
150
+ }}>
151
+ <h2 style={{ marginTop: 0, fontSize: '18px' }}>Event Namespaces</h2>
152
+ <p style={{ fontSize: '14px', color: '#666' }}>
153
+ Each node represents a namespace grouping related events.
154
+ </p>
155
+
156
+ <div style={{ marginTop: '20px' }}>
157
+ <h3 style={{ fontSize: '14px', marginBottom: '8px' }}>Discovery Phase</h3>
158
+ <ul style={{ fontSize: '13px', lineHeight: '1.6', paddingLeft: '20px' }}>
159
+ <li><strong>analysis</strong> - Pipeline start</li>
160
+ <li><strong>filetree</strong> - Repository scanning</li>
161
+ <li><strong>packages</strong> - Package discovery</li>
162
+ <li><strong>canvases</strong> - Canvas file discovery</li>
163
+ <li><strong>executions</strong> - Execution file discovery</li>
164
+ <li><strong>library</strong> - Component library loading</li>
165
+ <li><strong>discovery</strong> - Phase completion</li>
166
+ </ul>
167
+ </div>
168
+
169
+ <div style={{ marginTop: '20px' }}>
170
+ <h3 style={{ fontSize: '14px', marginBottom: '8px' }}>Validation Phase</h3>
171
+ <ul style={{ fontSize: '13px', lineHeight: '1.6', paddingLeft: '20px' }}>
172
+ <li><strong>validation</strong> - Lifecycle (started, complete, error)</li>
173
+ <li><strong>file</strong> - Parsing operations</li>
174
+ <li><strong>type</strong> - Type detection</li>
175
+ <li><strong>canvas</strong> - Canvas validation</li>
176
+ <li><strong>workflow</strong> - Workflow validation</li>
177
+ <li><strong>execution</strong> - Execution validation</li>
178
+ <li><strong>rules</strong> - Lint rule execution</li>
179
+ <li><strong>results</strong> - Result aggregation</li>
180
+ </ul>
181
+ </div>
182
+
183
+ <div style={{ marginTop: '20px', padding: '12px', backgroundColor: '#fff3cd', borderRadius: '4px' }}>
184
+ <h4 style={{ fontSize: '13px', marginTop: 0 }}>Adjacency Edges</h4>
185
+ <p style={{ fontSize: '12px', margin: 0, color: '#856404' }}>
186
+ Edges show when events from one namespace appear next to events from another namespace
187
+ in workflow scenarios. These are validated against actual execution traces.
188
+ </p>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ )}
193
+ </CanvasLoader>
194
+ ),
195
+ parameters: {
196
+ docs: {
197
+ description: {
198
+ story: `
199
+ Same canvas with a side panel showing namespace details and explanations.
200
+
201
+ This view helps:
202
+ - Understand what each namespace contains
203
+ - See the phase groupings (discovery vs validation)
204
+ - Learn the event namespace canvas concept
205
+ `,
206
+ },
207
+ },
208
+ },
209
+ };