gitmaps 1.0.0 → 1.1.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/README.md +5 -11
- package/app/[owner]/[repo]/page.client.tsx +5 -0
- package/app/[owner]/[repo]/page.tsx +6 -0
- package/app/[slug]/page.client.tsx +5 -0
- package/app/[slug]/page.tsx +6 -0
- package/app/api/manifest.json/route.ts +20 -0
- package/app/api/pwa-icon/route.ts +14 -0
- package/app/api/repo/clone-stream/route.ts +20 -12
- package/app/api/repo/imports/route.ts +21 -3
- package/app/api/repo/list/route.ts +30 -0
- package/app/api/repo/upload/route.ts +6 -9
- package/app/api/sw.js/route.ts +70 -0
- package/app/galaxy-canvas/page.client.tsx +2 -0
- package/app/galaxy-canvas/page.tsx +5 -0
- package/app/globals.css +477 -95
- package/app/icon.png +0 -0
- package/app/layout.tsx +30 -2
- package/app/lib/canvas-text.ts +4 -72
- package/app/lib/canvas.ts +1 -1
- package/app/lib/card-arrangement.ts +21 -7
- package/app/lib/card-context-menu.tsx +2 -2
- package/app/lib/card-groups.ts +9 -2
- package/app/lib/cards.tsx +3 -1
- package/app/lib/connections.tsx +34 -43
- package/app/lib/events.tsx +25 -0
- package/app/lib/file-card-plugin.ts +14 -0
- package/app/lib/file-preview.ts +68 -41
- package/app/lib/galaxydraw-bridge.ts +5 -0
- package/app/lib/global-search.ts +48 -27
- package/app/lib/layers.tsx +17 -18
- package/app/lib/perf-overlay.ts +78 -0
- package/app/lib/positions.ts +1 -1
- package/app/lib/repo.tsx +18 -8
- package/app/lib/shortcuts-panel.ts +2 -0
- package/app/lib/viewport-culling.ts +7 -0
- package/app/page.client.tsx +72 -18
- package/app/page.tsx +22 -86
- package/banner.png +0 -0
- package/package.json +2 -2
- package/packages/galaxydraw/README.md +2 -2
- package/packages/galaxydraw/package.json +1 -1
- package/server.ts +1 -1
- package/app/api/connections/route.ts +0 -72
- package/app/api/positions/route.ts +0 -80
- package/app/api/repo/browse/route.ts +0 -55
- package/app/lib/pr-review.ts +0 -374
package/app/icon.png
ADDED
|
Binary file
|
package/app/layout.tsx
CHANGED
|
@@ -11,9 +11,11 @@ export default function RootLayout({ children }: { children: any }) {
|
|
|
11
11
|
<head>
|
|
12
12
|
<meta charSet="UTF-8" />
|
|
13
13
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
14
|
-
<meta name="description" content="GitMaps
|
|
14
|
+
<meta name="description" content="Transcend the file tree. GitMaps renders knowledge on an infinite canvas — with layers, time-travel, and a minimap to never lose context." />
|
|
15
15
|
<title>GitMaps — Spatial Code Explorer</title>
|
|
16
|
-
<link rel="icon" href="
|
|
16
|
+
<link rel="icon" type="image/png" href="/api/pwa-icon" />
|
|
17
|
+
<link rel="manifest" href="/api/manifest.json" />
|
|
18
|
+
<meta name="theme-color" content="#7c3aed" />
|
|
17
19
|
<link
|
|
18
20
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
19
21
|
rel="stylesheet"
|
|
@@ -279,6 +281,25 @@ export default function RootLayout({ children }: { children: any }) {
|
|
|
279
281
|
<div className="changed-files-list" id="changedFilesList"></div>
|
|
280
282
|
</div>
|
|
281
283
|
|
|
284
|
+
{/* Connections Panel */}
|
|
285
|
+
<div className="connections-panel" id="connectionsPanel" style={{ display: 'none' }}>
|
|
286
|
+
<div className="panel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', borderBottom: '1px solid var(--border)' }}>
|
|
287
|
+
<span className="panel-title" style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-primary)' }}>
|
|
288
|
+
Connections <span id="connCount" style={{ marginLeft: '6px', background: 'rgba(255,255,255,0.1)', padding: '2px 8px', borderRadius: '12px', fontSize: '0.75rem' }}>0</span>
|
|
289
|
+
</span>
|
|
290
|
+
<button id="closeConnectionsPanel" className="btn-ghost btn-xs" title="Close">
|
|
291
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
292
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
293
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
294
|
+
</svg>
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
<div style={{ padding: '8px 16px', fontSize: '0.75rem', color: 'var(--text-muted)', borderBottom: '1px solid var(--border)', background: 'rgba(0,0,0,0.2)' }}>
|
|
298
|
+
💡 Tip: <strong>Alt+Click</strong> any line number then select a target to connect them.
|
|
299
|
+
</div>
|
|
300
|
+
<div className="connections-list" id="connectionsList" style={{ flex: 1, overflowY: 'auto' }}></div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
282
303
|
|
|
283
304
|
|
|
284
305
|
<div className="minimap-container">
|
|
@@ -487,6 +508,13 @@ export default function RootLayout({ children }: { children: any }) {
|
|
|
487
508
|
</div>
|
|
488
509
|
</div>
|
|
489
510
|
</div>
|
|
511
|
+
<script dangerouslySetInnerHTML={{
|
|
512
|
+
__html: `
|
|
513
|
+
if ('serviceWorker' in navigator) {
|
|
514
|
+
navigator.serviceWorker.register('/api/sw.js', { scope: '/' })
|
|
515
|
+
.catch(function(e) { console.warn('[SW] Registration failed:', e); });
|
|
516
|
+
}
|
|
517
|
+
` }} />
|
|
490
518
|
</body >
|
|
491
519
|
</html >
|
|
492
520
|
);
|
package/app/lib/canvas-text.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface CanvasTextOptions {
|
|
|
5
5
|
isAllAdded?: boolean;
|
|
6
6
|
isAllDeleted?: boolean;
|
|
7
7
|
visibleLineIndices?: Set<number>;
|
|
8
|
-
/** File path for
|
|
8
|
+
/** File path for connections (if set, enables line-click for connections) */
|
|
9
9
|
filePath?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -43,10 +43,7 @@ export class CanvasTextRenderer {
|
|
|
43
43
|
private _longLineGradW: number = 0;
|
|
44
44
|
/** rAF batching — only one render per animation frame */
|
|
45
45
|
private _rafId: number = 0;
|
|
46
|
-
|
|
47
|
-
private _fileComments: Map<number, import('./pr-review').ReviewComment[]> = new Map();
|
|
48
|
-
/** Unsubscribe from review change listener */
|
|
49
|
-
private _reviewUnsub: (() => void) | null = null;
|
|
46
|
+
|
|
50
47
|
|
|
51
48
|
constructor(container: HTMLElement, options: CanvasTextOptions) {
|
|
52
49
|
this.options = options;
|
|
@@ -221,46 +218,6 @@ export class CanvasTextRenderer {
|
|
|
221
218
|
this.render();
|
|
222
219
|
}
|
|
223
220
|
}) as EventListener);
|
|
224
|
-
// PR Review — load file comments and wire click handler
|
|
225
|
-
if (options.filePath) {
|
|
226
|
-
this._loadFileComments();
|
|
227
|
-
// Re-render when comments change
|
|
228
|
-
import('./pr-review').then(({ onReviewChange }) => {
|
|
229
|
-
this._reviewUnsub = onReviewChange(() => {
|
|
230
|
-
this._loadFileComments();
|
|
231
|
-
this.render();
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Click handler on gutter area — open comment popup
|
|
236
|
-
container.addEventListener('click', (e: MouseEvent) => {
|
|
237
|
-
if (!this.options.filePath) return;
|
|
238
|
-
const rect = container.getBoundingClientRect();
|
|
239
|
-
const x = e.clientX - rect.left;
|
|
240
|
-
const y = e.clientY - rect.top;
|
|
241
|
-
|
|
242
|
-
// Only trigger on gutter click (left of content area)
|
|
243
|
-
if (x > this.contentX) return;
|
|
244
|
-
|
|
245
|
-
// Calculate which line was clicked
|
|
246
|
-
const lineIdx = Math.floor((y + this.scrollTop) / this.lineHeight);
|
|
247
|
-
if (lineIdx < 0 || lineIdx >= this.drawnLines.length) return;
|
|
248
|
-
|
|
249
|
-
const lineData = this.drawnLines[lineIdx];
|
|
250
|
-
import('./pr-review').then(({ showCommentPopup }) => {
|
|
251
|
-
showCommentPopup(
|
|
252
|
-
this.options.filePath!,
|
|
253
|
-
lineData.num,
|
|
254
|
-
e.clientX,
|
|
255
|
-
e.clientY,
|
|
256
|
-
() => {
|
|
257
|
-
this._loadFileComments();
|
|
258
|
-
this.render();
|
|
259
|
-
}
|
|
260
|
-
);
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
221
|
|
|
265
222
|
this.render();
|
|
266
223
|
}
|
|
@@ -283,16 +240,7 @@ export class CanvasTextRenderer {
|
|
|
283
240
|
}
|
|
284
241
|
}
|
|
285
242
|
|
|
286
|
-
|
|
287
|
-
private _loadFileComments() {
|
|
288
|
-
if (!this.options.filePath) {
|
|
289
|
-
this._fileComments = new Map();
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
import('./pr-review').then(({ getAllFileComments }) => {
|
|
293
|
-
this._fileComments = getAllFileComments(this.options.filePath!);
|
|
294
|
-
});
|
|
295
|
-
}
|
|
243
|
+
|
|
296
244
|
|
|
297
245
|
/** Build a custom scrollbar track on the right side */
|
|
298
246
|
private _buildScrollTrack(container: HTMLElement) {
|
|
@@ -890,23 +838,7 @@ export class CanvasTextRenderer {
|
|
|
890
838
|
this.ctx.fillRect(w - 3, y + 3, 2, lh - 6);
|
|
891
839
|
}
|
|
892
840
|
|
|
893
|
-
|
|
894
|
-
if (this._fileComments.has(lineNum)) {
|
|
895
|
-
const commentCount = this._fileComments.get(lineNum)!.length;
|
|
896
|
-
// Purple dot
|
|
897
|
-
this.ctx.beginPath();
|
|
898
|
-
this.ctx.arc(diffGutterW - 3, y + lh / 2, 3, 0, Math.PI * 2);
|
|
899
|
-
this.ctx.fillStyle = 'rgba(168, 139, 250, 0.9)';
|
|
900
|
-
this.ctx.fill();
|
|
901
|
-
// Count badge if > 1
|
|
902
|
-
if (commentCount > 1) {
|
|
903
|
-
this.ctx.fillStyle = 'rgba(168, 139, 250, 0.7)';
|
|
904
|
-
this.ctx.font = '9px sans-serif';
|
|
905
|
-
this.ctx.fillText(String(commentCount), diffGutterW - 7, y + 2);
|
|
906
|
-
// Restore font
|
|
907
|
-
this.ctx.font = `${this.fontSize}px "JetBrains Mono", Consolas, monospace`;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
841
|
+
|
|
910
842
|
}
|
|
911
843
|
}
|
|
912
844
|
}
|
package/app/lib/canvas.ts
CHANGED
|
@@ -148,7 +148,7 @@ function _rebuildMinimap(ctx: CanvasContext) {
|
|
|
148
148
|
// Skip cards with invalid positions (NaN poisons Math.min/max)
|
|
149
149
|
if (isNaN(x) || isNaN(y)) return;
|
|
150
150
|
const w = card.offsetWidth || 580;
|
|
151
|
-
const h = card.offsetHeight ||
|
|
151
|
+
const h = card.offsetHeight || 700;
|
|
152
152
|
const name = path.split('/').pop() || path;
|
|
153
153
|
const parts = path.split('/');
|
|
154
154
|
const displayPath = parts.length > 1 ? parts.slice(-2).join('/') : name;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { measure } from 'measure-fn';
|
|
10
10
|
import type { CanvasContext } from './context';
|
|
11
|
-
import { savePosition } from './positions';
|
|
11
|
+
import { savePosition, flushPositions } from './positions';
|
|
12
12
|
import { updateMinimap } from './canvas';
|
|
13
13
|
import { renderConnections } from './connections';
|
|
14
14
|
|
|
@@ -35,11 +35,19 @@ function getSelectedCardsInfo(ctx: CanvasContext): CardInfo[] {
|
|
|
35
35
|
const x = parseFloat(card.style.left);
|
|
36
36
|
const y = parseFloat(card.style.top);
|
|
37
37
|
if (isNaN(x) || isNaN(y)) return;
|
|
38
|
+
// In pill mode, card might be display:none (0px) or rendered as a pill (~24px).
|
|
39
|
+
let cw = card.offsetWidth;
|
|
40
|
+
let ch = card.offsetHeight;
|
|
41
|
+
if (!cw || cw < 100) cw = 580;
|
|
42
|
+
if (!ch || ch < 100) ch = 700;
|
|
43
|
+
|
|
44
|
+
const pos = ctx.positions?.get(path);
|
|
45
|
+
const def = ctx.deferredCards?.get(path);
|
|
38
46
|
infos.push({
|
|
39
47
|
path, card,
|
|
40
48
|
x, y,
|
|
41
|
-
w:
|
|
42
|
-
h:
|
|
49
|
+
w: pos?.width || def?.size?.width || cw,
|
|
50
|
+
h: pos?.height || def?.size?.height || ch,
|
|
43
51
|
});
|
|
44
52
|
}
|
|
45
53
|
});
|
|
@@ -69,16 +77,19 @@ function getSelectedCardsInfo(ctx: CanvasContext): CardInfo[] {
|
|
|
69
77
|
if (infos.length < selected.length) {
|
|
70
78
|
selected.forEach(path => {
|
|
71
79
|
if (seen.has(path)) return;
|
|
72
|
-
const pill = document.querySelector(`.file-
|
|
80
|
+
const pill = document.querySelector(`.file-pill[data-path="${CSS.escape(path)}"]`) as HTMLElement;
|
|
73
81
|
if (pill) {
|
|
74
82
|
const x = parseFloat(pill.style.left);
|
|
75
83
|
const y = parseFloat(pill.style.top);
|
|
76
84
|
if (isNaN(x) || isNaN(y)) return;
|
|
85
|
+
// Pills are tiny (~24px) — use stored size or default card size
|
|
86
|
+
const pos = ctx.positions?.get(path);
|
|
87
|
+
const def = ctx.deferredCards?.get(path);
|
|
77
88
|
infos.push({
|
|
78
89
|
path, card: null,
|
|
79
90
|
x, y,
|
|
80
|
-
w:
|
|
81
|
-
h:
|
|
91
|
+
w: pos?.width || def?.size?.width || 580,
|
|
92
|
+
h: pos?.height || def?.size?.height || 700,
|
|
82
93
|
});
|
|
83
94
|
}
|
|
84
95
|
});
|
|
@@ -104,7 +115,7 @@ function applyPosition(ctx: CanvasContext, info: CardInfo, newX: number, newY: n
|
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
// Update pill element
|
|
107
|
-
const pill = document.querySelector(`.file-
|
|
118
|
+
const pill = document.querySelector(`.file-pill[data-path="${CSS.escape(info.path)}"]`) as HTMLElement;
|
|
108
119
|
if (pill) {
|
|
109
120
|
pill.style.left = `${newX}px`;
|
|
110
121
|
pill.style.top = `${newY}px`;
|
|
@@ -126,6 +137,7 @@ export function arrangeRow(ctx: CanvasContext) {
|
|
|
126
137
|
savePosition(ctx, commitHash, info.path, curX, startY);
|
|
127
138
|
curX += info.w + gap;
|
|
128
139
|
});
|
|
140
|
+
flushPositions(ctx); // Force immediate save — don't rely on debounce for batch arrange
|
|
129
141
|
renderConnections(ctx);
|
|
130
142
|
updateMinimap(ctx);
|
|
131
143
|
});
|
|
@@ -146,6 +158,7 @@ export function arrangeColumn(ctx: CanvasContext) {
|
|
|
146
158
|
savePosition(ctx, commitHash, info.path, startX, curY);
|
|
147
159
|
curY += info.h + gap;
|
|
148
160
|
});
|
|
161
|
+
flushPositions(ctx);
|
|
149
162
|
renderConnections(ctx);
|
|
150
163
|
updateMinimap(ctx);
|
|
151
164
|
});
|
|
@@ -182,6 +195,7 @@ export function arrangeGrid(ctx: CanvasContext) {
|
|
|
182
195
|
savePosition(ctx, commitHash, info.path, x, y);
|
|
183
196
|
});
|
|
184
197
|
|
|
198
|
+
flushPositions(ctx);
|
|
185
199
|
renderConnections(ctx);
|
|
186
200
|
updateMinimap(ctx);
|
|
187
201
|
});
|
|
@@ -93,8 +93,8 @@ function ContextMenu({ onAction, onActionLayer, onSelectFolder, isInActiveLayer,
|
|
|
93
93
|
</div>
|
|
94
94
|
</div>
|
|
95
95
|
{isInActiveLayer && (
|
|
96
|
-
<button className="ctx-item" onClick={() => onAction('remove-from-layer')} style="color: #
|
|
97
|
-
|
|
96
|
+
<button className="ctx-item" onClick={() => onAction('remove-from-layer')} style="color: #60a5fa">
|
|
97
|
+
↩ Move to Main
|
|
98
98
|
</button>
|
|
99
99
|
)}
|
|
100
100
|
<div className="ctx-divider"></div>
|
package/app/lib/card-groups.ts
CHANGED
|
@@ -24,8 +24,8 @@ const activeGroups = new Map<string, CollapsedGroup>();
|
|
|
24
24
|
|
|
25
25
|
// ─── Persistence ─────────────────────────────────────────
|
|
26
26
|
function getStorageKey(): string {
|
|
27
|
-
const
|
|
28
|
-
return `gitmaps:collapsed-dirs:${
|
|
27
|
+
const repoPath = _ctx?.snap().context.repoPath || 'default';
|
|
28
|
+
return `gitmaps:collapsed-dirs:${repoPath}`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function saveState() {
|
|
@@ -45,6 +45,13 @@ function loadState() {
|
|
|
45
45
|
} catch { }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function resetCardGroups() {
|
|
49
|
+
for (const group of activeGroups.values()) {
|
|
50
|
+
group.groupCard.remove();
|
|
51
|
+
}
|
|
52
|
+
activeGroups.clear();
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
// ─── Group card rendering ────────────────────────────────
|
|
49
56
|
function createGroupCard(dir: string, files: CollapsedGroup['files']): HTMLElement {
|
|
50
57
|
const card = document.createElement('div');
|
package/app/lib/cards.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import type { CanvasContext } from './context';
|
|
|
9
9
|
import { escapeHtml, getFileIcon, getFileIconClass, showToast } from './utils';
|
|
10
10
|
import { hideSelectedFiles } from './hidden-files';
|
|
11
11
|
import { savePosition, getPositionKey, isPathExpandedInPositions, setPathExpandedInPositions } from './positions';
|
|
12
|
-
import { updateMinimap, updateCanvasTransform, updateZoomUI, jumpToFile } from './canvas';
|
|
12
|
+
import { updateMinimap, updateCanvasTransform, updateZoomUI, jumpToFile, forceMinimapRebuild } from './canvas';
|
|
13
13
|
import { updateStatusBarSelected } from './status-bar';
|
|
14
14
|
import { renderConnections, scheduleRenderConnections, setupConnectionDrag, hasPendingConnection } from './connections';
|
|
15
15
|
import { highlightSyntax, buildModalDiffHTML } from './syntax';
|
|
@@ -226,6 +226,8 @@ export function setupCardInteraction(ctx: CanvasContext, card: HTMLElement, comm
|
|
|
226
226
|
savePosition(ctx, commitHash, info.path, x, y);
|
|
227
227
|
});
|
|
228
228
|
moveStartPositions = [];
|
|
229
|
+
// Force minimap rebuild so dot positions reflect the drag result
|
|
230
|
+
forceMinimapRebuild(ctx);
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
action = null;
|
package/app/lib/connections.tsx
CHANGED
|
@@ -824,59 +824,50 @@ export function navigateToConnection(ctx: CanvasContext, conn: any, navigateTo:
|
|
|
824
824
|
});
|
|
825
825
|
}
|
|
826
826
|
|
|
827
|
-
// ─── Save connections to
|
|
828
|
-
export
|
|
827
|
+
// ─── Save connections to localStorage ───────────────────
|
|
828
|
+
export function saveConnections(ctx: CanvasContext) {
|
|
829
829
|
const state = ctx.snap().context;
|
|
830
|
+
const repoPath = state.repoPath;
|
|
831
|
+
if (!repoPath) return;
|
|
830
832
|
try {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
headers: { 'Content-Type': 'application/json' },
|
|
834
|
-
body: JSON.stringify({ connections: state.connections })
|
|
835
|
-
});
|
|
833
|
+
const key = `gitcanvas:connections:${repoPath}`;
|
|
834
|
+
localStorage.setItem(key, JSON.stringify(state.connections));
|
|
836
835
|
} catch (e) {
|
|
837
836
|
measure('connections:saveError', () => e);
|
|
838
837
|
}
|
|
839
838
|
}
|
|
840
839
|
|
|
841
|
-
// ─── Load connections from
|
|
842
|
-
export
|
|
843
|
-
return measure('connections:load',
|
|
840
|
+
// ─── Load connections from localStorage ─────────────────
|
|
841
|
+
export function loadConnections(ctx: CanvasContext) {
|
|
842
|
+
return measure('connections:load', () => {
|
|
844
843
|
try {
|
|
845
|
-
const
|
|
846
|
-
if (!
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
});
|
|
868
|
-
ctx.actor.send({
|
|
869
|
-
type: 'COMPLETE_CONNECTION',
|
|
870
|
-
targetFile: conn.targetFile,
|
|
871
|
-
lineStart: conn.targetLineStart,
|
|
872
|
-
lineEnd: conn.targetLineEnd,
|
|
873
|
-
comment: conn.comment,
|
|
874
|
-
});
|
|
844
|
+
const repoPath = ctx.snap().context.repoPath;
|
|
845
|
+
if (!repoPath) return;
|
|
846
|
+
const key = `gitcanvas:connections:${repoPath}`;
|
|
847
|
+
const stored = localStorage.getItem(key);
|
|
848
|
+
if (!stored) return;
|
|
849
|
+
|
|
850
|
+
const connections = JSON.parse(stored);
|
|
851
|
+
if (!Array.isArray(connections) || connections.length === 0) return;
|
|
852
|
+
|
|
853
|
+
connections.forEach(conn => {
|
|
854
|
+
ctx.actor.send({
|
|
855
|
+
type: 'START_CONNECTION',
|
|
856
|
+
sourceFile: conn.sourceFile,
|
|
857
|
+
lineStart: conn.sourceLineStart,
|
|
858
|
+
lineEnd: conn.sourceLineEnd,
|
|
859
|
+
});
|
|
860
|
+
ctx.actor.send({
|
|
861
|
+
type: 'COMPLETE_CONNECTION',
|
|
862
|
+
targetFile: conn.targetFile,
|
|
863
|
+
lineStart: conn.targetLineStart,
|
|
864
|
+
lineEnd: conn.targetLineEnd,
|
|
865
|
+
comment: conn.comment || '',
|
|
875
866
|
});
|
|
867
|
+
});
|
|
876
868
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
869
|
+
renderConnections(ctx);
|
|
870
|
+
buildConnectionMarkers(ctx);
|
|
880
871
|
} catch (e) {
|
|
881
872
|
measure('connections:loadError', () => e);
|
|
882
873
|
}
|
package/app/lib/events.tsx
CHANGED
|
@@ -555,6 +555,31 @@ export function setupEventListeners(ctx: CanvasContext) {
|
|
|
555
555
|
repoSelect.value = ''; // Keep "Select a repository..." shown
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
// ── Also discover on-disk repos that may not be in localStorage ──
|
|
559
|
+
fetch('/api/repo/list').then(r => r.json()).then((data: any) => {
|
|
560
|
+
if (!data.repos || data.repos.length === 0) return;
|
|
561
|
+
const currentPaths = new Set(recentRepos);
|
|
562
|
+
let added = false;
|
|
563
|
+
for (const repo of data.repos) {
|
|
564
|
+
if (!currentPaths.has(repo.path)) {
|
|
565
|
+
// Add to localStorage recent repos
|
|
566
|
+
_addRecentRepo(repo.path);
|
|
567
|
+
// Add to dropdown (before the __new__ option)
|
|
568
|
+
const opt = document.createElement('option');
|
|
569
|
+
opt.value = repo.path;
|
|
570
|
+
opt.textContent = repo.name;
|
|
571
|
+
opt.title = repo.path;
|
|
572
|
+
const newOpt2 = repoSelect.querySelector('option[value="__new__"]');
|
|
573
|
+
if (newOpt2) {
|
|
574
|
+
repoSelect.insertBefore(opt, newOpt2);
|
|
575
|
+
} else {
|
|
576
|
+
repoSelect.add(opt);
|
|
577
|
+
}
|
|
578
|
+
added = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}).catch(() => { });
|
|
582
|
+
|
|
558
583
|
repoSelect.addEventListener('change', async () => {
|
|
559
584
|
const val = repoSelect.value;
|
|
560
585
|
if (val === '__new__') {
|
|
@@ -47,6 +47,13 @@ export function createFileCardPlugin(): CardPlugin {
|
|
|
47
47
|
// skipInteraction=true: CardManager handles drag/resize/z-order
|
|
48
48
|
const { createAllFileCard } = require('./cards');
|
|
49
49
|
const card = createAllFileCard(ctx, file, data.x, data.y, savedSize, true);
|
|
50
|
+
|
|
51
|
+
const { isDirCollapsed } = require('./card-groups');
|
|
52
|
+
const fileDir = file.path.includes('/') ? file.path.substring(0, file.path.lastIndexOf('/')) : '.';
|
|
53
|
+
if (isDirCollapsed(fileDir)) {
|
|
54
|
+
card.style.display = 'none';
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
return card;
|
|
51
58
|
},
|
|
52
59
|
|
|
@@ -104,6 +111,13 @@ export function createDiffCardPlugin(): CardPlugin {
|
|
|
104
111
|
|
|
105
112
|
const { createFileCard } = require('./cards');
|
|
106
113
|
const card = createFileCard(ctx, file, data.x, data.y, commitHash, true);
|
|
114
|
+
|
|
115
|
+
const { isDirCollapsed } = require('./card-groups');
|
|
116
|
+
const fileDir = file.path.includes('/') ? file.path.substring(0, file.path.lastIndexOf('/')) : '.';
|
|
117
|
+
if (isDirCollapsed(fileDir)) {
|
|
118
|
+
card.style.display = 'none';
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
return card;
|
|
108
122
|
},
|
|
109
123
|
|
package/app/lib/file-preview.ts
CHANGED
|
@@ -98,6 +98,7 @@ function renderPreviewCard(path: string): HTMLElement | null {
|
|
|
98
98
|
clone.style.position = 'relative';
|
|
99
99
|
clone.style.left = '0';
|
|
100
100
|
clone.style.top = '0';
|
|
101
|
+
clone.style.display = 'block'; // CRITICAL: cards are display:none in pill mode
|
|
101
102
|
clone.style.visibility = 'visible';
|
|
102
103
|
clone.style.contentVisibility = 'visible';
|
|
103
104
|
clone.style.opacity = '1';
|
|
@@ -112,22 +113,24 @@ function renderPreviewCard(path: string): HTMLElement | null {
|
|
|
112
113
|
delete clone.dataset.culled;
|
|
113
114
|
delete clone.dataset.expanded;
|
|
114
115
|
|
|
115
|
-
//
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
// Always re-render body with ALL lines (cards are 120-line limited)
|
|
117
|
+
const { _getCardFileData, _buildFileContentHTML } = require('./cards');
|
|
118
|
+
const file = _getCardFileData(existingCard);
|
|
119
|
+
if (file?.content) {
|
|
120
|
+
const addedLines = file.addedLines || new Set();
|
|
121
|
+
const deletedBeforeLine = file.deletedBeforeLine || new Map();
|
|
122
|
+
const isAllAdded = file.status === 'added';
|
|
123
|
+
const isAllDeleted = file.status === 'deleted';
|
|
124
|
+
const html = _buildFileContentHTML(
|
|
125
|
+
file.content, file.layerSections, addedLines, deletedBeforeLine,
|
|
126
|
+
isAllAdded, isAllDeleted, true, file.lines // true = expanded, show ALL lines
|
|
127
|
+
);
|
|
128
|
+
// Replace body content with full file
|
|
129
|
+
const body = clone.querySelector('.file-card-body');
|
|
130
|
+
if (body) body.innerHTML = html;
|
|
131
|
+
// Also replace canvas-text container if present
|
|
132
|
+
const canvasContainer = clone.querySelector('.canvas-container');
|
|
133
|
+
if (canvasContainer) canvasContainer.outerHTML = html;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
return clone;
|
|
@@ -267,6 +270,13 @@ function onMouseMove(e: MouseEvent) {
|
|
|
267
270
|
if (!isPreviewEnabled) return;
|
|
268
271
|
if (_isHoveringPopup) return; // Don't hide while interacting with popup
|
|
269
272
|
|
|
273
|
+
// Suppress popup during canvas panning (middle-button drag, space-held, isDragging)
|
|
274
|
+
if (e.buttons & 4) { hidePopup(); return; } // middle mouse button held
|
|
275
|
+
if (e.buttons & 1) { hidePopup(); return; } // left mouse button held (dragging)
|
|
276
|
+
const viewport = document.querySelector('.canvas-viewport');
|
|
277
|
+
if (viewport?.classList.contains('space-panning')) { hidePopup(); return; }
|
|
278
|
+
if (_ctx?.isDragging) { hidePopup(); return; }
|
|
279
|
+
|
|
270
280
|
const gdState = getGalaxyDrawState();
|
|
271
281
|
if (!gdState || gdState.zoom >= PREVIEW_ZOOM_THRESHOLD) {
|
|
272
282
|
hidePopup();
|
|
@@ -291,19 +301,8 @@ function onMouseMove(e: MouseEvent) {
|
|
|
291
301
|
}
|
|
292
302
|
|
|
293
303
|
if (path === currentCardPath) {
|
|
294
|
-
// Already showing for this card —
|
|
295
|
-
|
|
296
|
-
const vw = window.innerWidth;
|
|
297
|
-
const vh = window.innerHeight;
|
|
298
|
-
let x = e.clientX + OFFSET_X;
|
|
299
|
-
let y = e.clientY + OFFSET_Y;
|
|
300
|
-
if (x + POPUP_MAX_W > vw - 12) x = e.clientX - POPUP_MAX_W - OFFSET_X;
|
|
301
|
-
if (y + POPUP_MAX_H > vh - 12) y = e.clientY - POPUP_MAX_H - OFFSET_Y;
|
|
302
|
-
x = Math.max(8, x);
|
|
303
|
-
y = Math.max(8, y);
|
|
304
|
-
popup.style.left = `${x}px`;
|
|
305
|
-
popup.style.top = `${y}px`;
|
|
306
|
-
}
|
|
304
|
+
// Already showing for this card — DON'T reposition.
|
|
305
|
+
// Keep popup stationary so user can move their mouse to it for scrolling.
|
|
307
306
|
return;
|
|
308
307
|
}
|
|
309
308
|
|
|
@@ -327,6 +326,43 @@ function onMouseOut(e: MouseEvent) {
|
|
|
327
326
|
hidePopup();
|
|
328
327
|
}
|
|
329
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Wheel handler on viewport:
|
|
331
|
+
* - If popup visible + mouse over pill/placeholder + NO Ctrl → scroll popup
|
|
332
|
+
* - If Ctrl held → always let canvas zoom (never intercept)
|
|
333
|
+
* - If zoom crosses threshold → hide popup
|
|
334
|
+
*/
|
|
335
|
+
function onViewportWheel(e: WheelEvent) {
|
|
336
|
+
// Ctrl+wheel = zoom → never intercept
|
|
337
|
+
if (e.ctrlKey || e.metaKey) {
|
|
338
|
+
// Check if zoom crossed threshold after a tick
|
|
339
|
+
if (currentCardPath) {
|
|
340
|
+
setTimeout(() => {
|
|
341
|
+
const gd = getGalaxyDrawState();
|
|
342
|
+
if (gd && gd.zoom >= PREVIEW_ZOOM_THRESHOLD) hidePopup();
|
|
343
|
+
}, 50);
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If popup is visible and user is hovering over the pill/card that triggered it,
|
|
349
|
+
// forward wheel events to scroll the popup content
|
|
350
|
+
if (popup && currentCardPath && popup.style.opacity === '1') {
|
|
351
|
+
const target = e.target as HTMLElement;
|
|
352
|
+
const pill = target.closest?.('.file-pill') as HTMLElement | null;
|
|
353
|
+
const card = target.closest?.('.file-card') as HTMLElement | null;
|
|
354
|
+
const element = pill || card;
|
|
355
|
+
if (element && element.dataset.path === currentCardPath) {
|
|
356
|
+
// Only intercept if popup has scrollable content
|
|
357
|
+
if (popup.scrollHeight > popup.clientHeight) {
|
|
358
|
+
e.preventDefault();
|
|
359
|
+
e.stopPropagation();
|
|
360
|
+
popup.scrollTop += e.deltaY;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
330
366
|
// ─── Public API ──────────────────────────────────────────
|
|
331
367
|
|
|
332
368
|
/**
|
|
@@ -343,18 +379,8 @@ export function initFilePreview(viewportEl: HTMLElement, ctx?: CanvasContext) {
|
|
|
343
379
|
viewportEl.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
344
380
|
viewportEl.addEventListener('mouseout', onMouseOut, { passive: true });
|
|
345
381
|
|
|
346
|
-
//
|
|
347
|
-
viewportEl.addEventListener('wheel',
|
|
348
|
-
// Don't hide popup if user is hovering/scrolling it
|
|
349
|
-
if (_isHoveringPopup) return;
|
|
350
|
-
setTimeout(() => {
|
|
351
|
-
if (_isHoveringPopup) return;
|
|
352
|
-
const gd = getGalaxyDrawState();
|
|
353
|
-
if (gd && gd.zoom >= PREVIEW_ZOOM_THRESHOLD) {
|
|
354
|
-
hidePopup();
|
|
355
|
-
}
|
|
356
|
-
}, 50);
|
|
357
|
-
}, { passive: true });
|
|
382
|
+
// Wheel: scroll popup when hovering pill, Ctrl+wheel always zooms
|
|
383
|
+
viewportEl.addEventListener('wheel', onViewportWheel, { passive: false });
|
|
358
384
|
|
|
359
385
|
console.log('[file-preview] Initialized — full card preview below', (PREVIEW_ZOOM_THRESHOLD * 100).toFixed(0) + '% zoom');
|
|
360
386
|
}
|
|
@@ -365,6 +391,7 @@ export function initFilePreview(viewportEl: HTMLElement, ctx?: CanvasContext) {
|
|
|
365
391
|
export function destroyFilePreview(viewportEl: HTMLElement) {
|
|
366
392
|
viewportEl.removeEventListener('mousemove', onMouseMove);
|
|
367
393
|
viewportEl.removeEventListener('mouseout', onMouseOut);
|
|
394
|
+
viewportEl.removeEventListener('wheel', onViewportWheel);
|
|
368
395
|
if (popup) {
|
|
369
396
|
popup.remove();
|
|
370
397
|
popup = null;
|
|
@@ -272,6 +272,9 @@ export function renderAllFilesViaCardManager(ctx: CanvasContext, files: any[]) {
|
|
|
272
272
|
clearAllPills(ctx);
|
|
273
273
|
if (ctx.svgOverlay) ctx.svgOverlay.innerHTML = '';
|
|
274
274
|
|
|
275
|
+
const { resetCardGroups, restoreCollapsedDirs } = require('./card-groups');
|
|
276
|
+
resetCardGroups();
|
|
277
|
+
|
|
275
278
|
const visibleFiles = files.filter(f => !ctx.hiddenFiles.has(f.path));
|
|
276
279
|
updateHiddenUI(ctx);
|
|
277
280
|
|
|
@@ -432,6 +435,8 @@ export function renderAllFilesViaCardManager(ctx: CanvasContext, files: any[]) {
|
|
|
432
435
|
}
|
|
433
436
|
});
|
|
434
437
|
|
|
438
|
+
restoreCollapsedDirs(ctx);
|
|
439
|
+
|
|
435
440
|
console.log(`[gd-bridge] ${createdCount} created, ${deferredCount} deferred (${layerFiles.length} total)`);
|
|
436
441
|
return true; // Signal: we handled it
|
|
437
442
|
}
|