@tldiagram/core-ui 1.95.0 → 2.0.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 (102) hide show
  1. package/dist/api/client.d.ts +184 -3
  2. package/dist/components/ConnectorPanel.d.ts +5 -1
  3. package/dist/components/CrossBranchControls.d.ts +4 -3
  4. package/dist/components/ElementNode.d.ts +5 -0
  5. package/dist/components/ElementPanel.d.ts +6 -1
  6. package/dist/components/LayoutSection.d.ts +2 -1
  7. package/dist/components/MergeDialog.d.ts +16 -0
  8. package/dist/components/MiniZoomOnboarding.d.ts +2 -1
  9. package/dist/components/NodeContainer.d.ts +2 -0
  10. package/dist/components/ProxyConnectorPanel.d.ts +4 -1
  11. package/dist/components/ViewExplorer/index.d.ts +1 -1
  12. package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
  13. package/dist/components/ViewFloatingMenu.d.ts +8 -1
  14. package/dist/components/ViewGridNode.d.ts +3 -0
  15. package/dist/components/ViewPanel.d.ts +2 -1
  16. package/dist/components/WorkspacePanel.d.ts +2 -0
  17. package/dist/components/ZUI/ZUICanvas.d.ts +5 -0
  18. package/dist/components/ZUI/focus.d.ts +32 -0
  19. package/dist/components/ZUI/focus.test.d.ts +1 -0
  20. package/dist/components/ZUI/layout.d.ts +2 -2
  21. package/dist/components/ZUI/proxy.d.ts +20 -4
  22. package/dist/components/ZUI/renderer.d.ts +35 -1
  23. package/dist/components/ZUI/types.d.ts +6 -0
  24. package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
  25. package/dist/context/WorkspaceVersionContext.d.ts +49 -0
  26. package/dist/crossBranch/resolve.d.ts +39 -2
  27. package/dist/crossBranch/resolve.test.d.ts +1 -0
  28. package/dist/crossBranch/settings.d.ts +6 -1
  29. package/dist/crossBranch/types.d.ts +8 -0
  30. package/dist/hooks/useElementSearch.d.ts +8 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +14597 -12083
  33. package/dist/pages/InfiniteZoom.d.ts +1 -0
  34. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
  35. package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
  36. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
  37. package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
  38. package/dist/pages/viewsJumpSearch.d.ts +22 -0
  39. package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
  40. package/dist/store/useStore.d.ts +3 -0
  41. package/dist/types/index.d.ts +9 -0
  42. package/dist/utils/elementIcon.d.ts +2 -0
  43. package/dist/utils/elementIcon.test.d.ts +1 -0
  44. package/dist/utils/sourceEditor.d.ts +7 -0
  45. package/dist/utils/watchDiffSummary.d.ts +34 -0
  46. package/package.json +2 -2
  47. package/src/App.tsx +12 -8
  48. package/src/api/client.ts +488 -26
  49. package/src/components/CodePreviewPanel.tsx +90 -16
  50. package/src/components/ConnectorPanel.tsx +34 -3
  51. package/src/components/ContextNeighborElement.tsx +2 -5
  52. package/src/components/CrossBranchControls.tsx +46 -17
  53. package/src/components/ElementNode.tsx +98 -47
  54. package/src/components/ElementPanel.tsx +62 -25
  55. package/src/components/InlineElementAdder.tsx +8 -3
  56. package/src/components/LayoutSection.tsx +4 -1
  57. package/src/components/MergeDialog.tsx +269 -0
  58. package/src/components/MiniZoomOnboarding.tsx +29 -22
  59. package/src/components/NodeContainer.tsx +55 -17
  60. package/src/components/ProxyConnectorPanel.tsx +58 -16
  61. package/src/components/ViewBezierConnector.tsx +116 -21
  62. package/src/components/ViewExplorer/index.tsx +1 -1
  63. package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
  64. package/src/components/ViewFloatingMenu.tsx +110 -1
  65. package/src/components/ViewGridNode.tsx +59 -8
  66. package/src/components/ViewPanel.tsx +3 -2
  67. package/src/components/WorkspacePanel.tsx +938 -0
  68. package/src/components/ZUI/ZUICanvas.tsx +226 -127
  69. package/src/components/ZUI/focus.test.ts +534 -0
  70. package/src/components/ZUI/focus.ts +293 -0
  71. package/src/components/ZUI/layout.ts +7 -11
  72. package/src/components/ZUI/proxy.ts +470 -114
  73. package/src/components/ZUI/renderer.ts +510 -134
  74. package/src/components/ZUI/types.ts +6 -0
  75. package/src/components/ZUI/useZUIInteraction.ts +66 -29
  76. package/src/context/WorkspaceVersionContext.tsx +126 -0
  77. package/src/crossBranch/resolve.test.ts +342 -0
  78. package/src/crossBranch/resolve.ts +368 -68
  79. package/src/crossBranch/settings.ts +49 -3
  80. package/src/crossBranch/types.ts +9 -0
  81. package/src/hooks/useElementSearch.ts +45 -0
  82. package/src/index.css +11 -0
  83. package/src/index.ts +7 -0
  84. package/src/pages/AppearanceSettings.tsx +24 -1
  85. package/src/pages/Dependencies.tsx +231 -65
  86. package/src/pages/InfiniteZoom.tsx +76 -27
  87. package/src/pages/Settings.tsx +1 -1
  88. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
  89. package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
  90. package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
  91. package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
  92. package/src/pages/ViewEditor/index.tsx +549 -59
  93. package/src/pages/Views.tsx +112 -41
  94. package/src/pages/ViewsGrid.tsx +332 -113
  95. package/src/pages/viewsJumpSearch.test.ts +193 -0
  96. package/src/pages/viewsJumpSearch.ts +111 -0
  97. package/src/store/useStore.ts +58 -0
  98. package/src/types/index.ts +10 -0
  99. package/src/utils/elementIcon.test.ts +28 -0
  100. package/src/utils/elementIcon.ts +20 -0
  101. package/src/utils/sourceEditor.ts +46 -0
  102. package/src/utils/watchDiffSummary.ts +159 -0
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react'
2
2
  import { type Edge as RFEdge, type Node as RFNode } from 'reactflow'
3
- import type { PlacedElement } from '../../../types'
3
+ import type { Connector, PlacedElement } from '../../../types'
4
4
  import type { CrossBranchContextSettings, ProxyConnectorDetails, WorkspaceGraphSnapshot } from '../../../crossBranch/types'
5
5
  import { resolveViewProxyGraph } from '../../../crossBranch/resolve'
6
6
 
@@ -69,6 +69,61 @@ function isAncestorContextNode(
69
69
  (snapshot.descendantsByViewId[ownedViewId]?.includes(descendant.placementViewId) ?? false)
70
70
  }
71
71
 
72
+ function canonicalElementPairKey(leftId: number, rightId: number) {
73
+ return leftId <= rightId ? `${leftId}::${rightId}` : `${rightId}::${leftId}`
74
+ }
75
+
76
+ function canonicalNodePairKey(leftId: string, rightId: string) {
77
+ return leftId <= rightId ? `${leftId}::${rightId}` : `${rightId}::${leftId}`
78
+ }
79
+
80
+ function buildDirectConnectorPairSet(connectors: Connector[], visibleElementIds: Set<number>) {
81
+ const pairs = new Set<string>()
82
+ for (const connector of connectors) {
83
+ if (!visibleElementIds.has(connector.source_element_id) || !visibleElementIds.has(connector.target_element_id)) continue
84
+ pairs.add(canonicalElementPairKey(connector.source_element_id, connector.target_element_id))
85
+ }
86
+ return pairs
87
+ }
88
+
89
+ function mergeHiddenProxyDetails(
90
+ existing: ProxyConnectorDetails | undefined,
91
+ next: ProxyConnectorDetails,
92
+ ): ProxyConnectorDetails {
93
+ if (!existing) {
94
+ return {
95
+ ...next,
96
+ ownerViewIds: [...next.ownerViewIds],
97
+ ownerViewNames: [...next.ownerViewNames],
98
+ connectors: [...next.connectors],
99
+ }
100
+ }
101
+
102
+ const ownerViews = new Map<number, string>()
103
+ existing.ownerViewIds.forEach((ownerViewId, index) => {
104
+ ownerViews.set(ownerViewId, existing.ownerViewNames[index] ?? `View ${ownerViewId}`)
105
+ })
106
+ next.ownerViewIds.forEach((ownerViewId, index) => {
107
+ ownerViews.set(ownerViewId, next.ownerViewNames[index] ?? `View ${ownerViewId}`)
108
+ })
109
+
110
+ const connectors = [...existing.connectors, ...next.connectors]
111
+ const count = connectors.length
112
+
113
+ return {
114
+ key: existing.key,
115
+ label: count === 1 ? connectors[0]?.connector.label?.trim() || connectors[0]?.connector.relationship?.trim() || 'Cross-branch' : `${count} connectors`,
116
+ count,
117
+ sourceAnchorId: existing.sourceAnchorId,
118
+ targetAnchorId: existing.targetAnchorId,
119
+ sourceAnchorName: existing.sourceAnchorName,
120
+ targetAnchorName: existing.targetAnchorName,
121
+ ownerViewIds: Array.from(ownerViews.keys()),
122
+ ownerViewNames: Array.from(ownerViews.values()),
123
+ connectors,
124
+ }
125
+ }
126
+
72
127
  export function useViewContextNeighbours({
73
128
  snapshot,
74
129
  settings,
@@ -82,18 +137,38 @@ export function useViewContextNeighbours({
82
137
  }: Props) {
83
138
  return useMemo(() => {
84
139
  if (!snapshot || viewId == null || !settings.enabled) {
85
- return { contextNodes: [] as RFNode[], contextConnectors: [] as RFEdge[], proxyConnectorDetailsByKey: {} as Record<string, ProxyConnectorDetails> }
140
+ return {
141
+ contextNodes: [] as RFNode[],
142
+ contextConnectors: [] as RFEdge[],
143
+ proxyConnectorDetailsByKey: {} as Record<string, ProxyConnectorDetails>,
144
+ hiddenProxyCountsByPair: {} as Record<string, number>,
145
+ hiddenProxyDetailsByPair: {} as Record<string, ProxyConnectorDetails>,
146
+ }
86
147
  }
87
148
 
88
149
  const { proxyNodes, proxyConnectors, proxyConnectorDetailsByKey } = resolveViewProxyGraph(snapshot, viewId, viewElements, settings)
89
150
  if (proxyNodes.length === 0 && proxyConnectors.length === 0) {
90
- return { contextNodes: [] as RFNode[], contextConnectors: [] as RFEdge[], proxyConnectorDetailsByKey }
151
+ return {
152
+ contextNodes: [] as RFNode[],
153
+ contextConnectors: [] as RFEdge[],
154
+ proxyConnectorDetailsByKey,
155
+ hiddenProxyCountsByPair: {} as Record<string, number>,
156
+ hiddenProxyDetailsByPair: {} as Record<string, ProxyConnectorDetails>,
157
+ }
91
158
  }
92
159
 
93
160
  const mainNodes = rfNodes.filter((node) => node.type === 'elementNode')
94
161
  if (mainNodes.length === 0) {
95
- return { contextNodes: [] as RFNode[], contextConnectors: [] as RFEdge[], proxyConnectorDetailsByKey }
162
+ return {
163
+ contextNodes: [] as RFNode[],
164
+ contextConnectors: [] as RFEdge[],
165
+ proxyConnectorDetailsByKey,
166
+ hiddenProxyCountsByPair: {} as Record<string, number>,
167
+ hiddenProxyDetailsByPair: {} as Record<string, ProxyConnectorDetails>,
168
+ }
96
169
  }
170
+ const visibleElementIds = new Set(viewElements.map((element) => element.element_id))
171
+ const directConnectorPairs = buildDirectConnectorPairSet(snapshot.connectorsByViewId[viewId] ?? [], visibleElementIds)
97
172
 
98
173
  let minX = Infinity
99
174
  let minY = Infinity
@@ -460,6 +535,8 @@ export function useViewContextNeighbours({
460
535
  })
461
536
 
462
537
  const seenCollapsedPairs = new Set<string>()
538
+ const hiddenProxyCountsByPair: Record<string, number> = {}
539
+ const hiddenProxyDetailsByPair: Record<string, ProxyConnectorDetails> = {}
463
540
  const contextConnectors: RFEdge[] = proxyConnectors.flatMap((connector) => {
464
541
  let sourceId = connector.sourceAnchorId
465
542
  let targetId = connector.targetAnchorId
@@ -472,7 +549,20 @@ export function useViewContextNeighbours({
472
549
 
473
550
  if (sourceId === targetId) return []
474
551
 
475
- const pairKey = `${sourceId}::${targetId}`
552
+ const pairKey = canonicalNodePairKey(sourceId, targetId)
553
+ if (directConnectorPairs.has(pairKey)) {
554
+ hiddenProxyCountsByPair[pairKey] = (hiddenProxyCountsByPair[pairKey] ?? 0) + connector.details.count
555
+ hiddenProxyDetailsByPair[pairKey] = mergeHiddenProxyDetails(
556
+ hiddenProxyDetailsByPair[pairKey],
557
+ {
558
+ ...connector.details,
559
+ key: `hidden:${pairKey}`,
560
+ sourceAnchorId: sourceId,
561
+ targetAnchorId: targetId,
562
+ },
563
+ )
564
+ return []
565
+ }
476
566
  if (seenCollapsedPairs.has(pairKey)) return []
477
567
  seenCollapsedPairs.add(pairKey)
478
568
 
@@ -496,6 +586,12 @@ export function useViewContextNeighbours({
496
586
  }]
497
587
  })
498
588
 
499
- return { contextNodes: [ContextBoundaryElement, ...contextNodes], contextConnectors, proxyConnectorDetailsByKey }
589
+ return {
590
+ contextNodes: [ContextBoundaryElement, ...contextNodes],
591
+ contextConnectors,
592
+ proxyConnectorDetailsByKey,
593
+ hiddenProxyCountsByPair,
594
+ hiddenProxyDetailsByPair,
595
+ }
500
596
  }, [snapshot, settings, viewId, viewElements, rfNodes, stableOnNavigateToView, onSelectProxyDetails, expandedAncestorGroups, onToggleAncestorGroup])
501
597
  }
@@ -18,6 +18,7 @@ import {
18
18
  getVisualHandleSlot,
19
19
  } from '../../../utils/edgeDistribution'
20
20
  import { buildViewContentLinks, useStore } from '../../../store/useStore'
21
+ import type { WorkspaceVersionFollowTarget, WorkspaceVersionPreview } from '../../../context/WorkspaceVersionContext'
21
22
 
22
23
  interface ViewDataOptions {
23
24
  viewId: number | null
@@ -29,6 +30,8 @@ interface ViewDataOptions {
29
30
  hoveredLayerTags: string[] | null
30
31
  hoveredLayerColor: string | null
31
32
  tagColors: Record<string, Tag>
33
+ versionPreview?: WorkspaceVersionPreview | null
34
+ versionFollowTarget?: WorkspaceVersionFollowTarget | null
32
35
  // Node-level callbacks (stable refs from parent)
33
36
  stableOnZoomIn: (elementId: number) => Promise<void>
34
37
  stableOnZoomOut: (elementId: number) => Promise<void>
@@ -52,6 +55,7 @@ function alphaColor(color: string, opacity: number): string {
52
55
  // letting structural-sharing fast-path bail out without rebuilding the node.
53
56
  const HIDDEN_STYLE: CSSProperties = { opacity: 0.1, pointerEvents: 'none' }
54
57
  const SOFT_FOCUS_STYLE: CSSProperties = { opacity: 0.2 }
58
+ const VERSION_DIM_STYLE: CSSProperties = { opacity: 0.1 }
55
59
  const EMPTY_ARRAY: readonly never[] = Object.freeze([])
56
60
  const EMPTY_NODE_CONNECTION_META = Object.freeze({
57
61
  key: '',
@@ -158,6 +162,8 @@ export function useViewData({
158
162
  hoveredLayerTags,
159
163
  hoveredLayerColor,
160
164
  tagColors,
165
+ versionPreview,
166
+ versionFollowTarget,
161
167
  stableOnZoomIn,
162
168
  stableOnZoomOut,
163
169
  stableOnNavigateToView,
@@ -189,7 +195,6 @@ export function useViewData({
189
195
  const incomingLinks = useStore((state) => state.incomingLinks)
190
196
  const treeData = useStore((state) => state.treeData)
191
197
  const allElements = useStore((state) => state.allElements)
192
- const setAllElements = useStore((state) => state.setAllElements)
193
198
  const hydrateViewContent = useStore((state) => state.hydrateViewContent)
194
199
  const resetCanvas = useStore((state) => state.resetCanvas)
195
200
  const removeElementPlacement = useStore((state) => state.removeElementPlacement)
@@ -216,13 +221,14 @@ export function useViewData({
216
221
 
217
222
  // ── Fetch tree ─────────────────────────────────────────────────────────────
218
223
  const refreshGrid = useCallback(async () => {
224
+ if (viewId === null) return
219
225
  const tree = await queryClient.fetchQuery({
220
- queryKey: ['workspace', 'views', 'tree'],
221
- queryFn: () => api.workspace.views.tree(),
226
+ queryKey: ['workspace', 'views', viewId, 'editor-tree'],
227
+ queryFn: () => api.workspace.views.treeAround(viewId, { ancestorLevels: 2, descendantLevels: 2 }),
222
228
  staleTime: 0,
223
229
  }).catch(() => null)
224
230
  if (tree) useStore.getState().setTreeData(tree)
225
- }, [queryClient])
231
+ }, [queryClient, viewId])
226
232
 
227
233
  // ── Fetch view content ──────────────────────────────────────────────────
228
234
  const viewContentQuery = useQuery({
@@ -233,7 +239,7 @@ export function useViewData({
233
239
  const [diag, content, tree] = await Promise.all([
234
240
  api.workspace.views.get(viewId),
235
241
  api.workspace.views.content(viewId),
236
- api.workspace.views.tree(),
242
+ api.workspace.views.treeAround(viewId, { ancestorLevels: 2, descendantLevels: 2 }),
237
243
  ])
238
244
  const viewElements = content.placements || []
239
245
  const connectors = content.connectors || []
@@ -264,16 +270,6 @@ export function useViewData({
264
270
  resetCanvas()
265
271
  }, [resetCanvas, viewId])
266
272
 
267
- // ── Keep all-org elements for inline adder ──────────────────────────────────
268
- const allElementsQuery = useQuery({
269
- queryKey: ['elements', 'list'],
270
- queryFn: () => api.elements.list(),
271
- })
272
-
273
- useEffect(() => {
274
- if (allElementsQuery.data) setAllElements(allElementsQuery.data)
275
- }, [allElementsQuery.data, setAllElements])
276
-
277
273
  // ── Refresh elements ────────────────────────────────────────────────────────
278
274
  const refreshElements = useCallback(async () => {
279
275
  if (viewId === null) return
@@ -428,6 +424,9 @@ export function useViewData({
428
424
  const activeSet = activeTags.length > 0 ? new Set(activeTags) : null
429
425
  const hoveredSet = hoveredLayerTags !== null ? new Set(hoveredLayerTags) : null
430
426
  const isClickConnectMode = clickConnectMode !== null
427
+ const versionElementChanges = versionPreview?.elementChanges
428
+ const versionElementLineDeltas = versionPreview?.elementLineDeltas
429
+ const versionActive = !!versionPreview
431
430
 
432
431
  return viewElements.map((obj) => {
433
432
  const nodeId = String(obj.element_id)
@@ -438,13 +437,21 @@ export function useViewData({
438
437
  const isInactive = isHiddenByLayer || (activeSet !== null && !objTags.some((t) => activeSet.has(t)))
439
438
  const isLayerHighlighted = hoveredSet !== null && objTags.some((t) => hoveredSet.has(t))
440
439
  const isSoftFocused = hoveredSet !== null && !isLayerHighlighted
441
-
442
- const newZIndex = isLayerHighlighted ? 10 : interactionSourceId === obj.element_id ? 1000 : 0
440
+ const versionChangeType = versionElementChanges?.get(obj.element_id)
441
+ const versionLineDelta = versionElementLineDeltas?.get(obj.element_id)
442
+ const versionPulseChangeType = versionFollowTarget?.resourceType === 'element' && versionFollowTarget.resourceId === obj.element_id
443
+ ? versionFollowTarget.changeType ?? versionChangeType
444
+ : undefined
445
+ const isDimmedByVersionPreview = versionActive && !versionChangeType
446
+
447
+ const newZIndex = versionPulseChangeType ? 20 : isLayerHighlighted ? 10 : interactionSourceId === obj.element_id ? 1000 : 0
443
448
  const newStyle = isInactive
444
449
  ? HIDDEN_STYLE
445
450
  : isSoftFocused
446
451
  ? SOFT_FOCUS_STYLE
447
- : undefined
452
+ : isDimmedByVersionPreview
453
+ ? VERSION_DIM_STYLE
454
+ : undefined
448
455
  const layerHighlightColor = isLayerHighlighted ? (hoveredLayerColor ?? undefined) : undefined
449
456
  const position = existing?.dragging ? existing.position : { x: obj.position_x ?? 0, y: obj.position_y ?? 0 }
450
457
  const isZoomHovered = hoveredZoomRef.current?.elementId === obj.element_id ? hoveredZoomRef.current.type : null
@@ -487,7 +494,9 @@ export function useViewData({
487
494
  existing.data.connectedHandleIds === connectionMeta.connectedHandleIds &&
488
495
  existing.data.selectedHandleIds === connectionMeta.selectedHandleIds &&
489
496
  existing.data.reconnectCandidates === connectionMeta.reconnectCandidates &&
490
- existing.data.isConnectorHighlighted === connectionMeta.isConnectorHighlighted
497
+ existing.data.isConnectorHighlighted === connectionMeta.isConnectorHighlighted &&
498
+ existing.data.versionChangeType === versionPulseChangeType &&
499
+ existing.data.versionLineDelta === versionLineDelta
491
500
  ) {
492
501
  return existing
493
502
  }
@@ -527,6 +536,8 @@ export function useViewData({
527
536
  selectedHandleIds: connectionMeta.selectedHandleIds,
528
537
  reconnectCandidates: connectionMeta.reconnectCandidates,
529
538
  isConnectorHighlighted: connectionMeta.isConnectorHighlighted,
539
+ versionChangeType: versionPulseChangeType,
540
+ versionLineDelta,
530
541
  },
531
542
  }
532
543
  })
@@ -537,7 +548,7 @@ export function useViewData({
537
548
  stableOnZoomIn, stableOnZoomOut, stableOnNavigateToView, stableOnSelect,
538
549
  stableOnInteractionStart, stableOnConnectTo, stableOnStartHandleReconnect, stableOnRemoveElement, stableOnHoverZoom,
539
550
  stableOnOpenCodePreview, hoveredZoomRef, activeTags, hiddenLayerTags, hoveredLayerTags, hoveredLayerColor, tagColors,
540
- nodeConnectionMetaByElementId, setRfNodes,
551
+ nodeConnectionMetaByElementId, setRfNodes, versionPreview, versionFollowTarget,
541
552
  ])
542
553
 
543
554
  // ── Derive RF connectors ────────────────────────────────────────────────────────
@@ -545,6 +556,8 @@ export function useViewData({
545
556
  const hiddenSet = hiddenLayerTags.length > 0 ? new Set(hiddenLayerTags) : null
546
557
  const activeSet = activeTags.length > 0 ? new Set(activeTags) : null
547
558
  const hoveredSet = hoveredLayerTags !== null ? new Set(hoveredLayerTags) : null
559
+ const versionConnectorChanges = versionPreview?.connectorChanges
560
+ const versionActive = !!versionPreview
548
561
 
549
562
  setRfEdges((prevConnectors) => {
550
563
  const prevEdgeMap = new Map(prevConnectors.map((e) => [e.id, e]))
@@ -572,11 +585,13 @@ export function useViewData({
572
585
  !srcTags.some((t) => hoveredSet.has(t)) ||
573
586
  !tgtTags.some((t) => hoveredSet.has(t))
574
587
  )
575
- const edgeOpacity = isInactive ? 0.1 : isSoftFocused ? 0.2 : 0.8
576
- const markerOpacity = isInactive ? 0.1 : isSoftFocused ? 0.2 : 1
588
+ const versionChangeType = versionConnectorChanges?.get(e.id)
589
+ const isDimmedByVersionPreview = versionActive && !versionChangeType
590
+ const edgeOpacity = isInactive || isDimmedByVersionPreview ? 0.1 : isSoftFocused ? 0.2 : 0.8
591
+ const markerOpacity = isInactive || isDimmedByVersionPreview ? 0.1 : isSoftFocused ? 0.2 : 1
577
592
  const newZIndex = selectedEdgeId !== null && edgeId === String(selectedEdgeId) ? 1000 : 100
578
593
  const pointerEvents = (isInactive || isSoftFocused) ? 'none' : 'auto'
579
- const labelBgOpacity = isInactive ? 0.1 : isSoftFocused ? 0.2 : 0.95
594
+ const labelBgOpacity = isInactive || isDimmedByVersionPreview ? 0.1 : isSoftFocused ? 0.2 : 0.95
580
595
 
581
596
  // Structural sharing: when all user-visible outputs match prev exactly, reuse prev ref.
582
597
  // We match on the underlying connector ref plus every computed visibility/layout value.
@@ -594,7 +609,8 @@ export function useViewData({
594
609
  (existing.data as { sourceGroupIndex?: number }).sourceGroupIndex === layout.sourceGroupIndex &&
595
610
  (existing.data as { targetGroupIndex?: number }).targetGroupIndex === layout.targetGroupIndex &&
596
611
  (existing.data as { sourceGroupCount?: number }).sourceGroupCount === layout.sourceGroupCount &&
597
- (existing.data as { targetGroupCount?: number }).targetGroupCount === layout.targetGroupCount
612
+ (existing.data as { targetGroupCount?: number }).targetGroupCount === layout.targetGroupCount &&
613
+ (existing.data as { versionChangeType?: string }).versionChangeType === versionChangeType
598
614
  ) {
599
615
  return existing
600
616
  }
@@ -620,6 +636,7 @@ export function useViewData({
620
636
  targetHandleSide: layout.targetHandleSide,
621
637
  sourceHandleSlot: layout.sourceHandleSlot,
622
638
  targetHandleSlot: layout.targetHandleSlot,
639
+ versionChangeType,
623
640
  },
624
641
 
625
642
  style: { stroke: 'var(--accent)', strokeWidth: 2, opacity: edgeOpacity, pointerEvents },
@@ -632,7 +649,7 @@ export function useViewData({
632
649
  }
633
650
  })
634
651
  })
635
- }, [connectorLayouts, selectedEdgeId, activeTags, hiddenLayerTags, hoveredLayerTags, elementMap, setRfEdges])
652
+ }, [connectorLayouts, selectedEdgeId, activeTags, hiddenLayerTags, hoveredLayerTags, elementMap, setRfEdges, versionPreview])
636
653
 
637
654
 
638
655
  // ── Boost z-index of selected connector ────────────────────────────────────────
@@ -685,6 +702,5 @@ export function useViewData({
685
702
  handleElementDeleted,
686
703
  handleElementPermanentlyDeleted,
687
704
  handleElementSaved,
688
- setAllElements,
689
705
  }
690
706
  }
@@ -0,0 +1,62 @@
1
+ import { useCallback, useState } from 'react'
2
+
3
+ const MAX_HISTORY_ITEMS = 20
4
+
5
+ export interface ViewEditHistoryAction {
6
+ undo: () => Promise<void>
7
+ redo: () => Promise<void>
8
+ }
9
+
10
+ export function useViewEditHistory() {
11
+ const [undoStack, setUndoStack] = useState<ViewEditHistoryAction[]>([])
12
+ const [redoStack, setRedoStack] = useState<ViewEditHistoryAction[]>([])
13
+ const [isApplyingHistory, setIsApplyingHistory] = useState(false)
14
+
15
+ const pushAction = useCallback((action: ViewEditHistoryAction) => {
16
+ setUndoStack((stack) => [...stack, action].slice(-MAX_HISTORY_ITEMS))
17
+ setRedoStack([])
18
+ }, [])
19
+
20
+ const clearHistory = useCallback(() => {
21
+ setUndoStack([])
22
+ setRedoStack([])
23
+ }, [])
24
+
25
+ const undo = useCallback(async () => {
26
+ if (isApplyingHistory) return
27
+ const action = undoStack[undoStack.length - 1]
28
+ if (!action) return
29
+ setIsApplyingHistory(true)
30
+ try {
31
+ await action.undo()
32
+ setUndoStack((stack) => stack.slice(0, -1))
33
+ setRedoStack((stack) => [...stack, action].slice(-MAX_HISTORY_ITEMS))
34
+ } finally {
35
+ setIsApplyingHistory(false)
36
+ }
37
+ }, [isApplyingHistory, undoStack])
38
+
39
+ const redo = useCallback(async () => {
40
+ if (isApplyingHistory) return
41
+ const action = redoStack[redoStack.length - 1]
42
+ if (!action) return
43
+ setIsApplyingHistory(true)
44
+ try {
45
+ await action.redo()
46
+ setRedoStack((stack) => stack.slice(0, -1))
47
+ setUndoStack((stack) => [...stack, action].slice(-MAX_HISTORY_ITEMS))
48
+ } finally {
49
+ setIsApplyingHistory(false)
50
+ }
51
+ }, [isApplyingHistory, redoStack])
52
+
53
+ return {
54
+ canUndo: undoStack.length > 0,
55
+ canRedo: redoStack.length > 0,
56
+ isApplyingHistory,
57
+ pushAction,
58
+ clearHistory,
59
+ undo,
60
+ redo,
61
+ }
62
+ }