@principal-ai/subsystems-react 0.17.0 → 0.19.0

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 (108) hide show
  1. package/README.md +11 -85
  2. package/dist/index.d.ts +7 -44
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -32
  5. package/dist/index.js.map +1 -1
  6. package/dist/subsystem/SubsystemCarousel.d.ts +36 -0
  7. package/dist/subsystem/SubsystemCarousel.d.ts.map +1 -0
  8. package/dist/subsystem/SubsystemCarousel.js +163 -0
  9. package/dist/subsystem/SubsystemCarousel.js.map +1 -0
  10. package/dist/subsystem/SubsystemComponentGraph.d.ts +25 -0
  11. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  12. package/dist/subsystem/SubsystemComponentGraph.js +329 -68
  13. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  14. package/package.json +4 -4
  15. package/src/index.ts +9 -199
  16. package/src/subsystem/SubsystemComponentGraph.tsx +396 -50
  17. package/src/components/ConfigurationSelector.tsx +0 -133
  18. package/src/components/GraphRenderer.tsx +0 -3195
  19. package/src/components/MultiCanvasRenderer.tsx +0 -295
  20. package/src/components/NodeTooltip.tsx +0 -279
  21. package/src/components/PendingChanges.test.tsx +0 -375
  22. package/src/components/SelectionSidebar.tsx +0 -352
  23. package/src/components/SequenceDiagramRenderer.tsx +0 -1156
  24. package/src/components/WorkflowSequenceDiagram.tsx +0 -270
  25. package/src/components/dashboard/DashboardRenderer.tsx +0 -317
  26. package/src/components/dashboard/MetricPanel.tsx +0 -254
  27. package/src/components/dashboard/MockDataProvider.ts +0 -330
  28. package/src/components/dashboard/components/BarChart.tsx +0 -299
  29. package/src/components/dashboard/components/LineChart.tsx +0 -279
  30. package/src/components/dashboard/components/MetricCard.tsx +0 -270
  31. package/src/components/dashboard/components/SourceLink.tsx +0 -63
  32. package/src/components/dashboard/components/TimeRangeSelector.tsx +0 -280
  33. package/src/components/dashboard/components/index.ts +0 -5
  34. package/src/components/dashboard/index.ts +0 -47
  35. package/src/components/dashboard/types.ts +0 -126
  36. package/src/components/state-view/PipelineView.tsx +0 -347
  37. package/src/components/state-view/index.ts +0 -14
  38. package/src/components/state-view/types.ts +0 -261
  39. package/src/components/state-view/useStateView.ts +0 -205
  40. package/src/contexts/GraphEditContext.tsx +0 -27
  41. package/src/contexts/TooltipPortalContext.ts +0 -8
  42. package/src/edges/CustomEdge.tsx +0 -402
  43. package/src/edges/GenericEdge.tsx +0 -36
  44. package/src/hooks/usePathBasedEvents.ts +0 -192
  45. package/src/hooks/useSequenceLayout.ts +0 -543
  46. package/src/hooks/useUndoRedo.ts +0 -144
  47. package/src/nodes/CustomNode.tsx +0 -1231
  48. package/src/nodes/GenericNode.tsx +0 -55
  49. package/src/nodes/otel/OtelBoundaryNode.tsx +0 -224
  50. package/src/nodes/otel/OtelEventNode.tsx +0 -225
  51. package/src/nodes/otel/OtelResourceNode.tsx +0 -251
  52. package/src/nodes/otel/OtelScopeNode.tsx +0 -221
  53. package/src/nodes/otel/OtelSpanConventionNode.tsx +0 -295
  54. package/src/nodes/otel/index.ts +0 -23
  55. package/src/nodes/otel/shared/NodeBadges.tsx +0 -295
  56. package/src/nodes/otel/shared/NodeContent.tsx +0 -204
  57. package/src/nodes/otel/shared/index.ts +0 -8
  58. package/src/nodes/otel/shared/types.ts +0 -138
  59. package/src/nodes/otel/shared/useNodeBehavior.ts +0 -114
  60. package/src/stories/Audit/BoundaryNodes.stories.tsx +0 -385
  61. package/src/stories/Audit/CanvasEdgeTypes.stories.tsx +0 -1448
  62. package/src/stories/Audit/CanvasNodeTypes.stories.tsx +0 -1049
  63. package/src/stories/Audit/ColorPriority.stories.tsx +0 -895
  64. package/src/stories/Audit/DiamondBadges.stories.tsx +0 -629
  65. package/src/stories/Audit/NodeDimensionsTesting.stories.tsx +0 -724
  66. package/src/stories/Audit/NodeFieldsAudit.stories.tsx +0 -996
  67. package/src/stories/Audit/NodeFontSizeTesting.stories.tsx +0 -715
  68. package/src/stories/Audit/NodeShapes.stories.tsx +0 -1208
  69. package/src/stories/Audit/WideNodesTesting.stories.tsx +0 -1045
  70. package/src/stories/Components/DashboardRenderer.stories.tsx +0 -263
  71. package/src/stories/Components/GraphRenderer.stories.tsx +0 -3236
  72. package/src/stories/Components/MultiCanvasRenderer.stories.tsx +0 -370
  73. package/src/stories/Components/SequenceDiagram.stories.tsx +0 -255
  74. package/src/stories/Components/StateView.stories.tsx +0 -417
  75. package/src/stories/Components/TraceViewer.stories.tsx +0 -442
  76. package/src/stories/Components/WorkflowSequenceDiagram.stories.tsx +0 -802
  77. package/src/stories/Features/AnimationWorkshop.stories.tsx +0 -727
  78. package/src/stories/Features/ChangeTypeVisual.stories.tsx +0 -67
  79. package/src/stories/Features/EventDrivenAnimations.stories.tsx +0 -513
  80. package/src/stories/Features/MultiConfig.stories.tsx +0 -539
  81. package/src/stories/Layout/ElkEdgeRouting.stories.tsx +0 -539
  82. package/src/stories/Layout/GraphOrientation.stories.tsx +0 -500
  83. package/src/stories/Layout/MultiDirectionalConnections.stories.tsx +0 -352
  84. package/src/stories/Nodes/NodeDefinitionComparison.stories.tsx +0 -560
  85. package/src/stories/Nodes/NodeNameFallbacks.stories.tsx +0 -491
  86. package/src/stories/Nodes/NumericColors.stories.tsx +0 -440
  87. package/src/stories/OTEL/EventNameDisplay.stories.tsx +0 -694
  88. package/src/stories/OTEL/EventNameFallback.stories.tsx +0 -578
  89. package/src/stories/OTEL/OtelComponents.stories.tsx +0 -422
  90. package/src/stories/OTEL/OtelNodeTypesPrototype.stories.tsx +0 -806
  91. package/src/stories/OTEL/RawEventCards.stories.tsx +0 -57
  92. package/src/stories/OTEL/SpanBadges.stories.tsx +0 -840
  93. package/src/stories/OTEL/ValidationEventsCanvas.stories.tsx +0 -209
  94. package/src/stories/RealWorld/BacklogMdCanvases.stories.tsx +0 -721
  95. package/src/stories/RealWorld/FileCitySequence.stories.tsx +0 -516
  96. package/src/stories/RealWorld/RealCanvasFiles.stories.tsx +0 -336
  97. package/src/stories/__traces__/test-run.canvas.json +0 -241
  98. package/src/stories/data/activity-feed-analytics.dashboard.json +0 -300
  99. package/src/stories/data/file-city-images-transformed.otel.canvas.json +0 -295
  100. package/src/stories/data/file-city-workflows.json +0 -269
  101. package/src/stories/data/graph-converter-test-execution.json +0 -443
  102. package/src/stories/data/graph-converter-test.workflow.json +0 -105
  103. package/src/stories/data/graph-converter-validated-execution.json +0 -58
  104. package/src/utils/animationMapping.ts +0 -166
  105. package/src/utils/canvasBounds.ts +0 -146
  106. package/src/utils/graphConverter.ts +0 -148
  107. package/src/utils/iconResolver.tsx +0 -53
  108. package/src/utils/orientationUtils.ts +0 -73
@@ -1,1231 +0,0 @@
1
- import React, { useState, useRef, useCallback } from 'react';
2
- import { Handle, Position, NodeResizer, useNodeId } from '@xyflow/react';
3
- import type { NodeProps, Node } from '@xyflow/react';
4
- import type { NodeTypeDefinition } from '@principal-ai/subsystems-core';
5
- import { useTheme } from '@principal-ade/industry-theme';
6
- import { resolveIcon } from '../utils/iconResolver';
7
- import { NodeTooltip } from '../components/NodeTooltip';
8
- import type { OtelInfo } from '../components/NodeTooltip';
9
- import { useGraphEdit } from '../contexts/GraphEditContext';
10
-
11
- // OTEL node components
12
- import {
13
- OtelSpanConventionNode,
14
- OtelEventNode,
15
- OtelScopeNode,
16
- OtelResourceNode,
17
- OtelBoundaryNode,
18
- } from './otel';
19
-
20
- export interface CustomNodeData extends Record<string, unknown> {
21
- name: string;
22
- typeDefinition: NodeTypeDefinition;
23
- state?: string;
24
- hasViolations?: boolean;
25
- data: Record<string, unknown>;
26
- // Animation control
27
- animationType?: 'pulse' | 'flash' | 'shake' | 'entry' | null;
28
- animationDuration?: number;
29
- // Edit mode - shows larger connection handles
30
- editable?: boolean;
31
- // Pending text change (from inline editing, not yet saved)
32
- pendingText?: string;
33
- // Whether tooltips are enabled (defaults to true)
34
- tooltipsEnabled?: boolean;
35
- // Whether shift key is currently pressed (for tooltip control)
36
- shiftKeyPressed?: boolean;
37
- // Whether this node is highlighted (e.g., during execution playback)
38
- isHighlighted?: boolean;
39
- // Whether this node is active (involved in current execution scenario)
40
- // If false, node will be dimmed to de-emphasize it
41
- isActive?: boolean;
42
- // Whether this node is hidden by user (shift-click)
43
- // If true, node will be dimmed less than inactive nodes
44
- isHidden?: boolean;
45
- }
46
-
47
- /**
48
- * Custom node component for xyflow that renders based on NodeTypeDefinition
49
- *
50
- * This component now delegates to specialized OTEL node components when the
51
- * node type matches an OTEL concept. This allows each OTEL node type to have
52
- * its own specialized rendering and features (e.g., workflow chips on span nodes).
53
- */
54
- export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) => {
55
- const { data, selected, dragging } = props;
56
-
57
- // Determine the OTEL node type from nodeData.nodeType (pv.nodeType in canvas)
58
- // The typeDefinition.shape can also hint at OTEL types but nodeType is authoritative
59
- const nodeType = data.data?.nodeType as string | undefined;
60
-
61
- // Delegate to specialized OTEL node components
62
- switch (nodeType) {
63
- case 'otel-span-convention':
64
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
- return <OtelSpanConventionNode {...(props as any)} />;
66
- case 'otel-event':
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
- return <OtelEventNode {...(props as any)} />;
69
- case 'otel-scope':
70
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
- return <OtelScopeNode {...(props as any)} />;
72
- case 'otel-resource':
73
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
- return <OtelResourceNode {...(props as any)} />;
75
- case 'otel-boundary':
76
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
- return <OtelBoundaryNode {...(props as any)} />;
78
- }
79
-
80
- // Fall through to legacy rendering for non-OTEL nodes
81
- const { theme } = useTheme();
82
- const { onNodeResizeEnd, onNodeTextChange, onToggleNodeHidden, onHideUnconnectedNodes } = useGraphEdit();
83
- const nodeId = useNodeId();
84
- const [isHovered, setIsHovered] = useState(false);
85
- const [isEditing, setIsEditing] = useState(false);
86
- const [editingText, setEditingText] = useState('');
87
- const nodeRef = useRef<HTMLDivElement>(null);
88
- const textareaRef = useRef<HTMLTextAreaElement>(null);
89
- const nodeProps = data;
90
- const {
91
- typeDefinition,
92
- state,
93
- hasViolations,
94
- data: nodeData,
95
- animationType,
96
- animationDuration = 1000,
97
- editable = false,
98
- tooltipsEnabled = true,
99
- shiftKeyPressed = false,
100
- isHighlighted = false,
101
- isActive = true, // Default to active if not specified
102
- isHidden = false, // Default to not hidden
103
- } = nodeProps;
104
-
105
- // Calculate opacity based on node state
106
- // Hidden nodes (shift-clicked) are dimmed to 0.4
107
- // Inactive nodes (scenario filtering) are dimmed to 0.1
108
- const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
109
-
110
- // Handle resize end - notify parent to track the dimension change
111
- const handleResizeEnd = useCallback(
112
- (_event: unknown, params: { width: number; height: number }) => {
113
- if (nodeId && onNodeResizeEnd && params.width && params.height) {
114
- onNodeResizeEnd(nodeId, {
115
- width: Math.round(params.width),
116
- height: Math.round(params.height),
117
- });
118
- }
119
- },
120
- [nodeId, onNodeResizeEnd]
121
- );
122
-
123
- // Handle Cmd/Ctrl+mousedown to toggle hidden state
124
- // We use mousedown instead of click because in edit mode with draggable nodes,
125
- // ReactFlow's drag system intercepts Cmd+click before it becomes a click event
126
- // - Cmd/Ctrl+Shift+click: hide all nodes not directly connected to this node
127
- // - Cmd/Ctrl+click: toggle this single node's hidden state
128
- const handleMouseDown = useCallback(
129
- (event: React.MouseEvent) => {
130
- if ((event.metaKey || event.ctrlKey) && nodeId) {
131
- event.preventDefault();
132
- event.stopPropagation();
133
-
134
- if (event.shiftKey && onHideUnconnectedNodes) {
135
- // Cmd/Ctrl+Shift+click: hide unconnected nodes
136
- onHideUnconnectedNodes(nodeId);
137
- } else if (onToggleNodeHidden) {
138
- // Cmd/Ctrl+click: toggle single node
139
- onToggleNodeHidden(nodeId);
140
- }
141
- }
142
- },
143
- [nodeId, onToggleNodeHidden, onHideUnconnectedNodes]
144
- );
145
-
146
- // Show tooltip when:
147
- // 1. Hovering + not dragging + shift key pressed (always works), OR
148
- // 2. Node is selected AND not in edit mode (read-only selection shows tooltip)
149
- const showTooltip =
150
- (isHovered && !dragging && shiftKeyPressed) || (!editable && !!selected);
151
-
152
- // Extract OTEL info, description, sources/files, and references for tooltip
153
- const otelInfo = nodeData?.otel as (OtelInfo & { files?: string[] }) | undefined;
154
- const description = nodeData?.description as string | undefined;
155
- const sources = nodeData?.sources as string[] | undefined; // deprecated
156
- const references = nodeData?.references as string[] | undefined;
157
- // Files from otel.files - these are source code files where the event is instrumented
158
- const files = otelInfo?.files;
159
-
160
- // Get badge shape styles based on node shape
161
- const getBadgeShapeStyles = (): React.CSSProperties => {
162
- const shape = typeDefinition.shape;
163
- const baseSize = 18;
164
-
165
- switch (shape) {
166
- case 'circle':
167
- return {
168
- width: baseSize,
169
- height: baseSize,
170
- borderRadius: '50%',
171
- };
172
- case 'diamond':
173
- return {
174
- width: baseSize - 4,
175
- height: baseSize - 4,
176
- borderRadius: 0,
177
- transform: 'rotate(45deg)',
178
- };
179
- case 'hexagon':
180
- return {
181
- width: baseSize,
182
- height: baseSize,
183
- borderRadius: 0,
184
- clipPath: 'polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)',
185
- };
186
- case 'rectangle':
187
- default:
188
- return {
189
- width: baseSize,
190
- height: baseSize,
191
- borderRadius: 0,
192
- };
193
- }
194
- };
195
-
196
- // Get badge position based on shape - diamonds need badges at their points, not bounding box corners
197
- const getBadgePosition = (position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'left' | 'right' | 'top' | 'bottom'): React.CSSProperties => {
198
- const isDiamondShape = typeDefinition.shape === 'diamond';
199
-
200
- if (isDiamondShape) {
201
- // Diamond points are at the middle of each edge of the bounding box
202
- switch (position) {
203
- case 'top-left':
204
- case 'left':
205
- // Position at the LEFT point of the diamond (center-left)
206
- return { top: '50%', left: 0, transform: 'translate(-50%, -50%)' };
207
- case 'top-right':
208
- case 'right':
209
- // Position at the RIGHT point of the diamond (center-right)
210
- return { top: '50%', right: 0, transform: 'translate(50%, -50%)' };
211
- case 'top':
212
- // Position at the TOP point of the diamond
213
- return { top: 0, left: '50%', transform: 'translate(-50%, -50%)' };
214
- case 'bottom':
215
- case 'bottom-left':
216
- case 'bottom-right':
217
- // Position at the BOTTOM point of the diamond
218
- return { bottom: 0, left: '50%', transform: 'translate(-50%, 50%)' };
219
- }
220
- }
221
-
222
- // Default positioning for rectangles, circles, hexagons (bounding box corners)
223
- switch (position) {
224
- case 'top-left':
225
- return { top: -6, left: -6 };
226
- case 'top-right':
227
- return { top: -6, right: -6 };
228
- case 'bottom-left':
229
- return { bottom: -6, left: -6 };
230
- case 'bottom-right':
231
- return { bottom: -6, right: -6 };
232
- case 'left':
233
- return { top: -6, left: -6 };
234
- case 'right':
235
- return { top: -6, right: -6 };
236
- case 'top':
237
- return { top: -6, left: '50%', transform: 'translateX(-50%)' };
238
- case 'bottom':
239
- return { bottom: -6, left: '50%', transform: 'translateX(-50%)' };
240
- }
241
- };
242
-
243
- // Render Sources badge (top-right) - shows "S" for files where event is instrumented
244
- const renderSourcesBadge = () => {
245
- // Use otel.files (source code files where event is instrumented)
246
- // Fall back to deprecated sources field for backwards compatibility
247
- const sourceFiles = files || sources;
248
- if (!sourceFiles || sourceFiles.length === 0) return null;
249
-
250
- const shapeStyles = getBadgeShapeStyles();
251
- const positionStyles = getBadgePosition('top-right');
252
-
253
- return (
254
- <div
255
- style={{
256
- position: 'absolute',
257
- ...positionStyles,
258
- ...shapeStyles,
259
- // Override transform if shape has rotation but we already have a position transform
260
- ...(typeDefinition.shape === 'diamond' ? { transform: `${positionStyles.transform} rotate(45deg)` } : {}),
261
- backgroundColor: '#10b981', // Green for sources
262
- color: 'white',
263
- fontSize: theme.fontSizes[0],
264
- fontWeight: theme.fontWeights.bold,
265
- fontFamily: theme.fonts.body,
266
- display: 'flex',
267
- alignItems: 'center',
268
- justifyContent: 'center',
269
- boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
270
- zIndex: 10,
271
- opacity: nodeOpacity,
272
- }}
273
- title={`Source files: ${sourceFiles.join(', ')}`}
274
- >
275
- <span style={{ transform: shapeStyles.transform ? 'rotate(-45deg)' : undefined }}>S</span>
276
- </div>
277
- );
278
- };
279
-
280
- // Render References badge (bottom-left for rectangles, bottom for diamonds) - shows "R" for external references
281
- const renderReferencesBadge = () => {
282
- if (!references || references.length === 0) return null;
283
-
284
- const shapeStyles = getBadgeShapeStyles();
285
- const positionStyles = getBadgePosition('bottom-left');
286
-
287
- return (
288
- <div
289
- style={{
290
- position: 'absolute',
291
- ...positionStyles,
292
- ...shapeStyles,
293
- // Override transform if shape has rotation but we already have a position transform
294
- ...(typeDefinition.shape === 'diamond' ? { transform: `${positionStyles.transform} rotate(45deg)` } : {}),
295
- backgroundColor: '#8b5cf6', // Purple for references
296
- color: 'white',
297
- fontSize: theme.fontSizes[0],
298
- fontWeight: theme.fontWeights.bold,
299
- fontFamily: theme.fonts.body,
300
- display: 'flex',
301
- alignItems: 'center',
302
- justifyContent: 'center',
303
- boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
304
- zIndex: 10,
305
- opacity: nodeOpacity,
306
- }}
307
- title={`References: ${references.join(', ')}`}
308
- >
309
- <span style={{ transform: shapeStyles.transform ? 'rotate(-45deg)' : undefined }}>R</span>
310
- </div>
311
- );
312
- };
313
-
314
- // Render Boundary badge (right/left points) - shown instead of sources badge for boundary nodes
315
- const renderBoundaryBadge = () => {
316
- const boundary = nodeData?.boundary as { direction?: 'outbound' | 'inbound'; node?: Record<string, string> } | undefined;
317
- if (!boundary) return null;
318
-
319
- const direction = boundary.direction || 'outbound';
320
- const isOutbound = direction === 'outbound';
321
-
322
- // Direction indicators
323
- const directionIcon = isOutbound ? '↗' : '↙';
324
- const directionTitle = isOutbound ? 'Outbound boundary (calls external system)' : 'Inbound boundary (called by external system)';
325
-
326
- const positionStyles = getBadgePosition(isOutbound ? 'right' : 'left');
327
-
328
- return (
329
- <div
330
- style={{
331
- position: 'absolute',
332
- ...positionStyles,
333
- width: 18,
334
- height: 18,
335
- borderRadius: '50%', // Always circular for boundary badge
336
- backgroundColor: '#06b6d4', // Cyan for boundaries
337
- color: 'white',
338
- fontSize: 12,
339
- fontWeight: theme.fontWeights.bold,
340
- fontFamily: theme.fonts.body,
341
- display: 'flex',
342
- alignItems: 'center',
343
- justifyContent: 'center',
344
- boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
345
- zIndex: 10,
346
- opacity: nodeOpacity,
347
- }}
348
- title={directionTitle}
349
- >
350
- {directionIcon}
351
- </div>
352
- );
353
- };
354
-
355
- // Check if this is a boundary node
356
- const isBoundaryNode = nodeData?.nodeType === 'boundary';
357
-
358
- // Render Status badge (top-left, or top point for diamonds)
359
- const renderStatusBadge = () => {
360
- const status = nodeData?.status as 'draft' | 'approved' | 'implemented' | undefined;
361
- if (!status) return null;
362
-
363
- // Color mapping for status
364
- const statusColors = {
365
- draft: '#6b7280', // Gray
366
- approved: '#3b82f6', // Blue
367
- implemented: '#10b981', // Green
368
- };
369
-
370
- const statusLabels = {
371
- draft: 'D',
372
- approved: 'A',
373
- implemented: 'I',
374
- };
375
-
376
- const statusTitles = {
377
- draft: 'Draft - Design phase',
378
- approved: 'Approved - Ready for implementation',
379
- implemented: 'Implemented - Code exists',
380
- };
381
-
382
- const shapeStyles = getBadgeShapeStyles();
383
- const positionStyles = getBadgePosition('top-left');
384
-
385
- return (
386
- <div
387
- style={{
388
- position: 'absolute',
389
- ...positionStyles,
390
- ...shapeStyles,
391
- // Override transform if shape has rotation but we already have a position transform
392
- ...(typeDefinition.shape === 'diamond' ? { transform: `${positionStyles.transform} rotate(45deg)` } : {}),
393
- backgroundColor: statusColors[status],
394
- color: 'white',
395
- fontSize: theme.fontSizes[0],
396
- fontWeight: theme.fontWeights.bold,
397
- fontFamily: theme.fonts.body,
398
- display: 'flex',
399
- alignItems: 'center',
400
- justifyContent: 'center',
401
- boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
402
- zIndex: 10,
403
- opacity: nodeOpacity,
404
- }}
405
- title={statusTitles[status]}
406
- >
407
- <span style={{ transform: shapeStyles.transform ? 'rotate(-45deg)' : undefined }}>
408
- {statusLabels[status]}
409
- </span>
410
- </div>
411
- );
412
- };
413
-
414
- // Guard against missing typeDefinition
415
- if (!typeDefinition) {
416
- return (
417
- <div style={{ padding: '10px', border: '2px solid red', borderRadius: '4px' }}>
418
- <div style={{ fontSize: '12px', color: 'red' }}>Error: Missing node type definition</div>
419
- </div>
420
- );
421
- }
422
-
423
- // Color Contract:
424
- // - scopeColor: Used as FILL/background color (from library.yaml scopes)
425
- // - spanColor: Used as BORDER color in workflow context (from .spans.canvas)
426
- // - For non-event canvases, falls back to legacy behavior (node color or type color)
427
-
428
- // Get colors from node data (injected by GraphRenderer)
429
- const nodeDataColor = nodeData.color as string | undefined;
430
- const scopeColor = nodeData.scopeColor as string | undefined;
431
- const spanColor = nodeData.spanColor as string | undefined;
432
-
433
- // Check node's own states first (from pv.states), then fall back to type definition states
434
- const nodeDataStates = nodeData.states as
435
- | Record<string, { color?: string; label?: string; icon?: string }>
436
- | undefined;
437
- const stateColor =
438
- state && (nodeDataStates?.[state]?.color || typeDefinition.states?.[state]?.color);
439
-
440
- // Fill color priority: state color > node data color > scope color > type definition color > default
441
- // scopeColor provides background color from the scope definition in library.yaml
442
- const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#888';
443
- const fillColor = stateColor || baseFillColor;
444
-
445
- // Stroke/border color priority: explicit stroke > span color (workflow context) > fill color
446
- // spanColor provides border color in workflow context (from .spans.canvas)
447
- const nodeDataStroke = nodeData.stroke as string | undefined;
448
- const baseStrokeColor = nodeDataStroke || spanColor || fillColor;
449
-
450
- // Apply status-based border styling
451
- const status = nodeData?.status as 'draft' | 'approved' | 'implemented' | undefined;
452
- const strokeColor = baseStrokeColor;
453
-
454
- // Use fillColor as the primary "color" for backwards compatibility
455
- const color = fillColor;
456
-
457
- // Get display name - use pending text if available, otherwise use name from props
458
- const displayName = nodeProps.pendingText ?? nodeProps.name;
459
-
460
- // Extract identifier based on node type (for display below the label)
461
- // Supports: event.name, otel.spanPattern, otel.scope, otel.resourceMatch, boundary.direction
462
- const eventData = nodeData?.event as { name?: string; description?: string; attributes?: Record<string, unknown> } | undefined;
463
- const otelData = nodeData?.otel as { spanPattern?: string; scope?: string; resourceMatch?: Record<string, string | string[]> } | undefined;
464
- const boundaryData = nodeData?.boundary as { direction?: string } | undefined;
465
-
466
- // Get identifier from multiple sources
467
- const getNodeIdentifier = (): string | undefined => {
468
- // Event name (for otel-event nodes)
469
- if (eventData?.name) return eventData.name;
470
- // Event ref (for nodes using library events)
471
- if (nodeData?.eventRef) return nodeData.eventRef as string;
472
- // Span pattern (for otel-span-convention nodes)
473
- if (otelData?.spanPattern) return otelData.spanPattern;
474
- // Scope name (for otel-scope nodes)
475
- if (otelData?.scope) return otelData.scope;
476
- // Resource match (for otel-resource nodes) - show first key:value
477
- if (otelData?.resourceMatch) {
478
- const entries = Object.entries(otelData.resourceMatch);
479
- if (entries.length > 0) {
480
- const [key, value] = entries[0];
481
- return `${key}: ${Array.isArray(value) ? value[0] : value}`;
482
- }
483
- }
484
- // Boundary direction (for otel-boundary nodes)
485
- if (boundaryData?.direction) return boundaryData.direction;
486
- return undefined;
487
- };
488
-
489
- const identifier = getNodeIdentifier();
490
-
491
- // Show identifier if it differs from display name
492
- const showIdentifier = identifier && identifier !== displayName;
493
-
494
- // Helper to render text with word break opportunities after dots
495
- const renderWithDotBreaks = (text: string) => {
496
- const parts = text.split('.');
497
- return parts.map((part, i) => (
498
- <span key={i}>
499
- {part}
500
- {i < parts.length - 1 && (
501
- <>
502
- .<wbr />
503
- </>
504
- )}
505
- </span>
506
- ));
507
- };
508
-
509
- // Helper component for rendering name with optional identifier
510
- // Identifiers can come from: event.name, eventRef, otel.spanPattern, otel.scope, otel.resourceMatch, boundary.direction
511
- const renderNameWithIdentifier = (centered: boolean = true) => (
512
- <div style={{ textAlign: centered ? 'center' : 'left', wordBreak: 'break-word' }}>
513
- <div>{displayName}</div>
514
- {showIdentifier && (
515
- <div
516
- style={{
517
- fontSize: theme.fontSizes[0] * 0.75, // 75% of the main font size
518
- color: 'rgba(0, 0, 0, 0.5)', // 50% opacity for subtle appearance
519
- marginTop: '2px',
520
- fontFamily: theme.fonts.monospace,
521
- }}
522
- >
523
- {renderWithDotBreaks(identifier)}
524
- </div>
525
- )}
526
- </div>
527
- );
528
-
529
- // Icon priority: node data override > state icon (node data states first) > type definition icon
530
- const icon =
531
- (nodeData.icon as string) ||
532
- (state && (nodeDataStates?.[state]?.icon || typeDefinition.states?.[state]?.icon)) ||
533
- typeDefinition.icon;
534
-
535
- // Get animation class based on type
536
- const getAnimationClass = () => {
537
- switch (animationType) {
538
- case 'pulse':
539
- return 'node-pulse';
540
- case 'flash':
541
- return 'node-flash';
542
- case 'shake':
543
- return 'node-shake';
544
- case 'entry':
545
- return 'node-entry';
546
- default:
547
- return '';
548
- }
549
- };
550
-
551
- const animationClass = getAnimationClass();
552
-
553
- // Check if this is a group node or text node (canvas types that support inline editing)
554
- const isGroup = nodeData.canvasType === 'group';
555
- const isTextNode = nodeData.canvasType === 'text';
556
- const isInlineEditable = (isGroup || isTextNode) && editable;
557
-
558
- // Double-click handler for inline text editing
559
- const lastClickTimeRef = useRef<number>(0);
560
- const handleNodeDoubleClick = useCallback((event: React.MouseEvent) => {
561
- if (!isInlineEditable || !nodeId || !onNodeTextChange) return;
562
-
563
- event.stopPropagation();
564
- event.preventDefault();
565
-
566
- setEditingText(displayName || '');
567
- setIsEditing(true);
568
-
569
- // Focus the textarea after state update
570
- setTimeout(() => {
571
- textareaRef.current?.focus();
572
- textareaRef.current?.select();
573
- }, 0);
574
- }, [isInlineEditable, nodeId, onNodeTextChange, displayName]);
575
-
576
- // Single click tracking for double-click detection
577
- const handleNodeClick = useCallback((event: React.MouseEvent) => {
578
- const now = Date.now();
579
- const timeSinceLastClick = now - lastClickTimeRef.current;
580
-
581
- if (timeSinceLastClick < 300) {
582
- // Double-click detected
583
- handleNodeDoubleClick(event);
584
- }
585
-
586
- lastClickTimeRef.current = now;
587
- }, [handleNodeDoubleClick]);
588
-
589
- // Save text changes
590
- const handleSaveText = useCallback(() => {
591
- if (!nodeId || !onNodeTextChange) return;
592
-
593
- if (editingText !== displayName) {
594
- onNodeTextChange(nodeId, editingText);
595
- }
596
-
597
- setIsEditing(false);
598
- }, [nodeId, onNodeTextChange, editingText, displayName]);
599
-
600
- // Cancel text editing
601
- const handleCancelEdit = useCallback(() => {
602
- setIsEditing(false);
603
- setEditingText('');
604
- }, []);
605
-
606
- // Shape-specific styles
607
- const getShapeStyles = () => {
608
- const borderStyle = status === 'draft' ? 'dotted' : status === 'approved' ? 'dashed' : 'solid';
609
- const baseStyles = {
610
- padding: '12px 16px',
611
- backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' : fillColor,
612
- color: '#000',
613
- border: `2px ${borderStyle} ${hasViolations ? '#D0021B' : strokeColor}`,
614
- fontSize: theme.fontSizes[0],
615
- fontWeight: theme.fontWeights.medium,
616
- fontFamily: theme.fonts.body,
617
- // Use 100% width/height to fill the node container (for resizing support)
618
- width: '100%',
619
- height: '100%',
620
- // Use small absolute minimums - typeDefinition.size is the default, not the minimum
621
- minWidth: 20,
622
- minHeight: 20,
623
- display: 'flex',
624
- flexDirection: 'column' as const,
625
- alignItems: 'center',
626
- justifyContent: isGroup ? 'flex-start' : 'center',
627
- gap: '4px',
628
- boxShadow: isHighlighted
629
- ? `0 0 0 3px #3b82f6, 0 0 20px rgba(59, 130, 246, 0.5)`
630
- : selected
631
- ? `0 0 0 2px ${strokeColor}`
632
- : '0 2px 4px rgba(0,0,0,0.1)',
633
- opacity: nodeOpacity,
634
- transition: 'box-shadow 0.2s ease, opacity 0.3s ease',
635
- animationDuration: animationType ? `${animationDuration}ms` : undefined,
636
- boxSizing: 'border-box' as const,
637
- };
638
-
639
- switch (typeDefinition.shape) {
640
- case 'circle':
641
- return {
642
- ...baseStyles,
643
- borderRadius: '50%',
644
- padding: '8px',
645
- };
646
- case 'hexagon':
647
- // Hexagon uses wrapper approach for proper border - styles returned here are for inner fill
648
- // The outer border wrapper is rendered separately in the JSX
649
- return {
650
- ...baseStyles,
651
- border: 'none', // Border handled by wrapper
652
- clipPath: 'polygon(20% 0%, 80% 0%, 100% 50%, 80% 100%, 20% 100%, 0% 50%)',
653
- width: '100%',
654
- height: '100%',
655
- minWidth: 'unset',
656
- minHeight: 'unset',
657
- padding: '8px 20px',
658
- boxShadow: 'none', // Shadow handled by wrapper
659
- };
660
- case 'diamond':
661
- // Diamond uses wrapper approach for proper border - styles returned here are for inner fill
662
- // The outer border wrapper is rendered separately in the JSX
663
- return {
664
- ...baseStyles,
665
- border: 'none', // Border handled by wrapper
666
- clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
667
- width: '100%',
668
- height: '100%',
669
- minWidth: 'unset',
670
- minHeight: 'unset',
671
- padding: '8px 16px',
672
- boxShadow: 'none', // Shadow handled by wrapper
673
- };
674
- case 'rectangle':
675
- default:
676
- return {
677
- ...baseStyles,
678
- // Boundary nodes get rounded corners, regular rectangles get sharp corners
679
- borderRadius: isBoundaryNode ? '8px' : '0',
680
- };
681
- }
682
- };
683
-
684
- const isDiamond = typeDefinition.shape === 'diamond';
685
- const isHexagon = typeDefinition.shape === 'hexagon';
686
- const isCircle = typeDefinition.shape === 'circle';
687
-
688
- // Determine if aspect ratio should be locked (circles should maintain square aspect)
689
- const keepAspectRatio = isCircle;
690
-
691
- // Minimum dimensions for resizing - use small absolute values, not typeDefinition.size
692
- const minWidth = 40;
693
- const minHeight = isCircle ? 40 : 30;
694
-
695
- // Hexagon border wrapper styles (outer shape that acts as border)
696
- // Hexagon with gentle diagonals
697
- const hexagonClipPath = 'polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)';
698
- // Note: Dotted/dashed borders not supported for hexagon shape (CSS limitation with clip-path)
699
- // All statuses use 2px solid border for hexagons
700
- const hexagonBorderWidth = 2;
701
- const hexagonBorderStyle: React.CSSProperties = isHexagon
702
- ? {
703
- position: 'relative',
704
- clipPath: hexagonClipPath,
705
- backgroundColor: hasViolations ? '#D0021B' : strokeColor,
706
- // Use 100% to fill container for resizing support
707
- width: '100%',
708
- height: '100%',
709
- // Use small absolute minimums - typeDefinition.size is the default, not the minimum
710
- minWidth: 20,
711
- minHeight: 20,
712
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
713
- opacity: nodeOpacity,
714
- transition: 'opacity 0.3s ease',
715
- boxSizing: 'border-box',
716
- }
717
- : {};
718
-
719
- // Hexagon inner fill styles (light color background inset from border)
720
- const hexagonInnerStyle: React.CSSProperties = isHexagon
721
- ? {
722
- position: 'absolute',
723
- top: hexagonBorderWidth,
724
- left: hexagonBorderWidth,
725
- right: hexagonBorderWidth,
726
- bottom: hexagonBorderWidth,
727
- clipPath: hexagonClipPath,
728
- backgroundColor: fillColor,
729
- color: '#000',
730
- display: 'flex',
731
- flexDirection: 'column',
732
- alignItems: 'center',
733
- justifyContent: 'center',
734
- fontSize: theme.fontSizes[0],
735
- fontWeight: theme.fontWeights.medium,
736
- fontFamily: theme.fonts.body,
737
- gap: '4px',
738
- }
739
- : {};
740
-
741
- // Diamond clip-path
742
- const diamondClipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)';
743
- // Note: Dotted/dashed borders not supported for diamond shape (CSS limitation with clip-path)
744
- // All statuses use 2px solid border for diamonds
745
- const diamondBorderWidth = 2;
746
-
747
- // Diamond border wrapper styles (outer shape that acts as border)
748
- const diamondBorderStyle: React.CSSProperties = isDiamond
749
- ? {
750
- position: 'relative',
751
- clipPath: diamondClipPath,
752
- backgroundColor: hasViolations ? '#D0021B' : strokeColor,
753
- width: '100%',
754
- height: '100%',
755
- // Use small absolute minimums - typeDefinition.size is the default, not the minimum
756
- minWidth: 20,
757
- minHeight: 20,
758
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
759
- opacity: nodeOpacity,
760
- transition: 'opacity 0.3s ease',
761
- boxSizing: 'border-box',
762
- }
763
- : {};
764
-
765
- // Diamond inner fill styles (light color background inset from border)
766
- const diamondInnerStyle: React.CSSProperties = isDiamond
767
- ? {
768
- position: 'absolute',
769
- top: diamondBorderWidth,
770
- left: diamondBorderWidth,
771
- right: diamondBorderWidth,
772
- bottom: diamondBorderWidth,
773
- clipPath: diamondClipPath,
774
- backgroundColor: fillColor,
775
- color: '#000',
776
- display: 'flex',
777
- flexDirection: 'column',
778
- alignItems: 'center',
779
- justifyContent: 'center',
780
- fontSize: theme.fontSizes[0],
781
- fontWeight: theme.fontWeights.medium,
782
- fontFamily: theme.fonts.body,
783
- gap: '4px',
784
- }
785
- : {};
786
-
787
- // Handle styles - larger and more visible in edit mode, hidden otherwise
788
- const baseHandleStyle = editable
789
- ? {
790
- background: color,
791
- width: 12,
792
- height: 12,
793
- border: '2px solid white',
794
- boxShadow: '0 0 0 1px ' + color,
795
- }
796
- : {
797
- background: color,
798
- width: 8,
799
- height: 8,
800
- opacity: 0,
801
- pointerEvents: 'none' as const,
802
- };
803
-
804
- const getHandleStyle = (_position: 'top' | 'bottom' | 'left' | 'right') => {
805
- if (!isDiamond && !isHexagon) return baseHandleStyle;
806
-
807
- const offsetStyle: React.CSSProperties = { ...baseHandleStyle };
808
-
809
- // Bring handles above the clip-path layers for hexagon and diamond
810
- if (isHexagon || isDiamond) {
811
- offsetStyle.zIndex = 10;
812
- }
813
-
814
- return offsetStyle;
815
- };
816
-
817
- return (
818
- <>
819
- {/* Node Resizer - only shown in edit mode */}
820
- {editable && (
821
- <NodeResizer
822
- color={strokeColor}
823
- isVisible={selected}
824
- minWidth={minWidth}
825
- minHeight={minHeight}
826
- keepAspectRatio={keepAspectRatio}
827
- onResizeEnd={handleResizeEnd}
828
- handleStyle={{
829
- width: 8,
830
- height: 8,
831
- borderRadius: 2,
832
- zIndex: 20,
833
- }}
834
- lineStyle={{
835
- borderWidth: 1,
836
- zIndex: 20,
837
- }}
838
- />
839
- )}
840
-
841
- {/* Input handles - all 4 sides for incoming connections */}
842
- <Handle type="target" position={Position.Top} id="top" style={getHandleStyle('top')} />
843
- <Handle
844
- type="target"
845
- position={Position.Bottom}
846
- id="bottom"
847
- style={getHandleStyle('bottom')}
848
- />
849
- <Handle type="target" position={Position.Left} id="left" style={getHandleStyle('left')} />
850
- <Handle type="target" position={Position.Right} id="right" style={getHandleStyle('right')} />
851
-
852
- {/* Hexagon and Diamond need a wrapper for proper border rendering */}
853
- {isHexagon ? (
854
- <div
855
- ref={nodeRef}
856
- style={{
857
- position: 'relative',
858
- width: '100%',
859
- height: '100%',
860
- boxShadow: isHighlighted
861
- ? `0 0 0 3px #3b82f6, 0 0 20px rgba(59, 130, 246, 0.5)`
862
- : selected
863
- ? `0 0 0 2px ${strokeColor}`
864
- : 'none',
865
- transition: 'box-shadow 0.2s ease',
866
- }}
867
- onMouseDown={handleMouseDown}
868
- onMouseEnter={() => setIsHovered(true)}
869
- onMouseLeave={() => setIsHovered(false)}
870
- >
871
- {renderStatusBadge()}
872
- {isBoundaryNode ? renderBoundaryBadge() : (
873
- <>
874
- {renderSourcesBadge()}
875
- {renderReferencesBadge()}
876
- </>
877
- )}
878
- <div style={hexagonBorderStyle} className={animationClass}>
879
- <div style={hexagonInnerStyle}>
880
- {icon && (
881
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
882
- {resolveIcon(icon, 20)}
883
- </div>
884
- )}
885
- {renderNameWithIdentifier()}
886
- {state && (
887
- <div
888
- style={{
889
- fontSize: theme.fontSizes[0],
890
- fontFamily: theme.fonts.body,
891
- backgroundColor: color,
892
- color: 'white',
893
- padding: '2px 6px',
894
- borderRadius: '4px',
895
- textAlign: 'center',
896
- }}
897
- >
898
- {nodeDataStates?.[state]?.label || typeDefinition.states?.[state]?.label || state}
899
- </div>
900
- )}
901
- {hasViolations && (
902
- <div
903
- style={{
904
- fontSize: theme.fontSizes[0],
905
- fontFamily: theme.fonts.body,
906
- color: '#D0021B',
907
- fontWeight: theme.fontWeights.bold,
908
- }}
909
- >
910
- ⚠️
911
- </div>
912
- )}
913
- </div>
914
- </div>
915
- {tooltipsEnabled && (
916
- <NodeTooltip
917
- description={description}
918
- otel={otelInfo}
919
- sources={sources}
920
- references={references}
921
- visible={showTooltip}
922
- nodeRef={nodeRef}
923
- />
924
- )}
925
- </div>
926
- ) : isDiamond ? (
927
- <div
928
- ref={nodeRef}
929
- style={{
930
- position: 'relative',
931
- width: '100%',
932
- height: '100%',
933
- boxShadow: isHighlighted
934
- ? `0 0 0 3px #3b82f6, 0 0 20px rgba(59, 130, 246, 0.5)`
935
- : selected
936
- ? `0 0 0 2px ${strokeColor}`
937
- : 'none',
938
- transition: 'box-shadow 0.2s ease',
939
- }}
940
- onMouseDown={handleMouseDown}
941
- onMouseEnter={() => setIsHovered(true)}
942
- onMouseLeave={() => setIsHovered(false)}
943
- >
944
- {renderStatusBadge()}
945
- {isBoundaryNode ? renderBoundaryBadge() : (
946
- <>
947
- {renderSourcesBadge()}
948
- {renderReferencesBadge()}
949
- </>
950
- )}
951
- <div style={diamondBorderStyle} className={animationClass}>
952
- <div style={diamondInnerStyle}>
953
- {icon && (
954
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
955
- {resolveIcon(icon, 20)}
956
- </div>
957
- )}
958
- {renderNameWithIdentifier()}
959
- {state && (
960
- <div
961
- style={{
962
- fontSize: theme.fontSizes[0],
963
- fontFamily: theme.fonts.body,
964
- backgroundColor: color,
965
- color: 'white',
966
- padding: '2px 6px',
967
- borderRadius: '4px',
968
- textAlign: 'center',
969
- }}
970
- >
971
- {nodeDataStates?.[state]?.label || typeDefinition.states?.[state]?.label || state}
972
- </div>
973
- )}
974
- {hasViolations && (
975
- <div
976
- style={{
977
- fontSize: theme.fontSizes[0],
978
- fontFamily: theme.fonts.body,
979
- color: '#D0021B',
980
- fontWeight: theme.fontWeights.bold,
981
- }}
982
- >
983
- ⚠️
984
- </div>
985
- )}
986
- </div>
987
- </div>
988
- {tooltipsEnabled && (
989
- <NodeTooltip
990
- description={description}
991
- otel={otelInfo}
992
- sources={sources}
993
- references={references}
994
- visible={showTooltip}
995
- nodeRef={nodeRef}
996
- />
997
- )}
998
- </div>
999
- ) : (
1000
- <div
1001
- ref={nodeRef}
1002
- style={{ position: 'relative', width: '100%', height: '100%' }}
1003
- onMouseDown={handleMouseDown}
1004
- onClick={handleNodeClick}
1005
- onMouseEnter={() => setIsHovered(true)}
1006
- onMouseLeave={() => setIsHovered(false)}
1007
- >
1008
- {renderStatusBadge()}
1009
- {isBoundaryNode ? renderBoundaryBadge() : (
1010
- <>
1011
- {renderSourcesBadge()}
1012
- {renderReferencesBadge()}
1013
- </>
1014
- )}
1015
- <div style={getShapeStyles()} className={animationClass}>
1016
- {/* Inner content */}
1017
- <div
1018
- style={{
1019
- ...(isGroup ? { width: '100%' } : {}),
1020
- }}
1021
- >
1022
- {/* Groups: icon and text inline, centered */}
1023
- {isGroup ? (
1024
- <div
1025
- style={{
1026
- display: 'flex',
1027
- alignItems: 'center',
1028
- justifyContent: 'center',
1029
- gap: '6px',
1030
- width: '100%',
1031
- }}
1032
- >
1033
- {icon && resolveIcon(icon, 18)}
1034
- {renderNameWithIdentifier(false)}
1035
- </div>
1036
- ) : (
1037
- <>
1038
- {icon && (
1039
- <div
1040
- style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}
1041
- >
1042
- {resolveIcon(icon, 20)}
1043
- </div>
1044
- )}
1045
- {renderNameWithIdentifier()}
1046
- </>
1047
- )}
1048
- {state && (
1049
- <div
1050
- style={{
1051
- fontSize: theme.fontSizes[0],
1052
- fontFamily: theme.fonts.body,
1053
- backgroundColor: color,
1054
- color: 'white',
1055
- padding: '2px 6px',
1056
- borderRadius: '4px',
1057
- textAlign: 'center',
1058
- }}
1059
- >
1060
- {nodeDataStates?.[state]?.label || typeDefinition.states?.[state]?.label || state}
1061
- </div>
1062
- )}
1063
- {hasViolations && (
1064
- <div
1065
- style={{
1066
- fontSize: theme.fontSizes[0],
1067
- fontFamily: theme.fonts.body,
1068
- color: '#D0021B',
1069
- fontWeight: theme.fontWeights.bold,
1070
- }}
1071
- >
1072
- ⚠️
1073
- </div>
1074
- )}
1075
- </div>
1076
- </div>
1077
- {/* Inline text editor overlay */}
1078
- {isEditing && isInlineEditable && (
1079
- <div
1080
- style={{
1081
- position: 'absolute',
1082
- top: 0,
1083
- left: 0,
1084
- width: '100%',
1085
- height: '100%',
1086
- zIndex: 1000,
1087
- display: 'flex',
1088
- flexDirection: 'column',
1089
- padding: theme.space[1],
1090
- boxSizing: 'border-box',
1091
- }}
1092
- onClick={(e) => e.stopPropagation()}
1093
- >
1094
- <textarea
1095
- ref={textareaRef}
1096
- value={editingText}
1097
- onChange={(e) => setEditingText(e.target.value)}
1098
- placeholder="Enter text..."
1099
- style={{
1100
- flex: 1,
1101
- width: '100%',
1102
- padding: theme.space[2],
1103
- fontSize: theme.fontSizes[1],
1104
- fontFamily: theme.fonts.body,
1105
- color: theme.colors.text,
1106
- backgroundColor: theme.colors.background,
1107
- border: `3px solid ${theme.colors.primary}`,
1108
- borderRadius: theme.radii[1],
1109
- outline: 'none',
1110
- resize: 'none',
1111
- boxSizing: 'border-box',
1112
- boxShadow: '0 8px 24px rgba(0, 0, 0, 0.3)',
1113
- }}
1114
- onKeyDown={(e) => {
1115
- if (e.key === 'Enter' && !e.shiftKey) {
1116
- e.preventDefault();
1117
- handleSaveText();
1118
- } else if (e.key === 'Escape') {
1119
- e.preventDefault();
1120
- handleCancelEdit();
1121
- }
1122
- }}
1123
- onBlur={handleSaveText}
1124
- />
1125
- </div>
1126
- )}
1127
-
1128
- {tooltipsEnabled && (
1129
- <NodeTooltip
1130
- description={description}
1131
- otel={otelInfo}
1132
- sources={sources}
1133
- references={references}
1134
- visible={showTooltip}
1135
- nodeRef={nodeRef}
1136
- />
1137
- )}
1138
- </div>
1139
- )}
1140
-
1141
- {/* Output handles - all 4 sides for outgoing connections */}
1142
- <Handle type="source" position={Position.Top} id="top-out" style={getHandleStyle('top')} />
1143
- <Handle
1144
- type="source"
1145
- position={Position.Bottom}
1146
- id="bottom-out"
1147
- style={getHandleStyle('bottom')}
1148
- />
1149
- <Handle type="source" position={Position.Left} id="left-out" style={getHandleStyle('left')} />
1150
- <Handle
1151
- type="source"
1152
- position={Position.Right}
1153
- id="right-out"
1154
- style={getHandleStyle('right')}
1155
- />
1156
-
1157
- {/* CSS animations for node animation types */}
1158
- <style>{`
1159
- /* Processing pulse - continuous breathing effect */
1160
- .node-pulse {
1161
- animation: node-pulse ease-in-out infinite;
1162
- }
1163
-
1164
- @keyframes node-pulse {
1165
- 0%, 100% {
1166
- transform: scale(1);
1167
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1168
- }
1169
- 50% {
1170
- transform: scale(1.05);
1171
- box-shadow: 0 4px 12px rgba(0,0,0,0.2), 0 0 0 4px rgba(59, 130, 246, 0.3);
1172
- }
1173
- }
1174
-
1175
- /* Success flash - brief green glow */
1176
- .node-flash {
1177
- animation: node-flash ease-out forwards;
1178
- }
1179
-
1180
- @keyframes node-flash {
1181
- 0% {
1182
- box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.8);
1183
- background-color: rgba(34, 197, 94, 0.1);
1184
- }
1185
- 50% {
1186
- box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
1187
- background-color: rgba(34, 197, 94, 0.2);
1188
- }
1189
- 100% {
1190
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1191
- background-color: white;
1192
- }
1193
- }
1194
-
1195
- /* Error shake - vibrate effect */
1196
- .node-shake {
1197
- animation: node-shake ease-in-out;
1198
- }
1199
-
1200
- @keyframes node-shake {
1201
- 0%, 100% {
1202
- transform: translateX(0);
1203
- }
1204
- 10%, 30%, 50%, 70%, 90% {
1205
- transform: translateX(-4px);
1206
- }
1207
- 20%, 40%, 60%, 80% {
1208
- transform: translateX(4px);
1209
- }
1210
- }
1211
-
1212
- /* Entry animation - scale up and fade in */
1213
- .node-entry {
1214
- animation: node-entry ease-out forwards;
1215
- }
1216
-
1217
- @keyframes node-entry {
1218
- 0% {
1219
- opacity: 0;
1220
- transform: scale(0.8);
1221
- }
1222
- 100% {
1223
- opacity: 1;
1224
- transform: scale(1);
1225
- }
1226
- }
1227
-
1228
- `}</style>
1229
- </>
1230
- );
1231
- };