seat-editor 3.4.8 → 3.5.1

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 (145) hide show
  1. package/dist/app/constant.d.ts +1 -1
  2. package/dist/app/graph-view/page.d.ts +1 -0
  3. package/dist/app/graph-view/page.js +343 -0
  4. package/dist/app/graph-view/page.jsx +445 -0
  5. package/dist/app/graph-view-new/constant.d.ts +581 -0
  6. package/dist/app/graph-view-new/constant.js +6973 -0
  7. package/dist/app/graph-view-new/page.d.ts +1 -0
  8. package/dist/app/graph-view-new/page.js +71 -0
  9. package/dist/app/graph-view-new/page.jsx +98 -0
  10. package/dist/app/layout.d.ts +1 -1
  11. package/dist/app/new-board/page.d.ts +1 -1
  12. package/dist/app/new-board/page.js +43 -7
  13. package/dist/app/new-board/page.jsx +45 -12
  14. package/dist/app/old-board/page.d.ts +1 -2
  15. package/dist/app/only-view/chair.d.ts +1 -1
  16. package/dist/app/only-view/chair.js +2 -10
  17. package/dist/app/only-view/page.d.ts +1 -1
  18. package/dist/app/only-view/user.d.ts +1 -1
  19. package/dist/app/only-view/user.js +2 -10
  20. package/dist/app/page.d.ts +1 -1
  21. package/dist/app/test/page.d.ts +1 -2
  22. package/dist/app/v2/page.d.ts +1 -1
  23. package/dist/components/button-tools/index.d.ts +1 -1
  24. package/dist/components/button-tools/index.js +7 -5
  25. package/dist/components/button-tools/index.jsx +21 -9
  26. package/dist/components/form-tools/label.d.ts +1 -1
  27. package/dist/components/form-tools/label.js +9 -20
  28. package/dist/components/form-tools/label.jsx +38 -28
  29. package/dist/components/form-tools/shape.d.ts +1 -1
  30. package/dist/components/form-tools/shape.js +5 -5
  31. package/dist/components/form-tools/shape.jsx +8 -8
  32. package/dist/components/input/number-indicator.d.ts +1 -1
  33. package/dist/components/joystick/index.d.ts +1 -2
  34. package/dist/components/layer/index.d.ts +1 -1
  35. package/dist/components/layer-v2/index.d.ts +1 -1
  36. package/dist/components/layer-v3/index.d.ts +1 -1
  37. package/dist/components/layer-v3/index.js +44 -3
  38. package/dist/components/layer-v3/index.jsx +120 -3
  39. package/dist/components/layer-v4/index.d.ts +1 -1
  40. package/dist/components/layer-v5/constant.d.ts +60 -0
  41. package/dist/components/layer-v5/constant.js +93 -0
  42. package/dist/components/layer-v5/index.d.ts +24 -0
  43. package/dist/components/layer-v5/index.js +927 -0
  44. package/dist/components/layer-v5/index.jsx +1049 -0
  45. package/dist/components/lib/index.d.ts +1 -1
  46. package/dist/components/modal-preview/index.d.ts +1 -1
  47. package/dist/features/board/index.d.ts +1 -1
  48. package/dist/features/board-v2/index.d.ts +1 -2
  49. package/dist/features/board-v3/index.d.ts +1 -1
  50. package/dist/features/board-v3/index.js +350 -72
  51. package/dist/features/board-v3/index.jsx +369 -75
  52. package/dist/features/board-v3/resize-element.js +5 -0
  53. package/dist/features/board-v3/utils.d.ts +8 -0
  54. package/dist/features/board-v3/utils.js +23 -7
  55. package/dist/features/navbar/index.d.ts +1 -1
  56. package/dist/features/package/index.d.ts +3 -1
  57. package/dist/features/package/index.js +1 -1
  58. package/dist/features/package/index.jsx +6 -1
  59. package/dist/features/panel/index.d.ts +9 -1
  60. package/dist/features/panel/index.js +160 -38
  61. package/dist/features/panel/index.jsx +173 -46
  62. package/dist/features/panel/polygon.d.ts +2 -0
  63. package/dist/features/panel/polygon.js +44 -0
  64. package/dist/features/panel/polygon.jsx +70 -0
  65. package/dist/features/panel/select-tool.d.ts +1 -1
  66. package/dist/features/panel/select-tool.js +3 -0
  67. package/dist/features/panel/select-tool.jsx +3 -0
  68. package/dist/features/panel/selected-group.d.ts +1 -1
  69. package/dist/features/panel/selected-group.js +24 -26
  70. package/dist/features/panel/selected-group.jsx +56 -51
  71. package/dist/features/panel/square-circle-tool.d.ts +1 -1
  72. package/dist/features/panel/table-seat-circle.d.ts +1 -1
  73. package/dist/features/panel/table-seat-square.d.ts +1 -1
  74. package/dist/features/panel/text-tool.d.ts +1 -1
  75. package/dist/features/panel/text-tool.js +17 -2
  76. package/dist/features/panel/text-tool.jsx +19 -2
  77. package/dist/features/panel/upload-tool.d.ts +1 -1
  78. package/dist/features/panel/upload-tool.js +17 -3
  79. package/dist/features/panel/upload-tool.jsx +23 -4
  80. package/dist/features/side-tool/index.d.ts +1 -1
  81. package/dist/features/side-tool/index.js +43 -6
  82. package/dist/features/side-tool/index.jsx +47 -10
  83. package/dist/features/view-only/index.d.ts +1 -1
  84. package/dist/features/view-only-2/index.d.ts +1 -1
  85. package/dist/features/view-only-3/index.d.ts +1 -1
  86. package/dist/features/view-only-4/connect-handle.d.ts +13 -0
  87. package/dist/features/view-only-4/connect-handle.js +23 -0
  88. package/dist/features/view-only-4/connect-handle.jsx +30 -0
  89. package/dist/features/view-only-4/connection-layer.d.ts +21 -0
  90. package/dist/features/view-only-4/connection-layer.js +219 -0
  91. package/dist/features/view-only-4/connection-layer.jsx +291 -0
  92. package/dist/features/view-only-4/index.d.ts +99 -0
  93. package/dist/features/view-only-4/index.js +684 -0
  94. package/dist/features/view-only-4/index.jsx +722 -0
  95. package/dist/features/view-only-4/integration-guide.d.ts +0 -0
  96. package/dist/features/view-only-4/integration-guide.js +0 -0
  97. package/dist/features/view-only-4/use-connection-graph.d.ts +41 -0
  98. package/dist/features/view-only-4/use-connection-graph.js +182 -0
  99. package/dist/features/view-only-4/utils.d.ts +74 -0
  100. package/dist/features/view-only-4/utils.js +106 -0
  101. package/dist/features/view-only-5/connect-handle.d.ts +30 -0
  102. package/dist/features/view-only-5/connect-handle.js +88 -0
  103. package/dist/features/view-only-5/connect-handle.jsx +96 -0
  104. package/dist/features/view-only-5/connection-layer.d.ts +34 -0
  105. package/dist/features/view-only-5/connection-layer.js +182 -0
  106. package/dist/features/view-only-5/connection-layer.jsx +265 -0
  107. package/dist/features/view-only-5/index.d.ts +102 -0
  108. package/dist/features/view-only-5/index.js +585 -0
  109. package/dist/features/view-only-5/index.jsx +614 -0
  110. package/dist/features/view-only-5/use-connection-graph.d.ts +57 -0
  111. package/dist/features/view-only-5/use-connection-graph.js +196 -0
  112. package/dist/features/view-only-5/utils.d.ts +52 -0
  113. package/dist/features/view-only-5/utils.js +80 -0
  114. package/dist/features/view-only-6/connect-handle.d.ts +13 -0
  115. package/dist/features/view-only-6/connect-handle.js +20 -0
  116. package/dist/features/view-only-6/connect-handle.jsx +21 -0
  117. package/dist/features/view-only-6/connection-layer.d.ts +22 -0
  118. package/dist/features/view-only-6/connection-layer.js +191 -0
  119. package/dist/features/view-only-6/connection-layer.jsx +244 -0
  120. package/dist/features/view-only-6/index.d.ts +99 -0
  121. package/dist/features/view-only-6/index.js +687 -0
  122. package/dist/features/view-only-6/index.jsx +724 -0
  123. package/dist/features/view-only-6/use-connection-graph.d.ts +26 -0
  124. package/dist/features/view-only-6/use-connection-graph.js +103 -0
  125. package/dist/features/view-only-6/utils.d.ts +66 -0
  126. package/dist/features/view-only-6/utils.js +96 -0
  127. package/dist/features/view-only-7/connect-handle.d.ts +13 -0
  128. package/dist/features/view-only-7/connect-handle.js +23 -0
  129. package/dist/features/view-only-7/connect-handle.jsx +30 -0
  130. package/dist/features/view-only-7/connection-layer.d.ts +22 -0
  131. package/dist/features/view-only-7/connection-layer.js +165 -0
  132. package/dist/features/view-only-7/connection-layer.jsx +217 -0
  133. package/dist/features/view-only-7/index.d.ts +99 -0
  134. package/dist/features/view-only-7/index.js +687 -0
  135. package/dist/features/view-only-7/index.jsx +724 -0
  136. package/dist/features/view-only-7/use-connection-graph.d.ts +26 -0
  137. package/dist/features/view-only-7/use-connection-graph.js +104 -0
  138. package/dist/features/view-only-7/utils.d.ts +69 -0
  139. package/dist/features/view-only-7/utils.js +144 -0
  140. package/dist/index.d.ts +2 -1
  141. package/dist/index.js +2 -1
  142. package/dist/provider/redux-provider.d.ts +1 -1
  143. package/dist/provider/store-provider.d.ts +1 -1
  144. package/dist/seat-editor.css +1 -1
  145. package/package.json +1 -1
@@ -0,0 +1,445 @@
1
+ "use client";
2
+ import { useState, useRef, useCallback } from "react";
3
+ const GRID_SIZE = 20;
4
+ const NODE_WIDTH = 120;
5
+ const NODE_HEIGHT = 50;
6
+ const snap = (v) => Math.round(v / GRID_SIZE) * GRID_SIZE;
7
+ const getRectEdge = (from, to) => {
8
+ const dx = to.x - from.x;
9
+ const dy = to.y - from.y;
10
+ const angle = Math.atan2(dy, dx);
11
+ const absCos = Math.abs(Math.cos(angle));
12
+ const absSin = Math.abs(Math.sin(angle));
13
+ const offset = (NODE_HEIGHT / 2) * absCos <= (NODE_WIDTH / 2) * absSin
14
+ ? NODE_HEIGHT / 2 / absSin
15
+ : NODE_WIDTH / 2 / absCos;
16
+ return {
17
+ x: snap(from.x + Math.cos(angle) * offset),
18
+ y: snap(from.y + Math.sin(angle) * offset),
19
+ };
20
+ };
21
+ // Build orthogonal path through waypoints
22
+ const buildPath = (points) => {
23
+ if (points.length < 2)
24
+ return "";
25
+ let d = `M ${points[0].x} ${points[0].y}`;
26
+ for (let i = 1; i < points.length; i++) {
27
+ const prev = points[i - 1];
28
+ const curr = points[i];
29
+ const midX = snap((prev.x + curr.x) / 2);
30
+ if (i === points.length - 1) {
31
+ d += ` H ${midX} V ${curr.y} H ${curr.x}`;
32
+ }
33
+ else {
34
+ d += ` H ${midX} V ${curr.y} H ${curr.x}`;
35
+ }
36
+ }
37
+ return d;
38
+ };
39
+ // Generate default waypoints between two nodes
40
+ const defaultWaypoints = (start, end) => {
41
+ const midX = snap((start.x + end.x) / 2);
42
+ const midY = snap((start.y + end.y) / 2);
43
+ return [{ x: midX, y: midY }];
44
+ };
45
+ const NODE_COLORS = [
46
+ { bg: "#1e293b", border: "#38bdf8", text: "#e0f2fe" },
47
+ { bg: "#1e1b2e", border: "#a78bfa", text: "#ede9fe" },
48
+ { bg: "#0f2720", border: "#34d399", text: "#d1fae5" },
49
+ { bg: "#2d1515", border: "#f87171", text: "#fee2e2" },
50
+ { bg: "#1a1a10", border: "#fbbf24", text: "#fef9c3" },
51
+ ];
52
+ let nodeCounter = 3;
53
+ const initialNodes = [
54
+ { id: "1", x: 100, y: 160, label: "Start", colorIdx: 2 },
55
+ { id: "2", x: 380, y: 260, label: "Process", colorIdx: 0 },
56
+ { id: "3", x: 660, y: 160, label: "End", colorIdx: 3 },
57
+ ];
58
+ export default function DrawIO() {
59
+ const [nodes, setNodes] = useState(initialNodes);
60
+ const [edges, setEdges] = useState(() => {
61
+ const e1start = getRectEdge(initialNodes[0], initialNodes[1]);
62
+ const e1end = getRectEdge(initialNodes[1], initialNodes[0]);
63
+ const e2start = getRectEdge(initialNodes[1], initialNodes[2]);
64
+ const e2end = getRectEdge(initialNodes[2], initialNodes[1]);
65
+ return [
66
+ {
67
+ id: "e1",
68
+ from: "1",
69
+ to: "2",
70
+ waypoints: defaultWaypoints(e1start, e1end),
71
+ },
72
+ {
73
+ id: "e2",
74
+ from: "2",
75
+ to: "3",
76
+ waypoints: defaultWaypoints(e2start, e2end),
77
+ },
78
+ ];
79
+ });
80
+ const [draggingNode, setDraggingNode] = useState(null);
81
+ const [draggingWP, setDraggingWP] = useState(null); // { edgeId, index }
82
+ const [connecting, setConnecting] = useState(null);
83
+ const [selectedNode, setSelectedNode] = useState(null);
84
+ const [selectedEdge, setSelectedEdge] = useState(null);
85
+ const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
86
+ const [showGrid, setShowGrid] = useState(true);
87
+ const svgRef = useRef(null);
88
+ const getSVGPoint = useCallback((e) => {
89
+ const rect = svgRef.current.getBoundingClientRect();
90
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
91
+ }, []);
92
+ const handleMouseMove = useCallback((e) => {
93
+ const pt = getSVGPoint(e);
94
+ setMousePos(pt);
95
+ if (draggingNode) {
96
+ const snappedX = snap(pt.x - draggingNode.offsetX);
97
+ const snappedY = snap(pt.y - draggingNode.offsetY);
98
+ setNodes((prev) => prev.map((n) => n.id === draggingNode.id ? Object.assign(Object.assign({}, n), { x: snappedX, y: snappedY }) : n));
99
+ }
100
+ if (draggingWP) {
101
+ setEdges((prev) => prev.map((ed) => {
102
+ if (ed.id !== draggingWP.edgeId)
103
+ return ed;
104
+ const newWP = ed.waypoints.map((wp, i) => i === draggingWP.index ? { x: snap(pt.x), y: snap(pt.y) } : wp);
105
+ return Object.assign(Object.assign({}, ed), { waypoints: newWP });
106
+ }));
107
+ }
108
+ }, [draggingNode, draggingWP, getSVGPoint]);
109
+ const handleMouseUp = useCallback(() => {
110
+ setDraggingNode(null);
111
+ setDraggingWP(null);
112
+ }, []);
113
+ const handleNodeMouseDown = (e, nodeId) => {
114
+ e.stopPropagation();
115
+ if (connecting)
116
+ return;
117
+ const pt = getSVGPoint(e);
118
+ const node = nodes.find((n) => n.id === nodeId);
119
+ setDraggingNode({
120
+ id: nodeId,
121
+ offsetX: pt.x - node.x,
122
+ offsetY: pt.y - node.y,
123
+ });
124
+ setSelectedNode(nodeId);
125
+ setSelectedEdge(null);
126
+ };
127
+ const handleWaypointMouseDown = (e, edgeId, index) => {
128
+ e.stopPropagation();
129
+ setDraggingWP({ edgeId, index });
130
+ setSelectedEdge(edgeId);
131
+ setSelectedNode(null);
132
+ };
133
+ // Add waypoint on edge double-click
134
+ const handleEdgeDoubleClick = (e, edgeId) => {
135
+ e.stopPropagation();
136
+ const pt = getSVGPoint(e);
137
+ setEdges((prev) => prev.map((ed) => {
138
+ if (ed.id !== edgeId)
139
+ return ed;
140
+ // Insert waypoint near click position
141
+ const newWP = [...ed.waypoints, { x: snap(pt.x), y: snap(pt.y) }];
142
+ // Sort by proximity to path order (simple: by x if mostly horizontal)
143
+ return Object.assign(Object.assign({}, ed), { waypoints: newWP });
144
+ }));
145
+ };
146
+ // Remove waypoint on right-click
147
+ const handleWaypointRightClick = (e, edgeId, index) => {
148
+ e.preventDefault();
149
+ e.stopPropagation();
150
+ setEdges((prev) => prev.map((ed) => {
151
+ if (ed.id !== edgeId)
152
+ return ed;
153
+ if (ed.waypoints.length <= 1)
154
+ return ed; // keep at least 1
155
+ return Object.assign(Object.assign({}, ed), { waypoints: ed.waypoints.filter((_, i) => i !== index) });
156
+ }));
157
+ };
158
+ const handleConnectStart = (e, nodeId) => {
159
+ e.stopPropagation();
160
+ setConnecting({ fromId: nodeId });
161
+ setSelectedNode(null);
162
+ setSelectedEdge(null);
163
+ };
164
+ const handleConnectEnd = (e, nodeId) => {
165
+ e.stopPropagation();
166
+ if (connecting && connecting.fromId !== nodeId) {
167
+ const exists = edges.find((ed) => (ed.from === connecting.fromId && ed.to === nodeId) ||
168
+ (ed.from === nodeId && ed.to === connecting.fromId));
169
+ if (!exists) {
170
+ const fromNode = nodes.find((n) => n.id === connecting.fromId);
171
+ const toNode = nodes.find((n) => n.id === nodeId);
172
+ const start = getRectEdge(fromNode, toNode);
173
+ const end = getRectEdge(toNode, fromNode);
174
+ setEdges((prev) => [
175
+ ...prev,
176
+ {
177
+ id: `e${Date.now()}`,
178
+ from: connecting.fromId,
179
+ to: nodeId,
180
+ waypoints: defaultWaypoints(start, end),
181
+ },
182
+ ]);
183
+ }
184
+ }
185
+ setConnecting(null);
186
+ };
187
+ const addNode = () => {
188
+ nodeCounter++;
189
+ setNodes((prev) => [
190
+ ...prev,
191
+ {
192
+ id: String(nodeCounter),
193
+ x: snap(120 + Math.random() * 380),
194
+ y: snap(80 + Math.random() * 220),
195
+ label: `Node ${nodeCounter}`,
196
+ colorIdx: Math.floor(Math.random() * NODE_COLORS.length),
197
+ },
198
+ ]);
199
+ };
200
+ const deleteSelected = () => {
201
+ if (selectedNode) {
202
+ setNodes((prev) => prev.filter((n) => n.id !== selectedNode));
203
+ setEdges((prev) => prev.filter((e) => e.from !== selectedNode && e.to !== selectedNode));
204
+ setSelectedNode(null);
205
+ }
206
+ if (selectedEdge) {
207
+ setEdges((prev) => prev.filter((e) => e.id !== selectedEdge));
208
+ setSelectedEdge(null);
209
+ }
210
+ };
211
+ const renameSelected = () => {
212
+ if (!selectedNode)
213
+ return;
214
+ const node = nodes.find((n) => n.id === selectedNode);
215
+ const label = window.prompt("Rename node:", node.label);
216
+ if (label)
217
+ setNodes((prev) => prev.map((n) => (n.id === selectedNode ? Object.assign(Object.assign({}, n), { label }) : n)));
218
+ };
219
+ return (<div style={{
220
+ width: "100vw",
221
+ height: "100vh",
222
+ background: "#0a0e1a",
223
+ display: "flex",
224
+ flexDirection: "column",
225
+ fontFamily: "'IBM Plex Mono', monospace",
226
+ color: "#94a3b8",
227
+ }}>
228
+ {/* Toolbar */}
229
+ <div style={{
230
+ display: "flex",
231
+ alignItems: "center",
232
+ gap: 8,
233
+ padding: "10px 20px",
234
+ background: "#0f1629",
235
+ borderBottom: "1px solid #1e2d4a",
236
+ flexShrink: 0,
237
+ }}>
238
+ <span style={{
239
+ color: "#38bdf8",
240
+ fontWeight: 700,
241
+ fontSize: 15,
242
+ marginRight: 16,
243
+ letterSpacing: 2,
244
+ }}>
245
+ ◈ DIAGRAM
246
+ </span>
247
+ {[
248
+ { label: "+ Node", action: addNode, color: "#34d399" },
249
+ {
250
+ label: "✎ Rename",
251
+ action: renameSelected,
252
+ color: "#fbbf24",
253
+ disabled: !selectedNode,
254
+ },
255
+ {
256
+ label: "✕ Delete",
257
+ action: deleteSelected,
258
+ color: "#f87171",
259
+ disabled: !selectedNode && !selectedEdge,
260
+ },
261
+ ].map((btn) => (<button key={btn.label} onClick={btn.action} disabled={btn.disabled} style={{
262
+ background: "transparent",
263
+ border: `1px solid ${btn.disabled ? "#1e2d4a" : btn.color}`,
264
+ color: btn.disabled ? "#2d3f5a" : btn.color,
265
+ padding: "5px 14px",
266
+ borderRadius: 4,
267
+ cursor: btn.disabled ? "default" : "pointer",
268
+ fontSize: 12,
269
+ fontFamily: "inherit",
270
+ letterSpacing: 1,
271
+ }}>
272
+ {btn.label}
273
+ </button>))}
274
+ <label style={{
275
+ marginLeft: "auto",
276
+ display: "flex",
277
+ alignItems: "center",
278
+ gap: 6,
279
+ fontSize: 12,
280
+ cursor: "pointer",
281
+ }}>
282
+ <input type="checkbox" checked={showGrid} onChange={(e) => setShowGrid(e.target.checked)}/>{" "}
283
+ Grid
284
+ </label>
285
+ {connecting && (<span style={{ color: "#a78bfa", fontSize: 12, marginLeft: 8 }}>
286
+ ⟳ Click target node… (ESC)
287
+ </span>)}
288
+ </div>
289
+
290
+ {/* Hint bar */}
291
+ <div style={{
292
+ padding: "4px 20px",
293
+ fontSize: 11,
294
+ color: "#2d3f5a",
295
+ background: "#0a0e1a",
296
+ borderBottom: "1px solid #111827",
297
+ }}>
298
+ Drag nodes · ⊕ to connect · Double-click edge to add waypoint · Drag
299
+ waypoint ● to reshape · Right-click waypoint to remove
300
+ </div>
301
+
302
+ {/* SVG Canvas */}
303
+ <svg ref={svgRef} style={{ flex: 1, cursor: connecting ? "crosshair" : "default" }} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onClick={() => {
304
+ setSelectedNode(null);
305
+ setSelectedEdge(null);
306
+ if (connecting)
307
+ setConnecting(null);
308
+ }} tabIndex={0} onKeyDown={(e) => e.key === "Escape" && setConnecting(null)}>
309
+ <defs>
310
+ <pattern id="sg" width={GRID_SIZE} height={GRID_SIZE} patternUnits="userSpaceOnUse">
311
+ <path d={`M ${GRID_SIZE} 0 L 0 0 0 ${GRID_SIZE}`} fill="none" stroke="#111827" strokeWidth={0.5}/>
312
+ </pattern>
313
+ <pattern id="bg" width={GRID_SIZE * 5} height={GRID_SIZE * 5} patternUnits="userSpaceOnUse">
314
+ <rect width={GRID_SIZE * 5} height={GRID_SIZE * 5} fill="url(#sg)"/>
315
+ <path d={`M ${GRID_SIZE * 5} 0 L 0 0 0 ${GRID_SIZE * 5}`} fill="none" stroke="#1a2540" strokeWidth={1}/>
316
+ </pattern>
317
+ <marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">
318
+ <path d="M 0 0 L 8 3 L 0 6 Z" fill="#38bdf8" opacity={0.8}/>
319
+ </marker>
320
+ <marker id="arrow-sel" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">
321
+ <path d="M 0 0 L 8 3 L 0 6 Z" fill="#a78bfa"/>
322
+ </marker>
323
+ <marker id="arrow-preview" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">
324
+ <path d="M 0 0 L 8 3 L 0 6 Z" fill="#a78bfa" opacity={0.8}/>
325
+ </marker>
326
+ <filter id="glow">
327
+ <feGaussianBlur stdDeviation="3" result="coloredBlur"/>
328
+ <feMerge>
329
+ <feMergeNode in="coloredBlur"/>
330
+ <feMergeNode in="SourceGraphic"/>
331
+ </feMerge>
332
+ </filter>
333
+ </defs>
334
+
335
+ {showGrid && <rect width="100%" height="100%" fill="url(#bg)"/>}
336
+
337
+ {/* Edges */}
338
+ {edges.map((edge) => {
339
+ const fromNode = nodes.find((n) => n.id === edge.from);
340
+ const toNode = nodes.find((n) => n.id === edge.to);
341
+ if (!fromNode || !toNode)
342
+ return null;
343
+ const start = getRectEdge(fromNode, toNode);
344
+ const end = getRectEdge(toNode, fromNode);
345
+ const allPoints = [start, ...edge.waypoints, end];
346
+ const pathD = buildPath(allPoints);
347
+ const isSelected = selectedEdge === edge.id;
348
+ return (<g key={edge.id}>
349
+ {/* Invisible thick hit area */}
350
+ <path d={pathD} stroke="transparent" strokeWidth={12} fill="none" style={{ cursor: "pointer" }} onClick={(e) => {
351
+ e.stopPropagation();
352
+ setSelectedEdge(edge.id);
353
+ setSelectedNode(null);
354
+ }} onDoubleClick={(e) => handleEdgeDoubleClick(e, edge.id)}/>
355
+
356
+ {/* Glow when selected */}
357
+ {isSelected && (<path d={pathD} stroke="#a78bfa" strokeWidth={6} fill="none" opacity={0.15}/>)}
358
+
359
+ {/* Main line */}
360
+ <path d={pathD} stroke={isSelected ? "#a78bfa" : "#38bdf8"} strokeWidth={isSelected ? 2 : 1.5} fill="none" opacity={0.85} markerEnd={isSelected ? "url(#arrow-sel)" : "url(#arrow)"} style={{ pointerEvents: "none" }}/>
361
+
362
+ {/* Waypoints — always show, highlight when edge selected */}
363
+ {edge.waypoints.map((wp, i) => (<g key={i}>
364
+ {/* Outer ring */}
365
+ <circle cx={wp.x} cy={wp.y} r={isSelected ? 8 : 5} fill={isSelected ? "#1e1b2e" : "#0a0e1a"} stroke={isSelected ? "#a78bfa" : "#38bdf8"} strokeWidth={isSelected ? 2 : 1} style={{ cursor: "grab" }} onMouseDown={(e) => handleWaypointMouseDown(e, edge.id, i)} onContextMenu={(e) => handleWaypointRightClick(e, edge.id, i)}/>
366
+ {/* Inner dot */}
367
+ <circle cx={wp.x} cy={wp.y} r={isSelected ? 3 : 2} fill={isSelected ? "#a78bfa" : "#38bdf8"} style={{ pointerEvents: "none" }}/>
368
+ </g>))}
369
+
370
+ {/* Midpoint handles between waypoints (for adding new WP) */}
371
+ {isSelected &&
372
+ allPoints.slice(0, -1).map((pt, i) => {
373
+ const next = allPoints[i + 1];
374
+ const mx = snap((pt.x + next.x) / 2);
375
+ const my = snap((pt.y + next.y) / 2);
376
+ return (<circle key={`mid-${i}`} cx={mx} cy={my} r={4} fill="#1e1b2e" stroke="#a78bfa" strokeWidth={1} strokeDasharray="2 2" style={{ cursor: "crosshair" }} onMouseDown={(e) => {
377
+ e.stopPropagation();
378
+ // Insert new waypoint at this midpoint
379
+ setEdges((prev) => prev.map((ed) => {
380
+ if (ed.id !== edge.id)
381
+ return ed;
382
+ const newWP = [...ed.waypoints];
383
+ // Insert after i-1 (offset by 1 for start point)
384
+ newWP.splice(i, 0, { x: mx, y: my });
385
+ return Object.assign(Object.assign({}, ed), { waypoints: newWP });
386
+ }));
387
+ setDraggingWP({ edgeId: edge.id, index: i });
388
+ }}/>);
389
+ })}
390
+ </g>);
391
+ })}
392
+
393
+ {/* Preview line while connecting */}
394
+ {connecting &&
395
+ (() => {
396
+ const fromNode = nodes.find((n) => n.id === connecting.fromId);
397
+ if (!fromNode)
398
+ return null;
399
+ const start = getRectEdge(fromNode, mousePos);
400
+ const pathD = buildPath([
401
+ start,
402
+ { x: snap(mousePos.x), y: snap(mousePos.y) },
403
+ ]);
404
+ return (<path d={pathD} stroke="#a78bfa" strokeWidth={1.5} fill="none" strokeDasharray="6 3" markerEnd="url(#arrow-preview)" opacity={0.8}/>);
405
+ })()}
406
+
407
+ {/* Nodes */}
408
+ {nodes.map((node) => {
409
+ const c = NODE_COLORS[node.colorIdx % NODE_COLORS.length];
410
+ const isSelected = selectedNode === node.id;
411
+ const hw = NODE_WIDTH / 2;
412
+ const hh = NODE_HEIGHT / 2;
413
+ return (<g key={node.id} transform={`translate(${node.x}, ${node.y})`} onMouseDown={(e) => handleNodeMouseDown(e, node.id)} onMouseUp={(e) => connecting && handleConnectEnd(e, node.id)} style={{
414
+ cursor: (draggingNode === null || draggingNode === void 0 ? void 0 : draggingNode.id) === node.id ? "grabbing" : "grab",
415
+ }}>
416
+ {isSelected && (<rect x={-hw - 4} y={-hh - 4} width={NODE_WIDTH + 8} height={NODE_HEIGHT + 8} fill="none" stroke={c.border} strokeWidth={1} rx={6} opacity={0.4} filter="url(#glow)"/>)}
417
+
418
+ <rect x={-hw} y={-hh} width={NODE_WIDTH} height={NODE_HEIGHT} fill={c.bg} stroke={c.border} strokeWidth={isSelected ? 2 : 1} rx={4} filter={isSelected ? "url(#glow)" : "none"}/>
419
+
420
+ <rect x={-hw} y={-hh} width={NODE_WIDTH} height={3} fill={c.border} rx={4} opacity={0.8}/>
421
+
422
+ <text textAnchor="middle" dominantBaseline="middle" y={2} fill={c.text} fontSize={12} fontFamily="'IBM Plex Mono', monospace" fontWeight={500} style={{ pointerEvents: "none", userSelect: "none" }}>
423
+ {node.label}
424
+ </text>
425
+
426
+ {/* Connect button */}
427
+ <g transform={`translate(${hw - 2}, ${-hh - 8})`} style={{ cursor: "crosshair" }} onMouseDown={(e) => {
428
+ e.stopPropagation();
429
+ handleConnectStart(e, node.id);
430
+ }}>
431
+ <circle r={7} fill="#0a0e1a" stroke={c.border} strokeWidth={1}/>
432
+ <text textAnchor="middle" dominantBaseline="middle" fill={c.border} fontSize={10} fontWeight={700} style={{ pointerEvents: "none" }}>
433
+
434
+ </text>
435
+ </g>
436
+ </g>);
437
+ })}
438
+ </svg>
439
+
440
+ <style>{`
441
+ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&display=swap');
442
+ * { box-sizing: border-box; }
443
+ `}</style>
444
+ </div>);
445
+ }