seat-editor 3.4.7 → 3.5.0

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 (108) 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/new-board/page.js +43 -7
  11. package/dist/app/new-board/page.jsx +45 -12
  12. package/dist/components/button-tools/index.js +7 -5
  13. package/dist/components/button-tools/index.jsx +21 -9
  14. package/dist/components/form-tools/label.js +9 -20
  15. package/dist/components/form-tools/label.jsx +38 -28
  16. package/dist/components/form-tools/shape.js +5 -5
  17. package/dist/components/form-tools/shape.jsx +8 -8
  18. package/dist/components/layer-v3/index.js +44 -3
  19. package/dist/components/layer-v3/index.jsx +120 -3
  20. package/dist/components/layer-v4/index.js +3 -2
  21. package/dist/components/layer-v4/index.jsx +3 -2
  22. package/dist/components/layer-v5/constant.d.ts +60 -0
  23. package/dist/components/layer-v5/constant.js +93 -0
  24. package/dist/components/layer-v5/index.d.ts +24 -0
  25. package/dist/components/layer-v5/index.js +927 -0
  26. package/dist/components/layer-v5/index.jsx +1049 -0
  27. package/dist/features/board-v3/index.js +350 -72
  28. package/dist/features/board-v3/index.jsx +369 -75
  29. package/dist/features/board-v3/resize-element.js +5 -0
  30. package/dist/features/board-v3/utils.d.ts +8 -0
  31. package/dist/features/board-v3/utils.js +23 -7
  32. package/dist/features/package/index.d.ts +2 -0
  33. package/dist/features/package/index.js +1 -1
  34. package/dist/features/package/index.jsx +6 -1
  35. package/dist/features/panel/index.d.ts +8 -0
  36. package/dist/features/panel/index.js +160 -38
  37. package/dist/features/panel/index.jsx +173 -46
  38. package/dist/features/panel/polygon.d.ts +2 -0
  39. package/dist/features/panel/polygon.js +44 -0
  40. package/dist/features/panel/polygon.jsx +70 -0
  41. package/dist/features/panel/select-tool.js +3 -0
  42. package/dist/features/panel/select-tool.jsx +3 -0
  43. package/dist/features/panel/selected-group.js +24 -26
  44. package/dist/features/panel/selected-group.jsx +56 -51
  45. package/dist/features/panel/text-tool.js +17 -2
  46. package/dist/features/panel/text-tool.jsx +19 -2
  47. package/dist/features/panel/upload-tool.js +17 -3
  48. package/dist/features/panel/upload-tool.jsx +23 -4
  49. package/dist/features/side-tool/index.js +43 -6
  50. package/dist/features/side-tool/index.jsx +47 -10
  51. package/dist/features/view-only-4/connect-handle.d.ts +13 -0
  52. package/dist/features/view-only-4/connect-handle.js +23 -0
  53. package/dist/features/view-only-4/connect-handle.jsx +30 -0
  54. package/dist/features/view-only-4/connection-layer.d.ts +21 -0
  55. package/dist/features/view-only-4/connection-layer.js +219 -0
  56. package/dist/features/view-only-4/connection-layer.jsx +291 -0
  57. package/dist/features/view-only-4/index.d.ts +99 -0
  58. package/dist/features/view-only-4/index.js +684 -0
  59. package/dist/features/view-only-4/index.jsx +722 -0
  60. package/dist/features/view-only-4/integration-guide.d.ts +0 -0
  61. package/dist/features/view-only-4/integration-guide.js +0 -0
  62. package/dist/features/view-only-4/use-connection-graph.d.ts +41 -0
  63. package/dist/features/view-only-4/use-connection-graph.js +182 -0
  64. package/dist/features/view-only-4/utils.d.ts +74 -0
  65. package/dist/features/view-only-4/utils.js +106 -0
  66. package/dist/features/view-only-5/connect-handle.d.ts +30 -0
  67. package/dist/features/view-only-5/connect-handle.js +88 -0
  68. package/dist/features/view-only-5/connect-handle.jsx +96 -0
  69. package/dist/features/view-only-5/connection-layer.d.ts +34 -0
  70. package/dist/features/view-only-5/connection-layer.js +182 -0
  71. package/dist/features/view-only-5/connection-layer.jsx +265 -0
  72. package/dist/features/view-only-5/index.d.ts +102 -0
  73. package/dist/features/view-only-5/index.js +585 -0
  74. package/dist/features/view-only-5/index.jsx +614 -0
  75. package/dist/features/view-only-5/use-connection-graph.d.ts +57 -0
  76. package/dist/features/view-only-5/use-connection-graph.js +196 -0
  77. package/dist/features/view-only-5/utils.d.ts +52 -0
  78. package/dist/features/view-only-5/utils.js +80 -0
  79. package/dist/features/view-only-6/connect-handle.d.ts +13 -0
  80. package/dist/features/view-only-6/connect-handle.js +20 -0
  81. package/dist/features/view-only-6/connect-handle.jsx +21 -0
  82. package/dist/features/view-only-6/connection-layer.d.ts +22 -0
  83. package/dist/features/view-only-6/connection-layer.js +191 -0
  84. package/dist/features/view-only-6/connection-layer.jsx +244 -0
  85. package/dist/features/view-only-6/index.d.ts +99 -0
  86. package/dist/features/view-only-6/index.js +687 -0
  87. package/dist/features/view-only-6/index.jsx +724 -0
  88. package/dist/features/view-only-6/use-connection-graph.d.ts +26 -0
  89. package/dist/features/view-only-6/use-connection-graph.js +103 -0
  90. package/dist/features/view-only-6/utils.d.ts +66 -0
  91. package/dist/features/view-only-6/utils.js +96 -0
  92. package/dist/features/view-only-7/connect-handle.d.ts +13 -0
  93. package/dist/features/view-only-7/connect-handle.js +23 -0
  94. package/dist/features/view-only-7/connect-handle.jsx +30 -0
  95. package/dist/features/view-only-7/connection-layer.d.ts +22 -0
  96. package/dist/features/view-only-7/connection-layer.js +165 -0
  97. package/dist/features/view-only-7/connection-layer.jsx +217 -0
  98. package/dist/features/view-only-7/index.d.ts +99 -0
  99. package/dist/features/view-only-7/index.js +687 -0
  100. package/dist/features/view-only-7/index.jsx +724 -0
  101. package/dist/features/view-only-7/use-connection-graph.d.ts +26 -0
  102. package/dist/features/view-only-7/use-connection-graph.js +104 -0
  103. package/dist/features/view-only-7/utils.d.ts +69 -0
  104. package/dist/features/view-only-7/utils.js +144 -0
  105. package/dist/index.d.ts +2 -1
  106. package/dist/index.js +2 -1
  107. package/dist/seat-editor.css +1 -1
  108. package/package.json +1 -1
@@ -0,0 +1,196 @@
1
+ // use-connection-graph.ts
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { snap } from "./utils";
4
+ import { useAppSelector } from "@/hooks/use-redux";
5
+ export const useConnectionGraph = ({ svgRef, getNodeById, onEdgesChange, keyNode, mappingKey, tableMatchKey, statusKey, }) => {
6
+ const components = useAppSelector((state) => state.board.components);
7
+ const [edges, setEdges] = useState([]);
8
+ const [selectedEdge, setSelectedEdge] = useState(null);
9
+ const [selectedNode, setSelectedNode] = useState(null);
10
+ const [connecting, setConnecting] = useState(null);
11
+ const [draggingAnchor, setDraggingAnchor] = useState(null);
12
+ const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
13
+ const draggingWP = useRef(null);
14
+ const getSVGPoint = useCallback((clientX, clientY) => {
15
+ var _a;
16
+ const svg = svgRef.current;
17
+ if (!svg)
18
+ return { x: 0, y: 0 };
19
+ const pt = svg.createSVGPoint();
20
+ pt.x = clientX;
21
+ pt.y = clientY;
22
+ const p = pt.matrixTransform((_a = svg.getScreenCTM()) === null || _a === void 0 ? void 0 : _a.inverse());
23
+ return { x: p.x, y: p.y };
24
+ }, [svgRef]);
25
+ // ── Connect ──────────────────────────────────────────────────────
26
+ const startConnect = useCallback((fromId, fromPos) => {
27
+ setConnecting({ fromId, fromPos });
28
+ setSelectedEdge(null);
29
+ }, []);
30
+ const endConnect = useCallback((toId, toPos) => {
31
+ if (!connecting || connecting.fromId === toId) {
32
+ setConnecting(null);
33
+ return;
34
+ }
35
+ setEdges((prev) => {
36
+ const exists = prev.find((e) => (e.from === connecting.fromId && e.to === toId) ||
37
+ (e.from === toId && e.to === connecting.fromId));
38
+ if (exists)
39
+ return prev;
40
+ return [
41
+ ...prev,
42
+ {
43
+ id: `edge-${Date.now()}`,
44
+ from: connecting.fromId,
45
+ to: toId,
46
+ fromPos: connecting.fromPos,
47
+ toPos,
48
+ waypoints: [],
49
+ },
50
+ ];
51
+ });
52
+ setConnecting(null);
53
+ }, [connecting]);
54
+ const cancelConnect = useCallback(() => setConnecting(null), []);
55
+ // ── Drag endpoint ────────────────────────────────────────────────
56
+ const startDragAnchor = useCallback((e, edgeId, side) => {
57
+ e.stopPropagation();
58
+ setDraggingAnchor({ edgeId, side });
59
+ }, []);
60
+ const updateAnchor = useCallback((targetNodeId, newPos) => {
61
+ if (!draggingAnchor)
62
+ return;
63
+ const { edgeId, side } = draggingAnchor;
64
+ setEdges((prev) => prev.map((edge) => {
65
+ if (edge.id !== edgeId)
66
+ return edge;
67
+ if (side === "from") {
68
+ if (targetNodeId === edge.to)
69
+ return edge;
70
+ return Object.assign(Object.assign({}, edge), { from: targetNodeId, fromPos: newPos, waypoints: [] });
71
+ }
72
+ else {
73
+ if (targetNodeId === edge.from)
74
+ return edge;
75
+ return Object.assign(Object.assign({}, edge), { to: targetNodeId, toPos: newPos, waypoints: [] });
76
+ }
77
+ }));
78
+ setDraggingAnchor(null);
79
+ }, [draggingAnchor]);
80
+ const cancelDragAnchor = useCallback(() => setDraggingAnchor(null), []);
81
+ // ── Mouse ────────────────────────────────────────────────────────
82
+ const onMouseMove = useCallback((e) => {
83
+ const pos = getSVGPoint(e.clientX, e.clientY);
84
+ setMousePos(pos);
85
+ if (draggingWP.current) {
86
+ const { edgeId, index } = draggingWP.current;
87
+ setEdges((prev) => prev.map((ed) => {
88
+ if (ed.id !== edgeId)
89
+ return ed;
90
+ return Object.assign(Object.assign({}, ed), { waypoints: ed.waypoints.map((wp, i) => i === index ? { x: snap(pos.x), y: snap(pos.y) } : wp) });
91
+ }));
92
+ }
93
+ }, [getSVGPoint]);
94
+ const onMouseUp = useCallback(() => {
95
+ draggingWP.current = null;
96
+ if (draggingAnchor)
97
+ setDraggingAnchor(null);
98
+ }, [draggingAnchor]);
99
+ // ── Waypoints ────────────────────────────────────────────────────
100
+ const startDragWaypoint = useCallback((e, edgeId, index) => {
101
+ e.stopPropagation();
102
+ draggingWP.current = { edgeId, index };
103
+ setSelectedEdge(edgeId);
104
+ }, []);
105
+ const insertWaypoint = useCallback((e, edgeId, insertIndex, x, y) => {
106
+ e.stopPropagation();
107
+ setEdges((prev) => prev.map((ed) => {
108
+ if (ed.id !== edgeId)
109
+ return ed;
110
+ const newWP = [...ed.waypoints];
111
+ newWP.splice(insertIndex, 0, { x: snap(x), y: snap(y) });
112
+ return Object.assign(Object.assign({}, ed), { waypoints: newWP });
113
+ }));
114
+ draggingWP.current = { edgeId, index: insertIndex };
115
+ }, []);
116
+ const removeWaypoint = useCallback((e, edgeId, index) => {
117
+ e.preventDefault();
118
+ e.stopPropagation();
119
+ setEdges((prev) => prev.map((ed) => {
120
+ if (ed.id !== edgeId)
121
+ return ed;
122
+ if (ed.waypoints.length <= 1)
123
+ return ed;
124
+ return Object.assign(Object.assign({}, ed), { waypoints: ed.waypoints.filter((_, i) => i !== index) });
125
+ }));
126
+ }, []);
127
+ // __ Select Table
128
+ const selectNode = useCallback((nodeId) => setSelectedNode(nodeId), []);
129
+ // ── Select / delete ──────────────────────────────────────────────
130
+ const selectEdge = useCallback((id) => setSelectedEdge(id), []);
131
+ const deleteEdge = useCallback((id) => {
132
+ setEdges((p) => p.filter((e) => e.id !== id));
133
+ setSelectedEdge(null);
134
+ }, []);
135
+ const deleteSelectedEdge = useCallback(() => {
136
+ if (selectedEdge)
137
+ deleteEdge(selectedEdge);
138
+ }, [selectedEdge, deleteEdge]);
139
+ const clearSelection = useCallback(() => setSelectedEdge(null), []);
140
+ // ── Keyboard ─────────────────────────────────────────────────────
141
+ useEffect(() => {
142
+ const onKey = (e) => {
143
+ const tag = e.target.tagName.toLowerCase();
144
+ if (tag === "input" || tag === "textarea")
145
+ return;
146
+ if (e.key === "Delete" || e.key === "Backspace")
147
+ deleteSelectedEdge();
148
+ if (e.key === "Escape") {
149
+ cancelConnect();
150
+ cancelDragAnchor();
151
+ clearSelection();
152
+ }
153
+ };
154
+ window.addEventListener("keydown", onKey);
155
+ return () => window.removeEventListener("keydown", onKey);
156
+ }, [deleteSelectedEdge, cancelConnect, cancelDragAnchor, clearSelection]);
157
+ // ── onEdgesChange ────────────────────────────────────────────────
158
+ useEffect(() => {
159
+ const sourceToTargets = new Map();
160
+ edges.forEach((e) => {
161
+ if (!sourceToTargets.has(e.from))
162
+ sourceToTargets.set(e.from, []);
163
+ sourceToTargets.get(e.from).push(e.to);
164
+ });
165
+ const result = components === null || components === void 0 ? void 0 : components.map((table) => {
166
+ var _a, _b;
167
+ return (Object.assign(Object.assign({}, table), { [keyNode]: (_b = sourceToTargets.get(String((_a = table === null || table === void 0 ? void 0 : table[mappingKey]) === null || _a === void 0 ? void 0 : _a.id))) !== null && _b !== void 0 ? _b : [] }));
168
+ });
169
+ onEdgesChange === null || onEdgesChange === void 0 ? void 0 : onEdgesChange(edges, result);
170
+ }, [edges]);
171
+ return {
172
+ edges,
173
+ setEdges,
174
+ selectedEdge,
175
+ selectedNode,
176
+ connecting,
177
+ draggingAnchor,
178
+ mousePos,
179
+ startConnect,
180
+ endConnect,
181
+ cancelConnect,
182
+ startDragAnchor,
183
+ updateAnchor,
184
+ cancelDragAnchor,
185
+ startDragWaypoint,
186
+ insertWaypoint,
187
+ removeWaypoint,
188
+ selectEdge,
189
+ deleteEdge,
190
+ deleteSelectedEdge,
191
+ clearSelection,
192
+ onMouseMove,
193
+ onMouseUp,
194
+ selectNode
195
+ };
196
+ };
@@ -0,0 +1,52 @@
1
+ import { ComponentProps, TableMatchKey } from ".";
2
+ export declare const GRID_SIZE = 20;
3
+ export declare const NODE_WIDTH = 120;
4
+ export declare const NODE_HEIGHT = 50;
5
+ export declare const snap: (v: number) => number;
6
+ export type EdgeType = {
7
+ id: string;
8
+ from: string;
9
+ to: string;
10
+ fromPos?: {
11
+ x: number;
12
+ y: number;
13
+ };
14
+ toPos?: {
15
+ x: number;
16
+ y: number;
17
+ };
18
+ waypoints: {
19
+ x: number;
20
+ y: number;
21
+ }[];
22
+ };
23
+ export type NodeType = {
24
+ id: string;
25
+ x: number;
26
+ y: number;
27
+ width?: number;
28
+ height?: number;
29
+ rotation?: number;
30
+ };
31
+ export declare const rotatePoint: (px: number, py: number, deg: number) => {
32
+ x: number;
33
+ y: number;
34
+ };
35
+ export declare const getRectEdge: (from: {
36
+ x: number;
37
+ y: number;
38
+ width?: number;
39
+ height?: number;
40
+ rotation?: number;
41
+ }, to: {
42
+ x: number;
43
+ y: number;
44
+ }, width?: number, height?: number) => {
45
+ x: number;
46
+ y: number;
47
+ };
48
+ export declare const buildPath: (points: {
49
+ x: number;
50
+ y: number;
51
+ }[]) => string;
52
+ export declare const renderElements: (elementEditor: ComponentProps[], mappingKey?: string, tableMatchKey?: TableMatchKey[], statusKey?: string) => ComponentProps[];
@@ -0,0 +1,80 @@
1
+ export const GRID_SIZE = 20;
2
+ export const NODE_WIDTH = 120;
3
+ export const NODE_HEIGHT = 50;
4
+ export const snap = (v) => Math.round(v / GRID_SIZE) * GRID_SIZE;
5
+ // ─── Rotate ──────────────────────────────────────────────────────────────────
6
+ export const rotatePoint = (px, py, deg) => {
7
+ const rad = (deg * Math.PI) / 180;
8
+ return {
9
+ x: px * Math.cos(rad) - py * Math.sin(rad),
10
+ y: px * Math.sin(rad) + py * Math.cos(rad),
11
+ };
12
+ };
13
+ // ─── getRectEdge ─────────────────────────────────────────────────────────────
14
+ export const getRectEdge = (from, to, width, height) => {
15
+ var _a, _b, _c, _d, _e;
16
+ const w = (_b = (_a = from.width) !== null && _a !== void 0 ? _a : width) !== null && _b !== void 0 ? _b : NODE_WIDTH;
17
+ const h = (_d = (_c = from.height) !== null && _c !== void 0 ? _c : height) !== null && _d !== void 0 ? _d : NODE_HEIGHT;
18
+ const rotation = (_e = from.rotation) !== null && _e !== void 0 ? _e : 0;
19
+ const dx = to.x - from.x;
20
+ const dy = to.y - from.y;
21
+ if (dx === 0 && dy === 0)
22
+ return { x: from.x, y: from.y };
23
+ const angle = Math.atan2(dy, dx);
24
+ const rad = -(rotation * Math.PI) / 180;
25
+ const localAngle = angle + rad;
26
+ const absCos = Math.abs(Math.cos(localAngle));
27
+ const absSin = Math.abs(Math.sin(localAngle));
28
+ const offset = (h / 2) * absCos <= (w / 2) * absSin
29
+ ? (h / 2) / absSin
30
+ : (w / 2) / absCos;
31
+ const localEdgeX = Math.cos(localAngle) * offset;
32
+ const localEdgeY = Math.sin(localAngle) * offset;
33
+ const worldEdge = rotatePoint(localEdgeX, localEdgeY, rotation);
34
+ return { x: from.x + worldEdge.x, y: from.y + worldEdge.y };
35
+ };
36
+ // ─── buildPath (single cubic bezier curve) ───────────────────────────────────
37
+ export const buildPath = (points) => {
38
+ if (points.length < 2)
39
+ return "";
40
+ const start = points[0];
41
+ const end = points[points.length - 1];
42
+ if (points.length === 2) {
43
+ // Quadratic bezier — 1 control point di tengah, digeser tegak lurus
44
+ const mx = (start.x + end.x) / 2;
45
+ const my = (start.y + end.y) / 2;
46
+ const dx = end.x - start.x;
47
+ const dy = end.y - start.y;
48
+ const dist = Math.sqrt(dx * dx + dy * dy);
49
+ const bend = Math.min(dist * 0.35, 80);
50
+ // Control point tegak lurus dari midpoint
51
+ const cpx = mx - (dy / dist) * bend;
52
+ const cpy = my + (dx / dist) * bend;
53
+ return `M ${start.x} ${start.y} Q ${cpx} ${cpy}, ${end.x} ${end.y}`;
54
+ }
55
+ // Dengan waypoints — quadratic berantai lewat midpoints
56
+ let d = `M ${start.x} ${start.y}`;
57
+ for (let i = 0; i < points.length - 1; i++) {
58
+ const p1 = points[i];
59
+ const p2 = points[i + 1];
60
+ const cpx = (p1.x + p2.x) / 2;
61
+ const cpy = (p1.y + p2.y) / 2;
62
+ d += ` Q ${p1.x} ${p1.y}, ${cpx} ${cpy}`;
63
+ }
64
+ d += ` L ${end.x} ${end.y}`;
65
+ return d;
66
+ };
67
+ // ─── renderElements ──────────────────────────────────────────────────────────
68
+ export const renderElements = (elementEditor, mappingKey, tableMatchKey, statusKey) => {
69
+ return elementEditor.map((editorItem) => {
70
+ const isUsingMapping = mappingKey &&
71
+ typeof editorItem[mappingKey] === "object" &&
72
+ editorItem[mappingKey] !== null;
73
+ let finalProps = isUsingMapping ? editorItem[mappingKey] : editorItem;
74
+ if (tableMatchKey) {
75
+ const tableMatch = tableMatchKey.find((item) => item.key == (editorItem === null || editorItem === void 0 ? void 0 : editorItem[statusKey]));
76
+ finalProps = Object.assign(Object.assign(Object.assign({}, finalProps), tableMatch === null || tableMatch === void 0 ? void 0 : tableMatch.properties), { className: tableMatch === null || tableMatch === void 0 ? void 0 : tableMatch.className });
77
+ }
78
+ return finalProps;
79
+ });
80
+ };
@@ -0,0 +1,13 @@
1
+ type Props = {
2
+ cx: number;
3
+ cy: number;
4
+ width: number;
5
+ height: number;
6
+ nodeId: string;
7
+ rotation?: number;
8
+ isConnecting: boolean;
9
+ onStartConnect: (nodeId: string) => void;
10
+ onEndConnect: (nodeId: string) => void;
11
+ };
12
+ export declare const ConnectHandle: React.FC<Props>;
13
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ export const ConnectHandle = ({ cx, cy, width, height, nodeId, rotation = 0, isConnecting, onStartConnect, onEndConnect, }) => {
4
+ const hw = width / 2;
5
+ const hh = height / 2;
6
+ const [hovered, setHovered] = useState(false);
7
+ const isActive = isConnecting; // sedang jadi source
8
+ return (_jsx("g", { transform: `translate(${cx}, ${cy}) rotate(${rotation})`, children: _jsx("rect", { x: -hw - 4, y: -hh - 4, width: width + 8, height: height + 8, fill: "transparent", stroke: isActive ? "#a78bfa" // ungu kalau sedang connecting
9
+ : hovered ? "#38bdf8" // biru kalau hover
10
+ : "transparent" // invisible kalau idle
11
+ , strokeWidth: isActive ? 2 : 1.5, strokeDasharray: isActive ? "4 2" : "none", rx: 4, "data-connect-handle": "true", style: { cursor: "crosshair", pointerEvents: "all" }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onPointerDown: (e) => {
12
+ e.stopPropagation();
13
+ if (isActive) {
14
+ onEndConnect(nodeId);
15
+ }
16
+ else {
17
+ onStartConnect(nodeId);
18
+ }
19
+ } }) }));
20
+ };
@@ -0,0 +1,21 @@
1
+ import { useState } from "react";
2
+ export const ConnectHandle = ({ cx, cy, width, height, nodeId, rotation = 0, isConnecting, onStartConnect, onEndConnect, }) => {
3
+ const hw = width / 2;
4
+ const hh = height / 2;
5
+ const [hovered, setHovered] = useState(false);
6
+ const isActive = isConnecting; // sedang jadi source
7
+ return (<g transform={`translate(${cx}, ${cy}) rotate(${rotation})`}>
8
+ <rect x={-hw - 4} y={-hh - 4} width={width + 8} height={height + 8} fill="transparent" stroke={isActive ? "#a78bfa" // ungu kalau sedang connecting
9
+ : hovered ? "#38bdf8" // biru kalau hover
10
+ : "transparent" // invisible kalau idle
11
+ } strokeWidth={isActive ? 2 : 1.5} strokeDasharray={isActive ? "4 2" : "none"} rx={4} data-connect-handle="true" style={{ cursor: "crosshair", pointerEvents: "all" }} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} onPointerDown={(e) => {
12
+ e.stopPropagation();
13
+ if (isActive) {
14
+ onEndConnect(nodeId);
15
+ }
16
+ else {
17
+ onStartConnect(nodeId);
18
+ }
19
+ }}/>
20
+ </g>);
21
+ };
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { NodeType } from "./utils";
3
+ export type EdgeType = {
4
+ id: string;
5
+ from: string;
6
+ to: string;
7
+ };
8
+ type Props = {
9
+ edges: EdgeType[];
10
+ selectedEdge: string | null;
11
+ connecting: {
12
+ fromId: string;
13
+ } | null;
14
+ mousePos: {
15
+ x: number;
16
+ y: number;
17
+ };
18
+ getNodeById: (id: string) => NodeType | null;
19
+ onSelectEdge: (edgeId: string) => void;
20
+ };
21
+ export declare const ConnectionLayer: React.FC<Props>;
22
+ export {};
@@ -0,0 +1,191 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // components/ConnectionLayer.tsx
3
+ import { useMemo, useState } from "react";
4
+ import { buildCurvePath, getCurveEndAngle, getRectEdge, NODE_WIDTH, NODE_HEIGHT, } from "./utils";
5
+ // ─── Constants ────────────────────────────────────────────────────────────────
6
+ const LEVEL_COLORS = [
7
+ "#22c55e",
8
+ "#f59e0b",
9
+ "#f97316",
10
+ "#ef4444",
11
+ "#a78bfa",
12
+ "#38bdf8",
13
+ ];
14
+ const getLevelColor = (level) => LEVEL_COLORS[Math.min(level, LEVEL_COLORS.length - 1)];
15
+ // ─── BFS flood fill ───────────────────────────────────────────────────────────
16
+ const floodFill = (seedIds, edges) => {
17
+ const levelMap = new Map();
18
+ const queue = [];
19
+ seedIds.forEach((id) => {
20
+ levelMap.set(id, 0);
21
+ queue.push({ id, level: 0 });
22
+ });
23
+ while (queue.length > 0) {
24
+ const { id, level } = queue.shift();
25
+ edges.forEach((edge) => {
26
+ const neighbors = [];
27
+ if (edge.from === id)
28
+ neighbors.push(edge.to);
29
+ if (edge.to === id)
30
+ neighbors.push(edge.from);
31
+ neighbors.forEach((nId) => {
32
+ if (!levelMap.has(nId)) {
33
+ levelMap.set(nId, level + 1);
34
+ queue.push({ id: nId, level: level + 1 });
35
+ }
36
+ });
37
+ });
38
+ }
39
+ return levelMap;
40
+ };
41
+ // ─── Group edges by source ────────────────────────────────────────────────────
42
+ const groupBySource = (edges) => {
43
+ const map = new Map();
44
+ edges.forEach((edge) => {
45
+ if (!map.has(edge.from))
46
+ map.set(edge.from, []);
47
+ map.get(edge.from).push(edge);
48
+ });
49
+ return map;
50
+ };
51
+ // ─── Component ────────────────────────────────────────────────────────────────
52
+ export const ConnectionLayer = ({ edges, selectedEdge, connecting, mousePos, getNodeById, onSelectEdge,
53
+ // onDeleteEdge,
54
+ }) => {
55
+ const [hoveredEdge, setHoveredEdge] = useState(null);
56
+ // ── Flood fill highlight ──────────────────────────────────────────
57
+ const nodeHighlightMap = useMemo(() => {
58
+ if (!selectedEdge)
59
+ return new Map();
60
+ const sel = edges.find((e) => e.id === selectedEdge);
61
+ if (!sel)
62
+ return new Map();
63
+ return floodFill([sel.from, sel.to], edges);
64
+ }, [selectedEdge, edges]);
65
+ // ── Group by source for shared stem ──────────────────────────────
66
+ const sourceGroups = useMemo(() => groupBySource(edges), [edges]);
67
+ // ── Pre-compute curve data per edge ──────────────────────────────
68
+ const curveData = useMemo(() => {
69
+ const map = new Map();
70
+ edges.forEach((edge) => {
71
+ const fromNode = getNodeById(edge.from);
72
+ const toNode = getNodeById(edge.to);
73
+ if (!fromNode || !toNode)
74
+ return;
75
+ const start = getRectEdge(fromNode, toNode, fromNode.width, fromNode.height);
76
+ const end = getRectEdge(toNode, fromNode, toNode.width, toNode.height);
77
+ const pathD = buildCurvePath(start, end);
78
+ const angle = getCurveEndAngle(start, end);
79
+ // Midpoint di kurva (t=0.5) untuk label/badge
80
+ const dx = end.x - start.x;
81
+ const dy = end.y - start.y;
82
+ const absDx = Math.abs(dx);
83
+ const absDy = Math.abs(dy);
84
+ const strength = Math.max(Math.max(absDx, absDy) * 0.5, 60);
85
+ let cp1;
86
+ let cp2;
87
+ if (absDx >= absDy) {
88
+ cp1 = { x: start.x + strength, y: start.y };
89
+ cp2 = { x: end.x - strength, y: end.y };
90
+ }
91
+ else {
92
+ cp1 = { x: start.x, y: start.y + strength };
93
+ cp2 = { x: end.x, y: end.y - strength };
94
+ }
95
+ // Cubic bezier at t=0.5
96
+ const t = 0.5;
97
+ const mt = 1 - t;
98
+ const midX = mt * mt * mt * start.x +
99
+ 3 * mt * mt * t * cp1.x +
100
+ 3 * mt * t * t * cp2.x +
101
+ t * t * t * end.x;
102
+ const midY = mt * mt * mt * start.y +
103
+ 3 * mt * mt * t * cp1.y +
104
+ 3 * mt * t * t * cp2.y +
105
+ t * t * t * end.y;
106
+ map.set(edge.id, { start, end, pathD, angle, midX, midY });
107
+ });
108
+ return map;
109
+ }, [edges, getNodeById]);
110
+ // ── Shared stem exit point per source ────────────────────────────
111
+ const stemData = useMemo(() => {
112
+ const map = new Map();
113
+ sourceGroups.forEach((groupEdges, fromId) => {
114
+ if (groupEdges.length <= 1)
115
+ return;
116
+ const sourceNode = getNodeById(fromId);
117
+ if (!sourceNode)
118
+ return;
119
+ const exits = groupEdges
120
+ .map((edge) => {
121
+ const toNode = getNodeById(edge.to);
122
+ if (!toNode)
123
+ return null;
124
+ return getRectEdge(sourceNode, toNode, sourceNode.width, sourceNode.height);
125
+ })
126
+ .filter(Boolean);
127
+ if (exits.length === 0)
128
+ return;
129
+ map.set(fromId, {
130
+ x: exits.reduce((s, p) => s + p.x, 0) / exits.length,
131
+ y: exits.reduce((s, p) => s + p.y, 0) / exits.length,
132
+ });
133
+ });
134
+ return map;
135
+ }, [sourceGroups, getNodeById]);
136
+ return (_jsxs("g", { id: "connection-layer", children: [Array.from(nodeHighlightMap.entries()).map(([nodeId, level]) => {
137
+ var _a, _b, _c;
138
+ const node = getNodeById(nodeId);
139
+ if (!node)
140
+ return null;
141
+ const w = (_a = node.width) !== null && _a !== void 0 ? _a : NODE_WIDTH;
142
+ const h = (_b = node.height) !== null && _b !== void 0 ? _b : NODE_HEIGHT;
143
+ const rotation = (_c = node.rotation) !== null && _c !== void 0 ? _c : 0;
144
+ const color = getLevelColor(level);
145
+ const padding = 4;
146
+ return (_jsxs("g", { transform: `translate(${node.x}, ${node.y}) rotate(${rotation})`, style: { pointerEvents: "none" }, children: [_jsx("rect", { x: -w / 2 - padding, y: -h / 2 - padding, width: w + padding * 2, height: h + padding * 2, fill: "none", stroke: color, strokeWidth: 8, rx: 5, opacity: 0.08 }), _jsx("rect", { x: -w / 2 - padding, y: -h / 2 - padding, width: w + padding * 2, height: h + padding * 2, fill: "none", stroke: color, strokeWidth: level === 0 ? 2.5 : 1.5, strokeDasharray: level === 0 ? "none" : "5 3", rx: 5, opacity: level === 0 ? 1 : 0.75 }), _jsxs("g", { transform: `translate(${w / 2 + padding - 2}, ${-h / 2 - padding})`, children: [_jsx("circle", { r: 7, fill: color, opacity: 0.9 }), _jsx("text", { textAnchor: "middle", dominantBaseline: "middle", fill: "white", fontSize: 7, fontFamily: "monospace", fontWeight: 700, style: { userSelect: "none" }, children: level })] })] }, `highlight-${nodeId}`));
147
+ }), Array.from(stemData.entries()).map(([fromId, stemExit]) => {
148
+ var _a;
149
+ const isStemOfSelected = selectedEdge &&
150
+ ((_a = edges.find((e) => e.id === selectedEdge)) === null || _a === void 0 ? void 0 : _a.from) === fromId;
151
+ const color = isStemOfSelected ? "#a78bfa" : "#38bdf8";
152
+ return (_jsxs("g", { style: { pointerEvents: "none" }, children: [_jsx("circle", { cx: stemExit.x, cy: stemExit.y, r: 5, fill: "#0f172a", stroke: color, strokeWidth: 1.5 }), _jsx("circle", { cx: stemExit.x, cy: stemExit.y, r: 2.5, fill: color })] }, `stem-dot-${fromId}`));
153
+ }), edges.map((edge) => {
154
+ const data = curveData.get(edge.id);
155
+ if (!data)
156
+ return null;
157
+ const { pathD, angle, midX, midY } = data;
158
+ const isSel = selectedEdge === edge.id;
159
+ const isHov = hoveredEdge === edge.id;
160
+ const markerId = `conn-arrow-${edge.id}`;
161
+ const markerSelId = `conn-arrow-sel-${edge.id}`;
162
+ // Warna edge jika dalam network highlight
163
+ const fromLevel = nodeHighlightMap.get(edge.from);
164
+ const toLevel = nodeHighlightMap.get(edge.to);
165
+ const isInNetwork = fromLevel !== undefined && toLevel !== undefined;
166
+ const networkColor = isInNetwork
167
+ ? getLevelColor(Math.max(fromLevel, toLevel))
168
+ : "#38bdf8";
169
+ return (_jsxs("g", { children: [_jsx("defs", { children: _jsx("marker", { id: markerId, markerWidth: "8", markerHeight: "8", refX: "7", refY: "3", orient: `${angle}deg`, children: _jsx("path", { d: "M 0 0 L 8 3 L 0 6 Z", fill: isSel
170
+ ? "#a78bfa"
171
+ : isInNetwork && !isSel
172
+ ? networkColor
173
+ : "#38bdf8", opacity: 0.9 }) }) }), _jsx("path", { d: pathD, stroke: "transparent", strokeWidth: 14, fill: "none", style: { cursor: "pointer" }, onClick: (e) => {
174
+ e.stopPropagation();
175
+ onSelectEdge(edge.id);
176
+ }, onMouseEnter: () => setHoveredEdge(edge.id), onMouseLeave: () => setHoveredEdge(null) }), (isSel || isHov) && (_jsx("path", { d: pathD, stroke: isSel ? "#a78bfa" : "#38bdf8", strokeWidth: 8, fill: "none", opacity: isSel ? 0.12 : 0.06, style: { pointerEvents: "none" } })), _jsx("path", { d: pathD, stroke: isSel
177
+ ? "#a78bfa"
178
+ : isInNetwork && selectedEdge
179
+ ? networkColor
180
+ : "#38bdf8", strokeWidth: isSel ? 2 : isHov ? 2 : 1.5, fill: "none", opacity: selectedEdge && !isSel && !isInNetwork ? 0.25 : 0.9, markerEnd: `url(#${markerId})`, style: { pointerEvents: "none" } }), isSel && (_jsxs("g", { style: { pointerEvents: "none" }, children: [_jsx("rect", { x: midX - 16, y: midY - 10, width: 32, height: 18, rx: 4, fill: "#1e1b2e", stroke: "#a78bfa", strokeWidth: 1, opacity: 0.95 }), _jsx("text", { x: midX, y: midY, textAnchor: "middle", dominantBaseline: "middle", fill: "#a78bfa", fontSize: 9, fontFamily: "monospace", fontWeight: 600, children: "DEL" })] }))] }, edge.id));
181
+ }), connecting &&
182
+ (() => {
183
+ const fromNode = getNodeById(connecting.fromId);
184
+ if (!fromNode)
185
+ return null;
186
+ const start = getRectEdge(fromNode, mousePos, fromNode.width, fromNode.height);
187
+ const pathD = buildCurvePath(start, mousePos);
188
+ const angle = getCurveEndAngle(start, mousePos);
189
+ return (_jsxs("g", { children: [_jsx("defs", { children: _jsx("marker", { id: "conn-preview-arrow", markerWidth: "8", markerHeight: "8", refX: "7", refY: "3", orient: `${angle}deg`, children: _jsx("path", { d: "M 0 0 L 8 3 L 0 6 Z", fill: "#a78bfa", opacity: 0.85 }) }) }), _jsx("path", { d: pathD, stroke: "#a78bfa", strokeWidth: 1.5, fill: "none", strokeDasharray: "6 3", markerEnd: "url(#conn-preview-arrow)", opacity: 0.8, style: { pointerEvents: "none" } })] }));
190
+ })()] }));
191
+ };