@tldiagram/core-ui 1.89.7 → 1.91.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.
- package/dist/index.js +4941 -4902
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +1 -1
- package/package.json +1 -1
- package/src/pages/ViewEditor/components/EditorMenus.tsx +6 -4
- package/src/pages/ViewEditor/components/EditorOverlays.tsx +3 -2
- package/src/pages/ViewEditor/components/EmptyCanvasState.tsx +3 -2
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +17 -26
- package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +1 -1
- package/src/pages/ViewEditor/index.tsx +122 -51
|
@@ -161,7 +161,7 @@ export declare function useCanvasInteractions({ viewId, canEdit, drawingMode: _d
|
|
|
161
161
|
clientX: number;
|
|
162
162
|
clientY: number;
|
|
163
163
|
}) => void;
|
|
164
|
-
showAddingElementAt: (clientX: number, clientY: number, expandResults?: boolean, mode?: "add" | "connect") => void;
|
|
164
|
+
showAddingElementAt: (clientX: number, clientY: number, expandResults?: boolean, mode?: "add" | "connect", forceConnect?: boolean) => void;
|
|
165
165
|
onNodesChange: (changes: NodeChange[]) => void;
|
|
166
166
|
onEdgesChange: (changes: EdgeChange[]) => void;
|
|
167
167
|
onNodeDragStart: NodeDragHandler;
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ interface ConnectorContextMenuProps {
|
|
|
26
26
|
onDelete: (edgeId: number) => void
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export const ConnectorContextMenu: React.FC<ConnectorContextMenuProps> = ({
|
|
29
|
+
export const ConnectorContextMenu: React.FC<ConnectorContextMenuProps> = React.memo(({
|
|
30
30
|
menu,
|
|
31
31
|
onEdit,
|
|
32
32
|
onMoveSource,
|
|
@@ -68,14 +68,15 @@ export const ConnectorContextMenu: React.FC<ConnectorContextMenuProps> = ({
|
|
|
68
68
|
</VStack>
|
|
69
69
|
</Box>
|
|
70
70
|
)
|
|
71
|
-
}
|
|
71
|
+
})
|
|
72
|
+
ConnectorContextMenu.displayName = 'ConnectorContextMenu'
|
|
72
73
|
|
|
73
74
|
interface CanvasContextMenuProps {
|
|
74
75
|
menu: { x: number; y: number; flowX: number; flowY: number } | null
|
|
75
76
|
onAddElement: (x: number, y: number) => void
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
export const CanvasContextMenu: React.FC<CanvasContextMenuProps> = ({ menu, onAddElement }) => {
|
|
79
|
+
export const CanvasContextMenu: React.FC<CanvasContextMenuProps> = React.memo(({ menu, onAddElement }) => {
|
|
79
80
|
const { canEdit, snapToGrid, setSnapToGrid } = useViewEditorContext()
|
|
80
81
|
if (!menu) return null
|
|
81
82
|
|
|
@@ -109,4 +110,5 @@ export const CanvasContextMenu: React.FC<CanvasContextMenuProps> = ({ menu, onAd
|
|
|
109
110
|
</VStack>
|
|
110
111
|
</Box>
|
|
111
112
|
)
|
|
112
|
-
}
|
|
113
|
+
})
|
|
114
|
+
CanvasContextMenu.displayName = 'CanvasContextMenu'
|
|
@@ -22,7 +22,7 @@ interface EditorOverlaysProps {
|
|
|
22
22
|
rfNodes: RFNode[]
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export const EditorOverlays: React.FC<EditorOverlaysProps> = ({
|
|
25
|
+
export const EditorOverlays: React.FC<EditorOverlaysProps> = React.memo(({
|
|
26
26
|
connectGhostPos,
|
|
27
27
|
clickConnectMode,
|
|
28
28
|
clickConnectCursorPos,
|
|
@@ -169,4 +169,5 @@ export const EditorOverlays: React.FC<EditorOverlaysProps> = ({
|
|
|
169
169
|
|
|
170
170
|
</>
|
|
171
171
|
)
|
|
172
|
-
}
|
|
172
|
+
})
|
|
173
|
+
EditorOverlays.displayName = 'EditorOverlays'
|
|
@@ -6,7 +6,7 @@ interface EmptyCanvasStateProps {
|
|
|
6
6
|
hasNodes: boolean
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export const EmptyCanvasState: React.FC<EmptyCanvasStateProps> = ({ isMobile, hasNodes }) => {
|
|
9
|
+
export const EmptyCanvasState: React.FC<EmptyCanvasStateProps> = React.memo(({ isMobile, hasNodes }) => {
|
|
10
10
|
if (hasNodes) return null
|
|
11
11
|
|
|
12
12
|
return (
|
|
@@ -39,4 +39,5 @@ export const EmptyCanvasState: React.FC<EmptyCanvasStateProps> = ({ isMobile, ha
|
|
|
39
39
|
</Text>
|
|
40
40
|
</Box>
|
|
41
41
|
)
|
|
42
|
-
}
|
|
42
|
+
})
|
|
43
|
+
EmptyCanvasState.displayName = 'EmptyCanvasState'
|
|
@@ -36,7 +36,6 @@ import {
|
|
|
36
36
|
} from '../../../utils/edgeDistribution'
|
|
37
37
|
|
|
38
38
|
const SNAP_RADIUS = 75
|
|
39
|
-
const CONTEXT_BOUNDARY_INSET = 36
|
|
40
39
|
|
|
41
40
|
interface CanvasInteractionOptions {
|
|
42
41
|
viewId: number | null
|
|
@@ -260,37 +259,29 @@ export function useCanvasInteractions({
|
|
|
260
259
|
const openConnectorPanelRef = useRef(openConnectorPanel)
|
|
261
260
|
openConnectorPanelRef.current = openConnectorPanel
|
|
262
261
|
|
|
263
|
-
const resolvePickerMode = useCallback((flowX: number, flowY: number, preferredMode: 'add' | 'connect') => {
|
|
262
|
+
const resolvePickerMode = useCallback((flowX: number, flowY: number, preferredMode: 'add' | 'connect', forceConnect = false) => {
|
|
264
263
|
if (preferredMode !== 'connect') return preferredMode
|
|
264
|
+
if (forceConnect) return 'connect'
|
|
265
265
|
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
for (const node of mainNodes) {
|
|
275
|
-
const width = node.width ?? 200
|
|
276
|
-
const height = node.height ?? 90
|
|
277
|
-
minX = Math.min(minX, node.position.x)
|
|
278
|
-
minY = Math.min(minY, node.position.y)
|
|
279
|
-
maxX = Math.max(maxX, node.position.x + width)
|
|
280
|
-
maxY = Math.max(maxY, node.position.y + height)
|
|
281
|
-
}
|
|
266
|
+
const boundaryNode = rfNodesRef.current.find((node) => node.type === 'ContextBoundaryElement')
|
|
267
|
+
if (!boundaryNode) return 'add'
|
|
268
|
+
|
|
269
|
+
const boundaryData = boundaryNode.data as { width?: number; height?: number } | undefined
|
|
270
|
+
const width = boundaryData?.width ?? boundaryNode.width
|
|
271
|
+
const height = boundaryData?.height ?? boundaryNode.height
|
|
272
|
+
if (width == null || height == null) return 'add'
|
|
282
273
|
|
|
283
274
|
const withinBoundary =
|
|
284
|
-
flowX >=
|
|
285
|
-
flowX <=
|
|
286
|
-
flowY >=
|
|
287
|
-
flowY <=
|
|
275
|
+
flowX >= boundaryNode.position.x &&
|
|
276
|
+
flowX <= boundaryNode.position.x + width &&
|
|
277
|
+
flowY >= boundaryNode.position.y &&
|
|
278
|
+
flowY <= boundaryNode.position.y + height
|
|
288
279
|
|
|
289
280
|
return withinBoundary ? 'add' : 'connect'
|
|
290
281
|
}, [rfNodesRef])
|
|
291
282
|
|
|
292
283
|
// ── showAddingElementAt ─────────────────────────────────────────────────────
|
|
293
|
-
const showAddingElementAt = useCallback((clientX: number, clientY: number, expandResults = false, mode: 'add' | 'connect' = 'add') => {
|
|
284
|
+
const showAddingElementAt = useCallback((clientX: number, clientY: number, expandResults = false, mode: 'add' | 'connect' = 'add', forceConnect = false) => {
|
|
294
285
|
const rect = containerRef.current?.getBoundingClientRect()
|
|
295
286
|
if (!rect) return
|
|
296
287
|
const flowPos = screenToFlowPositionRef.current({ x: clientX, y: clientY })
|
|
@@ -305,7 +296,7 @@ export function useCanvasInteractions({
|
|
|
305
296
|
? Math.max(100, Math.min(px, rect.width - 450))
|
|
306
297
|
: Math.max(120, Math.min(px, rect.width - 120))
|
|
307
298
|
const y = Math.max(40, Math.min(py, rect.height - 250))
|
|
308
|
-
setAddingElementAt({ x, y, flowX, flowY, expandResults, mode: resolvePickerMode(flowX, flowY, mode) })
|
|
299
|
+
setAddingElementAt({ x, y, flowX, flowY, expandResults, mode: resolvePickerMode(flowX, flowY, mode, forceConnect) })
|
|
309
300
|
}, [containerRef, snapToGrid, resolvePickerMode])
|
|
310
301
|
|
|
311
302
|
// ── Inline element adder handlers ───────────────────────────────────────────
|
|
@@ -633,7 +624,7 @@ export function useCanvasInteractions({
|
|
|
633
624
|
} else {
|
|
634
625
|
setPendingConnectionSource(sourceElementId)
|
|
635
626
|
suppressNextPaneClickRef.current = true
|
|
636
|
-
showAddingElementAt(clientX, clientY, true, 'connect')
|
|
627
|
+
showAddingElementAt(clientX, clientY, true, 'connect', 'shiftKey' in event && event.shiftKey)
|
|
637
628
|
}
|
|
638
629
|
}, [canEdit, setConnectors, showAddingElementAt, rfNodesRef, viewIdRef])
|
|
639
630
|
|
|
@@ -913,7 +904,7 @@ export function useCanvasInteractions({
|
|
|
913
904
|
} else {
|
|
914
905
|
setInteractionSourceId(null)
|
|
915
906
|
setPendingConnectionSource(sourceId)
|
|
916
|
-
showAddingElementAt(e.clientX, e.clientY, true, 'connect')
|
|
907
|
+
showAddingElementAt(e.clientX, e.clientY, true, 'connect', e.shiftKey)
|
|
917
908
|
}
|
|
918
909
|
return
|
|
919
910
|
}
|
|
@@ -112,7 +112,7 @@ export function useViewContextNeighbours({
|
|
|
112
112
|
const centerY = (minY + maxY) / 2
|
|
113
113
|
const boundaryW = maxX - minX
|
|
114
114
|
const boundaryH = maxY - minY
|
|
115
|
-
const totalInset =
|
|
115
|
+
const totalInset = 200
|
|
116
116
|
const padding = 180
|
|
117
117
|
const radiusX = boundaryW / 2 + padding
|
|
118
118
|
const radiusY = boundaryH / 2 + padding
|
|
@@ -13,7 +13,7 @@ import ReactFlow, {
|
|
|
13
13
|
useReactFlow,
|
|
14
14
|
applyNodeChanges,
|
|
15
15
|
} from 'reactflow'
|
|
16
|
-
import type { EdgeMarker as RFEdgeMarker, Node as RFNode, NodeChange } from 'reactflow'
|
|
16
|
+
import type { Edge as RFEdge, EdgeMarker as RFEdgeMarker, Node as RFNode, NodeChange } from 'reactflow'
|
|
17
17
|
import 'reactflow/dist/style.css'
|
|
18
18
|
import { toPng, toSvg } from 'html-to-image'
|
|
19
19
|
import {
|
|
@@ -94,6 +94,11 @@ const nodeTypes = {
|
|
|
94
94
|
}
|
|
95
95
|
const edgeTypes = { default: ViewBezierConnector, contextStraightConnector: ContextStraightConnector, proxyConnectorEdge: ProxyConnectorEdge }
|
|
96
96
|
const EMPTY_LINKS: ViewConnector[] = []
|
|
97
|
+
const VIEW_EDITOR_MIN_ZOOM_FLOOR = 0.12
|
|
98
|
+
const VIEW_EDITOR_EMPTY_EXTENT_RATIO = 0.75
|
|
99
|
+
const VIEW_EDITOR_PAN_MARGIN_RATIO = 0.25
|
|
100
|
+
const VIEW_EDITOR_PAN_MARGIN_MIN = 180
|
|
101
|
+
const VIEW_EDITOR_PAN_MARGIN_MAX = 720
|
|
97
102
|
|
|
98
103
|
function alphaColor(color: string, opacity: number): string {
|
|
99
104
|
if (opacity >= 1) return color
|
|
@@ -703,64 +708,105 @@ function ViewEditorInner({
|
|
|
703
708
|
const contextNodeIdsRef = useRef<Set<string>>(new Set())
|
|
704
709
|
useEffect(() => {
|
|
705
710
|
contextNodeIdsRef.current = new Set(contextNodes.map((n) => n.id))
|
|
706
|
-
setLiveContextNodes((prev) =>
|
|
707
|
-
|
|
708
|
-
|
|
711
|
+
setLiveContextNodes((prev) => {
|
|
712
|
+
const prevById = new Map(prev.map((p) => [p.id, p]))
|
|
713
|
+
return contextNodes.map((n) => {
|
|
714
|
+
const existing = prevById.get(n.id)
|
|
709
715
|
if (existing?.width != null && existing?.height != null) {
|
|
710
716
|
return { ...n, width: existing.width, height: existing.height }
|
|
711
717
|
}
|
|
712
718
|
return n
|
|
713
719
|
})
|
|
714
|
-
)
|
|
720
|
+
})
|
|
715
721
|
}, [contextNodes])
|
|
716
722
|
|
|
723
|
+
const fadedNodeCacheRef = useRef<WeakMap<RFNode, RFNode>>(new WeakMap())
|
|
724
|
+
const fadedEdgeCacheRef = useRef<WeakMap<RFEdge, RFEdge>>(new WeakMap())
|
|
725
|
+
|
|
717
726
|
const flowNodes = useMemo(() => {
|
|
718
|
-
const allNodes =
|
|
719
|
-
|
|
727
|
+
const allNodes = liveContextNodes.length === 0
|
|
728
|
+
? rfNodes
|
|
729
|
+
: rfNodes.length === 0
|
|
730
|
+
? liveContextNodes
|
|
731
|
+
: [...liveContextNodes, ...rfNodes]
|
|
732
|
+
|
|
733
|
+
let hasNodeSel = false
|
|
734
|
+
const selectedNodeIds = new Set<string>()
|
|
735
|
+
for (const n of allNodes) {
|
|
736
|
+
if (n.selected) { selectedNodeIds.add(n.id); hasNodeSel = true }
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const allEdges = contextConnectors.length === 0
|
|
740
|
+
? rfEdges
|
|
741
|
+
: rfEdges.length === 0
|
|
742
|
+
? contextConnectors
|
|
743
|
+
: [...contextConnectors, ...rfEdges]
|
|
720
744
|
|
|
721
|
-
const allEdges = [...contextConnectors, ...rfEdges]
|
|
722
745
|
const selectedEdgeEndPoints = new Set<string>()
|
|
723
|
-
|
|
746
|
+
let hasEdgeSel = false
|
|
747
|
+
for (const e of allEdges) {
|
|
724
748
|
if (e.selected) {
|
|
725
749
|
selectedEdgeEndPoints.add(e.source)
|
|
726
750
|
selectedEdgeEndPoints.add(e.target)
|
|
751
|
+
hasEdgeSel = true
|
|
727
752
|
}
|
|
728
|
-
}
|
|
753
|
+
}
|
|
729
754
|
|
|
730
755
|
const neighborNodeIds = new Set<string>()
|
|
731
|
-
if (
|
|
732
|
-
|
|
756
|
+
if (hasNodeSel) {
|
|
757
|
+
for (const e of allEdges) {
|
|
733
758
|
if (selectedNodeIds.has(e.source)) neighborNodeIds.add(e.target)
|
|
734
759
|
if (selectedNodeIds.has(e.target)) neighborNodeIds.add(e.source)
|
|
735
|
-
}
|
|
760
|
+
}
|
|
736
761
|
}
|
|
737
762
|
|
|
738
|
-
if (
|
|
763
|
+
if (!hasNodeSel && !hasEdgeSel) return allNodes
|
|
739
764
|
|
|
765
|
+
const cache = fadedNodeCacheRef.current
|
|
740
766
|
return allNodes.map((n) => {
|
|
741
767
|
const isHighlighted = selectedNodeIds.has(n.id) || selectedEdgeEndPoints.has(n.id) || neighborNodeIds.has(n.id)
|
|
742
768
|
if (isHighlighted) return n
|
|
743
|
-
|
|
769
|
+
const cached = cache.get(n)
|
|
770
|
+
if (cached) return cached
|
|
771
|
+
const faded: RFNode = {
|
|
744
772
|
...n,
|
|
745
773
|
style: { ...n.style, opacity: (Number(n.style?.opacity ?? 1)) * 0.2 },
|
|
746
774
|
}
|
|
775
|
+
cache.set(n, faded)
|
|
776
|
+
return faded
|
|
747
777
|
})
|
|
748
778
|
}, [liveContextNodes, rfNodes, contextConnectors, rfEdges])
|
|
749
779
|
|
|
750
780
|
const flowEdges = useMemo(() => {
|
|
751
|
-
const allEdges =
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
781
|
+
const allEdges = contextConnectors.length === 0
|
|
782
|
+
? rfEdges
|
|
783
|
+
: rfEdges.length === 0
|
|
784
|
+
? contextConnectors
|
|
785
|
+
: [...contextConnectors, ...rfEdges]
|
|
786
|
+
const allNodes = liveContextNodes.length === 0
|
|
787
|
+
? rfNodes
|
|
788
|
+
: rfNodes.length === 0
|
|
789
|
+
? liveContextNodes
|
|
790
|
+
: [...liveContextNodes, ...rfNodes]
|
|
791
|
+
|
|
792
|
+
const selectedNodeIds = new Set<string>()
|
|
793
|
+
let hasNodeSel = false
|
|
794
|
+
for (const n of allNodes) {
|
|
795
|
+
if (n.selected) { selectedNodeIds.add(n.id); hasNodeSel = true }
|
|
796
|
+
}
|
|
797
|
+
let hasEdgeSel = false
|
|
798
|
+
for (const e of allEdges) { if (e.selected) { hasEdgeSel = true; break } }
|
|
755
799
|
|
|
756
|
-
if (
|
|
800
|
+
if (!hasNodeSel && !hasEdgeSel) return allEdges
|
|
757
801
|
|
|
802
|
+
const cache = fadedEdgeCacheRef.current
|
|
758
803
|
return allEdges.map((e) => {
|
|
759
804
|
const isHighlighted = e.selected || selectedNodeIds.has(e.source) || selectedNodeIds.has(e.target)
|
|
760
805
|
if (isHighlighted) return e
|
|
761
|
-
|
|
806
|
+
const cached = cache.get(e)
|
|
807
|
+
if (cached) return cached
|
|
762
808
|
const multiplier = 0.2
|
|
763
|
-
|
|
809
|
+
const faded: RFEdge = {
|
|
764
810
|
...e,
|
|
765
811
|
style: { ...e.style, opacity: (Number(e.style?.opacity ?? 0.8)) * multiplier },
|
|
766
812
|
labelStyle: e.labelStyle ? { ...e.labelStyle, opacity: (Number(e.labelStyle.opacity ?? 1)) * multiplier } : undefined,
|
|
@@ -768,6 +814,8 @@ function ViewEditorInner({
|
|
|
768
814
|
markerEnd: fadeMarker(e.markerEnd, multiplier),
|
|
769
815
|
markerStart: fadeMarker(e.markerStart, multiplier),
|
|
770
816
|
}
|
|
817
|
+
cache.set(e, faded)
|
|
818
|
+
return faded
|
|
771
819
|
})
|
|
772
820
|
}, [contextConnectors, rfEdges, liveContextNodes, rfNodes])
|
|
773
821
|
|
|
@@ -808,7 +856,7 @@ function ViewEditorInner({
|
|
|
808
856
|
// ── FitView ────────────────────────────────────────────────────────────────
|
|
809
857
|
const fitViewRef = useRef(safeFitView)
|
|
810
858
|
fitViewRef.current = safeFitView
|
|
811
|
-
const [computedMinZoom, setComputedMinZoom] = useState(
|
|
859
|
+
const [computedMinZoom, setComputedMinZoom] = useState(VIEW_EDITOR_MIN_ZOOM_FLOOR)
|
|
812
860
|
const [computedTranslateExtent, setComputedTranslateExtent] = useState<[[number, number], [number, number]] | undefined>(undefined)
|
|
813
861
|
const {
|
|
814
862
|
clampedRevealProgress,
|
|
@@ -861,14 +909,22 @@ function ViewEditorInner({
|
|
|
861
909
|
// ── Dynamic viewport bounds ────────────────────────────────────────────────
|
|
862
910
|
useEffect(() => {
|
|
863
911
|
const vw = window.innerWidth; const vh = window.innerHeight
|
|
864
|
-
const emptyExtent: [[number, number], [number, number]] = [
|
|
865
|
-
|
|
866
|
-
|
|
912
|
+
const emptyExtent: [[number, number], [number, number]] = [
|
|
913
|
+
[-vw * VIEW_EDITOR_EMPTY_EXTENT_RATIO, -vh * VIEW_EDITOR_EMPTY_EXTENT_RATIO],
|
|
914
|
+
[vw * VIEW_EDITOR_EMPTY_EXTENT_RATIO, vh * VIEW_EDITOR_EMPTY_EXTENT_RATIO],
|
|
915
|
+
]
|
|
916
|
+
const boundsNodes = liveContextNodes.length === 0
|
|
917
|
+
? rfNodes
|
|
918
|
+
: rfNodes.length === 0
|
|
919
|
+
? liveContextNodes
|
|
920
|
+
: [...liveContextNodes, ...rfNodes]
|
|
921
|
+
if (boundsNodes.length === 0 && drawingPaths.length === 0) {
|
|
922
|
+
setComputedMinZoom((prev) => prev === VIEW_EDITOR_MIN_ZOOM_FLOOR ? prev : VIEW_EDITOR_MIN_ZOOM_FLOOR)
|
|
867
923
|
setComputedTranslateExtent((prev) => areTranslateExtentsEqual(prev, emptyExtent) ? prev : emptyExtent)
|
|
868
924
|
return
|
|
869
925
|
}
|
|
870
926
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
|
871
|
-
for (const n of
|
|
927
|
+
for (const n of boundsNodes) {
|
|
872
928
|
minX = Math.min(minX, n.position.x); minY = Math.min(minY, n.position.y)
|
|
873
929
|
maxX = Math.max(maxX, n.position.x + (n.width ?? 180)); maxY = Math.max(maxY, n.position.y + (n.height ?? 80))
|
|
874
930
|
}
|
|
@@ -876,26 +932,26 @@ function ViewEditorInner({
|
|
|
876
932
|
for (const pt of p.points) { minX = Math.min(minX, pt.x); minY = Math.min(minY, pt.y); maxX = Math.max(maxX, pt.x); maxY = Math.max(maxY, pt.y) }
|
|
877
933
|
}
|
|
878
934
|
if (!isFinite(minX)) {
|
|
879
|
-
setComputedMinZoom((prev) => prev ===
|
|
935
|
+
setComputedMinZoom((prev) => prev === VIEW_EDITOR_MIN_ZOOM_FLOOR ? prev : VIEW_EDITOR_MIN_ZOOM_FLOOR)
|
|
880
936
|
setComputedTranslateExtent((prev) => areTranslateExtentsEqual(prev, emptyExtent) ? prev : emptyExtent)
|
|
881
937
|
return
|
|
882
938
|
}
|
|
883
939
|
const bboxW = maxX - minX; const bboxH = maxY - minY
|
|
884
940
|
let minZoom = Math.sqrt((0.12 * vw * vh) / Math.max(1, bboxW * bboxH))
|
|
885
|
-
if (!isFinite(minZoom) || isNaN(minZoom) || minZoom <= 0) minZoom =
|
|
886
|
-
const nextMinZoom = Math.max(
|
|
941
|
+
if (!isFinite(minZoom) || isNaN(minZoom) || minZoom <= 0) minZoom = VIEW_EDITOR_MIN_ZOOM_FLOOR
|
|
942
|
+
const nextMinZoom = Math.max(VIEW_EDITOR_MIN_ZOOM_FLOOR, Math.min(minZoom, 1))
|
|
887
943
|
setComputedMinZoom((prev) => prev === nextMinZoom ? prev : nextMinZoom)
|
|
888
944
|
// Extent must be ≥ viewport at minZoom (else pan locks). Center on content bbox.
|
|
889
|
-
//
|
|
945
|
+
// Keep only modest content-proportional slack so the canvas stays discoverable.
|
|
890
946
|
const vwFlowMax = vw / nextMinZoom; const vhFlowMax = vh / nextMinZoom
|
|
891
|
-
const slackX = Math.max(bboxW *
|
|
892
|
-
const slackY = Math.max(bboxH *
|
|
893
|
-
const spanX = Math.max(bboxW + 2 * slackX, vwFlowMax
|
|
894
|
-
const spanY = Math.max(bboxH + 2 * slackY, vhFlowMax
|
|
947
|
+
const slackX = Math.min(Math.max(bboxW * VIEW_EDITOR_PAN_MARGIN_RATIO, VIEW_EDITOR_PAN_MARGIN_MIN), VIEW_EDITOR_PAN_MARGIN_MAX)
|
|
948
|
+
const slackY = Math.min(Math.max(bboxH * VIEW_EDITOR_PAN_MARGIN_RATIO, VIEW_EDITOR_PAN_MARGIN_MIN), VIEW_EDITOR_PAN_MARGIN_MAX)
|
|
949
|
+
const spanX = Math.max(bboxW + 2 * slackX, vwFlowMax)
|
|
950
|
+
const spanY = Math.max(bboxH + 2 * slackY, vhFlowMax)
|
|
895
951
|
const cx = (minX + maxX) / 2; const cy = (minY + maxY) / 2
|
|
896
952
|
const nextTranslateExtent: [[number, number], [number, number]] = [[cx - spanX / 2, cy - spanY / 2], [cx + spanX / 2, cy + spanY / 2]]
|
|
897
953
|
setComputedTranslateExtent((prev) => areTranslateExtentsEqual(prev, nextTranslateExtent) ? prev : nextTranslateExtent)
|
|
898
|
-
}, [
|
|
954
|
+
}, [rfNodes, liveContextNodes, drawingPaths])
|
|
899
955
|
|
|
900
956
|
// ── Keyboard shortcuts for drawing ────────────────────────────────────────
|
|
901
957
|
useEffect(() => {
|
|
@@ -943,7 +999,27 @@ function ViewEditorInner({
|
|
|
943
999
|
// ── Share ──────────────────────────────────────────────────────────────────
|
|
944
1000
|
const onShare = useCallback(() => {}, [])
|
|
945
1001
|
|
|
1002
|
+
const handleExplorerHoverZoom = useCallback((elementId: number | null, type: 'in' | 'out' | null) => {
|
|
1003
|
+
setHoveredZoom(type && elementId ? { elementId, type } : null)
|
|
1004
|
+
}, [])
|
|
1005
|
+
const handleToggleExplorer = useCallback(() => setIsExplorerOpen((v) => !v), [])
|
|
1006
|
+
const handleCloseLibrary = useCallback(() => setLibraryOpen(false), [])
|
|
1007
|
+
const handleCreateNewLibraryRef = useRef<() => void>(() => {})
|
|
1008
|
+
const handleCreateNewLibrary = useCallback(() => handleCreateNewLibraryRef.current(), [])
|
|
1009
|
+
const handleFocusModeChange = useCallback((v: boolean) => setCrossBranchEnabled(!v), [setCrossBranchEnabled])
|
|
1010
|
+
const handleOpenExport = useCallback(() => exportModal.onOpen(), [exportModal])
|
|
1011
|
+
const handleConnectorSave = useCallback((updated: Connector) => {
|
|
1012
|
+
upsertConnectorGraphSnapshot(updated)
|
|
1013
|
+
setConnectors((prev) => prev.map((c) => (c.id === updated.id ? updated : c)))
|
|
1014
|
+
}, [setConnectors])
|
|
1015
|
+
const handleConnectorDeleteInPanel = useCallback((edgeId: number) => {
|
|
1016
|
+
if (viewId != null) removeConnectorGraphSnapshot(viewId, edgeId)
|
|
1017
|
+
setConnectors((prev) => prev.filter((c) => c.id !== edgeId))
|
|
1018
|
+
}, [setConnectors, viewId])
|
|
1019
|
+
const handleViewSave = useCallback((updated: ViewTreeNode) => setView(updated), [setView])
|
|
1020
|
+
|
|
946
1021
|
// ── Library helpers ────────────────────────────────────────────────────────
|
|
1022
|
+
// Assigned below; referenced by memoized callbacks (e.g. ElementLibrary onCreateNew).
|
|
947
1023
|
const handleAddElementAtCenter = useCallback((forceCenter = false) => {
|
|
948
1024
|
if (!canEdit) return
|
|
949
1025
|
const rect = containerRef.current?.getBoundingClientRect()
|
|
@@ -957,6 +1033,7 @@ function ViewEditorInner({
|
|
|
957
1033
|
}
|
|
958
1034
|
showAddingElementAt(cx, cy, true)
|
|
959
1035
|
}, [canEdit, showAddingElementAt, lastMousePosRef])
|
|
1036
|
+
handleCreateNewLibraryRef.current = () => handleAddElementAtCenter(true)
|
|
960
1037
|
|
|
961
1038
|
const handleTapAdd = useCallback(async (obj: WorkspaceElement) => {
|
|
962
1039
|
if (!canEdit || !viewId || existingElementIds.has(obj.id)) return
|
|
@@ -1169,8 +1246,8 @@ function ViewEditorInner({
|
|
|
1169
1246
|
treeNodes={treeData}
|
|
1170
1247
|
linksMap={linksMap} viewElements={viewElements}
|
|
1171
1248
|
onNavigate={canvas.stableOnNavigateToView}
|
|
1172
|
-
onHoverZoom={
|
|
1173
|
-
isOpen={isExplorerOpen} onToggle={
|
|
1249
|
+
onHoverZoom={handleExplorerHoverZoom}
|
|
1250
|
+
isOpen={isExplorerOpen} onToggle={handleToggleExplorer}
|
|
1174
1251
|
isMobile={isMobileLayout}
|
|
1175
1252
|
activeTags={activeTags}
|
|
1176
1253
|
setActiveTags={handleSetActiveTags}
|
|
@@ -1280,9 +1357,9 @@ function ViewEditorInner({
|
|
|
1280
1357
|
hasDrawingPaths={drawingPaths.length > 0} drawingVisible={drawingVisible} setDrawingVisible={setDrawingVisible}
|
|
1281
1358
|
extrasOpen={extrasOpen} setExtrasOpen={setExtrasOpen}
|
|
1282
1359
|
focusMode={!crossBranchSettings.enabled}
|
|
1283
|
-
onFocusModeChange={
|
|
1360
|
+
onFocusModeChange={handleFocusModeChange}
|
|
1284
1361
|
disableImportExport={disableImportExport}
|
|
1285
|
-
onImport={importModal.onOpen} onExport={
|
|
1362
|
+
onImport={importModal.onOpen} onExport={handleOpenExport} onShare={onShare}
|
|
1286
1363
|
allTags={availableTags}
|
|
1287
1364
|
layers={layers}
|
|
1288
1365
|
tagColors={tagColors}
|
|
@@ -1302,8 +1379,8 @@ function ViewEditorInner({
|
|
|
1302
1379
|
<ElementLibrary
|
|
1303
1380
|
existingElementIds={existingElementIds}
|
|
1304
1381
|
existingElements={existingElements}
|
|
1305
|
-
onCreateNew={
|
|
1306
|
-
isOpen={libraryOpen} onClose={
|
|
1382
|
+
onCreateNew={handleCreateNewLibrary} refresh={libraryRefresh}
|
|
1383
|
+
isOpen={libraryOpen} onClose={handleCloseLibrary}
|
|
1307
1384
|
onTapAdd={canEdit ? handleTapAdd : undefined}
|
|
1308
1385
|
onFindElement={handleFindElement}
|
|
1309
1386
|
onTouchDrop={canEdit ? handleTouchDrop : undefined}
|
|
@@ -1326,14 +1403,8 @@ function ViewEditorInner({
|
|
|
1326
1403
|
<ConnectorPanel
|
|
1327
1404
|
isOpen={connectorPanel.isOpen} onClose={connectorPanel.onClose} connector={selectedEdge}
|
|
1328
1405
|
orgId={''}
|
|
1329
|
-
onSave={
|
|
1330
|
-
|
|
1331
|
-
setConnectors((prev) => prev.map((connector) => (connector.id === updated.id ? updated : connector)))
|
|
1332
|
-
}} autoSave
|
|
1333
|
-
onDelete={(edgeId: number) => {
|
|
1334
|
-
if (viewId != null) removeConnectorGraphSnapshot(viewId, edgeId)
|
|
1335
|
-
setConnectors((prev) => prev.filter((connector) => connector.id !== edgeId))
|
|
1336
|
-
}}
|
|
1406
|
+
onSave={handleConnectorSave} autoSave
|
|
1407
|
+
onDelete={handleConnectorDeleteInPanel}
|
|
1337
1408
|
hasBackdrop={isMobileLayout}
|
|
1338
1409
|
connectorPanelAfterContentSlot={connectorPanelAfterContentSlot}
|
|
1339
1410
|
/>
|
|
@@ -1347,7 +1418,7 @@ function ViewEditorInner({
|
|
|
1347
1418
|
<ViewPanel
|
|
1348
1419
|
isOpen={viewDetails.isOpen} onClose={viewDetails.onClose}
|
|
1349
1420
|
view={view as ViewTreeNode}
|
|
1350
|
-
onSave={
|
|
1421
|
+
onSave={handleViewSave} hasBackdrop={isMobileLayout}
|
|
1351
1422
|
/>
|
|
1352
1423
|
|
|
1353
1424
|
<ExportModal
|