@aiready/components 0.1.0 → 0.1.4

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,218 @@
1
+ import React from 'react';
2
+ import { cn } from '../utils/cn';
3
+
4
+ export interface GraphControlsProps {
5
+ /**
6
+ * Whether dragging is enabled
7
+ */
8
+ dragEnabled?: boolean;
9
+
10
+ /**
11
+ * Callback to toggle drag mode
12
+ */
13
+ onDragToggle?: (enabled: boolean) => void;
14
+
15
+ /**
16
+ * Whether manual layout mode is enabled
17
+ */
18
+ manualLayout?: boolean;
19
+
20
+ /**
21
+ * Callback to toggle manual layout mode
22
+ */
23
+ onManualLayoutToggle?: (enabled: boolean) => void;
24
+
25
+ /**
26
+ * Callback to pin all nodes
27
+ */
28
+ onPinAll?: () => void;
29
+
30
+ /**
31
+ * Callback to unpin all nodes
32
+ */
33
+ onUnpinAll?: () => void;
34
+
35
+ /**
36
+ * Callback to center/reset the view
37
+ */
38
+ onReset?: () => void;
39
+
40
+ /**
41
+ * Callback to fit all nodes in view
42
+ */
43
+ onFitView?: () => void;
44
+
45
+ /**
46
+ * Number of pinned nodes
47
+ */
48
+ pinnedCount?: number;
49
+
50
+ /**
51
+ * Total number of nodes
52
+ */
53
+ totalNodes?: number;
54
+
55
+ /**
56
+ * Whether to show the controls
57
+ * @default true
58
+ */
59
+ visible?: boolean;
60
+
61
+ /**
62
+ * Position of the controls
63
+ * @default "top-left"
64
+ */
65
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
66
+
67
+ /**
68
+ * Additional CSS classes
69
+ */
70
+ className?: string;
71
+ }
72
+
73
+ /**
74
+ * GraphControls: Floating toolbar for manipulating graph layout and dragging
75
+ * Provides controls for toggling drag mode, manual layout, pinning nodes, and resetting the view
76
+ */
77
+ export const GraphControls: React.FC<GraphControlsProps> = ({
78
+ dragEnabled = true,
79
+ onDragToggle,
80
+ manualLayout = false,
81
+ onManualLayoutToggle,
82
+ onPinAll,
83
+ onUnpinAll,
84
+ onReset,
85
+ onFitView,
86
+ pinnedCount = 0,
87
+ totalNodes = 0,
88
+ visible = true,
89
+ position = 'top-left',
90
+ className,
91
+ }) => {
92
+
93
+ if (!visible) return null;
94
+
95
+ const positionClasses: Record<string, string> = {
96
+ 'top-left': 'top-4 left-4',
97
+ 'top-right': 'top-4 right-4',
98
+ 'bottom-left': 'bottom-4 left-4',
99
+ 'bottom-right': 'bottom-4 right-4',
100
+ };
101
+
102
+ const ControlButton: React.FC<{
103
+ onClick: () => void;
104
+ active?: boolean;
105
+ icon: string;
106
+ label: string;
107
+ disabled?: boolean;
108
+ }> = ({ onClick, active = false, icon, label, disabled = false }) => (
109
+ <div className="relative group">
110
+ <button
111
+ onClick={onClick}
112
+ disabled={disabled}
113
+ className={cn(
114
+ 'p-2 rounded-lg transition-all duration-200',
115
+ active
116
+ ? 'bg-blue-500 text-white shadow-md hover:bg-blue-600'
117
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200',
118
+ disabled && 'opacity-50 cursor-not-allowed hover:bg-gray-100',
119
+ 'dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 dark:active:bg-blue-600'
120
+ )}
121
+ title={label}
122
+ >
123
+ <span className="text-lg">{icon}</span>
124
+ </button>
125
+ <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">
126
+ {label}
127
+ </div>
128
+ </div>
129
+ );
130
+
131
+ return (
132
+ <div
133
+ className={cn(
134
+ 'fixed z-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-2 border border-gray-200 dark:border-gray-700',
135
+ positionClasses[position],
136
+ className
137
+ )}
138
+ >
139
+ <div className="flex flex-col gap-2">
140
+ {/* Drag Mode Toggle */}
141
+ <ControlButton
142
+ onClick={() => onDragToggle?.(!dragEnabled)}
143
+ active={dragEnabled}
144
+ icon="✋"
145
+ label={dragEnabled ? 'Drag enabled' : 'Drag disabled'}
146
+ />
147
+
148
+ {/* Manual Layout Toggle */}
149
+ <ControlButton
150
+ onClick={() => onManualLayoutToggle?.(!manualLayout)}
151
+ active={manualLayout}
152
+ icon="🔧"
153
+ label={manualLayout ? 'Manual layout: ON (drag freely)' : 'Manual layout: OFF (forces active)'}
154
+ />
155
+
156
+ {/* Divider */}
157
+ <div className="w-8 h-px bg-gray-300 dark:bg-gray-600 mx-auto my-1" />
158
+
159
+ {/* Pin/Unpin Controls */}
160
+ <div className="flex gap-1">
161
+ <ControlButton
162
+ onClick={() => onPinAll?.()}
163
+ disabled={totalNodes === 0}
164
+ icon="📌"
165
+ label={`Pin all nodes (${totalNodes})`}
166
+ />
167
+ <ControlButton
168
+ onClick={() => onUnpinAll?.()}
169
+ disabled={pinnedCount === 0}
170
+ icon="📍"
171
+ label={`Unpin all (${pinnedCount} pinned)`}
172
+ />
173
+ </div>
174
+
175
+ {/* Divider */}
176
+ <div className="w-8 h-px bg-gray-300 dark:bg-gray-600 mx-auto my-1" />
177
+
178
+ {/* View Controls */}
179
+ <ControlButton
180
+ onClick={() => onFitView?.()}
181
+ disabled={totalNodes === 0}
182
+ icon="🎯"
183
+ label="Fit all nodes in view"
184
+ />
185
+
186
+ <ControlButton
187
+ onClick={() => onReset?.()}
188
+ disabled={totalNodes === 0}
189
+ icon="↺"
190
+ label="Reset to auto-layout"
191
+ />
192
+ </div>
193
+
194
+ {/* Info Panel */}
195
+ <div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-600 dark:text-gray-400">
196
+ <div className="whitespace-nowrap">
197
+ <strong>Nodes:</strong> {totalNodes}
198
+ </div>
199
+ {pinnedCount > 0 && (
200
+ <div className="whitespace-nowrap">
201
+ <strong>Pinned:</strong> {pinnedCount}
202
+ </div>
203
+ )}
204
+ <div className="mt-2 text-gray-500 dark:text-gray-500 leading-snug">
205
+ <strong>Tips:</strong>
206
+ <ul className="mt-1 ml-1 space-y-0.5">
207
+ <li>• Drag nodes to reposition</li>
208
+ <li>• Double-click to pin/unpin</li>
209
+ <li>• Double-click canvas to unpin all</li>
210
+ <li>• Scroll to zoom</li>
211
+ </ul>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ );
216
+ };
217
+
218
+ GraphControls.displayName = 'GraphControls';
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import type { GraphLink, GraphNode } from './ForceDirectedGraph';
3
+
4
+ export interface LinkItemProps {
5
+ link: GraphLink;
6
+ onClick?: (l: GraphLink) => void;
7
+ defaultWidth?: number;
8
+ showLabel?: boolean;
9
+ nodes?: GraphNode[]; // Optional nodes array to resolve string IDs to node objects
10
+ }
11
+
12
+ export const LinkItem: React.FC<LinkItemProps> = ({ link, onClick, defaultWidth, showLabel = true, nodes = [] }) => {
13
+ const src = (link.source as any)?.id ?? (typeof link.source === 'string' ? link.source : undefined);
14
+ const tgt = (link.target as any)?.id ?? (typeof link.target === 'string' ? link.target : undefined);
15
+
16
+ // Helper to get node position from source/target (which could be node object or string ID)
17
+ const getNodePosition = (nodeOrId: string | GraphNode): { x: number; y: number } | null => {
18
+ if (typeof nodeOrId === 'object' && nodeOrId !== null) {
19
+ // It's a node object
20
+ const node = nodeOrId as GraphNode;
21
+ return { x: node.x ?? 0, y: node.y ?? 0 };
22
+ } else if (typeof nodeOrId === 'string') {
23
+ // It's a string ID, try to find in nodes array
24
+ const found = nodes.find(n => n.id === nodeOrId);
25
+ if (found) return { x: found.x ?? 0, y: found.y ?? 0 };
26
+ }
27
+ return null;
28
+ };
29
+
30
+ const sourcePos = getNodePosition(link.source);
31
+ const targetPos = getNodePosition(link.target);
32
+
33
+ // If we can't get positions, render nothing (or a placeholder)
34
+ if (!sourcePos || !targetPos) {
35
+ return null;
36
+ }
37
+
38
+ // Calculate midpoint for label positioning
39
+ const midX = (sourcePos.x + targetPos.x) / 2;
40
+ const midY = (sourcePos.y + targetPos.y) / 2;
41
+
42
+ return (
43
+ <g>
44
+ <line
45
+ x1={sourcePos.x}
46
+ y1={sourcePos.y}
47
+ x2={targetPos.x}
48
+ y2={targetPos.y}
49
+ data-source={src}
50
+ data-target={tgt}
51
+ stroke={link.color}
52
+ strokeWidth={link.width ?? defaultWidth ?? 1}
53
+ opacity={0.6}
54
+ className="cursor-pointer transition-opacity hover:opacity-100"
55
+ onClick={() => onClick?.(link)}
56
+ />
57
+ {showLabel && link.label && (
58
+ <text
59
+ x={midX}
60
+ y={midY}
61
+ fill="#666"
62
+ fontSize="10"
63
+ textAnchor="middle"
64
+ dominantBaseline="middle"
65
+ pointerEvents="none"
66
+ >
67
+ {link.label}
68
+ </text>
69
+ )}
70
+ </g>
71
+ );
72
+ };
73
+
74
+ export default LinkItem;
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import type { GraphNode } from './ForceDirectedGraph';
3
+
4
+ export interface NodeItemProps {
5
+ node: GraphNode;
6
+ isSelected: boolean;
7
+ isHovered: boolean;
8
+ pinned: boolean;
9
+ defaultNodeSize: number;
10
+ defaultNodeColor: string;
11
+ showLabel?: boolean;
12
+ onClick?: (n: GraphNode) => void;
13
+ onDoubleClick?: (e: React.MouseEvent, n: GraphNode) => void;
14
+ onMouseEnter?: (n: GraphNode) => void;
15
+ onMouseLeave?: () => void;
16
+ onMouseDown?: (e: React.MouseEvent, n: GraphNode) => void;
17
+ }
18
+
19
+ export const NodeItem: React.FC<NodeItemProps> = ({
20
+ node,
21
+ isSelected,
22
+ isHovered,
23
+ pinned,
24
+ defaultNodeSize,
25
+ defaultNodeColor,
26
+ showLabel = true,
27
+ onClick,
28
+ onDoubleClick,
29
+ onMouseEnter,
30
+ onMouseLeave,
31
+ onMouseDown,
32
+ }) => {
33
+ const nodeSize = node.size || defaultNodeSize;
34
+ const nodeColor = node.color || defaultNodeColor;
35
+
36
+ const x = node.x ?? 0;
37
+ const y = node.y ?? 0;
38
+
39
+ return (
40
+ <g
41
+ key={node.id}
42
+ className="cursor-pointer node"
43
+ data-id={node.id}
44
+ transform={`translate(${x},${y})`}
45
+ onClick={() => onClick?.(node)}
46
+ onDoubleClick={(e) => onDoubleClick?.(e, node)}
47
+ onMouseEnter={() => onMouseEnter?.(node)}
48
+ onMouseLeave={() => onMouseLeave?.()}
49
+ onMouseDown={(e) => onMouseDown?.(e, node)}
50
+ >
51
+ <circle
52
+ r={nodeSize}
53
+ fill={nodeColor}
54
+ stroke={isSelected ? '#000' : isHovered ? '#666' : 'none'}
55
+ strokeWidth={pinned ? 3 : isSelected ? 2.5 : isHovered ? 2 : 1.5}
56
+ opacity={isHovered || isSelected ? 1 : 0.9}
57
+ />
58
+ {pinned && (
59
+ <circle r={nodeSize + 4} fill="none" stroke="#ff6b6b" strokeWidth={1} opacity={0.5} className="pointer-events-none" />
60
+ )}
61
+ {showLabel && node.label && (
62
+ <text y={nodeSize + 15} fill="#333" fontSize="12" textAnchor="middle" dominantBaseline="middle" pointerEvents="none" className="select-none">
63
+ {node.label}
64
+ </text>
65
+ )}
66
+ </g>
67
+ );
68
+ };
69
+
70
+ export default NodeItem;