gitmaps 1.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 (121) hide show
  1. package/README.md +167 -0
  2. package/app/api/auth/favorites/route.ts +56 -0
  3. package/app/api/auth/github/callback/route.ts +103 -0
  4. package/app/api/auth/github/route.ts +32 -0
  5. package/app/api/auth/me/route.ts +52 -0
  6. package/app/api/auth/positions/route.ts +50 -0
  7. package/app/api/chat/route.ts +101 -0
  8. package/app/api/connections/route.ts +72 -0
  9. package/app/api/github/repos/route.ts +111 -0
  10. package/app/api/positions/route.ts +80 -0
  11. package/app/api/repo/branch-diff/route.ts +201 -0
  12. package/app/api/repo/branches/route.ts +53 -0
  13. package/app/api/repo/browse/route.ts +55 -0
  14. package/app/api/repo/clone/route.ts +78 -0
  15. package/app/api/repo/clone-stream/route.ts +131 -0
  16. package/app/api/repo/file-content/route.ts +28 -0
  17. package/app/api/repo/file-delete/route.ts +62 -0
  18. package/app/api/repo/file-history/route.ts +45 -0
  19. package/app/api/repo/file-rename/route.ts +83 -0
  20. package/app/api/repo/file-save/route.ts +45 -0
  21. package/app/api/repo/files/route.ts +169 -0
  22. package/app/api/repo/git-blame/route.ts +86 -0
  23. package/app/api/repo/git-commit/route.ts +40 -0
  24. package/app/api/repo/git-heatmap/route.ts +55 -0
  25. package/app/api/repo/imports/route.ts +154 -0
  26. package/app/api/repo/load/route.ts +56 -0
  27. package/app/api/repo/mode/route.ts +14 -0
  28. package/app/api/repo/search/route.ts +127 -0
  29. package/app/api/repo/tree/route.ts +104 -0
  30. package/app/api/repo/upload/route.ts +53 -0
  31. package/app/api/repo/validate-path.ts +53 -0
  32. package/app/canvas_users.db +0 -0
  33. package/app/canvas_users.db-shm +0 -0
  34. package/app/canvas_users.db-wal +0 -0
  35. package/app/globals.css +7899 -0
  36. package/app/layout.tsx +493 -0
  37. package/app/lib/auth.ts +193 -0
  38. package/app/lib/auto-save.ts +137 -0
  39. package/app/lib/branch-compare.ts +443 -0
  40. package/app/lib/breadcrumbs.ts +170 -0
  41. package/app/lib/canvas-export.ts +358 -0
  42. package/app/lib/canvas-text.ts +912 -0
  43. package/app/lib/canvas.ts +564 -0
  44. package/app/lib/card-arrangement.ts +188 -0
  45. package/app/lib/card-context-menu.tsx +453 -0
  46. package/app/lib/card-diff-markers.ts +270 -0
  47. package/app/lib/card-expand.ts +189 -0
  48. package/app/lib/card-groups.ts +246 -0
  49. package/app/lib/cards.tsx +914 -0
  50. package/app/lib/chat.tsx +308 -0
  51. package/app/lib/code-editor.ts +508 -0
  52. package/app/lib/command-palette.ts +262 -0
  53. package/app/lib/connections.tsx +1037 -0
  54. package/app/lib/context.ts +94 -0
  55. package/app/lib/cursor-sharing.ts +281 -0
  56. package/app/lib/dependency-graph.ts +438 -0
  57. package/app/lib/events.tsx +1747 -0
  58. package/app/lib/file-card-plugin.ts +134 -0
  59. package/app/lib/file-modal.tsx +849 -0
  60. package/app/lib/file-preview.ts +400 -0
  61. package/app/lib/file-tabs.ts +318 -0
  62. package/app/lib/galaxydraw-bridge.ts +477 -0
  63. package/app/lib/galaxydraw.test.ts +229 -0
  64. package/app/lib/global-search.ts +264 -0
  65. package/app/lib/goto-definition.ts +224 -0
  66. package/app/lib/heatmap.ts +178 -0
  67. package/app/lib/hidden-files.tsx +222 -0
  68. package/app/lib/layers.ts +0 -0
  69. package/app/lib/layers.tsx +365 -0
  70. package/app/lib/loading.tsx +45 -0
  71. package/app/lib/multi-repo.ts +286 -0
  72. package/app/lib/new-file-dialog.tsx +230 -0
  73. package/app/lib/onboarding.tsx +213 -0
  74. package/app/lib/perf-overlay.ts +360 -0
  75. package/app/lib/positions.ts +176 -0
  76. package/app/lib/pr-review.ts +374 -0
  77. package/app/lib/production-mode.ts +47 -0
  78. package/app/lib/repo.tsx +977 -0
  79. package/app/lib/settings-modal.tsx +374 -0
  80. package/app/lib/settings.ts +97 -0
  81. package/app/lib/shortcuts-panel.ts +141 -0
  82. package/app/lib/status-bar.ts +128 -0
  83. package/app/lib/symbol-outline.ts +212 -0
  84. package/app/lib/syntax.ts +177 -0
  85. package/app/lib/tab-diff.ts +238 -0
  86. package/app/lib/user.tsx +133 -0
  87. package/app/lib/utils.ts +78 -0
  88. package/app/lib/viewport-culling.ts +728 -0
  89. package/app/page.client.tsx +215 -0
  90. package/app/page.tsx +291 -0
  91. package/app/state/machine.js +196 -0
  92. package/app/styles/main.css +2168 -0
  93. package/banner.png +0 -0
  94. package/cli.ts +44 -0
  95. package/package.json +75 -0
  96. package/packages/galaxydraw/README.md +296 -0
  97. package/packages/galaxydraw/banner.png +0 -0
  98. package/packages/galaxydraw/demo/build-static.ts +100 -0
  99. package/packages/galaxydraw/demo/client.ts +154 -0
  100. package/packages/galaxydraw/demo/dist/client.js +8 -0
  101. package/packages/galaxydraw/demo/index.html +256 -0
  102. package/packages/galaxydraw/demo/server.ts +96 -0
  103. package/packages/galaxydraw/dist/index.js +984 -0
  104. package/packages/galaxydraw/dist/index.js.map +16 -0
  105. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  106. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  109. package/packages/galaxydraw/package.json +49 -0
  110. package/packages/galaxydraw/perf.test.ts +284 -0
  111. package/packages/galaxydraw/src/core/cards.ts +435 -0
  112. package/packages/galaxydraw/src/core/engine.ts +339 -0
  113. package/packages/galaxydraw/src/core/events.ts +81 -0
  114. package/packages/galaxydraw/src/core/layout.ts +136 -0
  115. package/packages/galaxydraw/src/core/minimap.ts +216 -0
  116. package/packages/galaxydraw/src/core/state.ts +177 -0
  117. package/packages/galaxydraw/src/core/viewport.ts +106 -0
  118. package/packages/galaxydraw/src/galaxydraw.css +166 -0
  119. package/packages/galaxydraw/src/index.ts +40 -0
  120. package/packages/galaxydraw/tsconfig.json +30 -0
  121. package/server.ts +62 -0
@@ -0,0 +1,438 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Dependency Graph View — force-directed layout of import relationships.
4
+ *
5
+ * Flow:
6
+ * 1. Fetch import edges from /api/repo/imports API
7
+ * 2. Build adjacency graph from file cards on canvas
8
+ * 3. Run force-directed simulation (spring-charge model)
9
+ * 4. Smoothly animate cards to new positions
10
+ * 5. Draw dependency lines as SVG arrows on the overlay
11
+ *
12
+ * Toggle between spatial ↔ dependency layout with Ctrl+G or toolbar button.
13
+ */
14
+ import { measure } from 'measure-fn';
15
+ import type { CanvasContext } from './context';
16
+ import { savePosition } from './positions';
17
+ import { updateMinimap } from './canvas';
18
+ import { renderConnections } from './connections';
19
+ import { showToast } from './utils';
20
+
21
+ // ─── State ──────────────────────────────────────────────
22
+ let _isGraphMode = false;
23
+ let _savedPositions: Map<string, { x: number; y: number }> = new Map();
24
+ let _graphEdges: { source: string; target: string }[] = [];
25
+ let _graphSvg: SVGGElement | null = null;
26
+
27
+ export function isGraphMode(): boolean { return _isGraphMode; }
28
+
29
+ // ─── Types ──────────────────────────────────────────────
30
+ interface Node {
31
+ path: string;
32
+ x: number;
33
+ y: number;
34
+ vx: number;
35
+ vy: number;
36
+ w: number;
37
+ h: number;
38
+ pinned: boolean;
39
+ }
40
+
41
+ // ─── Force-Directed Layout ──────────────────────────────
42
+ function forceDirectedLayout(
43
+ nodes: Node[],
44
+ edges: { source: string; target: string }[],
45
+ iterations = 150,
46
+ ): void {
47
+ const nodeMap = new Map(nodes.map(n => [n.path, n]));
48
+
49
+ // Scale forces based on number of nodes
50
+ const N = nodes.length;
51
+ const REPULSION = N > 50 ? 1_200_000 : 800_000;
52
+ const SPRING_K = 0.006;
53
+ const IDEAL_LEN = N > 80 ? 600 : N > 40 ? 500 : 400;
54
+ const DAMPING = 0.88;
55
+ const MAX_FORCE = 150;
56
+
57
+ // ── Scatter initial positions around centroid ──
58
+ // Starting from actual canvas positions (which may be spread over 20,000+ px)
59
+ // means repulsion is negligible. Cluster nodes tightly first so forces work.
60
+ const cx = nodes.reduce((s, n) => s + n.x, 0) / N;
61
+ const cy = nodes.reduce((s, n) => s + n.y, 0) / N;
62
+ const spread = Math.sqrt(N) * 80; // Scale cluster size with node count
63
+
64
+ for (const node of nodes) {
65
+ node.x = cx + (Math.random() - 0.5) * spread;
66
+ node.y = cy + (Math.random() - 0.5) * spread;
67
+ node.vx = 0;
68
+ node.vy = 0;
69
+ }
70
+
71
+ // ── Build adjacency for hub detection ──
72
+ const degree = new Map<string, number>();
73
+ for (const e of edges) {
74
+ degree.set(e.source, (degree.get(e.source) || 0) + 1);
75
+ degree.set(e.target, (degree.get(e.target) || 0) + 1);
76
+ }
77
+
78
+ for (let iter = 0; iter < iterations; iter++) {
79
+ const temp = 1 - (iter / iterations) * 0.7; // Slower cooling
80
+
81
+ // ── Repulsion (all pairs) ──
82
+ for (let i = 0; i < nodes.length; i++) {
83
+ for (let j = i + 1; j < nodes.length; j++) {
84
+ const a = nodes[i]!, b = nodes[j]!;
85
+ let dx = b.x - a.x;
86
+ let dy = b.y - a.y;
87
+ const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 30);
88
+ const force = REPULSION / (dist * dist);
89
+ const fx = Math.min(Math.max((dx / dist) * force, -MAX_FORCE), MAX_FORCE);
90
+ const fy = Math.min(Math.max((dy / dist) * force, -MAX_FORCE), MAX_FORCE);
91
+ a.vx -= fx * temp;
92
+ a.vy -= fy * temp;
93
+ b.vx += fx * temp;
94
+ b.vy += fy * temp;
95
+ }
96
+ }
97
+
98
+ // ── Attraction (edges only) ──
99
+ for (const edge of edges) {
100
+ const a = nodeMap.get(edge.source);
101
+ const b = nodeMap.get(edge.target);
102
+ if (!a || !b) continue;
103
+ const dx = b.x - a.x;
104
+ const dy = b.y - a.y;
105
+ const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
106
+ const displacement = dist - IDEAL_LEN;
107
+ const fx = Math.min(Math.max(SPRING_K * displacement * (dx / dist), -MAX_FORCE), MAX_FORCE);
108
+ const fy = Math.min(Math.max(SPRING_K * displacement * (dy / dist), -MAX_FORCE), MAX_FORCE);
109
+ a.vx += fx * temp;
110
+ a.vy += fy * temp;
111
+ b.vx -= fx * temp;
112
+ b.vy -= fy * temp;
113
+ }
114
+
115
+ // ── Gravity toward center to prevent drift ──
116
+ const gcx = nodes.reduce((s, n) => s + n.x, 0) / N;
117
+ const gcy = nodes.reduce((s, n) => s + n.y, 0) / N;
118
+ for (const node of nodes) {
119
+ node.vx += (gcx - node.x) * 0.0005 * temp;
120
+ node.vy += (gcy - node.y) * 0.0005 * temp;
121
+ }
122
+
123
+ // ── Apply velocities ──
124
+ for (const node of nodes) {
125
+ if (node.pinned) continue;
126
+ node.vx *= DAMPING;
127
+ node.vy *= DAMPING;
128
+ node.x += node.vx;
129
+ node.y += node.vy;
130
+ }
131
+ }
132
+ }
133
+
134
+ // ─── Animate cards to target positions ──────────────────
135
+ function animateToPositions(
136
+ ctx: CanvasContext,
137
+ targets: Map<string, { x: number; y: number }>,
138
+ durationMs = 600,
139
+ ) {
140
+ const starts = new Map<string, { x: number; y: number }>();
141
+ for (const [path, target] of targets) {
142
+ const card = ctx.fileCards.get(path);
143
+ if (card) {
144
+ starts.set(path, {
145
+ x: parseFloat(card.style.left) || 0,
146
+ y: parseFloat(card.style.top) || 0,
147
+ });
148
+ }
149
+ // Also animate deferred cards
150
+ const deferred = ctx.deferredCards?.get(path);
151
+ if (deferred && !starts.has(path)) {
152
+ starts.set(path, { x: deferred.x, y: deferred.y });
153
+ }
154
+ }
155
+
156
+ const t0 = performance.now();
157
+ const ease = (t: number) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; // easeInOutCubic
158
+
159
+ function frame() {
160
+ const elapsed = performance.now() - t0;
161
+ const progress = Math.min(elapsed / durationMs, 1);
162
+ const eased = ease(progress);
163
+ const commitHash = ctx.snap().context.currentCommitHash || 'allfiles';
164
+
165
+ for (const [path, target] of targets) {
166
+ const start = starts.get(path);
167
+ if (!start) continue;
168
+ const x = start.x + (target.x - start.x) * eased;
169
+ const y = start.y + (target.y - start.y) * eased;
170
+
171
+ // Update DOM card
172
+ const card = ctx.fileCards.get(path);
173
+ if (card) {
174
+ card.style.left = `${x}px`;
175
+ card.style.top = `${y}px`;
176
+ }
177
+ // Update deferred entry
178
+ const deferred = ctx.deferredCards?.get(path);
179
+ if (deferred) { deferred.x = x; deferred.y = y; }
180
+ // Update pill
181
+ const pill = document.querySelector(`.file-card-pill[data-path="${CSS.escape(path)}"]`) as HTMLElement;
182
+ if (pill) { pill.style.left = `${x}px`; pill.style.top = `${y}px`; }
183
+
184
+ if (progress >= 1) {
185
+ savePosition(ctx, commitHash, path, target.x, target.y);
186
+ }
187
+ }
188
+
189
+ renderConnections(ctx);
190
+
191
+ if (progress < 1) {
192
+ requestAnimationFrame(frame);
193
+ } else {
194
+ updateMinimap(ctx);
195
+ renderGraphEdges(ctx);
196
+ }
197
+ }
198
+
199
+ requestAnimationFrame(frame);
200
+ }
201
+
202
+ // ─── Render graph edges as SVG arrows ───────────────────
203
+ function renderGraphEdges(ctx: CanvasContext) {
204
+ if (!ctx.svgOverlay) return;
205
+
206
+ // Remove old graph edges
207
+ if (_graphSvg) { _graphSvg.remove(); _graphSvg = null; }
208
+ if (!_isGraphMode || _graphEdges.length === 0) return;
209
+
210
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
211
+ g.setAttribute('class', 'dependency-graph-edges');
212
+
213
+ // Ensure arrowhead marker exists
214
+ let defs = ctx.svgOverlay.querySelector('defs');
215
+ if (!defs) {
216
+ defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
217
+ ctx.svgOverlay.prepend(defs);
218
+ }
219
+ if (!defs.querySelector('#dep-arrow')) {
220
+ const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
221
+ marker.setAttribute('id', 'dep-arrow');
222
+ marker.setAttribute('viewBox', '0 0 10 10');
223
+ marker.setAttribute('refX', '10');
224
+ marker.setAttribute('refY', '5');
225
+ marker.setAttribute('markerWidth', '8');
226
+ marker.setAttribute('markerHeight', '8');
227
+ marker.setAttribute('orient', 'auto-start-reverse');
228
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
229
+ path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
230
+ path.setAttribute('fill', 'rgba(168, 130, 255, 0.7)');
231
+ marker.appendChild(path);
232
+ defs.appendChild(marker);
233
+ }
234
+
235
+ // Build lookup for inbound edge count per file to colorize
236
+ const inbound = new Map<string, number>();
237
+ for (const edge of _graphEdges) {
238
+ inbound.set(edge.target, (inbound.get(edge.target) || 0) + 1);
239
+ }
240
+ const maxInbound = Math.max(...inbound.values(), 1);
241
+
242
+ for (const edge of _graphEdges) {
243
+ const srcCard = ctx.fileCards.get(edge.source);
244
+ const tgtCard = ctx.fileCards.get(edge.target);
245
+ // Fall back to deferred card positions
246
+ const srcDeferred = ctx.deferredCards?.get(edge.source);
247
+ const tgtDeferred = ctx.deferredCards?.get(edge.target);
248
+
249
+ let sx: number, sy: number, tx: number, ty: number;
250
+ if (srcCard) {
251
+ sx = parseFloat(srcCard.style.left) + (srcCard.offsetWidth || 580) / 2;
252
+ sy = parseFloat(srcCard.style.top) + (srcCard.offsetHeight || 400) / 2;
253
+ } else if (srcDeferred) {
254
+ sx = srcDeferred.x + (srcDeferred.size?.width || 580) / 2;
255
+ sy = srcDeferred.y + (srcDeferred.size?.height || 400) / 2;
256
+ } else continue;
257
+
258
+ if (tgtCard) {
259
+ tx = parseFloat(tgtCard.style.left) + (tgtCard.offsetWidth || 580) / 2;
260
+ ty = parseFloat(tgtCard.style.top) + (tgtCard.offsetHeight || 400) / 2;
261
+ } else if (tgtDeferred) {
262
+ tx = tgtDeferred.x + (tgtDeferred.size?.width || 580) / 2;
263
+ ty = tgtDeferred.y + (tgtDeferred.size?.height || 400) / 2;
264
+ } else continue;
265
+
266
+ // Use curved path for better visual clarity
267
+ const dx = tx - sx;
268
+ const dy = ty - sy;
269
+ const dist = Math.sqrt(dx * dx + dy * dy);
270
+ const curvature = Math.min(dist * 0.15, 80);
271
+ const mx = (sx + tx) / 2 - (dy / dist) * curvature;
272
+ const my = (sy + ty) / 2 + (dx / dist) * curvature;
273
+
274
+ const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
275
+ pathEl.setAttribute('d', `M ${sx} ${sy} Q ${mx} ${my} ${tx} ${ty}`);
276
+
277
+ // Color intensity based on how many things import the target
278
+ const intensity = Math.min((inbound.get(edge.target) || 1) / maxInbound + 0.3, 1);
279
+ pathEl.setAttribute('stroke', `rgba(168, 130, 255, ${(0.2 + intensity * 0.4).toFixed(2)})`);
280
+ pathEl.setAttribute('stroke-width', '2');
281
+ pathEl.setAttribute('fill', 'none');
282
+ pathEl.setAttribute('stroke-dasharray', '8,4');
283
+ pathEl.setAttribute('marker-end', 'url(#dep-arrow)');
284
+ g.appendChild(pathEl);
285
+ }
286
+
287
+ ctx.svgOverlay.appendChild(g);
288
+ _graphSvg = g;
289
+ }
290
+
291
+ // ─── Toggle Graph Mode ──────────────────────────────────
292
+ export async function toggleDependencyGraph(ctx: CanvasContext) {
293
+ if (_isGraphMode) {
294
+ // Restore original positions
295
+ _isGraphMode = false;
296
+ document.getElementById('dep-graph-btn')?.classList.remove('active');
297
+
298
+ if (_savedPositions.size > 0) {
299
+ showToast('Restoring spatial layout...', 'info');
300
+ animateToPositions(ctx, _savedPositions, 500);
301
+ _savedPositions.clear();
302
+ }
303
+
304
+ // Remove graph edges
305
+ if (_graphSvg) { _graphSvg.remove(); _graphSvg = null; }
306
+
307
+ return;
308
+ }
309
+
310
+ // Enter graph mode
311
+ _isGraphMode = true;
312
+ document.getElementById('dep-graph-btn')?.classList.add('active');
313
+ showToast('Building dependency graph...', 'info');
314
+
315
+ await measure('depGraph:layout', async () => {
316
+ const state = ctx.snap().context;
317
+ const repoPath = state.repoPath;
318
+ const commit = state.currentCommitHash || 'HEAD';
319
+
320
+ if (!repoPath) {
321
+ _isGraphMode = false;
322
+ document.getElementById('dep-graph-btn')?.classList.remove('active');
323
+ showToast('Load a repository first', 'error');
324
+ return;
325
+ }
326
+
327
+ // ── 1. Save current positions ──
328
+ _savedPositions.clear();
329
+ for (const [path, card] of ctx.fileCards) {
330
+ _savedPositions.set(path, {
331
+ x: parseFloat(card.style.left) || 0,
332
+ y: parseFloat(card.style.top) || 0,
333
+ });
334
+ }
335
+ if (ctx.deferredCards) {
336
+ for (const [path, entry] of ctx.deferredCards) {
337
+ if (!_savedPositions.has(path)) {
338
+ _savedPositions.set(path, { x: entry.x, y: entry.y });
339
+ }
340
+ }
341
+ }
342
+
343
+ // ── 2. Fetch import edges ──
344
+ try {
345
+ const res = await fetch('/api/repo/imports', {
346
+ method: 'POST',
347
+ headers: { 'Content-Type': 'application/json' },
348
+ body: JSON.stringify({ path: repoPath, commit }),
349
+ });
350
+ if (!res.ok) {
351
+ _isGraphMode = false;
352
+ document.getElementById('dep-graph-btn')?.classList.remove('active');
353
+ showToast('Failed to fetch import data', 'error');
354
+ return;
355
+ }
356
+ const data = await res.json();
357
+ const edges = (data.edges || []) as { source: string; target: string; line: number }[];
358
+
359
+ // Filter to edges where both files are on the canvas
360
+ const canvasFiles = new Set([...ctx.fileCards.keys()]);
361
+ if (ctx.deferredCards) {
362
+ for (const path of ctx.deferredCards.keys()) canvasFiles.add(path);
363
+ }
364
+
365
+ _graphEdges = edges.filter(e => canvasFiles.has(e.source) && canvasFiles.has(e.target));
366
+
367
+ if (_graphEdges.length === 0) {
368
+ _isGraphMode = false;
369
+ document.getElementById('dep-graph-btn')?.classList.remove('active');
370
+ showToast(`No import relationships found (${data.filesScanned} files scanned)`, 'info');
371
+ return;
372
+ }
373
+
374
+ console.log(`[dep-graph] ${_graphEdges.length} edges across ${canvasFiles.size} canvas files`);
375
+ } catch (err) {
376
+ _isGraphMode = false;
377
+ document.getElementById('dep-graph-btn')?.classList.remove('active');
378
+ showToast('Error scanning imports', 'error');
379
+ return;
380
+ }
381
+
382
+ // ── 3. Build nodes from canvas cards ──
383
+ const connectedFiles = new Set<string>();
384
+ for (const e of _graphEdges) { connectedFiles.add(e.source); connectedFiles.add(e.target); }
385
+
386
+ // Center of current viewport
387
+ const centerX = _savedPositions.size > 0
388
+ ? [..._savedPositions.values()].reduce((s, p) => s + p.x, 0) / _savedPositions.size
389
+ : 2000;
390
+ const centerY = _savedPositions.size > 0
391
+ ? [..._savedPositions.values()].reduce((s, p) => s + p.y, 0) / _savedPositions.size
392
+ : 2000;
393
+
394
+ const nodes: Node[] = [];
395
+ for (const path of connectedFiles) {
396
+ const saved = _savedPositions.get(path);
397
+ const card = ctx.fileCards.get(path);
398
+ nodes.push({
399
+ path,
400
+ x: saved?.x ?? centerX + (Math.random() - 0.5) * 1000,
401
+ y: saved?.y ?? centerY + (Math.random() - 0.5) * 1000,
402
+ vx: 0,
403
+ vy: 0,
404
+ w: card?.offsetWidth || 580,
405
+ h: card?.offsetHeight || 400,
406
+ pinned: false,
407
+ });
408
+ }
409
+
410
+ // ── 4. Run force simulation ──
411
+ forceDirectedLayout(nodes, _graphEdges, 150);
412
+
413
+ // ── 5. Center the result around the original centroid ──
414
+ const graphCenterX = nodes.reduce((s, n) => s + n.x, 0) / nodes.length;
415
+ const graphCenterY = nodes.reduce((s, n) => s + n.y, 0) / nodes.length;
416
+ const offsetX = centerX - graphCenterX;
417
+ const offsetY = centerY - graphCenterY;
418
+
419
+ const targets = new Map<string, { x: number; y: number }>();
420
+ for (const node of nodes) {
421
+ targets.set(node.path, { x: node.x + offsetX, y: node.y + offsetY });
422
+ }
423
+
424
+ // ── 6. Animate to new positions ──
425
+ showToast(`📊 ${connectedFiles.size} files, ${_graphEdges.length} dependencies`, 'success');
426
+ animateToPositions(ctx, targets, 800);
427
+ });
428
+ }
429
+
430
+ // ─── Keyboard shortcut registration ─────────────────────
431
+ export function setupDependencyGraphShortcut(ctx: CanvasContext) {
432
+ document.addEventListener('keydown', (e) => {
433
+ if (e.ctrlKey && e.key === 'g' && !e.shiftKey && !e.altKey) {
434
+ e.preventDefault();
435
+ toggleDependencyGraph(ctx);
436
+ }
437
+ });
438
+ }