@aiready/components 0.13.18 → 0.13.20
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.
- package/dist/charts/ForceDirectedGraph.d.ts +7 -13
- package/dist/charts/ForceDirectedGraph.js +451 -337
- package/dist/charts/ForceDirectedGraph.js.map +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +728 -586
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/charts/ForceDirectedGraph.tsx +12 -509
- package/src/charts/LinkItem.tsx +1 -1
- package/src/charts/NodeItem.tsx +1 -1
- package/src/charts/force-directed/ControlButton.tsx +39 -0
- package/src/charts/force-directed/ForceDirectedGraph.tsx +250 -0
- package/src/charts/force-directed/GraphCanvas.tsx +129 -0
- package/src/charts/{GraphControls.tsx → force-directed/GraphControls.tsx} +3 -110
- package/src/charts/force-directed/index.ts +31 -0
- package/src/charts/force-directed/types.ts +102 -0
- package/src/charts/{hooks.ts → force-directed/useGraphInteractions.ts} +64 -1
- package/src/charts/force-directed/useGraphLayout.ts +54 -0
- package/src/charts/force-directed/useImperativeHandle.ts +131 -0
- package/src/charts/layout-utils.ts +1 -1
- package/src/components/feedback/__tests__/badge.test.tsx +92 -0
- package/src/components/ui/__tests__/button.test.tsx +203 -0
- package/src/data-display/__tests__/ScoreBar.test.tsx +215 -0
- package/src/index.ts +4 -1
- package/src/utils/__tests__/score.test.ts +28 -7
- package/src/utils/score.ts +67 -29
- package/src/charts/types.ts +0 -24
|
@@ -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 '
|
|
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
|
+
}
|