@principal-ai/subsystems-react 0.17.0 → 0.18.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 (98) hide show
  1. package/dist/index.d.ts +7 -44
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -32
  4. package/dist/index.js.map +1 -1
  5. package/package.json +2 -2
  6. package/src/index.ts +9 -199
  7. package/src/components/ConfigurationSelector.tsx +0 -133
  8. package/src/components/GraphRenderer.tsx +0 -3195
  9. package/src/components/MultiCanvasRenderer.tsx +0 -295
  10. package/src/components/NodeTooltip.tsx +0 -279
  11. package/src/components/PendingChanges.test.tsx +0 -375
  12. package/src/components/SelectionSidebar.tsx +0 -352
  13. package/src/components/SequenceDiagramRenderer.tsx +0 -1156
  14. package/src/components/WorkflowSequenceDiagram.tsx +0 -270
  15. package/src/components/dashboard/DashboardRenderer.tsx +0 -317
  16. package/src/components/dashboard/MetricPanel.tsx +0 -254
  17. package/src/components/dashboard/MockDataProvider.ts +0 -330
  18. package/src/components/dashboard/components/BarChart.tsx +0 -299
  19. package/src/components/dashboard/components/LineChart.tsx +0 -279
  20. package/src/components/dashboard/components/MetricCard.tsx +0 -270
  21. package/src/components/dashboard/components/SourceLink.tsx +0 -63
  22. package/src/components/dashboard/components/TimeRangeSelector.tsx +0 -280
  23. package/src/components/dashboard/components/index.ts +0 -5
  24. package/src/components/dashboard/index.ts +0 -47
  25. package/src/components/dashboard/types.ts +0 -126
  26. package/src/components/state-view/PipelineView.tsx +0 -347
  27. package/src/components/state-view/index.ts +0 -14
  28. package/src/components/state-view/types.ts +0 -261
  29. package/src/components/state-view/useStateView.ts +0 -205
  30. package/src/contexts/GraphEditContext.tsx +0 -27
  31. package/src/contexts/TooltipPortalContext.ts +0 -8
  32. package/src/edges/CustomEdge.tsx +0 -402
  33. package/src/edges/GenericEdge.tsx +0 -36
  34. package/src/hooks/usePathBasedEvents.ts +0 -192
  35. package/src/hooks/useSequenceLayout.ts +0 -543
  36. package/src/hooks/useUndoRedo.ts +0 -144
  37. package/src/nodes/CustomNode.tsx +0 -1231
  38. package/src/nodes/GenericNode.tsx +0 -55
  39. package/src/nodes/otel/OtelBoundaryNode.tsx +0 -224
  40. package/src/nodes/otel/OtelEventNode.tsx +0 -225
  41. package/src/nodes/otel/OtelResourceNode.tsx +0 -251
  42. package/src/nodes/otel/OtelScopeNode.tsx +0 -221
  43. package/src/nodes/otel/OtelSpanConventionNode.tsx +0 -295
  44. package/src/nodes/otel/index.ts +0 -23
  45. package/src/nodes/otel/shared/NodeBadges.tsx +0 -295
  46. package/src/nodes/otel/shared/NodeContent.tsx +0 -204
  47. package/src/nodes/otel/shared/index.ts +0 -8
  48. package/src/nodes/otel/shared/types.ts +0 -138
  49. package/src/nodes/otel/shared/useNodeBehavior.ts +0 -114
  50. package/src/stories/Audit/BoundaryNodes.stories.tsx +0 -385
  51. package/src/stories/Audit/CanvasEdgeTypes.stories.tsx +0 -1448
  52. package/src/stories/Audit/CanvasNodeTypes.stories.tsx +0 -1049
  53. package/src/stories/Audit/ColorPriority.stories.tsx +0 -895
  54. package/src/stories/Audit/DiamondBadges.stories.tsx +0 -629
  55. package/src/stories/Audit/NodeDimensionsTesting.stories.tsx +0 -724
  56. package/src/stories/Audit/NodeFieldsAudit.stories.tsx +0 -996
  57. package/src/stories/Audit/NodeFontSizeTesting.stories.tsx +0 -715
  58. package/src/stories/Audit/NodeShapes.stories.tsx +0 -1208
  59. package/src/stories/Audit/WideNodesTesting.stories.tsx +0 -1045
  60. package/src/stories/Components/DashboardRenderer.stories.tsx +0 -263
  61. package/src/stories/Components/GraphRenderer.stories.tsx +0 -3236
  62. package/src/stories/Components/MultiCanvasRenderer.stories.tsx +0 -370
  63. package/src/stories/Components/SequenceDiagram.stories.tsx +0 -255
  64. package/src/stories/Components/StateView.stories.tsx +0 -417
  65. package/src/stories/Components/TraceViewer.stories.tsx +0 -442
  66. package/src/stories/Components/WorkflowSequenceDiagram.stories.tsx +0 -802
  67. package/src/stories/Features/AnimationWorkshop.stories.tsx +0 -727
  68. package/src/stories/Features/ChangeTypeVisual.stories.tsx +0 -67
  69. package/src/stories/Features/EventDrivenAnimations.stories.tsx +0 -513
  70. package/src/stories/Features/MultiConfig.stories.tsx +0 -539
  71. package/src/stories/Layout/ElkEdgeRouting.stories.tsx +0 -539
  72. package/src/stories/Layout/GraphOrientation.stories.tsx +0 -500
  73. package/src/stories/Layout/MultiDirectionalConnections.stories.tsx +0 -352
  74. package/src/stories/Nodes/NodeDefinitionComparison.stories.tsx +0 -560
  75. package/src/stories/Nodes/NodeNameFallbacks.stories.tsx +0 -491
  76. package/src/stories/Nodes/NumericColors.stories.tsx +0 -440
  77. package/src/stories/OTEL/EventNameDisplay.stories.tsx +0 -694
  78. package/src/stories/OTEL/EventNameFallback.stories.tsx +0 -578
  79. package/src/stories/OTEL/OtelComponents.stories.tsx +0 -422
  80. package/src/stories/OTEL/OtelNodeTypesPrototype.stories.tsx +0 -806
  81. package/src/stories/OTEL/RawEventCards.stories.tsx +0 -57
  82. package/src/stories/OTEL/SpanBadges.stories.tsx +0 -840
  83. package/src/stories/OTEL/ValidationEventsCanvas.stories.tsx +0 -209
  84. package/src/stories/RealWorld/BacklogMdCanvases.stories.tsx +0 -721
  85. package/src/stories/RealWorld/FileCitySequence.stories.tsx +0 -516
  86. package/src/stories/RealWorld/RealCanvasFiles.stories.tsx +0 -336
  87. package/src/stories/__traces__/test-run.canvas.json +0 -241
  88. package/src/stories/data/activity-feed-analytics.dashboard.json +0 -300
  89. package/src/stories/data/file-city-images-transformed.otel.canvas.json +0 -295
  90. package/src/stories/data/file-city-workflows.json +0 -269
  91. package/src/stories/data/graph-converter-test-execution.json +0 -443
  92. package/src/stories/data/graph-converter-test.workflow.json +0 -105
  93. package/src/stories/data/graph-converter-validated-execution.json +0 -58
  94. package/src/utils/animationMapping.ts +0 -166
  95. package/src/utils/canvasBounds.ts +0 -146
  96. package/src/utils/graphConverter.ts +0 -148
  97. package/src/utils/iconResolver.tsx +0 -53
  98. 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
- };