@aiready/components 0.13.14 → 0.13.19

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.
@@ -0,0 +1,250 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ forwardRef,
7
+ useImperativeHandle,
8
+ } from 'react';
9
+ import * as d3 from 'd3';
10
+ import {
11
+ GraphNode,
12
+ LayoutType,
13
+ ForceDirectedGraphHandle,
14
+ ForceDirectedGraphProps,
15
+ } from './types';
16
+ import {
17
+ useGraphZoom,
18
+ useWindowDrag,
19
+ useNodeInteractions,
20
+ } from './useGraphInteractions';
21
+ import { useGraphLayout, useSimulationControls } from './useGraphLayout';
22
+ import { useImperativeHandleMethods } from './useImperativeHandle';
23
+ import { GraphCanvas } from './GraphCanvas';
24
+
25
+ export const ForceDirectedGraph = forwardRef<
26
+ ForceDirectedGraphHandle,
27
+ ForceDirectedGraphProps
28
+ >(
29
+ (
30
+ {
31
+ nodes: initialNodes,
32
+ links: initialLinks,
33
+ width,
34
+ height,
35
+ enableZoom = true,
36
+ enableDrag = true,
37
+ onNodeClick,
38
+ onNodeHover,
39
+ onLinkClick,
40
+ selectedNodeId,
41
+ hoveredNodeId,
42
+ defaultNodeColor,
43
+ defaultNodeSize,
44
+ defaultLinkColor,
45
+ defaultLinkWidth,
46
+ showNodeLabels,
47
+ showLinkLabels,
48
+ className,
49
+ manualLayout = false,
50
+ onManualLayoutChange,
51
+ packageBounds,
52
+ layout: externalLayout,
53
+ onLayoutChange,
54
+ },
55
+ ref
56
+ ) => {
57
+ const svgRef = useRef<SVGSVGElement>(null);
58
+ const gRef = useRef<SVGGElement>(null);
59
+ const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
60
+ const transformRef = useRef(transform);
61
+ const dragNodeRef = useRef<GraphNode | null>(null);
62
+ const dragActiveRef = useRef(false);
63
+ const [pinnedNodes, setPinnedNodes] = useState<Set<string>>(new Set());
64
+ const internalDragEnabledRef = useRef(enableDrag);
65
+ const [layout, setLayout] = useState<LayoutType>(externalLayout || 'force');
66
+
67
+ useEffect(() => {
68
+ if (externalLayout && externalLayout !== layout)
69
+ setLayout(externalLayout);
70
+ }, [externalLayout, layout]);
71
+
72
+ const handleLayoutChange = useCallback(
73
+ (newLayout: LayoutType) => {
74
+ setLayout(newLayout);
75
+ onLayoutChange?.(newLayout);
76
+ },
77
+ [onLayoutChange]
78
+ );
79
+
80
+ useEffect(() => {
81
+ internalDragEnabledRef.current = enableDrag;
82
+ }, [enableDrag]);
83
+
84
+ const { restart, stop, setForcesEnabled } = useSimulationControls();
85
+ const { nodes } = useGraphLayout(
86
+ initialNodes,
87
+ width,
88
+ height,
89
+ layout,
90
+ restart
91
+ );
92
+
93
+ useEffect(() => {
94
+ setForcesEnabled(!(manualLayout || pinnedNodes.size > 0));
95
+ }, [manualLayout, pinnedNodes, setForcesEnabled]);
96
+
97
+ useImperativeHandle(
98
+ ref,
99
+ () =>
100
+ useImperativeHandleMethods({
101
+ nodes,
102
+ pinnedNodes,
103
+ setPinnedNodes,
104
+ restart,
105
+ width,
106
+ height,
107
+ layout,
108
+ handleLayoutChange,
109
+ setForcesEnabled,
110
+ svgRef,
111
+ gRef,
112
+ setTransform,
113
+ internalDragEnabledRef,
114
+ }),
115
+ [
116
+ nodes,
117
+ pinnedNodes,
118
+ restart,
119
+ width,
120
+ height,
121
+ layout,
122
+ handleLayoutChange,
123
+ setForcesEnabled,
124
+ ]
125
+ );
126
+
127
+ useEffect(() => {
128
+ onManualLayoutChange?.(manualLayout);
129
+ }, [manualLayout, onManualLayoutChange]);
130
+
131
+ useGraphZoom(svgRef, gRef, enableZoom, setTransform, transformRef);
132
+ useWindowDrag(
133
+ enableDrag,
134
+ svgRef,
135
+ transformRef,
136
+ dragActiveRef,
137
+ dragNodeRef,
138
+ () => {
139
+ setForcesEnabled(true);
140
+ restart();
141
+ }
142
+ );
143
+
144
+ useEffect(() => {
145
+ if (!gRef.current) return;
146
+ const g = d3.select(gRef.current);
147
+ g.selectAll('g.node').each(function (this: any) {
148
+ const d = d3.select(this).datum() as any;
149
+ if (d)
150
+ d3.select(this).attr(
151
+ 'transform',
152
+ `translate(${d.x || 0},${d.y || 0})`
153
+ );
154
+ });
155
+ g.selectAll('line').each(function (this: any) {
156
+ const l = d3.select(this).datum() as any;
157
+ if (!l) return;
158
+ const s =
159
+ typeof l.source === 'object'
160
+ ? l.source
161
+ : nodes.find((n) => n.id === l.source);
162
+ const t =
163
+ typeof l.target === 'object'
164
+ ? l.target
165
+ : nodes.find((n) => n.id === l.target);
166
+ if (s && t)
167
+ d3.select(this)
168
+ .attr('x1', s.x)
169
+ .attr('y1', s.y)
170
+ .attr('x2', t.x)
171
+ .attr('y2', t.y);
172
+ });
173
+ }, [nodes, initialLinks]);
174
+
175
+ const { handleDragStart, handleNodeDoubleClick } = useNodeInteractions(
176
+ enableDrag,
177
+ nodes,
178
+ pinnedNodes,
179
+ setPinnedNodes,
180
+ restart,
181
+ stop
182
+ );
183
+
184
+ useEffect(() => {
185
+ if (!gRef.current || !enableDrag) return;
186
+ const g = d3.select(gRef.current);
187
+ const dragBehavior = (d3 as any)
188
+ .drag()
189
+ .on('start', (event: any) => {
190
+ const target = (event.sourceEvent?.target || event.target) as Element;
191
+ const id = target.closest?.('g.node')?.getAttribute('data-id');
192
+ if (!id || !internalDragEnabledRef.current) return;
193
+ const node = nodes.find((n) => n.id === id);
194
+ if (!node) return;
195
+ if (!event.active) restart();
196
+ dragActiveRef.current = true;
197
+ dragNodeRef.current = node;
198
+ })
199
+ .on('drag', (event: any) => {
200
+ if (!dragActiveRef.current || !dragNodeRef.current || !svgRef.current)
201
+ return;
202
+ const rect = svgRef.current.getBoundingClientRect();
203
+ dragNodeRef.current.fx =
204
+ (event.sourceEvent.clientX - rect.left - transform.x) / transform.k;
205
+ dragNodeRef.current.fy =
206
+ (event.sourceEvent.clientY - rect.top - transform.y) / transform.k;
207
+ })
208
+ .on('end', () => {
209
+ setForcesEnabled(true);
210
+ restart();
211
+ });
212
+
213
+ g.selectAll('g.node').call(dragBehavior);
214
+ return () => {
215
+ g.selectAll('g.node').on('.drag', null);
216
+ };
217
+ }, [gRef, enableDrag, nodes, transform, restart, setForcesEnabled]);
218
+
219
+ return (
220
+ <GraphCanvas
221
+ svgRef={svgRef}
222
+ gRef={gRef}
223
+ width={width}
224
+ height={height}
225
+ className={className}
226
+ nodes={nodes}
227
+ links={initialLinks}
228
+ pinnedNodes={pinnedNodes}
229
+ selectedNodeId={selectedNodeId}
230
+ hoveredNodeId={hoveredNodeId}
231
+ defaultNodeColor={defaultNodeColor}
232
+ defaultNodeSize={defaultNodeSize}
233
+ defaultLinkColor={defaultLinkColor}
234
+ defaultLinkWidth={defaultLinkWidth}
235
+ showNodeLabels={showNodeLabels}
236
+ showLinkLabels={showLinkLabels}
237
+ onNodeClick={onNodeClick}
238
+ onNodeHover={onNodeHover}
239
+ onLinkClick={onLinkClick}
240
+ packageBounds={packageBounds}
241
+ handleNodeDoubleClick={handleNodeDoubleClick}
242
+ handleDragStart={handleDragStart}
243
+ restart={restart}
244
+ setPinnedNodes={setPinnedNodes}
245
+ />
246
+ );
247
+ }
248
+ );
249
+
250
+ ForceDirectedGraph.displayName = 'ForceDirectedGraph';
@@ -0,0 +1,129 @@
1
+ import React from 'react';
2
+ import { cn } from '../../utils/cn';
3
+ import NodeItem from '../NodeItem';
4
+ import LinkItem from '../LinkItem';
5
+ import { PackageBoundaries } from '../PackageBoundaries';
6
+ import {
7
+ DEFAULT_NODE_COLOR,
8
+ DEFAULT_NODE_SIZE,
9
+ DEFAULT_LINK_COLOR,
10
+ DEFAULT_LINK_WIDTH,
11
+ } from '../constants';
12
+ import { GraphNode, GraphLink, ForceDirectedGraphProps } from './types';
13
+ import { unpinAllNodes } from './useGraphInteractions';
14
+
15
+ interface GraphCanvasProps extends Pick<
16
+ ForceDirectedGraphProps,
17
+ | 'width'
18
+ | 'height'
19
+ | 'className'
20
+ | 'selectedNodeId'
21
+ | 'hoveredNodeId'
22
+ | 'defaultNodeColor'
23
+ | 'defaultNodeSize'
24
+ | 'defaultLinkColor'
25
+ | 'defaultLinkWidth'
26
+ | 'showNodeLabels'
27
+ | 'showLinkLabels'
28
+ | 'onNodeClick'
29
+ | 'onNodeHover'
30
+ | 'onLinkClick'
31
+ | 'packageBounds'
32
+ > {
33
+ svgRef: React.RefObject<SVGSVGElement | null>;
34
+ gRef: React.RefObject<SVGGElement | null>;
35
+ nodes: GraphNode[];
36
+ links: GraphLink[];
37
+ pinnedNodes: Set<string>;
38
+ handleNodeDoubleClick: (e: React.MouseEvent, node: GraphNode) => void;
39
+ handleDragStart: (e: React.MouseEvent, node: GraphNode) => void;
40
+ restart: () => void;
41
+ setPinnedNodes: React.Dispatch<React.SetStateAction<Set<string>>>;
42
+ }
43
+
44
+ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
45
+ svgRef,
46
+ gRef,
47
+ width,
48
+ height,
49
+ className,
50
+ nodes,
51
+ links,
52
+ pinnedNodes,
53
+ selectedNodeId,
54
+ hoveredNodeId,
55
+ defaultNodeColor = DEFAULT_NODE_COLOR,
56
+ defaultNodeSize = DEFAULT_NODE_SIZE,
57
+ defaultLinkColor = DEFAULT_LINK_COLOR,
58
+ defaultLinkWidth = DEFAULT_LINK_WIDTH,
59
+ showNodeLabels = true,
60
+ showLinkLabels = false,
61
+ onNodeClick,
62
+ onNodeHover,
63
+ onLinkClick,
64
+ packageBounds,
65
+ handleNodeDoubleClick,
66
+ handleDragStart,
67
+ restart,
68
+ setPinnedNodes,
69
+ }) => {
70
+ return (
71
+ <svg
72
+ ref={svgRef}
73
+ width={width}
74
+ height={height}
75
+ className={cn('bg-white dark:bg-gray-900', className)}
76
+ onDoubleClick={() => {
77
+ unpinAllNodes(nodes);
78
+ setPinnedNodes(new Set());
79
+ restart();
80
+ }}
81
+ >
82
+ <defs>
83
+ <marker
84
+ id="arrow"
85
+ viewBox="0 0 10 10"
86
+ refX="20"
87
+ refY="5"
88
+ markerWidth="6"
89
+ markerHeight="6"
90
+ orient="auto"
91
+ >
92
+ <path d="M 0 0 L 10 5 L 0 10 z" fill={defaultLinkColor} />
93
+ </marker>
94
+ </defs>
95
+
96
+ <g ref={gRef}>
97
+ {links.map((link, i) => (
98
+ <LinkItem
99
+ key={`link-${i}`}
100
+ link={link as GraphLink}
101
+ onClick={onLinkClick}
102
+ defaultWidth={defaultLinkWidth}
103
+ showLabel={showLinkLabels}
104
+ nodes={nodes}
105
+ />
106
+ ))}
107
+
108
+ {nodes.map((node) => (
109
+ <NodeItem
110
+ key={node.id}
111
+ node={node}
112
+ isSelected={selectedNodeId === node.id}
113
+ isHovered={hoveredNodeId === node.id}
114
+ pinned={pinnedNodes.has(node.id)}
115
+ defaultNodeSize={defaultNodeSize}
116
+ defaultNodeColor={defaultNodeColor}
117
+ showLabel={showNodeLabels}
118
+ onClick={onNodeClick}
119
+ onDoubleClick={handleNodeDoubleClick}
120
+ onMouseEnter={(n) => onNodeHover?.(n)}
121
+ onMouseLeave={() => onNodeHover?.(null)}
122
+ onMouseDown={handleDragStart}
123
+ />
124
+ ))}
125
+ <PackageBoundaries packageBounds={packageBounds || {}} />
126
+ </g>
127
+ </svg>
128
+ );
129
+ };
@@ -1,81 +1,23 @@
1
1
  import React from 'react';
2
- import { cn } from '../utils/cn';
2
+ import { cn } from '../../utils/cn';
3
+ import { ControlButton } from './ControlButton';
3
4
 
4
5
  export interface GraphControlsProps {
5
- /**
6
- * Whether dragging is enabled
7
- */
8
6
  dragEnabled?: boolean;
9
-
10
- /**
11
- * Callback to toggle drag mode
12
- * @param enabled - True when drag mode should be enabled, false when disabled
13
- */
14
7
  onDragToggle?: (enabled: boolean) => void;
15
-
16
- /**
17
- * Whether manual layout mode is enabled
18
- */
19
8
  manualLayout?: boolean;
20
-
21
- /**
22
- * Callback to toggle manual layout mode
23
- * @param enabled - True when manual layout should be enabled, false when disabled
24
- */
25
9
  onManualLayoutToggle?: (enabled: boolean) => void;
26
-
27
- /**
28
- * Callback to pin all nodes
29
- */
30
10
  onPinAll?: () => void;
31
-
32
- /**
33
- * Callback to unpin all nodes
34
- */
35
11
  onUnpinAll?: () => void;
36
-
37
- /**
38
- * Callback to center/reset the view
39
- */
40
12
  onReset?: () => void;
41
-
42
- /**
43
- * Callback to fit all nodes in view
44
- */
45
13
  onFitView?: () => void;
46
-
47
- /**
48
- * Number of pinned nodes
49
- */
50
14
  pinnedCount?: number;
51
-
52
- /**
53
- * Total number of nodes
54
- */
55
15
  totalNodes?: number;
56
-
57
- /**
58
- * Whether to show the controls
59
- * @default true
60
- */
61
16
  visible?: boolean;
62
-
63
- /**
64
- * Position of the controls
65
- * @default "top-left"
66
- */
67
17
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
68
-
69
- /**
70
- * Additional CSS classes
71
- */
72
18
  className?: string;
73
19
  }
74
20
 
75
- /**
76
- * GraphControls: Floating toolbar for manipulating graph layout and dragging
77
- * Provides controls for toggling drag mode, manual layout, pinning nodes, and resetting the view
78
- */
79
21
  export const GraphControls: React.FC<GraphControlsProps> = ({
80
22
  dragEnabled = true,
81
23
  onDragToggle,
@@ -100,35 +42,6 @@ export const GraphControls: React.FC<GraphControlsProps> = ({
100
42
  'bottom-right': 'bottom-4 right-4',
101
43
  };
102
44
 
103
- const ControlButton: React.FC<{
104
- onClick: () => void;
105
- active?: boolean;
106
- icon: string;
107
- label: string;
108
- disabled?: boolean;
109
- }> = ({ onClick, active = false, icon, label, disabled = false }) => (
110
- <div className="relative group">
111
- <button
112
- onClick={onClick}
113
- disabled={disabled}
114
- className={cn(
115
- 'p-2 rounded-lg transition-all duration-200',
116
- active
117
- ? 'bg-blue-500 text-white shadow-md hover:bg-blue-600'
118
- : 'bg-gray-100 text-gray-700 hover:bg-gray-200',
119
- disabled && 'opacity-50 cursor-not-allowed hover:bg-gray-100',
120
- 'dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 dark:active:bg-blue-600'
121
- )}
122
- title={label}
123
- >
124
- <span className="text-lg">{icon}</span>
125
- </button>
126
- <div className="absolute left-full ml-2 px-2 py-1 bg-gray-900 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-50">
127
- {label}
128
- </div>
129
- </div>
130
- );
131
-
132
45
  return (
133
46
  <div
134
47
  className={cn(
@@ -138,7 +51,6 @@ export const GraphControls: React.FC<GraphControlsProps> = ({
138
51
  )}
139
52
  >
140
53
  <div className="flex flex-col gap-2">
141
- {/* Drag Mode Toggle */}
142
54
  <ControlButton
143
55
  onClick={() => onDragToggle?.(!dragEnabled)}
144
56
  active={dragEnabled}
@@ -146,22 +58,15 @@ export const GraphControls: React.FC<GraphControlsProps> = ({
146
58
  label={dragEnabled ? 'Drag enabled' : 'Drag disabled'}
147
59
  />
148
60
 
149
- {/* Manual Layout Toggle */}
150
61
  <ControlButton
151
62
  onClick={() => onManualLayoutToggle?.(!manualLayout)}
152
63
  active={manualLayout}
153
64
  icon="🔧"
154
- label={
155
- manualLayout
156
- ? 'Manual layout: ON (drag freely)'
157
- : 'Manual layout: OFF (forces active)'
158
- }
65
+ label={manualLayout ? 'Manual layout: ON' : 'Manual layout: OFF'}
159
66
  />
160
67
 
161
- {/* Divider */}
162
68
  <div className="w-8 h-px bg-gray-300 dark:bg-gray-600 mx-auto my-1" />
163
69
 
164
- {/* Pin/Unpin Controls */}
165
70
  <div className="flex gap-1">
166
71
  <ControlButton
167
72
  onClick={() => onPinAll?.()}
@@ -177,10 +82,8 @@ export const GraphControls: React.FC<GraphControlsProps> = ({
177
82
  />
178
83
  </div>
179
84
 
180
- {/* Divider */}
181
85
  <div className="w-8 h-px bg-gray-300 dark:bg-gray-600 mx-auto my-1" />
182
86
 
183
- {/* View Controls */}
184
87
  <ControlButton
185
88
  onClick={() => onFitView?.()}
186
89
  disabled={totalNodes === 0}
@@ -196,7 +99,6 @@ export const GraphControls: React.FC<GraphControlsProps> = ({
196
99
  />
197
100
  </div>
198
101
 
199
- {/* Info Panel */}
200
102
  <div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-600 dark:text-gray-400">
201
103
  <div className="whitespace-nowrap">
202
104
  <strong>Nodes:</strong> {totalNodes}
@@ -206,15 +108,6 @@ export const GraphControls: React.FC<GraphControlsProps> = ({
206
108
  <strong>Pinned:</strong> {pinnedCount}
207
109
  </div>
208
110
  )}
209
- <div className="mt-2 text-gray-500 dark:text-gray-500 leading-snug">
210
- <strong>Tips:</strong>
211
- <ul className="mt-1 ml-1 space-y-0.5">
212
- <li>• Drag nodes to reposition</li>
213
- <li>• Double-click to pin/unpin</li>
214
- <li>• Double-click canvas to unpin all</li>
215
- <li>• Scroll to zoom</li>
216
- </ul>
217
- </div>
218
111
  </div>
219
112
  </div>
220
113
  );
@@ -0,0 +1,31 @@
1
+ // Re-export types
2
+ export type {
3
+ GraphNode,
4
+ GraphLink,
5
+ LayoutType,
6
+ ForceDirectedGraphHandle,
7
+ ForceDirectedGraphProps,
8
+ } from './types';
9
+
10
+ // Re-export hooks
11
+ export {
12
+ pinNode,
13
+ unpinNode,
14
+ unpinAllNodes,
15
+ useGraphZoom,
16
+ useWindowDrag,
17
+ useNodeInteractions,
18
+ } from './useGraphInteractions';
19
+
20
+ export { useGraphLayout, useSimulationControls } from './useGraphLayout';
21
+ export { useImperativeHandleMethods } from './useImperativeHandle';
22
+
23
+ // Re-export components
24
+ export { GraphControls } from './GraphControls';
25
+ export type { GraphControlsProps } from './GraphControls';
26
+ export { ControlButton } from './ControlButton';
27
+ export type { ControlButtonProps } from './ControlButton';
28
+ export { GraphCanvas } from './GraphCanvas';
29
+
30
+ // Re-export the main component
31
+ export { ForceDirectedGraph } from './ForceDirectedGraph';
@@ -0,0 +1,102 @@
1
+ import type { BaseGraphNode, BaseGraphLink } from '@aiready/core/client';
2
+
3
+ /**
4
+ * Graph node extending core BaseGraphNode with component-specific properties
5
+ */
6
+ export interface GraphNode extends BaseGraphNode {
7
+ label?: string;
8
+ color?: string;
9
+ size?: number;
10
+ group?: string;
11
+ kind?: 'file' | 'package';
12
+ packageGroup?: string;
13
+ }
14
+
15
+ /**
16
+ * Graph link extending core BaseGraphLink with component-specific properties
17
+ */
18
+ export interface GraphLink extends BaseGraphLink {
19
+ color?: string;
20
+ width?: number;
21
+ label?: string;
22
+ type?: string;
23
+ }
24
+
25
+ export type LayoutType = 'force' | 'hierarchical' | 'circular';
26
+
27
+ /**
28
+ * Handle for imperative actions on the ForceDirectedGraph.
29
+ */
30
+ export interface ForceDirectedGraphHandle {
31
+ /** Pins all nodes to their current positions. */
32
+ pinAll: () => void;
33
+ /** Unpins all nodes, allowing them to move freely in the simulation. */
34
+ unpinAll: () => void;
35
+ /** Resets the layout by unpinning all nodes and restarting the simulation. */
36
+ resetLayout: () => void;
37
+ /** Rescales and re-centers the view to fit all nodes. */
38
+ fitView: () => void;
39
+ /** Returns the IDs of all currently pinned nodes. */
40
+ getPinnedNodes: () => string[];
41
+ /**
42
+ * Enable or disable drag mode for nodes.
43
+ * @param enabled - When true, nodes can be dragged; when false, dragging is disabled
44
+ */
45
+ setDragMode: (enabled: boolean) => void;
46
+ /** Sets the current layout type. */
47
+ setLayout: (layout: LayoutType) => void;
48
+ /** Gets the current layout type. */
49
+ getLayout: () => LayoutType;
50
+ }
51
+
52
+ /**
53
+ * Props for the ForceDirectedGraph component.
54
+ */
55
+ export interface ForceDirectedGraphProps {
56
+ /** Array of node objects to render. */
57
+ nodes: GraphNode[];
58
+ /** Array of link objects to render. */
59
+ links: GraphLink[];
60
+ /** Width of the SVG canvas. */
61
+ width: number;
62
+ /** Height of the SVG canvas. */
63
+ height: number;
64
+ /** Whether to enable zoom and pan interactions. */
65
+ enableZoom?: boolean;
66
+ /** Whether to enable node dragging. */
67
+ enableDrag?: boolean;
68
+ /** Callback fired when a node is clicked. */
69
+ onNodeClick?: (node: GraphNode) => void;
70
+ /** Callback fired when a node is hovered. */
71
+ onNodeHover?: (node: GraphNode | null) => void;
72
+ /** Callback fired when a link is clicked. */
73
+ onLinkClick?: (link: GraphLink) => void;
74
+ /** ID of the currently selected node. */
75
+ selectedNodeId?: string;
76
+ /** ID of the currently hovered node. */
77
+ hoveredNodeId?: string;
78
+ /** Default fallback color for nodes. */
79
+ defaultNodeColor?: string;
80
+ /** Default fallback size for nodes. */
81
+ defaultNodeSize?: number;
82
+ /** Default fallback color for links. */
83
+ defaultLinkColor?: string;
84
+ /** Default fallback width for links. */
85
+ defaultLinkWidth?: number;
86
+ /** Whether to show labels on nodes. */
87
+ showNodeLabels?: boolean;
88
+ /** Whether to show labels on links. */
89
+ showLinkLabels?: boolean;
90
+ /** Additional CSS classes for the SVG element. */
91
+ className?: string;
92
+ /** Whether manual layout mode is active. */
93
+ manualLayout?: boolean;
94
+ /** Callback fired when manual layout mode changes. */
95
+ onManualLayoutChange?: (enabled: boolean) => void;
96
+ /** Optional bounds for package groups. */
97
+ packageBounds?: Record<string, { x: number; y: number; r: number }>;
98
+ /** Current layout algorithm. */
99
+ layout?: LayoutType;
100
+ /** Callback fired when layout changes. */
101
+ onLayoutChange?: (layout: LayoutType) => void;
102
+ }