@principal-ai/principal-view-react 0.15.0 → 0.15.2

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.
@@ -84,12 +84,20 @@ export interface NodeDimensionChange {
84
84
  dimensions: { width: number; height: number };
85
85
  }
86
86
 
87
+ /** Text change event for tracking inline text edits */
88
+ export interface NodeTextChange {
89
+ nodeId: string;
90
+ text: string;
91
+ }
92
+
87
93
  /** All pending changes that can be saved */
88
94
  export interface PendingChanges {
89
95
  /** Node position changes */
90
96
  positionChanges: NodePositionChange[];
91
97
  /** Node dimension changes (from resizing) */
92
98
  dimensionChanges: NodeDimensionChange[];
99
+ /** Text changes for text and group nodes */
100
+ textChanges: NodeTextChange[];
93
101
  /** Node updates (type, data changes) */
94
102
  nodeUpdates: Array<{
95
103
  nodeId: string;
@@ -398,6 +406,7 @@ interface AlignmentGuide {
398
406
  interface EditState {
399
407
  positionChanges: Map<string, { x: number; y: number }>;
400
408
  dimensionChanges: Map<string, { width: number; height: number }>;
409
+ textChanges: Map<string, string>;
401
410
  nodeUpdates: Map<string, { type?: string; data?: Record<string, unknown> }>;
402
411
  deletedNodeIds: Set<string>;
403
412
  createdEdges: Array<{
@@ -414,6 +423,7 @@ interface EditState {
414
423
  const createEmptyEditState = (): EditState => ({
415
424
  positionChanges: new Map(),
416
425
  dimensionChanges: new Map(),
426
+ textChanges: new Map(),
417
427
  nodeUpdates: new Map(),
418
428
  deletedNodeIds: new Set(),
419
429
  createdEdges: [],
@@ -573,6 +583,7 @@ interface GraphRendererInnerProps {
573
583
  onEditStateChange?: (editState: EditState) => void;
574
584
  editStateRef: React.MutableRefObject<EditState>;
575
585
  resetVisualStateRef: React.MutableRefObject<(() => void) | null>;
586
+ resetTextChangesVersionRef: React.MutableRefObject<(() => void) | null>;
576
587
  undoRedoFunctionsRef: React.MutableRefObject<UndoRedoFunctionsRef | null>;
577
588
  pushHistory: (entries: HistoryEntry[]) => void;
578
589
  clearHistory: () => void;
@@ -629,6 +640,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
629
640
  onEditStateChange,
630
641
  editStateRef,
631
642
  resetVisualStateRef,
643
+ resetTextChangesVersionRef,
632
644
  undoRedoFunctionsRef,
633
645
  pushHistory,
634
646
  clearHistory,
@@ -652,6 +664,9 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
652
664
  // Track shift key state for tooltip control
653
665
  const [shiftKeyPressed, setShiftKeyPressed] = useState(false);
654
666
 
667
+ // Track text changes version to force re-renders when text is edited
668
+ const [textChangesVersion, setTextChangesVersion] = useState(0);
669
+
655
670
  // Track if we're currently processing a node hide operation
656
671
  const hidingNodeRef = useRef(false);
657
672
 
@@ -822,6 +837,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
822
837
  return (
823
838
  state.positionChanges.size > 0 ||
824
839
  state.dimensionChanges.size > 0 ||
840
+ state.textChanges.size > 0 ||
825
841
  state.nodeUpdates.size > 0 ||
826
842
  state.deletedNodeIds.size > 0 ||
827
843
  state.createdEdges.length > 0 ||
@@ -841,6 +857,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
841
857
  console.log('[GraphRenderer] Edit state updated:', {
842
858
  positionChanges: newState.positionChanges.size,
843
859
  dimensionChanges: newState.dimensionChanges.size,
860
+ textChanges: newState.textChanges.size,
844
861
  nodeUpdates: newState.nodeUpdates.size,
845
862
  hasChanges,
846
863
  });
@@ -882,6 +899,39 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
882
899
  [editable, updateEditState, pushHistory]
883
900
  );
884
901
 
902
+ // Handler for node text change - called from CustomNode via context
903
+ const handleNodeTextChange = useCallback(
904
+ (nodeId: string, text: string) => {
905
+ if (!editable) return;
906
+
907
+ // Capture before text for undo
908
+ // For text nodes, the text is in the canvas node's 'text' field
909
+ // For group nodes, it's in the 'label' field
910
+ // We need to look at the original canvas to get the before value
911
+ const beforeText = editStateRef.current.textChanges.get(nodeId) ?? '';
912
+
913
+ // Push to history
914
+ pushHistory([
915
+ {
916
+ type: 'text',
917
+ nodeId,
918
+ before: beforeText,
919
+ after: text,
920
+ },
921
+ ]);
922
+
923
+ updateEditState((prev) => {
924
+ const newTextChanges = new Map(prev.textChanges);
925
+ newTextChanges.set(nodeId, text);
926
+ return { ...prev, textChanges: newTextChanges };
927
+ });
928
+
929
+ // Force re-render by incrementing version
930
+ setTextChangesVersion((v) => v + 1);
931
+ },
932
+ [editable, updateEditState, pushHistory]
933
+ );
934
+
885
935
  // Handle toggling node hidden state (Cmd/Ctrl+click)
886
936
  // This is exposed via context so CustomNode can call it on mousedown
887
937
  // (mousedown works in edit mode where click is intercepted by drag handling)
@@ -961,10 +1011,11 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
961
1011
  const graphEditContextValue = useMemo(
962
1012
  () => ({
963
1013
  onNodeResizeEnd: handleNodeResizeEnd,
1014
+ onNodeTextChange: handleNodeTextChange,
964
1015
  onToggleNodeHidden: handleToggleNodeHidden,
965
1016
  onHideUnconnectedNodes: handleHideUnconnectedNodes,
966
1017
  }),
967
- [handleNodeResizeEnd, handleToggleNodeHidden, handleHideUnconnectedNodes]
1018
+ [handleNodeResizeEnd, handleNodeTextChange, handleToggleNodeHidden, handleHideUnconnectedNodes]
968
1019
  );
969
1020
 
970
1021
  // ============================================
@@ -1647,6 +1698,8 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1647
1698
  const animation = animationState.nodeAnimations[node.id];
1648
1699
  // Apply any pending position changes
1649
1700
  const pendingPosition = editStateRef.current.positionChanges.get(node.id);
1701
+ // Apply any pending text changes
1702
+ const pendingText = editStateRef.current.textChanges.get(node.id);
1650
1703
  // Allow specific nodes to be draggable even when not in edit mode
1651
1704
  const isDraggable = editable || draggableNodeIds?.has(node.id);
1652
1705
  // When draggableNodeIds is provided, we need to explicitly control each node's draggability
@@ -1668,6 +1721,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1668
1721
  data: {
1669
1722
  ...node.data,
1670
1723
  editable,
1724
+ pendingText,
1671
1725
  tooltipsEnabled: showTooltips,
1672
1726
  shiftKeyPressed,
1673
1727
  isHighlighted: highlightedNodeId === node.id,
@@ -1682,7 +1736,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1682
1736
  } as CustomNodeData,
1683
1737
  };
1684
1738
  });
1685
- }, [localNodes, configuration, violations, animationState.nodeAnimations, editable, showTooltips, highlightedNodeId, activeNodeIds, editStateRef, shiftKeyPressed, selectedNodeIds, hiddenNodeIds, draggableNodeIds]);
1739
+ }, [localNodes, configuration, violations, animationState.nodeAnimations, editable, showTooltips, highlightedNodeId, activeNodeIds, editStateRef, shiftKeyPressed, selectedNodeIds, hiddenNodeIds, draggableNodeIds, textChangesVersion]);
1686
1740
 
1687
1741
  const baseNodesKey = useMemo(() => {
1688
1742
  return nodes
@@ -1747,6 +1801,24 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1747
1801
  );
1748
1802
  }, [editable, hiddenNodeIds]);
1749
1803
 
1804
+ // Sync pending text changes to local nodes when textChangesVersion changes (edit mode only)
1805
+ const prevTextChangesVersionRef = useRef(textChangesVersion);
1806
+ useEffect(() => {
1807
+ if (!editable) return;
1808
+ if (prevTextChangesVersionRef.current === textChangesVersion) return;
1809
+ prevTextChangesVersionRef.current = textChangesVersion;
1810
+
1811
+ setXyflowLocalNodes((nodes) =>
1812
+ nodes.map((n) => {
1813
+ const pendingText = editStateRef.current.textChanges.get(n.id);
1814
+ return {
1815
+ ...n,
1816
+ data: { ...n.data, pendingText },
1817
+ };
1818
+ })
1819
+ );
1820
+ }, [editable, textChangesVersion]);
1821
+
1750
1822
  // Also sync when entering edit mode or when base nodes change content
1751
1823
  const prevEditableRef = useRef(editable);
1752
1824
  useEffect(() => {
@@ -1865,6 +1937,18 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1865
1937
  });
1866
1938
  break;
1867
1939
 
1940
+ case 'text':
1941
+ // Restore previous text
1942
+ // Update edit state
1943
+ updateEditState((prev) => {
1944
+ const newTextChanges = new Map(prev.textChanges);
1945
+ newTextChanges.set(entry.nodeId, entry.before);
1946
+ return { ...prev, textChanges: newTextChanges };
1947
+ });
1948
+ // Force re-render
1949
+ setTextChangesVersion((v) => v + 1);
1950
+ break;
1951
+
1868
1952
  case 'edgeCreate':
1869
1953
  // Remove the created edge
1870
1954
  setLocalEdges((edges) => edges.filter((e) => e.id !== entry.edge.id));
@@ -1930,6 +2014,18 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1930
2014
  });
1931
2015
  break;
1932
2016
 
2017
+ case 'text':
2018
+ // Apply new text
2019
+ // Update edit state
2020
+ updateEditState((prev) => {
2021
+ const newTextChanges = new Map(prev.textChanges);
2022
+ newTextChanges.set(entry.nodeId, entry.after);
2023
+ return { ...prev, textChanges: newTextChanges };
2024
+ });
2025
+ // Force re-render
2026
+ setTextChangesVersion((v) => v + 1);
2027
+ break;
2028
+
1933
2029
  case 'edgeCreate':
1934
2030
  // Re-create the edge
1935
2031
  setLocalEdges((edges) => [...edges, entry.edge]);
@@ -2314,6 +2410,13 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
2314
2410
  };
2315
2411
  }, [xyflowNodesBase, xyflowEdgesWithElk, onPendingChangesChange]);
2316
2412
 
2413
+ // Set the reset text changes version function for use by resetEditState
2414
+ useEffect(() => {
2415
+ resetTextChangesVersionRef.current = () => {
2416
+ setTextChangesVersion(0);
2417
+ };
2418
+ }, []);
2419
+
2317
2420
  // Use local edges in edit mode, base edges otherwise
2318
2421
  const xyflowEdges = editable ? xyflowLocalEdges : xyflowEdgesWithElk;
2319
2422
 
@@ -2823,6 +2926,9 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2823
2926
  // Ref to hold the reset visual state function - will be set after xyflowLocalNodes is defined
2824
2927
  const resetVisualStateRef = useRef<(() => void) | null>(null);
2825
2928
 
2929
+ // Ref to hold the reset text changes version function - will be set by inner component
2930
+ const resetTextChangesVersionRef = useRef<(() => void) | null>(null);
2931
+
2826
2932
  // Undo/redo management
2827
2933
  const {
2828
2934
  canUndo: canUndoState,
@@ -2855,6 +2961,12 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2855
2961
  dimensions,
2856
2962
  })
2857
2963
  ),
2964
+ textChanges: Array.from(state.textChanges.entries()).map(
2965
+ ([nodeId, text]) => ({
2966
+ nodeId,
2967
+ text,
2968
+ })
2969
+ ),
2858
2970
  nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
2859
2971
  nodeId,
2860
2972
  updates,
@@ -2871,6 +2983,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2871
2983
  hasChanges:
2872
2984
  state.positionChanges.size > 0 ||
2873
2985
  state.dimensionChanges.size > 0 ||
2986
+ state.textChanges.size > 0 ||
2874
2987
  state.nodeUpdates.size > 0 ||
2875
2988
  state.deletedNodeIds.size > 0 ||
2876
2989
  state.createdEdges.length > 0 ||
@@ -2879,6 +2992,8 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2879
2992
  },
2880
2993
  resetEditState: () => {
2881
2994
  editStateRef.current = createEmptyEditState();
2995
+ // Reset text changes version to trigger re-render
2996
+ resetTextChangesVersionRef.current?.();
2882
2997
  // Also reset visual state (node positions/dimensions) if available
2883
2998
  resetVisualStateRef.current?.();
2884
2999
  // Clear undo/redo history
@@ -2889,6 +3004,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2889
3004
  return (
2890
3005
  state.positionChanges.size > 0 ||
2891
3006
  state.dimensionChanges.size > 0 ||
3007
+ state.textChanges.size > 0 ||
2892
3008
  state.nodeUpdates.size > 0 ||
2893
3009
  state.deletedNodeIds.size > 0 ||
2894
3010
  state.createdEdges.length > 0 ||
@@ -2988,6 +3104,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2988
3104
  onPendingChangesChange={onPendingChangesChange}
2989
3105
  editStateRef={editStateRef}
2990
3106
  resetVisualStateRef={resetVisualStateRef}
3107
+ resetTextChangesVersionRef={resetTextChangesVersionRef}
2991
3108
  undoRedoFunctionsRef={undoRedoFunctionsRef}
2992
3109
  pushHistory={pushHistory}
2993
3110
  clearHistory={clearHistory}
@@ -7,6 +7,7 @@ import { TooltipPortalContext } from '../contexts/TooltipPortalContext';
7
7
  export interface OtelInfo {
8
8
  kind: 'type' | 'service' | 'instance';
9
9
  category?: string;
10
+ scope?: string;
10
11
  files?: string[];
11
12
  }
12
13
 
@@ -138,7 +139,7 @@ export const NodeTooltip: React.FC<NodeTooltipProps> = ({
138
139
  display: 'flex',
139
140
  alignItems: 'center',
140
141
  gap: '6px',
141
- marginBottom: description ? '6px' : 0,
142
+ marginBottom: description || otel.scope ? '6px' : 0,
142
143
  }}
143
144
  >
144
145
  <span
@@ -163,6 +164,24 @@ export const NodeTooltip: React.FC<NodeTooltipProps> = ({
163
164
  </div>
164
165
  )}
165
166
 
167
+ {/* Scope */}
168
+ {otel?.scope && (
169
+ <div style={{
170
+ marginBottom: description ? '6px' : 0,
171
+ fontSize: theme.fontSizes[0],
172
+ fontFamily: theme.fonts.body,
173
+ color: 'rgba(255,255,255,0.8)'
174
+ }}>
175
+ <span style={{
176
+ color: 'rgba(255,255,255,0.6)',
177
+ fontWeight: theme.fontWeights.semibold
178
+ }}>
179
+ Scope:
180
+ </span>{' '}
181
+ <span style={{ fontFamily: 'monospace' }}>{otel.scope}</span>
182
+ </div>
183
+ )}
184
+
166
185
  {/* Description - rendered as markdown */}
167
186
  {description ? (
168
187
  <div style={{ lineHeight: '1.4', color: 'rgba(255,255,255,0.9)' }}>
@@ -3,6 +3,8 @@ import React, { createContext, useContext } from 'react';
3
3
  export interface GraphEditContextValue {
4
4
  /** Called when a node resize operation completes */
5
5
  onNodeResizeEnd?: (nodeId: string, dimensions: { width: number; height: number }) => void;
6
+ /** Called when node text is edited (for text and group nodes) */
7
+ onNodeTextChange?: (nodeId: string, text: string) => void;
6
8
  /** Called to toggle node hidden state (Cmd/Ctrl+click) */
7
9
  onToggleNodeHidden?: (nodeId: string) => void;
8
10
  /** Called to hide all nodes not directly connected to the given node (Cmd/Ctrl+Shift+click) */
@@ -26,6 +26,12 @@ export type HistoryEntry =
26
26
  before: { width: number; height: number };
27
27
  after: { width: number; height: number };
28
28
  }
29
+ | {
30
+ type: 'text';
31
+ nodeId: string;
32
+ before: string;
33
+ after: string;
34
+ }
29
35
  | {
30
36
  type: 'nodeDelete';
31
37
  nodeId: string;
@@ -28,6 +28,8 @@ export interface CustomNodeData extends Record<string, unknown> {
28
28
  animationDuration?: number;
29
29
  // Edit mode - shows larger connection handles
30
30
  editable?: boolean;
31
+ // Pending text change (from inline editing, not yet saved)
32
+ pendingText?: string;
31
33
  // Whether tooltips are enabled (defaults to true)
32
34
  tooltipsEnabled?: boolean;
33
35
  // Whether shift key is currently pressed (for tooltip control)
@@ -77,10 +79,13 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
77
79
 
78
80
  // Fall through to legacy rendering for non-OTEL nodes
79
81
  const { theme } = useTheme();
80
- const { onNodeResizeEnd, onToggleNodeHidden, onHideUnconnectedNodes } = useGraphEdit();
82
+ const { onNodeResizeEnd, onNodeTextChange, onToggleNodeHidden, onHideUnconnectedNodes } = useGraphEdit();
81
83
  const nodeId = useNodeId();
82
84
  const [isHovered, setIsHovered] = useState(false);
85
+ const [isEditing, setIsEditing] = useState(false);
86
+ const [editingText, setEditingText] = useState('');
83
87
  const nodeRef = useRef<HTMLDivElement>(null);
88
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
84
89
  const nodeProps = data;
85
90
  const {
86
91
  typeDefinition,
@@ -449,8 +454,8 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
449
454
  // Use fillColor as the primary "color" for backwards compatibility
450
455
  const color = fillColor;
451
456
 
452
- // Get display name - use name from props (falls back to node.id in converter)
453
- const displayName = nodeProps.name;
457
+ // Get display name - use pending text if available, otherwise use name from props
458
+ const displayName = nodeProps.pendingText ?? nodeProps.name;
454
459
 
455
460
  // Extract identifier based on node type (for display below the label)
456
461
  // Supports: event.name, otel.spanPattern, otel.scope, otel.resourceMatch, boundary.direction
@@ -545,8 +550,58 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
545
550
 
546
551
  const animationClass = getAnimationClass();
547
552
 
548
- // Check if this is a group node
553
+ // Check if this is a group node or text node (canvas types that support inline editing)
549
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
+ }, []);
550
605
 
551
606
  // Shape-specific styles
552
607
  const getShapeStyles = () => {
@@ -946,6 +1001,7 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
946
1001
  ref={nodeRef}
947
1002
  style={{ position: 'relative', width: '100%', height: '100%' }}
948
1003
  onMouseDown={handleMouseDown}
1004
+ onClick={handleNodeClick}
949
1005
  onMouseEnter={() => setIsHovered(true)}
950
1006
  onMouseLeave={() => setIsHovered(false)}
951
1007
  >
@@ -1018,6 +1074,57 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
1018
1074
  )}
1019
1075
  </div>
1020
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
+
1021
1128
  {tooltipsEnabled && (
1022
1129
  <NodeTooltip
1023
1130
  description={description}
@@ -243,7 +243,79 @@ export const TooltipVariants: StoryObj = {
243
243
  </div>
244
244
  <NodeTooltip
245
245
  description="A specific running instance of a service"
246
- otel={{ kind: 'instance', category: 'runtime' }}
246
+ otel={{ kind: 'instance', category: 'runtime', scope: 'auth-flow' }}
247
+ visible={true}
248
+ />
249
+ </div>
250
+ </div>
251
+ </ThemeProvider>
252
+ ),
253
+ };
254
+
255
+ /**
256
+ * Demonstrates tooltips with scope information.
257
+ * Shows how scope names are displayed in the tooltip.
258
+ */
259
+ export const TooltipWithScope: StoryObj = {
260
+ render: () => (
261
+ <ThemeProvider theme={defaultEditorTheme}>
262
+ <div style={{ padding: '40px', display: 'flex', gap: '80px', flexWrap: 'wrap' }}>
263
+ <div style={{ position: 'relative', width: '200px', height: '180px' }}>
264
+ <div
265
+ style={{
266
+ padding: '20px',
267
+ border: '2px solid #DC2626',
268
+ borderRadius: '8px',
269
+ textAlign: 'center',
270
+ backgroundColor: '#DC2626',
271
+ color: 'white',
272
+ }}
273
+ >
274
+ Auth Event
275
+ </div>
276
+ <NodeTooltip
277
+ description="User authentication completed successfully"
278
+ otel={{ kind: 'type', category: 'event', scope: 'auth-flow' }}
279
+ visible={true}
280
+ />
281
+ </div>
282
+
283
+ <div style={{ position: 'relative', width: '200px', height: '180px' }}>
284
+ <div
285
+ style={{
286
+ padding: '20px',
287
+ border: '2px solid #2563EB',
288
+ borderRadius: '8px',
289
+ textAlign: 'center',
290
+ backgroundColor: '#2563EB',
291
+ color: 'white',
292
+ }}
293
+ >
294
+ Terminal Event
295
+ </div>
296
+ <NodeTooltip
297
+ description="Command executed in terminal session"
298
+ otel={{ kind: 'type', category: 'event', scope: 'terminal-activity' }}
299
+ visible={true}
300
+ />
301
+ </div>
302
+
303
+ <div style={{ position: 'relative', width: '200px', height: '180px' }}>
304
+ <div
305
+ style={{
306
+ padding: '20px',
307
+ border: '2px solid #16A34A',
308
+ borderRadius: '8px',
309
+ textAlign: 'center',
310
+ backgroundColor: '#16A34A',
311
+ color: 'white',
312
+ }}
313
+ >
314
+ Quality Event
315
+ </div>
316
+ <NodeTooltip
317
+ description="Code quality check completed with metrics"
318
+ otel={{ kind: 'type', category: 'event', scope: 'quality-panel' }}
247
319
  visible={true}
248
320
  />
249
321
  </div>