@tldiagram/core-ui 1.95.1 → 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.
- package/dist/api/client.d.ts +184 -3
- package/dist/components/ConnectorPanel.d.ts +5 -1
- package/dist/components/CrossBranchControls.d.ts +4 -3
- package/dist/components/ElementNode.d.ts +5 -0
- package/dist/components/ElementPanel.d.ts +6 -1
- package/dist/components/LayoutSection.d.ts +2 -1
- package/dist/components/MergeDialog.d.ts +16 -0
- package/dist/components/NodeContainer.d.ts +2 -0
- package/dist/components/ProxyConnectorPanel.d.ts +4 -1
- package/dist/components/ViewExplorer/index.d.ts +1 -1
- package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
- package/dist/components/ViewFloatingMenu.d.ts +8 -1
- package/dist/components/ViewGridNode.d.ts +3 -0
- package/dist/components/ViewPanel.d.ts +2 -1
- package/dist/components/WorkspacePanel.d.ts +2 -0
- package/dist/components/ZUI/ZUICanvas.d.ts +4 -0
- package/dist/components/ZUI/focus.d.ts +32 -0
- package/dist/components/ZUI/focus.test.d.ts +1 -0
- package/dist/components/ZUI/layout.d.ts +2 -2
- package/dist/components/ZUI/proxy.d.ts +20 -4
- package/dist/components/ZUI/renderer.d.ts +35 -1
- package/dist/components/ZUI/types.d.ts +6 -0
- package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
- package/dist/context/WorkspaceVersionContext.d.ts +49 -0
- package/dist/crossBranch/resolve.d.ts +39 -2
- package/dist/crossBranch/resolve.test.d.ts +1 -0
- package/dist/crossBranch/settings.d.ts +6 -1
- package/dist/crossBranch/types.d.ts +8 -0
- package/dist/hooks/useElementSearch.d.ts +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16529 -14030
- package/dist/pages/InfiniteZoom.d.ts +1 -0
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
- package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
- package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
- package/dist/pages/viewsJumpSearch.d.ts +22 -0
- package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
- package/dist/store/useStore.d.ts +3 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/elementIcon.d.ts +2 -0
- package/dist/utils/elementIcon.test.d.ts +1 -0
- package/dist/utils/sourceEditor.d.ts +7 -0
- package/dist/utils/watchDiffSummary.d.ts +34 -0
- package/package.json +2 -2
- package/src/App.tsx +12 -8
- package/src/api/client.ts +488 -26
- package/src/components/CodePreviewPanel.tsx +90 -16
- package/src/components/ConnectorPanel.tsx +34 -3
- package/src/components/ContextNeighborElement.tsx +2 -5
- package/src/components/CrossBranchControls.tsx +46 -17
- package/src/components/ElementNode.tsx +98 -47
- package/src/components/ElementPanel.tsx +62 -25
- package/src/components/InlineElementAdder.tsx +8 -3
- package/src/components/LayoutSection.tsx +4 -1
- package/src/components/MergeDialog.tsx +269 -0
- package/src/components/NodeContainer.tsx +55 -17
- package/src/components/ProxyConnectorPanel.tsx +58 -16
- package/src/components/ViewBezierConnector.tsx +116 -21
- package/src/components/ViewExplorer/index.tsx +1 -1
- package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
- package/src/components/ViewFloatingMenu.tsx +110 -1
- package/src/components/ViewGridNode.tsx +59 -8
- package/src/components/ViewPanel.tsx +3 -2
- package/src/components/WorkspacePanel.tsx +938 -0
- package/src/components/ZUI/ZUICanvas.tsx +216 -122
- package/src/components/ZUI/focus.test.ts +534 -0
- package/src/components/ZUI/focus.ts +293 -0
- package/src/components/ZUI/layout.ts +7 -11
- package/src/components/ZUI/proxy.ts +470 -114
- package/src/components/ZUI/renderer.ts +510 -134
- package/src/components/ZUI/types.ts +6 -0
- package/src/components/ZUI/useZUIInteraction.ts +66 -29
- package/src/context/WorkspaceVersionContext.tsx +126 -0
- package/src/crossBranch/resolve.test.ts +342 -0
- package/src/crossBranch/resolve.ts +368 -68
- package/src/crossBranch/settings.ts +49 -3
- package/src/crossBranch/types.ts +9 -0
- package/src/hooks/useElementSearch.ts +45 -0
- package/src/index.css +11 -0
- package/src/index.ts +7 -0
- package/src/pages/AppearanceSettings.tsx +24 -1
- package/src/pages/Dependencies.tsx +231 -65
- package/src/pages/InfiniteZoom.tsx +41 -19
- package/src/pages/Settings.tsx +1 -1
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
- package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
- package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
- package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
- package/src/pages/ViewEditor/index.tsx +549 -59
- package/src/pages/Views.tsx +112 -41
- package/src/pages/ViewsGrid.tsx +332 -113
- package/src/pages/viewsJumpSearch.test.ts +193 -0
- package/src/pages/viewsJumpSearch.ts +111 -0
- package/src/store/useStore.ts +58 -0
- package/src/types/index.ts +10 -0
- package/src/utils/elementIcon.test.ts +28 -0
- package/src/utils/elementIcon.ts +20 -0
- package/src/utils/sourceEditor.ts +46 -0
- package/src/utils/watchDiffSummary.ts +159 -0
package/src/pages/ViewsGrid.tsx
CHANGED
|
@@ -54,57 +54,59 @@ function flattenTree(roots: ViewTreeNode[]): ViewTreeNode[] {
|
|
|
54
54
|
return result
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function filterTreeForGrid(nodes: ViewTreeNode[], allowedIds: Set<number> | null): ViewTreeNode[] {
|
|
58
|
+
if (!allowedIds) return nodes
|
|
59
|
+
|
|
60
|
+
const visit = (node: ViewTreeNode): ViewTreeNode | null => {
|
|
61
|
+
const children = node.children
|
|
62
|
+
.map(visit)
|
|
63
|
+
.filter((child): child is ViewTreeNode => child !== null)
|
|
64
|
+
const include = allowedIds.has(node.id) || (node.parent_view_id === null && children.length > 0)
|
|
65
|
+
if (!include) return null
|
|
66
|
+
return { ...node, children }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return nodes
|
|
70
|
+
.map(visit)
|
|
71
|
+
.filter((node): node is ViewTreeNode => node !== null)
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
// ── Layout algorithm ──────────────────────────────────────────────────────────
|
|
58
75
|
|
|
59
76
|
const CELL_W = 260
|
|
60
77
|
const CELL_H = 150
|
|
61
78
|
const GAP_H = 80
|
|
62
79
|
const GAP_V = 120
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
const COMPACT_WORKSPACE_THRESHOLD = 32
|
|
81
|
+
const LAYOUT_TRANSITION = 'transform 560ms cubic-bezier(0.16, 1, 0.3, 1), opacity 260ms ease, filter 260ms ease'
|
|
82
|
+
|
|
83
|
+
interface GridDisplayNode {
|
|
84
|
+
id: string
|
|
85
|
+
kind: 'view' | 'cluster'
|
|
86
|
+
view: ViewTreeNode
|
|
87
|
+
parentId: string | null
|
|
88
|
+
depth: number
|
|
89
|
+
children: GridDisplayNode[]
|
|
90
|
+
collapsedCount?: number
|
|
91
|
+
dimmed?: boolean
|
|
67
92
|
}
|
|
68
93
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
function visit(node: ViewTreeNode): Set<number> {
|
|
73
|
-
const set = new Set([node.id])
|
|
74
|
-
node.children.forEach((child) => {
|
|
75
|
-
const childSet = visit(child)
|
|
76
|
-
childSet.forEach((id) => set.add(id))
|
|
77
|
-
})
|
|
78
|
-
map.set(node.id, set)
|
|
79
|
-
return set
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
roots.forEach(visit)
|
|
83
|
-
return map
|
|
94
|
+
function displaySubtreeWidth(node: GridDisplayNode): number {
|
|
95
|
+
if (node.children.length === 0) return 1
|
|
96
|
+
return node.children.reduce((sum, child) => sum + displaySubtreeWidth(child), 0)
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
/**
|
|
87
|
-
* Compute layout positions.
|
|
88
|
-
*
|
|
89
|
-
* Y-axis: node.depth (= node.level) - honours manual level overrides so a
|
|
90
|
-
* diagram at L2 is always rendered in the L2 row even if its parent
|
|
91
|
-
* is at L0.
|
|
92
|
-
*
|
|
93
|
-
* X-axis: column derived from the tree-walk (pre-order rank within each
|
|
94
|
-
* level band), then a de-overlap pass shifts any colliding nodes and
|
|
95
|
-
* their subtrees rightward so nothing overlaps on the same row.
|
|
100
|
+
* Compute layout positions for real cards plus collapsed cluster cards.
|
|
101
|
+
* Y follows view depth so manual level overrides still read as horizontal bands.
|
|
96
102
|
*/
|
|
97
|
-
function
|
|
98
|
-
const positions = new Map<
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
roots.forEach(visit)
|
|
102
|
-
|
|
103
|
-
if (flat.length === 0) return positions
|
|
103
|
+
function computeDisplayLayout(roots: GridDisplayNode[]): Map<string, { x: number; y: number }> {
|
|
104
|
+
const positions = new Map<string, { x: number; y: number }>()
|
|
105
|
+
if (roots.length === 0) return positions
|
|
106
|
+
const flat = flattenDisplayTree(roots)
|
|
104
107
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const w = subtreeWidth(node)
|
|
108
|
+
function layoutNode(node: GridDisplayNode, startCol: number) {
|
|
109
|
+
const w = displaySubtreeWidth(node)
|
|
108
110
|
const centerCol = startCol + (w - 1) / 2
|
|
109
111
|
positions.set(node.id, {
|
|
110
112
|
x: centerCol * (CELL_W + GAP_H),
|
|
@@ -113,41 +115,44 @@ function computeLayout(roots: ViewTreeNode[]): Map<number, { x: number; y: numbe
|
|
|
113
115
|
let childStart = startCol
|
|
114
116
|
for (const child of node.children) {
|
|
115
117
|
layoutNode(child, childStart)
|
|
116
|
-
childStart +=
|
|
118
|
+
childStart += displaySubtreeWidth(child)
|
|
117
119
|
}
|
|
118
120
|
}
|
|
121
|
+
|
|
119
122
|
let col = 0
|
|
120
123
|
for (const root of roots) {
|
|
121
124
|
layoutNode(root, col)
|
|
122
|
-
col +=
|
|
125
|
+
col += displaySubtreeWidth(root)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const descendants = new Map<string, Set<string>>()
|
|
129
|
+
const collectDescendants = (node: GridDisplayNode): Set<string> => {
|
|
130
|
+
const set = new Set([node.id])
|
|
131
|
+
node.children.forEach((child) => {
|
|
132
|
+
collectDescendants(child).forEach((id) => set.add(id))
|
|
133
|
+
})
|
|
134
|
+
descendants.set(node.id, set)
|
|
135
|
+
return set
|
|
123
136
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const byY = new Map<number, number[]>()
|
|
130
|
-
flat.forEach((n) => {
|
|
131
|
-
const y = n.depth * (CELL_H + GAP_V)
|
|
137
|
+
roots.forEach(collectDescendants)
|
|
138
|
+
|
|
139
|
+
const byY = new Map<number, string[]>()
|
|
140
|
+
flat.forEach((node) => {
|
|
141
|
+
const y = node.depth * (CELL_H + GAP_V)
|
|
132
142
|
if (!byY.has(y)) byY.set(y, [])
|
|
133
|
-
byY.get(y)!.push(
|
|
143
|
+
byY.get(y)!.push(node.id)
|
|
134
144
|
})
|
|
135
145
|
|
|
136
|
-
// Process rows top-down (ascending Y) so parent shifts propagate downward first
|
|
137
146
|
const sortedYRows = Array.from(byY.entries()).sort(([ya], [yb]) => ya - yb)
|
|
138
|
-
|
|
147
|
+
const step = CELL_W + GAP_H
|
|
139
148
|
for (const [rowY, ids] of sortedYRows) {
|
|
140
|
-
|
|
141
|
-
// this prevents a just-shifted node's new position from cascading
|
|
142
|
-
// into the next comparison and wrongly pushing correct neighbors right.
|
|
143
|
-
const origX = new Map<number, number>(ids.map((id) => [id, positions.get(id)?.x ?? 0]))
|
|
149
|
+
const origX = new Map<string, number>(ids.map((id) => [id, positions.get(id)?.x ?? 0]))
|
|
144
150
|
ids.sort((a, b) => (origX.get(a) ?? 0) - (origX.get(b) ?? 0))
|
|
145
|
-
|
|
146
151
|
let rightmostX = origX.get(ids[0]) ?? 0
|
|
147
152
|
|
|
148
153
|
for (let i = 1; i < ids.length; i++) {
|
|
149
154
|
const originalX = origX.get(ids[i]) ?? 0
|
|
150
|
-
const placedX = Math.max(originalX, rightmostX +
|
|
155
|
+
const placedX = Math.max(originalX, rightmostX + step)
|
|
151
156
|
|
|
152
157
|
if (placedX > originalX) {
|
|
153
158
|
const delta = placedX - originalX
|
|
@@ -167,6 +172,150 @@ function computeLayout(roots: ViewTreeNode[]): Map<number, { x: number; y: numbe
|
|
|
167
172
|
return positions
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
function countDescendantViews(node: ViewTreeNode): number {
|
|
176
|
+
return node.children.reduce((sum, child) => sum + 1 + countDescendantViews(child), 0)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function flattenDisplayTree(roots: GridDisplayNode[]): GridDisplayNode[] {
|
|
180
|
+
const result: GridDisplayNode[] = []
|
|
181
|
+
const visit = (node: GridDisplayNode) => {
|
|
182
|
+
result.push(node)
|
|
183
|
+
node.children.forEach(visit)
|
|
184
|
+
}
|
|
185
|
+
roots.forEach(visit)
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sumContentCounts(
|
|
190
|
+
nodes: ViewTreeNode[],
|
|
191
|
+
countsByView: Record<number, { nodes: number; edges: number }>
|
|
192
|
+
): { nodes: number; edges: number } {
|
|
193
|
+
let nodesCount = 0
|
|
194
|
+
let edgesCount = 0
|
|
195
|
+
|
|
196
|
+
const visit = (node: ViewTreeNode) => {
|
|
197
|
+
const counts = countsByView[node.id]
|
|
198
|
+
nodesCount += counts?.nodes ?? 0
|
|
199
|
+
edgesCount += counts?.edges ?? 0
|
|
200
|
+
node.children.forEach(visit)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
nodes.forEach(visit)
|
|
204
|
+
return { nodes: nodesCount, edges: edgesCount }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildDisplayTree(roots: ViewTreeNode[], focusedId: number | null): GridDisplayNode[] {
|
|
208
|
+
const flat = flattenTree(roots)
|
|
209
|
+
if (flat.length <= COMPACT_WORKSPACE_THRESHOLD) {
|
|
210
|
+
const convert = (node: ViewTreeNode, parentId: string | null): GridDisplayNode => ({
|
|
211
|
+
id: String(node.id),
|
|
212
|
+
kind: 'view',
|
|
213
|
+
view: node,
|
|
214
|
+
parentId,
|
|
215
|
+
depth: node.depth,
|
|
216
|
+
children: node.children.map((child) => convert(child, String(node.id))),
|
|
217
|
+
})
|
|
218
|
+
return roots.map((root) => convert(root, null))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const byId = new Map(flat.map((node) => [node.id, node]))
|
|
222
|
+
const focusedNode = focusedId ? byId.get(focusedId) ?? null : null
|
|
223
|
+
const visible = new Set<number>()
|
|
224
|
+
const emphasis = new Set<number>()
|
|
225
|
+
|
|
226
|
+
if (!focusedNode) {
|
|
227
|
+
flat.forEach((node) => {
|
|
228
|
+
if (node.parent_view_id === null || node.depth <= 1) visible.add(node.id)
|
|
229
|
+
})
|
|
230
|
+
} else {
|
|
231
|
+
let cursor: ViewTreeNode | undefined = focusedNode
|
|
232
|
+
while (cursor) {
|
|
233
|
+
visible.add(cursor.id)
|
|
234
|
+
emphasis.add(cursor.id)
|
|
235
|
+
cursor = cursor.parent_view_id ? byId.get(cursor.parent_view_id) : undefined
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
roots.forEach((root) => visible.add(root.id))
|
|
239
|
+
|
|
240
|
+
const parent = focusedNode.parent_view_id ? byId.get(focusedNode.parent_view_id) : null
|
|
241
|
+
const siblings = flat.filter((node) => node.parent_view_id === focusedNode.parent_view_id)
|
|
242
|
+
siblings.forEach((node) => {
|
|
243
|
+
visible.add(node.id)
|
|
244
|
+
emphasis.add(node.id)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
focusedNode.children.forEach((child) => {
|
|
248
|
+
visible.add(child.id)
|
|
249
|
+
emphasis.add(child.id)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
parent?.children.forEach((child) => visible.add(child.id))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const makeNode = (node: ViewTreeNode, parentId: string | null): GridDisplayNode | null => {
|
|
256
|
+
if (!visible.has(node.id)) return null
|
|
257
|
+
|
|
258
|
+
const displayId = String(node.id)
|
|
259
|
+
const visibleChildren = node.children
|
|
260
|
+
.map((child) => makeNode(child, displayId))
|
|
261
|
+
.filter((child): child is GridDisplayNode => child !== null)
|
|
262
|
+
|
|
263
|
+
const hiddenChildren = node.children.filter((child) => !visible.has(child.id))
|
|
264
|
+
const hiddenCount = hiddenChildren.reduce((sum, child) => sum + 1 + countDescendantViews(child), 0)
|
|
265
|
+
const cluster: GridDisplayNode[] = hiddenCount > 0 ? [{
|
|
266
|
+
id: `${node.id}:cluster`,
|
|
267
|
+
kind: 'cluster',
|
|
268
|
+
view: node,
|
|
269
|
+
parentId: displayId,
|
|
270
|
+
depth: node.depth + 1,
|
|
271
|
+
children: [],
|
|
272
|
+
collapsedCount: hiddenCount,
|
|
273
|
+
dimmed: focusedNode ? !emphasis.has(node.id) : false,
|
|
274
|
+
}] : []
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
id: displayId,
|
|
278
|
+
kind: 'view',
|
|
279
|
+
view: node,
|
|
280
|
+
parentId,
|
|
281
|
+
depth: node.depth,
|
|
282
|
+
children: [...visibleChildren, ...cluster],
|
|
283
|
+
dimmed: focusedNode ? !emphasis.has(node.id) : false,
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return roots
|
|
288
|
+
.map((root) => makeNode(root, null))
|
|
289
|
+
.filter((node): node is GridDisplayNode => node !== null)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function buildStableLayoutIds(flat: ViewTreeNode[], focusedId: number | null): Set<string> {
|
|
293
|
+
const stable = new Set<string>()
|
|
294
|
+
const byId = new Map(flat.map((node) => [node.id, node]))
|
|
295
|
+
|
|
296
|
+
if (focusedId === null) {
|
|
297
|
+
flat.forEach((node) => {
|
|
298
|
+
if (node.parent_view_id === null || node.depth <= 1) stable.add(String(node.id))
|
|
299
|
+
})
|
|
300
|
+
return stable
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const focused = byId.get(focusedId)
|
|
304
|
+
if (!focused) return stable
|
|
305
|
+
|
|
306
|
+
let cursor: ViewTreeNode | undefined = focused
|
|
307
|
+
while (cursor) {
|
|
308
|
+
stable.add(String(cursor.id))
|
|
309
|
+
cursor = cursor.parent_view_id ? byId.get(cursor.parent_view_id) : undefined
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
flat.forEach((node) => {
|
|
313
|
+
if (node.parent_view_id === focused.parent_view_id) stable.add(String(node.id))
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return stable
|
|
317
|
+
}
|
|
318
|
+
|
|
170
319
|
|
|
171
320
|
|
|
172
321
|
|
|
@@ -387,7 +536,8 @@ function ViewGridInner({ onShare, treeData, loading, focusedId, onFocusChange, s
|
|
|
387
536
|
}, [zoomIn, zoomOut])
|
|
388
537
|
|
|
389
538
|
// ── Derived tree structures ─────────────────────────────────────────────────
|
|
390
|
-
const
|
|
539
|
+
const [gridViewIds, setGridViewIds] = useState<Set<number> | null>(null)
|
|
540
|
+
const roots = useMemo(() => filterTreeForGrid(treeData, gridViewIds), [treeData, gridViewIds])
|
|
391
541
|
const flatTree = useMemo(() => flattenTree(roots), [roots])
|
|
392
542
|
|
|
393
543
|
// Rename
|
|
@@ -422,28 +572,41 @@ function ViewGridInner({ onShare, treeData, loading, focusedId, onFocusChange, s
|
|
|
422
572
|
}
|
|
423
573
|
}, [loading, treeData.length])
|
|
424
574
|
|
|
425
|
-
// Fetch node/edge counts
|
|
575
|
+
// Fetch visible grid cards and node/edge counts in one workspace roundtrip.
|
|
426
576
|
useEffect(() => {
|
|
427
577
|
let cancelled = false
|
|
428
|
-
|
|
429
|
-
|
|
578
|
+
if (treeData.length === 0) {
|
|
579
|
+
setGridViewIds(new Set())
|
|
580
|
+
setCountsByDiagram({})
|
|
581
|
+
return
|
|
582
|
+
}
|
|
430
583
|
; (async () => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
584
|
+
try {
|
|
585
|
+
const workspace = await api.workspace.views.gridData()
|
|
586
|
+
if (cancelled) return
|
|
587
|
+
|
|
588
|
+
const visibleIds = new Set(flattenTree(workspace.views).map((view) => view.id))
|
|
589
|
+
const next: Record<number, { nodes: number; edges: number }> = {}
|
|
590
|
+
|
|
591
|
+
visibleIds.forEach((id) => {
|
|
592
|
+
const content = workspace.content[id]
|
|
593
|
+
next[id] = {
|
|
594
|
+
nodes: content?.placements.length ?? 0,
|
|
595
|
+
edges: content?.connectors.length ?? 0,
|
|
596
|
+
}
|
|
441
597
|
})
|
|
442
|
-
|
|
443
|
-
|
|
598
|
+
|
|
599
|
+
setGridViewIds(visibleIds)
|
|
600
|
+
setCountsByDiagram(next)
|
|
601
|
+
} catch {
|
|
602
|
+
if (!cancelled) {
|
|
603
|
+
setGridViewIds(null)
|
|
604
|
+
setCountsByDiagram({})
|
|
605
|
+
}
|
|
606
|
+
}
|
|
444
607
|
})()
|
|
445
608
|
return () => { cancelled = true }
|
|
446
|
-
}, [
|
|
609
|
+
}, [treeData])
|
|
447
610
|
|
|
448
611
|
// ── Rename ──────────────────────────────────────────────────────────────────
|
|
449
612
|
const startEdit = useCallback((id: number, name: string) => {
|
|
@@ -545,7 +708,49 @@ function ViewGridInner({ onShare, treeData, loading, focusedId, onFocusChange, s
|
|
|
545
708
|
}
|
|
546
709
|
|
|
547
710
|
// ── RF nodes - pure derivation, no useState/useEffect ───────────────────────
|
|
548
|
-
const
|
|
711
|
+
const displayTree = useMemo(
|
|
712
|
+
() => buildDisplayTree(roots, focusedId),
|
|
713
|
+
[roots, focusedId]
|
|
714
|
+
)
|
|
715
|
+
const displayFlat = useMemo(() => flattenDisplayTree(displayTree), [displayTree])
|
|
716
|
+
const rawLayoutPositions = useMemo(() => computeDisplayLayout(displayTree), [displayTree])
|
|
717
|
+
const stableLayoutIds = useMemo(() => buildStableLayoutIds(flatTree, focusedId), [flatTree, focusedId])
|
|
718
|
+
const previousLayoutRef = useRef<Map<string, { x: number; y: number }>>(new Map())
|
|
719
|
+
|
|
720
|
+
const layoutPositions = useMemo(() => {
|
|
721
|
+
const next = new Map(rawLayoutPositions)
|
|
722
|
+
const previousLayout = previousLayoutRef.current
|
|
723
|
+
|
|
724
|
+
if (focusedId !== null) {
|
|
725
|
+
const focusedKey = String(focusedId)
|
|
726
|
+
const previousFocusedPosition = previousLayout.get(focusedKey)
|
|
727
|
+
const nextFocusedPosition = next.get(focusedKey)
|
|
728
|
+
|
|
729
|
+
if (previousFocusedPosition && nextFocusedPosition) {
|
|
730
|
+
const dx = previousFocusedPosition.x - nextFocusedPosition.x
|
|
731
|
+
const dy = previousFocusedPosition.y - nextFocusedPosition.y
|
|
732
|
+
|
|
733
|
+
if (dx !== 0 || dy !== 0) {
|
|
734
|
+
next.forEach((position, id) => {
|
|
735
|
+
next.set(id, { x: position.x + dx, y: position.y + dy })
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
stableLayoutIds.forEach((id) => {
|
|
742
|
+
const previousPosition = previousLayout.get(id)
|
|
743
|
+
if (previousPosition && next.has(id)) {
|
|
744
|
+
next.set(id, previousPosition)
|
|
745
|
+
}
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
return next
|
|
749
|
+
}, [rawLayoutPositions, focusedId, stableLayoutIds])
|
|
750
|
+
|
|
751
|
+
useEffect(() => {
|
|
752
|
+
previousLayoutRef.current = layoutPositions
|
|
753
|
+
}, [layoutPositions])
|
|
549
754
|
|
|
550
755
|
// Stable during drag (layoutPositions only changes after treeData refresh, never on mouse moves)
|
|
551
756
|
const computedMinZoom = useMemo(() => {
|
|
@@ -599,35 +804,50 @@ function ViewGridInner({ onShare, treeData, loading, focusedId, onFocusChange, s
|
|
|
599
804
|
}, [focusedId, flatTree])
|
|
600
805
|
|
|
601
806
|
const rfNodes = useMemo((): RFNode[] =>
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
807
|
+
displayFlat.map((displayNode): RFNode => {
|
|
808
|
+
const n = displayNode.view
|
|
809
|
+
const isCluster = displayNode.kind === 'cluster'
|
|
810
|
+
const hiddenChildren = isCluster
|
|
811
|
+
? n.children.filter((child) => !displayFlat.some((visibleNode) => visibleNode.kind === 'view' && visibleNode.view.id === child.id))
|
|
812
|
+
: []
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
id: displayNode.id,
|
|
816
|
+
type: 'diagramGrid',
|
|
817
|
+
position: layoutPositions.get(displayNode.id) ?? { x: 0, y: 0 },
|
|
818
|
+
data: {
|
|
819
|
+
id: n.id,
|
|
820
|
+
name: isCluster ? `${n.name} descendants` : n.name,
|
|
821
|
+
level_label: isCluster ? 'Collapsed stack' : n.level_label,
|
|
822
|
+
counts: isCluster ? sumContentCounts(hiddenChildren, countsByView) : countsByView[n.id],
|
|
823
|
+
kind: displayNode.kind,
|
|
824
|
+
collapsedCount: displayNode.collapsedCount,
|
|
825
|
+
dimmed: displayNode.dimmed,
|
|
826
|
+
focused: !isCluster && focusedId === n.id,
|
|
827
|
+
canEdit: !isCluster && canEdit,
|
|
828
|
+
isEditing: !isCluster && editingId === n.id,
|
|
829
|
+
editName,
|
|
830
|
+
onFocus: () => onFocusChange(n.id),
|
|
831
|
+
onOpen: () => isCluster ? onFocusChange(n.id) : navigate(`/views/${n.id}`),
|
|
832
|
+
onStartRename: () => startEdit(n.id, n.name),
|
|
833
|
+
onDetails: () => handleDetailsOpen(n.id),
|
|
834
|
+
onDelete: () => { setDeleteTargetId(n.id); onDeleteOpen() },
|
|
835
|
+
onShare: onShare ? () => onShare(n.id) : () => {},
|
|
836
|
+
onEditNameChange: setEditName,
|
|
837
|
+
onEditCommit: commitEdit,
|
|
838
|
+
onEditCancel: cancelEdit,
|
|
839
|
+
isMobile: isMobileLayout,
|
|
840
|
+
wasdKey: isCluster ? undefined : wasdTargets[n.id],
|
|
841
|
+
} satisfies ViewGridNodeData,
|
|
842
|
+
draggable: false,
|
|
843
|
+
style: {
|
|
844
|
+
transition: LAYOUT_TRANSITION,
|
|
845
|
+
zIndex: displayNode.kind === 'cluster' ? 1 : focusedId === n.id ? 3 : 2,
|
|
846
|
+
},
|
|
847
|
+
}
|
|
848
|
+
}),
|
|
629
849
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
630
|
-
[
|
|
850
|
+
[displayFlat, layoutPositions, focusedId, countsByView,
|
|
631
851
|
editingId, editName, canEdit, navigate, startEdit, handleDetailsOpen,
|
|
632
852
|
commitEdit, cancelEdit, onDeleteOpen,
|
|
633
853
|
wasdTargets, levelEditingNodeId]
|
|
@@ -674,17 +894,17 @@ function ViewGridInner({ onShare, treeData, loading, focusedId, onFocusChange, s
|
|
|
674
894
|
|
|
675
895
|
// ── RF edges ────────────────────────────────────────────────────────────────
|
|
676
896
|
const rfEdges = useMemo((): RFEdge[] =>
|
|
677
|
-
|
|
678
|
-
.filter((n) => n.
|
|
897
|
+
displayFlat
|
|
898
|
+
.filter((n) => n.parentId)
|
|
679
899
|
.map((n) => ({
|
|
680
|
-
id: `${n.
|
|
681
|
-
source:
|
|
682
|
-
target:
|
|
900
|
+
id: `${n.parentId}-${n.id}`,
|
|
901
|
+
source: n.parentId!,
|
|
902
|
+
target: n.id,
|
|
683
903
|
type: 'floating',
|
|
684
904
|
animated: false,
|
|
685
|
-
data: { color: HIERARCHY_EDGE_COLOR, dashed:
|
|
905
|
+
data: { color: n.kind === 'cluster' ? hexToRgba(accent, 0.28) : HIERARCHY_EDGE_COLOR, dashed: n.kind === 'cluster' },
|
|
686
906
|
})),
|
|
687
|
-
[
|
|
907
|
+
[displayFlat, accent]
|
|
688
908
|
)
|
|
689
909
|
|
|
690
910
|
const allRfEdges = rfEdges
|
|
@@ -743,7 +963,7 @@ function ViewGridInner({ onShare, treeData, loading, focusedId, onFocusChange, s
|
|
|
743
963
|
// ── Camera: pan to focused node only when it's out of view ──────────────────
|
|
744
964
|
useEffect(() => {
|
|
745
965
|
if (!focusedId) return
|
|
746
|
-
const pos = layoutPositions.get(focusedId)
|
|
966
|
+
const pos = layoutPositions.get(String(focusedId))
|
|
747
967
|
if (!pos) return
|
|
748
968
|
const t = setTimeout(() => {
|
|
749
969
|
const { x: vpX, y: vpY, zoom } = getViewport()
|
|
@@ -846,8 +1066,7 @@ function ViewGridInner({ onShare, treeData, loading, focusedId, onFocusChange, s
|
|
|
846
1066
|
onFocusChange(null)
|
|
847
1067
|
}}
|
|
848
1068
|
style={{
|
|
849
|
-
background: 'var(--bg-canvas)'
|
|
850
|
-
boxShadow: 'inset 0 0 100px rgba(0,0,0,0.6)'
|
|
1069
|
+
background: 'var(--bg-canvas)'
|
|
851
1070
|
}}
|
|
852
1071
|
>
|
|
853
1072
|
{/* Micro dots for high precision technical feel */}
|