gitmaps 1.1.0 → 1.1.2
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 +267 -118
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/og-image/route.ts +14 -0
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/version/route.ts +26 -0
- package/app/globals.css +5706 -4938
- package/app/layout.tsx +1279 -490
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas.ts +625 -564
- package/app/lib/cards.tsx +1361 -916
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +71 -93
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -148
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -427
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/positions.ts +190 -121
- package/app/lib/recent-commits.test.ts +947 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -987
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -735
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -269
- package/app/page.tsx +15 -16
- package/app/state/machine.js +13 -0
- package/package.json +84 -75
- package/server.ts +10 -0
- package/app/[owner]/[repo]/page.tsx +0 -6
- package/app/[slug]/page.tsx +0 -6
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
package/app/lib/file-preview.ts
CHANGED
|
@@ -1,427 +1,486 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* File Preview — renders the EXACT same card component when hovering
|
|
4
|
-
* over pill placeholders or file cards at low zoom levels.
|
|
5
|
-
*
|
|
6
|
-
* Instead of a simplified tooltip with plain text, this clones or
|
|
7
|
-
* re-renders the full file card (diff markers, syntax highlighting,
|
|
8
|
-
* status badges, connections) and shows it in a fixed popup container
|
|
9
|
-
* at readable scale.
|
|
10
|
-
*
|
|
11
|
-
* Architecture:
|
|
12
|
-
* - Single shared popup container (avoids DOM thrashing)
|
|
13
|
-
* - Debounced show (180ms) to prevent flicker during fast panning
|
|
14
|
-
* - Looks up file data from ctx.fileCards (clone existing) or
|
|
15
|
-
* ctx.deferredCards (render fresh via createAllFileCard)
|
|
16
|
-
* - Positioned near cursor, clamped to viewport bounds
|
|
17
|
-
* - Hides on mouseout, zoom change above threshold, or scroll
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { getGalaxyDrawState } from
|
|
21
|
-
import type { CanvasContext } from
|
|
22
|
-
|
|
23
|
-
// ─── Config ──────────────────────────────────────────────
|
|
24
|
-
const PREVIEW_ZOOM_THRESHOLD = 0.25; // Match LOD_ZOOM_THRESHOLD in viewport-culling.ts
|
|
25
|
-
const SHOW_DELAY_MS = 180;
|
|
26
|
-
const OFFSET_X = 16;
|
|
27
|
-
const OFFSET_Y = 16;
|
|
28
|
-
const POPUP_MAX_W = 520;
|
|
29
|
-
const POPUP_MAX_H = 600;
|
|
30
|
-
|
|
31
|
-
// ─── State ───────────────────────────────────────────────
|
|
32
|
-
let popup: HTMLElement | null = null;
|
|
33
|
-
let showTimer: ReturnType<typeof setTimeout> | null = null;
|
|
34
|
-
let currentCardPath: string | null = null;
|
|
35
|
-
let isInitialized = false;
|
|
36
|
-
let _ctx: CanvasContext | null = null;
|
|
37
|
-
let isPreviewEnabled =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
max-
|
|
55
|
-
|
|
56
|
-
overflow-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
0
|
|
61
|
-
0 0
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
*
|
|
390
|
-
*/
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* File Preview — renders the EXACT same card component when hovering
|
|
4
|
+
* over pill placeholders or file cards at low zoom levels.
|
|
5
|
+
*
|
|
6
|
+
* Instead of a simplified tooltip with plain text, this clones or
|
|
7
|
+
* re-renders the full file card (diff markers, syntax highlighting,
|
|
8
|
+
* status badges, connections) and shows it in a fixed popup container
|
|
9
|
+
* at readable scale.
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* - Single shared popup container (avoids DOM thrashing)
|
|
13
|
+
* - Debounced show (180ms) to prevent flicker during fast panning
|
|
14
|
+
* - Looks up file data from ctx.fileCards (clone existing) or
|
|
15
|
+
* ctx.deferredCards (render fresh via createAllFileCard)
|
|
16
|
+
* - Positioned near cursor, clamped to viewport bounds
|
|
17
|
+
* - Hides on mouseout, zoom change above threshold, or scroll
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { getGalaxyDrawState } from "./xydraw-bridge";
|
|
21
|
+
import type { CanvasContext } from "./context";
|
|
22
|
+
|
|
23
|
+
// ─── Config ──────────────────────────────────────────────
|
|
24
|
+
const PREVIEW_ZOOM_THRESHOLD = 0.25; // Match LOD_ZOOM_THRESHOLD in viewport-culling.ts
|
|
25
|
+
const SHOW_DELAY_MS = 180;
|
|
26
|
+
const OFFSET_X = 16;
|
|
27
|
+
const OFFSET_Y = 16;
|
|
28
|
+
const POPUP_MAX_W = 520;
|
|
29
|
+
const POPUP_MAX_H = 600;
|
|
30
|
+
|
|
31
|
+
// ─── State ───────────────────────────────────────────────
|
|
32
|
+
let popup: HTMLElement | null = null;
|
|
33
|
+
let showTimer: ReturnType<typeof setTimeout> | null = null;
|
|
34
|
+
let currentCardPath: string | null = null;
|
|
35
|
+
let isInitialized = false;
|
|
36
|
+
let _ctx: CanvasContext | null = null;
|
|
37
|
+
let isPreviewEnabled =
|
|
38
|
+
localStorage.getItem("gitmaps:previewEnabled") !== "false"; // enabled by default
|
|
39
|
+
let _isHoveringPopup = false;
|
|
40
|
+
|
|
41
|
+
// ─── Popup container ─────────────────────────────────────
|
|
42
|
+
function ensurePopup(): HTMLElement {
|
|
43
|
+
if (popup) return popup;
|
|
44
|
+
|
|
45
|
+
popup = document.createElement("div");
|
|
46
|
+
popup.className = "file-preview-popup";
|
|
47
|
+
popup.style.cssText = `
|
|
48
|
+
position: fixed;
|
|
49
|
+
z-index: 9999;
|
|
50
|
+
pointer-events: auto;
|
|
51
|
+
opacity: 0;
|
|
52
|
+
transform: translateY(6px) scale(0.97);
|
|
53
|
+
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
54
|
+
max-width: ${POPUP_MAX_W}px;
|
|
55
|
+
max-height: ${POPUP_MAX_H}px;
|
|
56
|
+
overflow-y: auto;
|
|
57
|
+
overflow-x: hidden;
|
|
58
|
+
border-radius: 12px;
|
|
59
|
+
box-shadow:
|
|
60
|
+
0 12px 48px rgba(0, 0, 0, 0.6),
|
|
61
|
+
0 0 0 1px rgba(124, 58, 237, 0.25),
|
|
62
|
+
0 0 24px rgba(124, 58, 237, 0.12);
|
|
63
|
+
background: var(--bg-primary, #0a0a14);
|
|
64
|
+
`;
|
|
65
|
+
// Keep popup visible while hovering over it (for scrolling)
|
|
66
|
+
popup.addEventListener("mouseenter", () => {
|
|
67
|
+
_isHoveringPopup = true;
|
|
68
|
+
});
|
|
69
|
+
popup.addEventListener("mouseleave", () => {
|
|
70
|
+
_isHoveringPopup = false;
|
|
71
|
+
hidePopup();
|
|
72
|
+
});
|
|
73
|
+
// Capture wheel events to scroll popup content, not zoom canvas
|
|
74
|
+
popup.addEventListener(
|
|
75
|
+
"wheel",
|
|
76
|
+
(e) => {
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
// Manually scroll the popup content
|
|
79
|
+
popup.scrollTop += e.deltaY;
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
},
|
|
82
|
+
{ passive: false },
|
|
83
|
+
);
|
|
84
|
+
document.body.appendChild(popup);
|
|
85
|
+
return popup;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Render the full card preview inside the popup container.
|
|
90
|
+
* Strategy:
|
|
91
|
+
* 1. If the card is already materialized in ctx.fileCards → deep clone it
|
|
92
|
+
* 2. If it's deferred in ctx.deferredCards → render a fresh card
|
|
93
|
+
*
|
|
94
|
+
* Important: Canvas-text rendering (CanvasTextRenderer) doesn't survive
|
|
95
|
+
* cloning, so we always force DOM-based HTML rendering for previews.
|
|
96
|
+
*/
|
|
97
|
+
function renderPreviewCard(path: string): HTMLElement | null {
|
|
98
|
+
if (!_ctx) return null;
|
|
99
|
+
|
|
100
|
+
// Strategy 1: Clone existing materialized card
|
|
101
|
+
const existingCard = _ctx.fileCards.get(path);
|
|
102
|
+
if (existingCard) {
|
|
103
|
+
const clone = existingCard.cloneNode(true) as HTMLElement;
|
|
104
|
+
// Reset positioning — we'll position the popup itself
|
|
105
|
+
clone.style.position = "relative";
|
|
106
|
+
clone.style.left = "0";
|
|
107
|
+
clone.style.top = "0";
|
|
108
|
+
clone.style.display = "block"; // CRITICAL: cards are display:none in pill mode
|
|
109
|
+
clone.style.visibility = "visible";
|
|
110
|
+
clone.style.contentVisibility = "visible";
|
|
111
|
+
clone.style.opacity = "1";
|
|
112
|
+
clone.style.maxHeight = "none";
|
|
113
|
+
clone.style.width = `${POPUP_MAX_W - 2}px`;
|
|
114
|
+
clone.style.overflow = "visible";
|
|
115
|
+
clone.style.pointerEvents = "auto";
|
|
116
|
+
clone.style.transition = "none";
|
|
117
|
+
clone.style.transform = "none";
|
|
118
|
+
clone.style.outline = "none";
|
|
119
|
+
clone.style.boxShadow = "none";
|
|
120
|
+
delete clone.dataset.culled;
|
|
121
|
+
delete clone.dataset.expanded;
|
|
122
|
+
|
|
123
|
+
// Always re-render body with ALL lines (cards are 120-line limited)
|
|
124
|
+
const { _getCardFileData, _buildFileContentHTML } = require("./cards");
|
|
125
|
+
const file = _getCardFileData(existingCard);
|
|
126
|
+
if (file?.content) {
|
|
127
|
+
const addedLines = file.addedLines || new Set();
|
|
128
|
+
const deletedBeforeLine = file.deletedBeforeLine || new Map();
|
|
129
|
+
const isAllAdded = file.status === "added";
|
|
130
|
+
const isAllDeleted = file.status === "deleted";
|
|
131
|
+
const html = _buildFileContentHTML(
|
|
132
|
+
file.content,
|
|
133
|
+
file.layerSections,
|
|
134
|
+
addedLines,
|
|
135
|
+
deletedBeforeLine,
|
|
136
|
+
isAllAdded,
|
|
137
|
+
isAllDeleted,
|
|
138
|
+
true,
|
|
139
|
+
file.lines, // true = expanded, show ALL lines
|
|
140
|
+
);
|
|
141
|
+
// Replace body content with full file
|
|
142
|
+
const body = clone.querySelector(".file-card-body");
|
|
143
|
+
if (body) body.innerHTML = html;
|
|
144
|
+
// Also replace canvas-text container if present
|
|
145
|
+
const canvasContainer = clone.querySelector(".canvas-container");
|
|
146
|
+
if (canvasContainer) canvasContainer.outerHTML = html;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return clone;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Strategy 2: Render from deferred card data
|
|
153
|
+
const deferred = _ctx.deferredCards.get(path);
|
|
154
|
+
if (deferred) {
|
|
155
|
+
// Temporarily force DOM rendering (canvas-text doesn't work in detached elements)
|
|
156
|
+
const wasCanvasText = _ctx.useCanvasText;
|
|
157
|
+
_ctx.useCanvasText = false;
|
|
158
|
+
const { createAllFileCard } = require("./cards");
|
|
159
|
+
const card = createAllFileCard(
|
|
160
|
+
_ctx,
|
|
161
|
+
deferred.file,
|
|
162
|
+
0,
|
|
163
|
+
0,
|
|
164
|
+
null,
|
|
165
|
+
true,
|
|
166
|
+
) as HTMLElement;
|
|
167
|
+
_ctx.useCanvasText = wasCanvasText;
|
|
168
|
+
card.style.position = "relative";
|
|
169
|
+
card.style.left = "0";
|
|
170
|
+
card.style.top = "0";
|
|
171
|
+
card.style.maxHeight = "none";
|
|
172
|
+
card.style.width = `${POPUP_MAX_W - 2}px`;
|
|
173
|
+
card.style.overflow = "visible";
|
|
174
|
+
card.style.pointerEvents = "auto";
|
|
175
|
+
|
|
176
|
+
card.style.transition = "none";
|
|
177
|
+
return card;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Image preview support ───────────────────────────────
|
|
184
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
185
|
+
"png",
|
|
186
|
+
"jpg",
|
|
187
|
+
"jpeg",
|
|
188
|
+
"gif",
|
|
189
|
+
"svg",
|
|
190
|
+
"webp",
|
|
191
|
+
"ico",
|
|
192
|
+
"bmp",
|
|
193
|
+
"avif",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
function renderImagePreview(path: string): HTMLElement | null {
|
|
197
|
+
if (!_ctx) return null;
|
|
198
|
+
const state = _ctx.snap().context;
|
|
199
|
+
const repoPath = state.repoPath;
|
|
200
|
+
if (!repoPath) return null;
|
|
201
|
+
|
|
202
|
+
const container = document.createElement("div");
|
|
203
|
+
container.style.cssText = `
|
|
204
|
+
padding: 12px;
|
|
205
|
+
display: flex;
|
|
206
|
+
flex-direction: column;
|
|
207
|
+
align-items: center;
|
|
208
|
+
gap: 8px;
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
// File name label
|
|
212
|
+
const label = document.createElement("div");
|
|
213
|
+
label.style.cssText =
|
|
214
|
+
"font-size: 11px; color: var(--text-muted); font-family: monospace;";
|
|
215
|
+
label.textContent = path.split("/").pop() || path;
|
|
216
|
+
container.appendChild(label);
|
|
217
|
+
|
|
218
|
+
// Image element
|
|
219
|
+
const img = document.createElement("img");
|
|
220
|
+
img.src = `/api/repo/file-content?path=${encodeURIComponent(repoPath)}&filePath=file=${encodeURIComponent(path)}`;
|
|
221
|
+
img.alt = path;
|
|
222
|
+
img.style.cssText = `
|
|
223
|
+
max-width: ${POPUP_MAX_W - 24}px;
|
|
224
|
+
max-height: ${POPUP_MAX_H - 50}px;
|
|
225
|
+
object-fit: contain;
|
|
226
|
+
border-radius: 6px;
|
|
227
|
+
background: repeating-conic-gradient(#222 0% 25%, #333 0% 50%) 50% / 16px 16px; /* checkerboard for transparency */
|
|
228
|
+
`;
|
|
229
|
+
img.onerror = () => {
|
|
230
|
+
img.style.display = "none";
|
|
231
|
+
label.textContent = `⚠ Could not load: ${path.split("/").pop()}`;
|
|
232
|
+
};
|
|
233
|
+
container.appendChild(img);
|
|
234
|
+
|
|
235
|
+
return container;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function showPopup(path: string, screenX: number, screenY: number) {
|
|
239
|
+
// Cancel any pending show to prevent multiple popups
|
|
240
|
+
if (showTimer) {
|
|
241
|
+
clearTimeout(showTimer);
|
|
242
|
+
showTimer = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Hide existing popup first to prevent duplicates
|
|
246
|
+
if (popup && popup.style.opacity === "1") {
|
|
247
|
+
popup.style.opacity = "0";
|
|
248
|
+
popup.innerHTML = "";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const el = ensurePopup();
|
|
252
|
+
|
|
253
|
+
// Check if this is an image file
|
|
254
|
+
const ext = path.split(".").pop()?.toLowerCase() || "";
|
|
255
|
+
let previewContent: HTMLElement | null;
|
|
256
|
+
|
|
257
|
+
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
258
|
+
previewContent = renderImagePreview(path);
|
|
259
|
+
} else {
|
|
260
|
+
previewContent = renderPreviewCard(path);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!previewContent) {
|
|
264
|
+
hidePopup();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Clear previous and insert
|
|
269
|
+
el.innerHTML = "";
|
|
270
|
+
el.appendChild(previewContent);
|
|
271
|
+
|
|
272
|
+
// Position: near mouse, clamped to viewport
|
|
273
|
+
const vw = window.innerWidth;
|
|
274
|
+
const vh = window.innerHeight;
|
|
275
|
+
|
|
276
|
+
let x = screenX + OFFSET_X;
|
|
277
|
+
let y = screenY + OFFSET_Y;
|
|
278
|
+
|
|
279
|
+
// Clamp right edge
|
|
280
|
+
if (x + POPUP_MAX_W > vw - 12) x = screenX - POPUP_MAX_W - OFFSET_X;
|
|
281
|
+
// Clamp bottom edge
|
|
282
|
+
if (y + POPUP_MAX_H > vh - 12) y = screenY - POPUP_MAX_H - OFFSET_Y;
|
|
283
|
+
// Clamp left/top
|
|
284
|
+
x = Math.max(8, x);
|
|
285
|
+
y = Math.max(8, y);
|
|
286
|
+
|
|
287
|
+
el.style.left = `${x}px`;
|
|
288
|
+
el.style.top = `${y}px`;
|
|
289
|
+
el.style.opacity = "1";
|
|
290
|
+
el.style.transform = "translateY(0) scale(1)";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function hidePopup() {
|
|
294
|
+
if (showTimer) {
|
|
295
|
+
clearTimeout(showTimer);
|
|
296
|
+
showTimer = null;
|
|
297
|
+
}
|
|
298
|
+
currentCardPath = null;
|
|
299
|
+
if (popup) {
|
|
300
|
+
popup.style.opacity = "0";
|
|
301
|
+
popup.style.transform = "translateY(6px) scale(0.97)";
|
|
302
|
+
// Clear content after fade to free memory
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
if (popup && popup.style.opacity === "0") {
|
|
305
|
+
popup.innerHTML = "";
|
|
306
|
+
}
|
|
307
|
+
}, 200);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── Event handlers ──────────────────────────────────────
|
|
312
|
+
function onMouseMove(e: MouseEvent) {
|
|
313
|
+
if (!isPreviewEnabled) return;
|
|
314
|
+
if (_isHoveringPopup) return; // Don't hide while interacting with popup
|
|
315
|
+
|
|
316
|
+
// Suppress popup during canvas panning (middle-button drag, space-held, isDragging)
|
|
317
|
+
if (e.buttons & 4) {
|
|
318
|
+
hidePopup();
|
|
319
|
+
return;
|
|
320
|
+
} // middle mouse button held
|
|
321
|
+
if (e.buttons & 1) {
|
|
322
|
+
hidePopup();
|
|
323
|
+
return;
|
|
324
|
+
} // left mouse button held (dragging)
|
|
325
|
+
const viewport = document.querySelector(".canvas-viewport");
|
|
326
|
+
if (viewport?.classList.contains("space-panning")) {
|
|
327
|
+
hidePopup();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (_ctx?.isDragging) {
|
|
331
|
+
hidePopup();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const gdState = getGalaxyDrawState();
|
|
336
|
+
if (!gdState || gdState.zoom >= PREVIEW_ZOOM_THRESHOLD) {
|
|
337
|
+
hidePopup();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Find the closest pill card or file card ancestor
|
|
342
|
+
const target = e.target as HTMLElement;
|
|
343
|
+
const pill = target.closest?.(".file-pill") as HTMLElement | null;
|
|
344
|
+
const card = target.closest?.(".file-card") as HTMLElement | null;
|
|
345
|
+
const element = pill || card;
|
|
346
|
+
|
|
347
|
+
if (!element) {
|
|
348
|
+
hidePopup();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const path = element.dataset.path || "";
|
|
353
|
+
if (!path) {
|
|
354
|
+
hidePopup();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (path === currentCardPath) {
|
|
359
|
+
// Already showing for this card — DON'T reposition.
|
|
360
|
+
// Keep popup stationary so user can move their mouse to it for scrolling.
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// New card — debounce show
|
|
365
|
+
hidePopup();
|
|
366
|
+
currentCardPath = path;
|
|
367
|
+
showTimer = setTimeout(() => {
|
|
368
|
+
// Re-verify zoom is still low
|
|
369
|
+
const gd = getGalaxyDrawState();
|
|
370
|
+
if (!gd || gd.zoom >= PREVIEW_ZOOM_THRESHOLD) return;
|
|
371
|
+
showPopup(path, e.clientX, e.clientY);
|
|
372
|
+
}, SHOW_DELAY_MS);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function onMouseOut(e: MouseEvent) {
|
|
376
|
+
const related = e.relatedTarget as HTMLElement | null;
|
|
377
|
+
if (related?.closest?.(".file-pill") || related?.closest?.(".file-card"))
|
|
378
|
+
return;
|
|
379
|
+
// Don't hide if mouse moved to the popup itself
|
|
380
|
+
if (related?.closest?.(".file-preview-popup")) return;
|
|
381
|
+
if (_isHoveringPopup) return;
|
|
382
|
+
hidePopup();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Wheel handler on viewport:
|
|
387
|
+
* - If popup visible + mouse over pill/placeholder + NO Ctrl → scroll popup
|
|
388
|
+
* - If Ctrl held → always let canvas zoom (never intercept)
|
|
389
|
+
* - If zoom crosses threshold → hide popup
|
|
390
|
+
*/
|
|
391
|
+
function onViewportWheel(e: WheelEvent) {
|
|
392
|
+
// Ctrl+wheel = zoom → never intercept
|
|
393
|
+
if (e.ctrlKey || e.metaKey) {
|
|
394
|
+
// Check if zoom crossed threshold after a tick
|
|
395
|
+
if (currentCardPath) {
|
|
396
|
+
setTimeout(() => {
|
|
397
|
+
const gd = getGalaxyDrawState();
|
|
398
|
+
if (gd && gd.zoom >= PREVIEW_ZOOM_THRESHOLD) hidePopup();
|
|
399
|
+
}, 50);
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// If popup is visible and user is hovering over the pill/card that triggered it,
|
|
405
|
+
// forward wheel events to scroll the popup content
|
|
406
|
+
if (popup && currentCardPath && popup.style.opacity === "1") {
|
|
407
|
+
const target = e.target as HTMLElement;
|
|
408
|
+
const pill = target.closest?.(".file-pill") as HTMLElement | null;
|
|
409
|
+
const card = target.closest?.(".file-card") as HTMLElement | null;
|
|
410
|
+
const element = pill || card;
|
|
411
|
+
if (element && element.dataset.path === currentCardPath) {
|
|
412
|
+
// Only intercept if popup has scrollable content
|
|
413
|
+
if (popup.scrollHeight > popup.clientHeight) {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
e.stopPropagation();
|
|
416
|
+
popup.scrollTop += e.deltaY;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ─── Public API ──────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Initialize file preview on the canvas viewport.
|
|
426
|
+
* Call once after the canvas is mounted.
|
|
427
|
+
* @param viewportEl - The canvas viewport element
|
|
428
|
+
* @param ctx - The CanvasContext for looking up file data
|
|
429
|
+
*/
|
|
430
|
+
export function initFilePreview(viewportEl: HTMLElement, ctx?: CanvasContext) {
|
|
431
|
+
if (isInitialized) return;
|
|
432
|
+
isInitialized = true;
|
|
433
|
+
if (ctx) _ctx = ctx;
|
|
434
|
+
|
|
435
|
+
viewportEl.addEventListener("mousemove", onMouseMove, { passive: true });
|
|
436
|
+
viewportEl.addEventListener("mouseout", onMouseOut, { passive: true });
|
|
437
|
+
|
|
438
|
+
// Wheel: scroll popup when hovering pill, Ctrl+wheel always zooms
|
|
439
|
+
viewportEl.addEventListener("wheel", onViewportWheel, { passive: false });
|
|
440
|
+
|
|
441
|
+
console.log(
|
|
442
|
+
"[file-preview] Initialized — full card preview below",
|
|
443
|
+
(PREVIEW_ZOOM_THRESHOLD * 100).toFixed(0) + "% zoom",
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Destroy file preview. Call on cleanup.
|
|
449
|
+
*/
|
|
450
|
+
export function destroyFilePreview(viewportEl: HTMLElement) {
|
|
451
|
+
viewportEl.removeEventListener("mousemove", onMouseMove);
|
|
452
|
+
viewportEl.removeEventListener("mouseout", onMouseOut);
|
|
453
|
+
viewportEl.removeEventListener("wheel", onViewportWheel);
|
|
454
|
+
if (popup) {
|
|
455
|
+
popup.remove();
|
|
456
|
+
popup = null;
|
|
457
|
+
}
|
|
458
|
+
_ctx = null;
|
|
459
|
+
isInitialized = false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Toggle file preview on/off. Persists to localStorage.
|
|
464
|
+
*/
|
|
465
|
+
export function toggleFilePreview(): boolean {
|
|
466
|
+
isPreviewEnabled = !isPreviewEnabled;
|
|
467
|
+
localStorage.setItem("gitmaps:previewEnabled", String(isPreviewEnabled));
|
|
468
|
+
if (!isPreviewEnabled) hidePopup();
|
|
469
|
+
return isPreviewEnabled;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Set file preview enabled state. Persists to localStorage.
|
|
474
|
+
*/
|
|
475
|
+
export function setFilePreviewEnabled(enabled: boolean) {
|
|
476
|
+
isPreviewEnabled = enabled;
|
|
477
|
+
localStorage.setItem("gitmaps:previewEnabled", String(enabled));
|
|
478
|
+
if (!enabled) hidePopup();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get current preview enabled state.
|
|
483
|
+
*/
|
|
484
|
+
export function isFilePreviewEnabled(): boolean {
|
|
485
|
+
return isPreviewEnabled;
|
|
486
|
+
}
|