seat-editor 3.4.8 → 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 (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,26 @@
1
+ import { NodeType } from "./utils";
2
+ import { EdgeType } from "./connection-layer";
3
+ export type UseConnectionGraphOptions = {
4
+ svgRef: React.RefObject<SVGSVGElement>;
5
+ getNodeById: (id: string) => NodeType | null;
6
+ onEdgesChange?: (edges: EdgeType[]) => void;
7
+ };
8
+ export declare const useConnectionGraph: ({ svgRef, getNodeById, onEdgesChange, }: UseConnectionGraphOptions) => {
9
+ edges: EdgeType[];
10
+ setEdges: import("react").Dispatch<import("react").SetStateAction<EdgeType[]>>;
11
+ selectedEdge: string;
12
+ connecting: {
13
+ fromId: string;
14
+ };
15
+ mousePos: {
16
+ x: number;
17
+ y: number;
18
+ };
19
+ startConnect: (fromId: string) => void;
20
+ endConnect: (toId: string) => void;
21
+ cancelConnect: () => void;
22
+ selectEdge: (edgeId: string) => void;
23
+ deleteEdge: (edgeId: string) => void;
24
+ clearSelection: () => void;
25
+ onMouseMove: (e: React.MouseEvent<SVGSVGElement> | MouseEvent) => void;
26
+ };
@@ -0,0 +1,103 @@
1
+ // hooks/use-connection-graph.ts
2
+ import { useCallback, useEffect, useState } from "react";
3
+ export const useConnectionGraph = ({ svgRef, getNodeById, onEdgesChange, }) => {
4
+ const [edges, setEdges] = useState([]);
5
+ const [selectedEdge, setSelectedEdge] = useState(null);
6
+ const [connecting, setConnecting] = useState(null);
7
+ const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
8
+ // Notify parent setiap edges berubah
9
+ useEffect(() => {
10
+ onEdgesChange === null || onEdgesChange === void 0 ? void 0 : onEdgesChange(edges);
11
+ }, [edges]);
12
+ // Keyboard: Delete/Backspace → hapus edge, Escape → cancel connect
13
+ useEffect(() => {
14
+ const handleKeyDown = (e) => {
15
+ const tag = e.target.tagName.toLowerCase();
16
+ if (tag === "input" || tag === "textarea")
17
+ return;
18
+ if ((e.key === "Delete" || e.key === "Backspace") && selectedEdge) {
19
+ deleteEdge(selectedEdge);
20
+ }
21
+ if (e.key === "Escape") {
22
+ setConnecting(null);
23
+ setSelectedEdge(null);
24
+ }
25
+ };
26
+ window.addEventListener("keydown", handleKeyDown);
27
+ return () => window.removeEventListener("keydown", handleKeyDown);
28
+ }, [selectedEdge]);
29
+ // Convert client coords → SVG coords
30
+ const getSVGPoint = useCallback((clientX, clientY) => {
31
+ var _a;
32
+ const svg = svgRef.current;
33
+ if (!svg)
34
+ return { x: 0, y: 0 };
35
+ const pt = svg.createSVGPoint();
36
+ pt.x = clientX;
37
+ pt.y = clientY;
38
+ const svgP = pt.matrixTransform((_a = svg.getScreenCTM()) === null || _a === void 0 ? void 0 : _a.inverse());
39
+ return { x: svgP.x, y: svgP.y };
40
+ }, [svgRef]);
41
+ // ── Mouse move ───────────────────────────────────────────────────
42
+ const onMouseMove = useCallback((e) => {
43
+ const clientX = "clientX" in e ? e.clientX : 0;
44
+ const clientY = "clientY" in e ? e.clientY : 0;
45
+ setMousePos(getSVGPoint(clientX, clientY));
46
+ }, [getSVGPoint]);
47
+ // ── Connect ──────────────────────────────────────────────────────
48
+ const startConnect = useCallback((fromId) => {
49
+ setConnecting({ fromId });
50
+ setSelectedEdge(null);
51
+ }, []);
52
+ const endConnect = useCallback((toId) => {
53
+ if (!connecting || connecting.fromId === toId) {
54
+ setConnecting(null);
55
+ return;
56
+ }
57
+ const fromNode = getNodeById(connecting.fromId);
58
+ const toNode = getNodeById(toId);
59
+ if (!fromNode || !toNode) {
60
+ setConnecting(null);
61
+ return;
62
+ }
63
+ setEdges((prev) => {
64
+ const exists = prev.find((e) => (e.from === connecting.fromId && e.to === toId) ||
65
+ (e.from === toId && e.to === connecting.fromId));
66
+ if (exists)
67
+ return prev;
68
+ return [
69
+ ...prev,
70
+ { id: `edge-${Date.now()}`, from: connecting.fromId, to: toId },
71
+ ];
72
+ });
73
+ setConnecting(null);
74
+ }, [connecting, getNodeById]);
75
+ const cancelConnect = useCallback(() => setConnecting(null), []);
76
+ // ── Edge select / delete ─────────────────────────────────────────
77
+ const selectEdge = useCallback((edgeId) => {
78
+ setSelectedEdge(edgeId);
79
+ }, []);
80
+ const deleteEdge = useCallback((edgeId) => {
81
+ setEdges((prev) => prev.filter((e) => e.id !== edgeId));
82
+ setSelectedEdge(null);
83
+ }, []);
84
+ const clearSelection = useCallback(() => {
85
+ setSelectedEdge(null);
86
+ }, []);
87
+ return {
88
+ edges,
89
+ setEdges,
90
+ selectedEdge,
91
+ connecting,
92
+ mousePos,
93
+ // actions
94
+ startConnect,
95
+ endConnect,
96
+ cancelConnect,
97
+ selectEdge,
98
+ deleteEdge,
99
+ clearSelection,
100
+ // svg handlers
101
+ onMouseMove,
102
+ };
103
+ };
@@ -0,0 +1,66 @@
1
+ export declare const GRID_SIZE = 20;
2
+ export declare const NODE_WIDTH = 120;
3
+ export declare const NODE_HEIGHT = 50;
4
+ export declare const snap: (v: number) => number;
5
+ export type EdgeType = {
6
+ id: string;
7
+ from: string;
8
+ to: string;
9
+ };
10
+ export type NodeType = {
11
+ id: string;
12
+ x: number;
13
+ y: number;
14
+ width?: number;
15
+ height?: number;
16
+ rotation?: number;
17
+ };
18
+ /**
19
+ * Rotate point (px, py) sekitar origin (0,0) sebesar deg derajat.
20
+ */
21
+ export declare const rotatePoint: (px: number, py: number, deg: number) => {
22
+ x: number;
23
+ y: number;
24
+ };
25
+ /**
26
+ * Hitung titik edge pada sisi rect node yang menghadap ke arah `to`,
27
+ * dengan mempertimbangkan rotation node.
28
+ */
29
+ export declare const getRectEdge: (from: {
30
+ x: number;
31
+ y: number;
32
+ width?: number;
33
+ height?: number;
34
+ rotation?: number;
35
+ }, to: {
36
+ x: number;
37
+ y: number;
38
+ }, width: number, height: number) => {
39
+ x: number;
40
+ y: number;
41
+ };
42
+ /**
43
+ * Build cubic bezier path dari start ke end.
44
+ * Control points dihitung otomatis berdasarkan arah dominan.
45
+ *
46
+ * Horizontal dominan: CP horizontal (kiri-kanan)
47
+ * Vertikal dominan: CP vertikal (atas-bawah)
48
+ */
49
+ export declare const buildCurvePath: (start: {
50
+ x: number;
51
+ y: number;
52
+ }, end: {
53
+ x: number;
54
+ y: number;
55
+ }) => string;
56
+ /**
57
+ * Hitung arah arrow dari CP2 → end point
58
+ * agar panah sejajar dengan ujung kurva.
59
+ */
60
+ export declare const getCurveEndAngle: (start: {
61
+ x: number;
62
+ y: number;
63
+ }, end: {
64
+ x: number;
65
+ y: number;
66
+ }) => number;
@@ -0,0 +1,96 @@
1
+ // utils.ts
2
+ export const GRID_SIZE = 20;
3
+ export const NODE_WIDTH = 120;
4
+ export const NODE_HEIGHT = 50;
5
+ export const snap = (v) => Math.round(v / GRID_SIZE) * GRID_SIZE;
6
+ /**
7
+ * Rotate point (px, py) sekitar origin (0,0) sebesar deg derajat.
8
+ */
9
+ export const rotatePoint = (px, py, deg) => {
10
+ const rad = (deg * Math.PI) / 180;
11
+ return {
12
+ x: px * Math.cos(rad) - py * Math.sin(rad),
13
+ y: px * Math.sin(rad) + py * Math.cos(rad),
14
+ };
15
+ };
16
+ /**
17
+ * Hitung titik edge pada sisi rect node yang menghadap ke arah `to`,
18
+ * dengan mempertimbangkan rotation node.
19
+ */
20
+ export const getRectEdge = (from, to, width, height) => {
21
+ var _a, _b, _c;
22
+ const w = (_a = from.width) !== null && _a !== void 0 ? _a : NODE_WIDTH;
23
+ const h = (_b = from.height) !== null && _b !== void 0 ? _b : NODE_HEIGHT;
24
+ const rotation = (_c = from.rotation) !== null && _c !== void 0 ? _c : 0;
25
+ const dx = to.x - from.x;
26
+ const dy = to.y - from.y;
27
+ if (dx === 0 && dy === 0)
28
+ return { x: from.x, y: from.y };
29
+ const angle = Math.atan2(dy, dx);
30
+ // Un-rotate arah ke local space node
31
+ const rad = -(rotation * Math.PI) / 180;
32
+ const localAngle = angle + rad;
33
+ const absCos = Math.abs(Math.cos(localAngle));
34
+ const absSin = Math.abs(Math.sin(localAngle));
35
+ const offset = (h / 2) * absCos <= (w / 2) * absSin
36
+ ? (h / 2) / absSin
37
+ : (w / 2) / absCos;
38
+ const localEdgeX = Math.cos(localAngle) * offset;
39
+ const localEdgeY = Math.sin(localAngle) * offset;
40
+ // Re-rotate balik ke world space
41
+ const worldEdge = rotatePoint(localEdgeX, localEdgeY, rotation);
42
+ return {
43
+ x: snap(from.x + worldEdge.x),
44
+ y: snap(from.y + worldEdge.y),
45
+ };
46
+ };
47
+ /**
48
+ * Build cubic bezier path dari start ke end.
49
+ * Control points dihitung otomatis berdasarkan arah dominan.
50
+ *
51
+ * Horizontal dominan: CP horizontal (kiri-kanan)
52
+ * Vertikal dominan: CP vertikal (atas-bawah)
53
+ */
54
+ export const buildCurvePath = (start, end) => {
55
+ const dx = end.x - start.x;
56
+ const dy = end.y - start.y;
57
+ const absDx = Math.abs(dx);
58
+ const absDy = Math.abs(dy);
59
+ // Kekuatan lengkungan — makin jauh makin melengkung, min 60px
60
+ const strength = Math.max(Math.max(absDx, absDy) * 0.5, 60);
61
+ let cp1;
62
+ let cp2;
63
+ if (absDx >= absDy) {
64
+ // Dominan horizontal → CP keluar ke kiri/kanan
65
+ cp1 = { x: start.x + strength, y: start.y };
66
+ cp2 = { x: end.x - strength, y: end.y };
67
+ }
68
+ else {
69
+ // Dominan vertikal → CP keluar ke atas/bawah
70
+ cp1 = { x: start.x, y: start.y + strength };
71
+ cp2 = { x: end.x, y: end.y - strength };
72
+ }
73
+ return `M ${start.x} ${start.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${end.x} ${end.y}`;
74
+ };
75
+ /**
76
+ * Hitung arah arrow dari CP2 → end point
77
+ * agar panah sejajar dengan ujung kurva.
78
+ */
79
+ export const getCurveEndAngle = (start, end) => {
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 cp2;
86
+ if (absDx >= absDy) {
87
+ cp2 = { x: end.x - strength, y: end.y };
88
+ }
89
+ else {
90
+ cp2 = { x: end.x, y: end.y - strength };
91
+ }
92
+ // Sudut dari CP2 ke end
93
+ const adx = end.x - cp2.x;
94
+ const ady = end.y - cp2.y;
95
+ return (Math.atan2(ady, adx) * 180) / Math.PI;
96
+ };
@@ -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,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const ConnectHandle = ({ cx, cy, width, height, nodeId, rotation = 0, // ← tambah ini
3
+ isConnecting, onStartConnect, onEndConnect, }) => {
4
+ const hw = width / 2;
5
+ const hh = height / 2;
6
+ // Tombol ⊕ di pojok kanan atas (dalam local space)
7
+ const btnLocalX = hw - 2;
8
+ const btnLocalY = -hh - 8;
9
+ return (
10
+ // Semua di-wrap dalam g dengan translate ke center + rotate
11
+ // Sama persis seperti struktur render node aslinya
12
+ _jsx("g", { transform: `translate(${cx}, ${cy}) rotate(${rotation})`, children: isConnecting ? (
13
+ // Mode connecting: overlay seluruh node
14
+ _jsx("rect", { x: -hw - 4, y: -hh - 4, width: width + 8, height: height + 8, fill: "transparent", stroke: "#a78bfa", strokeWidth: 1.5, strokeDasharray: "4 2", rx: 4, style: { cursor: "crosshair" }, onMouseDown: (e) => {
15
+ e.stopPropagation();
16
+ onEndConnect(nodeId);
17
+ } })) : (
18
+ // Tombol ⊕ — posisi relatif dari center node
19
+ _jsxs("g", { transform: `translate(${btnLocalX}, ${btnLocalY})`, style: { cursor: "crosshair" }, onMouseDown: (e) => {
20
+ e.stopPropagation();
21
+ onStartConnect(nodeId);
22
+ }, children: [_jsx("circle", { r: 8, fill: "#0a0e1a", stroke: "#38bdf8", strokeWidth: 1.2, opacity: 0.9 }), _jsx("text", { textAnchor: "middle", dominantBaseline: "middle", fill: "#38bdf8", fontSize: 11, fontWeight: 700, style: { pointerEvents: "none", userSelect: "none" }, children: "\u2295" })] })) }));
23
+ };
@@ -0,0 +1,30 @@
1
+ export const ConnectHandle = ({ cx, cy, width, height, nodeId, rotation = 0, // ← tambah ini
2
+ isConnecting, onStartConnect, onEndConnect, }) => {
3
+ const hw = width / 2;
4
+ const hh = height / 2;
5
+ // Tombol ⊕ di pojok kanan atas (dalam local space)
6
+ const btnLocalX = hw - 2;
7
+ const btnLocalY = -hh - 8;
8
+ return (
9
+ // Semua di-wrap dalam g dengan translate ke center + rotate
10
+ // Sama persis seperti struktur render node aslinya
11
+ <g transform={`translate(${cx}, ${cy}) rotate(${rotation})`}>
12
+
13
+ {isConnecting ? (
14
+ // Mode connecting: overlay seluruh node
15
+ <rect x={-hw - 4} y={-hh - 4} width={width + 8} height={height + 8} fill="transparent" stroke="#a78bfa" strokeWidth={1.5} strokeDasharray="4 2" rx={4} style={{ cursor: "crosshair" }} onMouseDown={(e) => {
16
+ e.stopPropagation();
17
+ onEndConnect(nodeId);
18
+ }}/>) : (
19
+ // Tombol ⊕ — posisi relatif dari center node
20
+ <g transform={`translate(${btnLocalX}, ${btnLocalY})`} style={{ cursor: "crosshair" }} onMouseDown={(e) => {
21
+ e.stopPropagation();
22
+ onStartConnect(nodeId);
23
+ }}>
24
+ <circle r={8} fill="#0a0e1a" stroke="#38bdf8" strokeWidth={1.2} opacity={0.9}/>
25
+ <text textAnchor="middle" dominantBaseline="middle" fill="#38bdf8" fontSize={11} fontWeight={700} style={{ pointerEvents: "none", userSelect: "none" }}>
26
+
27
+ </text>
28
+ </g>)}
29
+ </g>);
30
+ };
@@ -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,165 @@
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) => { levelMap.set(id, 0); queue.push({ id, level: 0 }); });
20
+ while (queue.length > 0) {
21
+ const { id, level } = queue.shift();
22
+ edges.forEach((edge) => {
23
+ const neighbors = [];
24
+ if (edge.from === id)
25
+ neighbors.push(edge.to);
26
+ if (edge.to === id)
27
+ neighbors.push(edge.from);
28
+ neighbors.forEach((nId) => {
29
+ if (!levelMap.has(nId)) {
30
+ levelMap.set(nId, level + 1);
31
+ queue.push({ id: nId, level: level + 1 });
32
+ }
33
+ });
34
+ });
35
+ }
36
+ return levelMap;
37
+ };
38
+ // ─── Group edges by source ────────────────────────────────────────────────────
39
+ const groupBySource = (edges) => {
40
+ const map = new Map();
41
+ edges.forEach((edge) => {
42
+ if (!map.has(edge.from))
43
+ map.set(edge.from, []);
44
+ map.get(edge.from).push(edge);
45
+ });
46
+ return map;
47
+ };
48
+ // ─── Component ────────────────────────────────────────────────────────────────
49
+ export const ConnectionLayer = ({ edges, selectedEdge, connecting, mousePos, getNodeById, onSelectEdge,
50
+ // onDeleteEdge,
51
+ }) => {
52
+ const [hoveredEdge, setHoveredEdge] = useState(null);
53
+ // ── Flood fill highlight ──────────────────────────────────────────
54
+ const nodeHighlightMap = useMemo(() => {
55
+ if (!selectedEdge)
56
+ return new Map();
57
+ const sel = edges.find((e) => e.id === selectedEdge);
58
+ if (!sel)
59
+ return new Map();
60
+ return floodFill([sel.from, sel.to], edges);
61
+ }, [selectedEdge, edges]);
62
+ // ── Group by source for shared stem ──────────────────────────────
63
+ const sourceGroups = useMemo(() => groupBySource(edges), [edges]);
64
+ // ── Pre-compute curve data per edge ──────────────────────────────
65
+ const curveData = useMemo(() => {
66
+ const map = new Map();
67
+ edges.forEach((edge) => {
68
+ const fromNode = getNodeById(edge.from);
69
+ const toNode = getNodeById(edge.to);
70
+ if (!fromNode || !toNode)
71
+ return;
72
+ const start = getRectEdge(fromNode, toNode, fromNode.width, fromNode.height);
73
+ const end = getRectEdge(toNode, fromNode, toNode.width, toNode.height);
74
+ const pathD = buildCurvePath(start, end);
75
+ const angle = getCurveEndAngle(start, end);
76
+ // Midpoint arc/elbow → titik belok pertama (elbow kiri/atas)
77
+ const dx = end.x - start.x;
78
+ const dy = end.y - start.y;
79
+ const absDx = Math.abs(dx);
80
+ const absDy = Math.abs(dy);
81
+ let midX;
82
+ let midY;
83
+ if (absDx >= absDy) {
84
+ // Dominan horizontal → elbow di (midX, start.y)
85
+ midX = (start.x + end.x) / 2;
86
+ midY = (start.y + end.y) / 2;
87
+ }
88
+ else {
89
+ // Dominan vertikal → elbow di (start.x, midY)
90
+ midX = (start.x + end.x) / 2;
91
+ midY = (start.y + end.y) / 2;
92
+ }
93
+ map.set(edge.id, { start, end, pathD, angle, midX, midY });
94
+ });
95
+ return map;
96
+ }, [edges, getNodeById]);
97
+ // ── Shared stem exit point per source ────────────────────────────
98
+ const stemData = useMemo(() => {
99
+ const map = new Map();
100
+ sourceGroups.forEach((groupEdges, fromId) => {
101
+ if (groupEdges.length <= 1)
102
+ return;
103
+ const sourceNode = getNodeById(fromId);
104
+ if (!sourceNode)
105
+ return;
106
+ const exits = groupEdges
107
+ .map((edge) => {
108
+ const toNode = getNodeById(edge.to);
109
+ if (!toNode)
110
+ return null;
111
+ return getRectEdge(sourceNode, toNode, sourceNode.width, sourceNode.height);
112
+ })
113
+ .filter(Boolean);
114
+ if (exits.length === 0)
115
+ return;
116
+ map.set(fromId, {
117
+ x: exits.reduce((s, p) => s + p.x, 0) / exits.length,
118
+ y: exits.reduce((s, p) => s + p.y, 0) / exits.length,
119
+ });
120
+ });
121
+ return map;
122
+ }, [sourceGroups, getNodeById]);
123
+ return (_jsxs("g", { id: "connection-layer", children: [Array.from(nodeHighlightMap.entries()).map(([nodeId, level]) => {
124
+ var _a, _b, _c;
125
+ const node = getNodeById(nodeId);
126
+ if (!node)
127
+ return null;
128
+ const w = (_a = node.width) !== null && _a !== void 0 ? _a : NODE_WIDTH;
129
+ const h = (_b = node.height) !== null && _b !== void 0 ? _b : NODE_HEIGHT;
130
+ const rotation = (_c = node.rotation) !== null && _c !== void 0 ? _c : 0;
131
+ const color = getLevelColor(level);
132
+ const padding = 4;
133
+ 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}`));
134
+ }), Array.from(stemData.entries()).map(([fromId, stemExit]) => {
135
+ var _a;
136
+ const isStemOfSelected = selectedEdge && ((_a = edges.find((e) => e.id === selectedEdge)) === null || _a === void 0 ? void 0 : _a.from) === fromId;
137
+ const color = isStemOfSelected ? "#a78bfa" : "#38bdf8";
138
+ 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}`));
139
+ }), edges.map((edge) => {
140
+ const data = curveData.get(edge.id);
141
+ if (!data)
142
+ return null;
143
+ const { pathD, angle, midX, midY } = data;
144
+ const isSel = selectedEdge === edge.id;
145
+ const isHov = hoveredEdge === edge.id;
146
+ const markerId = `conn-arrow-${edge.id}`;
147
+ const markerSelId = `conn-arrow-sel-${edge.id}`;
148
+ // Warna edge jika dalam network highlight
149
+ const fromLevel = nodeHighlightMap.get(edge.from);
150
+ const toLevel = nodeHighlightMap.get(edge.to);
151
+ const isInNetwork = fromLevel !== undefined && toLevel !== undefined;
152
+ const networkColor = isInNetwork
153
+ ? getLevelColor(Math.max(fromLevel, toLevel))
154
+ : "#38bdf8";
155
+ 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 ? "#a78bfa" : isInNetwork && !isSel ? networkColor : "#38bdf8", opacity: 0.9 }) }) }), _jsx("path", { d: pathD, stroke: "transparent", strokeWidth: 14, fill: "none", style: { cursor: "pointer" }, onClick: (e) => { e.stopPropagation(); onSelectEdge(edge.id); }, 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 ? "#a78bfa" : isInNetwork && selectedEdge ? networkColor : "#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));
156
+ }), connecting && (() => {
157
+ const fromNode = getNodeById(connecting.fromId);
158
+ if (!fromNode)
159
+ return null;
160
+ const start = getRectEdge(fromNode, mousePos, fromNode.width, fromNode.height);
161
+ const pathD = buildCurvePath(start, mousePos);
162
+ const angle = getCurveEndAngle(start, mousePos);
163
+ 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" } })] }));
164
+ })()] }));
165
+ };