gitmaps 1.0.0 → 1.1.1
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 +265 -122
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/[owner]/[repo]/page.client.tsx +5 -0
- package/app/[slug]/page.client.tsx +5 -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/manifest.json/route.ts +20 -0
- package/app/api/og-image/route.ts +14 -0
- package/app/api/pwa-icon/route.ts +14 -0
- package/app/api/repo/clone-stream/route.ts +20 -12
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/imports/route.ts +21 -3
- package/app/api/repo/list/route.ts +30 -0
- 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/repo/upload/route.ts +6 -9
- package/app/api/sw.js/route.ts +70 -0
- package/app/api/version/route.ts +26 -0
- package/app/galaxy-canvas/page.client.tsx +2 -0
- package/app/galaxy-canvas/page.tsx +5 -0
- package/app/globals.css +5844 -4694
- package/app/icon.png +0 -0
- package/app/layout.tsx +1284 -467
- 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-text.ts +4 -72
- package/app/lib/canvas.ts +625 -564
- 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 +1361 -914
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/connections.tsx +34 -43
- 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 +76 -73
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -134
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -400
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/global-search.ts +48 -27
- 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/layers.tsx +17 -18
- 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/perf-overlay.ts +78 -0
- package/app/lib/positions.ts +191 -122
- package/app/lib/recent-commits.test.ts +869 -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 -977
- 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/shortcuts-panel.ts +2 -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 -728
- 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 -477
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -215
- package/app/page.tsx +27 -92
- package/app/state/machine.js +13 -0
- package/banner.png +0 -0
- package/package.json +17 -8
- package/server.ts +11 -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/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/cards.tsx
CHANGED
|
@@ -1,914 +1,1361 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* File cards — creation (diff + all-files), interaction (click/drag/resize),
|
|
4
|
-
* selection, arrangement, and the file modal.
|
|
5
|
-
*/
|
|
6
|
-
import { measure } from
|
|
7
|
-
import { render } from
|
|
8
|
-
import type { CanvasContext } from
|
|
9
|
-
import { escapeHtml, getFileIcon, getFileIconClass, showToast } from
|
|
10
|
-
import { hideSelectedFiles } from
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
{
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
</
|
|
737
|
-
<
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* File cards — creation (diff + all-files), interaction (click/drag/resize),
|
|
4
|
+
* selection, arrangement, and the file modal.
|
|
5
|
+
*/
|
|
6
|
+
import { measure } from "measure-fn";
|
|
7
|
+
import { render } from "melina/client";
|
|
8
|
+
import type { CanvasContext } from "./context";
|
|
9
|
+
import { escapeHtml, getFileIcon, getFileIconClass, showToast } from "./utils";
|
|
10
|
+
import { hideSelectedFiles } from "./hidden-files";
|
|
11
|
+
import { isFollower, detectRole } from "./role";
|
|
12
|
+
import {
|
|
13
|
+
savePosition,
|
|
14
|
+
getPositionKey,
|
|
15
|
+
isPathExpandedInPositions,
|
|
16
|
+
setPathExpandedInPositions,
|
|
17
|
+
} from "./positions";
|
|
18
|
+
import {
|
|
19
|
+
updateMinimap,
|
|
20
|
+
updateCanvasTransform,
|
|
21
|
+
updateZoomUI,
|
|
22
|
+
jumpToFile,
|
|
23
|
+
forceMinimapRebuild,
|
|
24
|
+
} from "./canvas";
|
|
25
|
+
import { updateStatusBarSelected } from "./status-bar";
|
|
26
|
+
import {
|
|
27
|
+
renderConnections,
|
|
28
|
+
scheduleRenderConnections,
|
|
29
|
+
setupConnectionDrag,
|
|
30
|
+
hasPendingConnection,
|
|
31
|
+
} from "./connections";
|
|
32
|
+
import { highlightSyntax, buildModalDiffHTML } from "./syntax";
|
|
33
|
+
import {
|
|
34
|
+
filterFileContentByLayer,
|
|
35
|
+
layerState,
|
|
36
|
+
createLayer,
|
|
37
|
+
addFileToLayer,
|
|
38
|
+
removeFileFromLayer,
|
|
39
|
+
getActiveLayer,
|
|
40
|
+
} from "./layers";
|
|
41
|
+
import { openFileChatInModal } from "./chat";
|
|
42
|
+
import {
|
|
43
|
+
buildDiffMarkerStrip as _buildDiffMarkerStrip,
|
|
44
|
+
setupDeletedLinesOverlay as _setupDeletedLinesOverlay,
|
|
45
|
+
} from "./card-diff-markers";
|
|
46
|
+
import { updateHiddenLinesIndicator as _updateHiddenLinesIndicator } from "./card-expand";
|
|
47
|
+
|
|
48
|
+
// ─── Constants ──────────────────────────────────────────
|
|
49
|
+
const CORNER_CURSORS = {
|
|
50
|
+
tl: "nwse-resize",
|
|
51
|
+
tr: "nesw-resize",
|
|
52
|
+
bl: "nesw-resize",
|
|
53
|
+
br: "nwse-resize",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Max lines rendered in DOM for collapsed (folded) cards.
|
|
57
|
+
// Files with more lines than this will show a truncated view until expanded with F.
|
|
58
|
+
// This is the #1 performance optimization — a 10K-line file produces 10K <span> elements
|
|
59
|
+
// which all participate in layout during pan/zoom, crushing frame rate.
|
|
60
|
+
const VISIBLE_LINE_LIMIT = 120;
|
|
61
|
+
|
|
62
|
+
const cardFileData = new WeakMap<HTMLElement, any>();
|
|
63
|
+
|
|
64
|
+
// ─── Accessor for cardFileData (used by card-expand.ts via lazy require) ──
|
|
65
|
+
export function _getCardFileData(card: HTMLElement) {
|
|
66
|
+
return cardFileData.get(card);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Expanded state persistence ─────────────────────────
|
|
70
|
+
// NOTE: Expanded state is now stored in the positions system (positions.ts)
|
|
71
|
+
// so it automatically syncs to the server for logged-in users.
|
|
72
|
+
// The old localStorage-only functions below are kept as thin wrappers
|
|
73
|
+
// for backward compatibility but should not be used directly.
|
|
74
|
+
// Use isPathExpandedInPositions / setPathExpandedInPositions from positions.ts.
|
|
75
|
+
|
|
76
|
+
/** @deprecated Use isPathExpandedInPositions(ctx, filePath) instead */
|
|
77
|
+
export function isPathExpanded(filePath: string): boolean {
|
|
78
|
+
// Legacy fallback: check localStorage for old data
|
|
79
|
+
// New code should use isPathExpandedInPositions which checks ctx.positions
|
|
80
|
+
const key = _getExpandedStorageKey();
|
|
81
|
+
if (!key) return false;
|
|
82
|
+
try {
|
|
83
|
+
const raw = localStorage.getItem(key);
|
|
84
|
+
if (raw) return new Set(JSON.parse(raw)).has(filePath);
|
|
85
|
+
} catch {}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @deprecated Use setPathExpandedInPositions(ctx, filePath, expanded) instead */
|
|
90
|
+
export function setPathExpanded(filePath: string, expanded: boolean) {
|
|
91
|
+
// Legacy: only used if ctx is not available
|
|
92
|
+
const key = _getExpandedStorageKey();
|
|
93
|
+
if (!key) return;
|
|
94
|
+
try {
|
|
95
|
+
const raw = localStorage.getItem(key);
|
|
96
|
+
const paths = raw ? new Set(JSON.parse(raw)) : new Set();
|
|
97
|
+
if (expanded) paths.add(filePath);
|
|
98
|
+
else paths.delete(filePath);
|
|
99
|
+
if (paths.size === 0) localStorage.removeItem(key);
|
|
100
|
+
else localStorage.setItem(key, JSON.stringify(Array.from(paths)));
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _getExpandedStorageKey(): string | null {
|
|
105
|
+
const hashSlug = decodeURIComponent(window.location.hash.replace("#", ""));
|
|
106
|
+
const repo =
|
|
107
|
+
(hashSlug && localStorage.getItem(`gitcanvas:slug:${hashSlug}`)) ||
|
|
108
|
+
localStorage.getItem("gitcanvas:lastRepo");
|
|
109
|
+
if (!repo) return null;
|
|
110
|
+
return `gitcanvas:expanded:${repo}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Selection highlights ───────────────────────────────
|
|
114
|
+
export function updateSelectionHighlights(ctx: CanvasContext) {
|
|
115
|
+
const selected = ctx.snap().context.selectedCards;
|
|
116
|
+
ctx.fileCards.forEach((card, path) => {
|
|
117
|
+
card.classList.toggle("selected", selected.includes(path));
|
|
118
|
+
});
|
|
119
|
+
updateStatusBarSelected(selected.length);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function clearSelectionHighlights(ctx: CanvasContext) {
|
|
123
|
+
ctx.fileCards.forEach((card) => card.classList.remove("selected"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Arrange toolbar visibility ─────────────────────────
|
|
127
|
+
export function updateArrangeToolbar(ctx: CanvasContext) {
|
|
128
|
+
measure("arrange:updateToolbar", () => {
|
|
129
|
+
const toolbar = document.getElementById("arrangeToolbar");
|
|
130
|
+
if (!toolbar) return;
|
|
131
|
+
const selected = ctx.snap().context.selectedCards;
|
|
132
|
+
toolbar.style.display = selected.length >= 2 ? "flex" : "none";
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Corner detection for resize ────────────────────────
|
|
137
|
+
function isNearCorner(
|
|
138
|
+
e: MouseEvent,
|
|
139
|
+
card: HTMLElement,
|
|
140
|
+
cornerSize: number,
|
|
141
|
+
zoom: number,
|
|
142
|
+
): string | null {
|
|
143
|
+
const rect = card.getBoundingClientRect();
|
|
144
|
+
const x = e.clientX - rect.left;
|
|
145
|
+
const y = e.clientY - rect.top;
|
|
146
|
+
const w = rect.width;
|
|
147
|
+
const h = rect.height;
|
|
148
|
+
// Scale corner hit-area: base size + proportion of card size, capped at 80px
|
|
149
|
+
// This makes large cards much easier to grab at corners
|
|
150
|
+
const dynamicCorner = Math.min(
|
|
151
|
+
80,
|
|
152
|
+
Math.max(cornerSize, Math.min(w, h) * 0.12),
|
|
153
|
+
);
|
|
154
|
+
const c = dynamicCorner * zoom;
|
|
155
|
+
|
|
156
|
+
if (x > w - c && y > h - c) return "br";
|
|
157
|
+
if (x < c && y > h - c) return "bl";
|
|
158
|
+
if (x > w - c && y < c) return "tr";
|
|
159
|
+
if (x < c && y < c) return "tl";
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Setup card interaction (click-select + drag) ────────
|
|
164
|
+
export function setupCardInteraction(
|
|
165
|
+
ctx: CanvasContext,
|
|
166
|
+
card: HTMLElement,
|
|
167
|
+
commitHash: string,
|
|
168
|
+
) {
|
|
169
|
+
// Follower mode: read-only, no drag/edit
|
|
170
|
+
const role = detectRole();
|
|
171
|
+
console.log(
|
|
172
|
+
`[cards] setupCardInteraction: ${role} mode for ${card.dataset.path}`,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (role === "follower") {
|
|
176
|
+
card.style.cursor = "default";
|
|
177
|
+
card.style.pointerEvents = "auto";
|
|
178
|
+
card.addEventListener("click", (e) => {
|
|
179
|
+
// Click to select is still allowed
|
|
180
|
+
const filePath = card.dataset.path || "";
|
|
181
|
+
ctx.actor.send({
|
|
182
|
+
type: "SELECT_CARD",
|
|
183
|
+
path: filePath,
|
|
184
|
+
shift: e.shiftKey || e.ctrlKey,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
// Block drag by preventing mousedown
|
|
188
|
+
card.addEventListener("mousedown", (e) => {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
e.stopPropagation();
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let action = null; // null | 'move' | 'pending'
|
|
196
|
+
let startX: number, startY: number;
|
|
197
|
+
let moveStartPositions: any[] = [];
|
|
198
|
+
let rafPending = false;
|
|
199
|
+
const DRAG_THRESHOLD = 3;
|
|
200
|
+
|
|
201
|
+
function onMouseDown(e) {
|
|
202
|
+
// Only respond to left-click (button 0). Middle-click/right-click should not start card interaction.
|
|
203
|
+
if (e.button !== 0) return;
|
|
204
|
+
if (e.target.tagName === "BUTTON" || e.target.closest("button")) return;
|
|
205
|
+
const bodyEl = e.target.closest(".file-card-body");
|
|
206
|
+
if (
|
|
207
|
+
bodyEl &&
|
|
208
|
+
(e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight)
|
|
209
|
+
)
|
|
210
|
+
return;
|
|
211
|
+
|
|
212
|
+
// If a connection is pending and the click is inside body (on a diff-line),
|
|
213
|
+
// don't start drag — let the connection click handler handle it
|
|
214
|
+
if (
|
|
215
|
+
hasPendingConnection() &&
|
|
216
|
+
bodyEl &&
|
|
217
|
+
(e.target as HTMLElement).closest(".diff-line")
|
|
218
|
+
)
|
|
219
|
+
return;
|
|
220
|
+
|
|
221
|
+
e.stopPropagation();
|
|
222
|
+
startX = e.clientX;
|
|
223
|
+
startY = e.clientY;
|
|
224
|
+
action = "pending";
|
|
225
|
+
|
|
226
|
+
window.addEventListener("mousemove", onMouseMove);
|
|
227
|
+
window.addEventListener("mouseup", onMouseUp);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function onMouseMove(e) {
|
|
231
|
+
const state = ctx.snap().context;
|
|
232
|
+
const dx = (e.clientX - startX) / state.zoom;
|
|
233
|
+
const dy = (e.clientY - startY) / state.zoom;
|
|
234
|
+
|
|
235
|
+
if (action === "pending") {
|
|
236
|
+
const screenDist = Math.sqrt(
|
|
237
|
+
(e.clientX - startX) ** 2 + (e.clientY - startY) ** 2,
|
|
238
|
+
);
|
|
239
|
+
if (screenDist < DRAG_THRESHOLD) return;
|
|
240
|
+
|
|
241
|
+
action = "move";
|
|
242
|
+
card.style.cursor = "move";
|
|
243
|
+
|
|
244
|
+
const selected = ctx.snap().context.selectedCards;
|
|
245
|
+
const cardPath = card.dataset.path;
|
|
246
|
+
if (!selected.includes(cardPath)) {
|
|
247
|
+
if (!e.shiftKey && !e.ctrlKey) {
|
|
248
|
+
ctx.actor.send({ type: "SELECT_CARD", path: cardPath, shift: false });
|
|
249
|
+
} else {
|
|
250
|
+
ctx.actor.send({ type: "SELECT_CARD", path: cardPath, shift: true });
|
|
251
|
+
}
|
|
252
|
+
updateSelectionHighlights(ctx);
|
|
253
|
+
updateArrangeToolbar(ctx);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const nowSelected = ctx.snap().context.selectedCards;
|
|
257
|
+
moveStartPositions = [];
|
|
258
|
+
nowSelected.forEach((path) => {
|
|
259
|
+
const c = ctx.fileCards.get(path);
|
|
260
|
+
if (c) {
|
|
261
|
+
c.style.cursor = "grabbing";
|
|
262
|
+
moveStartPositions.push({
|
|
263
|
+
card: c,
|
|
264
|
+
path,
|
|
265
|
+
startLeft: parseInt(c.style.left) || 0,
|
|
266
|
+
startTop: parseInt(c.style.top) || 0,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (action === "move") {
|
|
273
|
+
moveStartPositions.forEach((info) => {
|
|
274
|
+
info.card.style.left = `${info.startLeft + dx}px`;
|
|
275
|
+
info.card.style.top = `${info.startTop + dy}px`;
|
|
276
|
+
});
|
|
277
|
+
// Throttle expensive DOM updates to once per frame
|
|
278
|
+
if (!rafPending) {
|
|
279
|
+
rafPending = true;
|
|
280
|
+
requestAnimationFrame(() => {
|
|
281
|
+
rafPending = false;
|
|
282
|
+
scheduleRenderConnections(ctx);
|
|
283
|
+
updateMinimap(ctx);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function onMouseUp(e) {
|
|
291
|
+
window.removeEventListener("mousemove", onMouseMove);
|
|
292
|
+
window.removeEventListener("mouseup", onMouseUp);
|
|
293
|
+
|
|
294
|
+
if (action === "pending") {
|
|
295
|
+
if (e.shiftKey || e.ctrlKey) {
|
|
296
|
+
ctx.actor.send({
|
|
297
|
+
type: "SELECT_CARD",
|
|
298
|
+
path: card.dataset.path,
|
|
299
|
+
shift: true,
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
ctx.actor.send({
|
|
303
|
+
type: "SELECT_CARD",
|
|
304
|
+
path: card.dataset.path,
|
|
305
|
+
shift: false,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
updateSelectionHighlights(ctx);
|
|
309
|
+
updateArrangeToolbar(ctx);
|
|
310
|
+
} else if (action === "move") {
|
|
311
|
+
document.body.style.cursor = "";
|
|
312
|
+
card.style.cursor = "";
|
|
313
|
+
moveStartPositions.forEach((info) => {
|
|
314
|
+
info.card.style.cursor = "";
|
|
315
|
+
});
|
|
316
|
+
moveStartPositions.forEach((info) => {
|
|
317
|
+
const x = parseInt(info.card.style.left) || 0;
|
|
318
|
+
const y = parseInt(info.card.style.top) || 0;
|
|
319
|
+
savePosition(ctx, commitHash, info.path, x, y, undefined, undefined, true);
|
|
320
|
+
});
|
|
321
|
+
moveStartPositions = [];
|
|
322
|
+
// Force minimap rebuild so dot positions reflect the drag result
|
|
323
|
+
forceMinimapRebuild(ctx);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
action = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
card.addEventListener("mousedown", onMouseDown);
|
|
330
|
+
|
|
331
|
+
// ── Double-click to open in editor modal ──
|
|
332
|
+
card.addEventListener("dblclick", (e) => {
|
|
333
|
+
// Don't trigger on buttons
|
|
334
|
+
if (
|
|
335
|
+
(e.target as HTMLElement).tagName === "BUTTON" ||
|
|
336
|
+
(e.target as HTMLElement).closest("button")
|
|
337
|
+
)
|
|
338
|
+
return;
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
e.stopPropagation();
|
|
341
|
+
|
|
342
|
+
const filePath = card.dataset.path;
|
|
343
|
+
if (filePath) {
|
|
344
|
+
const file = ctx.allFilesData?.find((f) => f.path === filePath) || {
|
|
345
|
+
path: filePath,
|
|
346
|
+
name: filePath.split("/").pop(),
|
|
347
|
+
lines: 0,
|
|
348
|
+
};
|
|
349
|
+
// Read visible line from canvas text renderer scroll position
|
|
350
|
+
const renderer = (card as any)._canvasTextRenderer;
|
|
351
|
+
const initialLine = renderer ? renderer.getVisibleLine?.() : undefined;
|
|
352
|
+
import("./file-modal").then(({ openFileModal }) =>
|
|
353
|
+
openFileModal(ctx, file, undefined, initialLine),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// ── Right-click context menu ──
|
|
359
|
+
card.addEventListener("contextmenu", (e) => {
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
e.stopPropagation();
|
|
362
|
+
showCardContextMenu(ctx, card, e.clientX, e.clientY);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ─── Context menu & file history (extracted to card-context-menu.tsx) ────
|
|
367
|
+
import { showCardContextMenu, showFileHistory } from "./card-context-menu";
|
|
368
|
+
export { showCardContextMenu, showFileHistory };
|
|
369
|
+
|
|
370
|
+
// ─── Arrangement functions (extracted to card-arrangement.ts) ────
|
|
371
|
+
import { arrangeRow, arrangeColumn, arrangeGrid } from "./card-arrangement";
|
|
372
|
+
export { arrangeRow, arrangeColumn, arrangeGrid };
|
|
373
|
+
|
|
374
|
+
// ─── Scroll debounce ────────────────────────────────────
|
|
375
|
+
export function debounceSaveScroll(
|
|
376
|
+
ctx: CanvasContext,
|
|
377
|
+
filePath: string,
|
|
378
|
+
scrollTop: number,
|
|
379
|
+
) {
|
|
380
|
+
if (ctx.scrollTimers[filePath]) clearTimeout(ctx.scrollTimers[filePath]);
|
|
381
|
+
ctx.scrollTimers[filePath] = setTimeout(() => {
|
|
382
|
+
ctx.actor.send({ type: "SAVE_SCROLL", path: filePath, scrollTop });
|
|
383
|
+
savePosition(ctx, "scroll", filePath, scrollTop, 0);
|
|
384
|
+
}, 300);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ─── JSX sub-components for file card content ───────────
|
|
388
|
+
|
|
389
|
+
function DiffLine({
|
|
390
|
+
type,
|
|
391
|
+
lineNum,
|
|
392
|
+
content,
|
|
393
|
+
}: {
|
|
394
|
+
type: string;
|
|
395
|
+
lineNum: number;
|
|
396
|
+
content: string;
|
|
397
|
+
}) {
|
|
398
|
+
return (
|
|
399
|
+
<span className={`diff-line diff-${type}`} data-line={lineNum}>
|
|
400
|
+
<span className="line-num">{String(lineNum).padStart(4, " ")}</span>
|
|
401
|
+
{content}
|
|
402
|
+
</span>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function DiffHunk({ hunk, hunkIdx }: { hunk: any; hunkIdx: number }) {
|
|
407
|
+
const header = `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@${hunk.context ? " " + hunk.context : ""}`;
|
|
408
|
+
let oldLine = hunk.oldStart;
|
|
409
|
+
let newLine = hunk.newStart;
|
|
410
|
+
|
|
411
|
+
const currentItems: { type: string; ln: number; content: string }[] = [];
|
|
412
|
+
const previousItems: { type: string; ln: number; content: string }[] = [];
|
|
413
|
+
|
|
414
|
+
hunk.lines.forEach((l: any) => {
|
|
415
|
+
if (l.type === "add") {
|
|
416
|
+
currentItems.push({ type: "add", ln: newLine++, content: l.content });
|
|
417
|
+
} else if (l.type === "del") {
|
|
418
|
+
previousItems.push({ type: "del", ln: oldLine++, content: l.content });
|
|
419
|
+
} else {
|
|
420
|
+
const curLn = newLine++;
|
|
421
|
+
const prevLn = oldLine++;
|
|
422
|
+
currentItems.push({ type: "ctx", ln: curLn, content: l.content });
|
|
423
|
+
previousItems.push({ type: "ctx", ln: prevLn, content: l.content });
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const hasDeletions = previousItems.some((l) => l.type === "del");
|
|
428
|
+
|
|
429
|
+
function toggle(e: Event, view: string) {
|
|
430
|
+
e.stopPropagation();
|
|
431
|
+
const hunkEl = (e.target as HTMLElement).closest(".diff-hunk");
|
|
432
|
+
if (!hunkEl) return;
|
|
433
|
+
hunkEl
|
|
434
|
+
.querySelectorAll(".hunk-toggle-btn")
|
|
435
|
+
.forEach((b) => b.classList.remove("active"));
|
|
436
|
+
(e.target as HTMLElement).classList.add("active");
|
|
437
|
+
const cur = hunkEl.querySelector(".hunk-pane--current") as HTMLElement;
|
|
438
|
+
const prev = hunkEl.querySelector(".hunk-pane--previous") as HTMLElement;
|
|
439
|
+
if (cur) cur.style.display = view === "current" ? "" : "none";
|
|
440
|
+
if (prev) prev.style.display = view === "previous" ? "" : "none";
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<div className="diff-hunk">
|
|
445
|
+
<div className="diff-hunk-header">
|
|
446
|
+
<span className="hunk-range">{header}</span>
|
|
447
|
+
{hasDeletions ? (
|
|
448
|
+
<span className="hunk-view-toggle" data-hunk={hunkIdx}>
|
|
449
|
+
<button
|
|
450
|
+
className="hunk-toggle-btn active"
|
|
451
|
+
data-view="current"
|
|
452
|
+
onclick={(e) => toggle(e, "current")}
|
|
453
|
+
>
|
|
454
|
+
Current
|
|
455
|
+
</button>
|
|
456
|
+
<button
|
|
457
|
+
className="hunk-toggle-btn"
|
|
458
|
+
data-view="previous"
|
|
459
|
+
onclick={(e) => toggle(e, "previous")}
|
|
460
|
+
>
|
|
461
|
+
Previous
|
|
462
|
+
</button>
|
|
463
|
+
</span>
|
|
464
|
+
) : null}
|
|
465
|
+
</div>
|
|
466
|
+
<div className="diff-hunk-body">
|
|
467
|
+
<div className="hunk-pane hunk-pane--current">
|
|
468
|
+
<pre>
|
|
469
|
+
<code>
|
|
470
|
+
{currentItems.map((l) => (
|
|
471
|
+
<DiffLine type={l.type} lineNum={l.ln} content={l.content} />
|
|
472
|
+
))}
|
|
473
|
+
</code>
|
|
474
|
+
</pre>
|
|
475
|
+
</div>
|
|
476
|
+
<div className="hunk-pane hunk-pane--previous" style="display:none">
|
|
477
|
+
<pre>
|
|
478
|
+
<code>
|
|
479
|
+
{previousItems.map((l) => (
|
|
480
|
+
<DiffLine type={l.type} lineNum={l.ln} content={l.content} />
|
|
481
|
+
))}
|
|
482
|
+
</code>
|
|
483
|
+
</pre>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function FileCardContent({ file }: { file: any }) {
|
|
491
|
+
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
|
492
|
+
const IMAGE_EXTS = new Set([
|
|
493
|
+
"png",
|
|
494
|
+
"jpg",
|
|
495
|
+
"jpeg",
|
|
496
|
+
"gif",
|
|
497
|
+
"webp",
|
|
498
|
+
"svg",
|
|
499
|
+
"bmp",
|
|
500
|
+
"ico",
|
|
501
|
+
]);
|
|
502
|
+
const isImage = IMAGE_EXTS.has(ext);
|
|
503
|
+
|
|
504
|
+
if (isImage) {
|
|
505
|
+
const repoPath = (window as any).__GITCANVAS_REPO_PATH__ || "";
|
|
506
|
+
return (
|
|
507
|
+
<div
|
|
508
|
+
className="file-content-preview file-image-preview"
|
|
509
|
+
style={{
|
|
510
|
+
display: "flex",
|
|
511
|
+
alignItems: "center",
|
|
512
|
+
justifyContent: "center",
|
|
513
|
+
height: "100%",
|
|
514
|
+
background: "var(--bg-card)",
|
|
515
|
+
overflow: "hidden",
|
|
516
|
+
}}
|
|
517
|
+
>
|
|
518
|
+
<img
|
|
519
|
+
src={`/api/repo/file-content?path=${encodeURIComponent(repoPath)}&file=${encodeURIComponent(file.path)}`}
|
|
520
|
+
alt={file.name}
|
|
521
|
+
style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain" }}
|
|
522
|
+
loading="lazy"
|
|
523
|
+
/>
|
|
524
|
+
</div>
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (file.status === "added" && file.content) {
|
|
529
|
+
const lines = file.content.split("\n");
|
|
530
|
+
return (
|
|
531
|
+
<div className="file-content-preview">
|
|
532
|
+
<pre>
|
|
533
|
+
<code>
|
|
534
|
+
{lines.map((line, i) =>
|
|
535
|
+
!file.visibleLineIndices || file.visibleLineIndices.has(i) ? (
|
|
536
|
+
<DiffLine type="add" lineNum={i + 1} content={line} />
|
|
537
|
+
) : null,
|
|
538
|
+
)}
|
|
539
|
+
</code>
|
|
540
|
+
</pre>
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (file.status === "deleted" && file.content) {
|
|
545
|
+
const lines = file.content.split("\n");
|
|
546
|
+
return (
|
|
547
|
+
<div className="file-content-preview">
|
|
548
|
+
<pre>
|
|
549
|
+
<code>
|
|
550
|
+
{lines.map((line, i) =>
|
|
551
|
+
!file.visibleLineIndices || file.visibleLineIndices.has(i) ? (
|
|
552
|
+
<DiffLine type="del" lineNum={i + 1} content={line} />
|
|
553
|
+
) : null,
|
|
554
|
+
)}
|
|
555
|
+
</code>
|
|
556
|
+
</pre>
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (
|
|
561
|
+
(file.status === "modified" ||
|
|
562
|
+
file.status === "renamed" ||
|
|
563
|
+
file.status === "copied") &&
|
|
564
|
+
file.hunks?.length > 0
|
|
565
|
+
) {
|
|
566
|
+
return (
|
|
567
|
+
<div className="file-content-preview">
|
|
568
|
+
{file.hunks.map((hunk, idx) => (
|
|
569
|
+
<DiffHunk hunk={hunk} hunkIdx={idx} />
|
|
570
|
+
))}
|
|
571
|
+
</div>
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (
|
|
575
|
+
(file.status === "renamed" || file.status === "copied") &&
|
|
576
|
+
(!file.hunks || file.hunks.length === 0)
|
|
577
|
+
) {
|
|
578
|
+
const simText = file.similarity ? ` (${file.similarity}% similar)` : "";
|
|
579
|
+
return (
|
|
580
|
+
<div className="file-content-preview">
|
|
581
|
+
<pre>
|
|
582
|
+
<code>
|
|
583
|
+
<span className="rename-notice">
|
|
584
|
+
{"File " + file.status + simText + "\nNo content changes"}
|
|
585
|
+
</span>
|
|
586
|
+
</code>
|
|
587
|
+
</pre>
|
|
588
|
+
</div>
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const msg = file.contentError || "No changes to display";
|
|
592
|
+
return (
|
|
593
|
+
<div className="file-content-preview">
|
|
594
|
+
<pre>
|
|
595
|
+
<code>
|
|
596
|
+
<span className="error-notice">{msg}</span>
|
|
597
|
+
</code>
|
|
598
|
+
</pre>
|
|
599
|
+
</div>
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
604
|
+
added: "#22c55e",
|
|
605
|
+
modified: "#eab308",
|
|
606
|
+
deleted: "#ef4444",
|
|
607
|
+
renamed: "#a78bfa",
|
|
608
|
+
copied: "#60a5fa",
|
|
609
|
+
};
|
|
610
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
611
|
+
added: "+ ADDED",
|
|
612
|
+
modified: "~ MODIFIED",
|
|
613
|
+
deleted: "- DELETED",
|
|
614
|
+
renamed: "→ RENAMED",
|
|
615
|
+
copied: "⊕ COPIED",
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
function _handleChatClick(ctx: CanvasContext, file: any) {
|
|
619
|
+
const filePath = file.path;
|
|
620
|
+
const content = file.content || "";
|
|
621
|
+
const status = file.status || "";
|
|
622
|
+
|
|
623
|
+
let extraContext = "";
|
|
624
|
+
|
|
625
|
+
if (file.hunks && file.hunks.length > 0) {
|
|
626
|
+
extraContext += `\n--- DIFF SUMMARY ---\n`;
|
|
627
|
+
extraContext += file.hunks
|
|
628
|
+
.map(
|
|
629
|
+
(h: any) =>
|
|
630
|
+
`@@ -${h.oldStart},${h.oldCount} +${h.newStart},${h.newCount} @@\n` +
|
|
631
|
+
h.lines
|
|
632
|
+
.map(
|
|
633
|
+
(l: any) =>
|
|
634
|
+
`${l.type === "add" ? "+" : l.type === "del" ? "-" : " "} ${l.content}`,
|
|
635
|
+
)
|
|
636
|
+
.join("\n"),
|
|
637
|
+
)
|
|
638
|
+
.join("\n");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const connections = ctx.snap().context.connections;
|
|
642
|
+
const relatedLinks = connections.filter(
|
|
643
|
+
(c: any) => c.sourceFile === filePath || c.targetFile === filePath,
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
if (relatedLinks.length > 0) {
|
|
647
|
+
extraContext += `\n\n--- ARCHITECTURE CONNECTIONS ---\n`;
|
|
648
|
+
extraContext += `This file is logically connected to the following modules in the visual graph:\n`;
|
|
649
|
+
relatedLinks.forEach((c: any) => {
|
|
650
|
+
if (c.sourceFile === filePath) {
|
|
651
|
+
extraContext += `- Outbound dependency on \`${c.targetFile}\` (Lines ${c.sourceLineStart}-${c.sourceLineEnd} -> Lines ${c.targetLineStart}-${c.targetLineEnd}). Note: "${c.comment || "None"}"\n`;
|
|
652
|
+
} else {
|
|
653
|
+
extraContext += `- Inbound dependency from \`${c.sourceFile}\` (Lines ${c.sourceLineStart}-${c.sourceLineEnd} -> Lines ${c.targetLineStart}-${c.targetLineEnd}). Note: "${c.comment || "None"}"\n`;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const repoPath = ctx.snap().context.repoPath;
|
|
659
|
+
openFileChatInModal(repoPath, filePath, content, status, extraContext);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ─── Create file card (commit diff) ─────────────────────
|
|
663
|
+
export function createFileCard(
|
|
664
|
+
ctx: CanvasContext,
|
|
665
|
+
file: any,
|
|
666
|
+
x: number,
|
|
667
|
+
y: number,
|
|
668
|
+
commitHash: string,
|
|
669
|
+
skipInteraction = false,
|
|
670
|
+
): HTMLElement {
|
|
671
|
+
const card = document.createElement("div");
|
|
672
|
+
card.className = `file-card file-card--${file.status || "modified"}`;
|
|
673
|
+
card.style.left = `${x}px`;
|
|
674
|
+
card.style.top = `${y}px`;
|
|
675
|
+
card.dataset.path = file.path;
|
|
676
|
+
|
|
677
|
+
if (file.layerSections && file.layerSections.length > 0) {
|
|
678
|
+
if (file.content) {
|
|
679
|
+
const { visibleLineIndices } = filterFileContentByLayer(
|
|
680
|
+
file.content,
|
|
681
|
+
file.layerSections,
|
|
682
|
+
);
|
|
683
|
+
file.visibleLineIndices = visibleLineIndices;
|
|
684
|
+
}
|
|
685
|
+
if (file.hunks) {
|
|
686
|
+
// Very simplistic filtering for hunks
|
|
687
|
+
file.hunks = file.hunks.filter((h) => {
|
|
688
|
+
// If the hunk's content has ANY line overlapping with visible lines, keep it. But we don't have exactly the full file contents to compare.
|
|
689
|
+
// Keep all hunks for now if layers view, else users might miss diffs.
|
|
690
|
+
return true;
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Apply saved size
|
|
696
|
+
const posKey = getPositionKey(file.path, commitHash);
|
|
697
|
+
if (ctx.positions.has(posKey)) {
|
|
698
|
+
const pos = ctx.positions.get(posKey);
|
|
699
|
+
if (pos.width) card.style.width = `${pos.width}px`;
|
|
700
|
+
if (pos.height) {
|
|
701
|
+
card.style.height = `${pos.height}px`;
|
|
702
|
+
card.style.maxHeight = `${pos.height}px`;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const ext = file.name.split(".").pop().toLowerCase();
|
|
707
|
+
const iconClass = getFileIconClass(ext);
|
|
708
|
+
const statusColor = STATUS_COLORS[file.status] || "#a855f7";
|
|
709
|
+
const statusLabel =
|
|
710
|
+
STATUS_LABELS[file.status] || file.status?.toUpperCase() || "CHANGED";
|
|
711
|
+
const hunkCount = file.hunks?.length || 0;
|
|
712
|
+
const metaInfo =
|
|
713
|
+
hunkCount > 0
|
|
714
|
+
? `${hunkCount} hunk${hunkCount > 1 ? "s" : ""}`
|
|
715
|
+
: `${file.lines || 0} lines`;
|
|
716
|
+
|
|
717
|
+
const iconSvg = getFileIcon(file.type, ext);
|
|
718
|
+
|
|
719
|
+
// Render JSX into card
|
|
720
|
+
render(
|
|
721
|
+
<>
|
|
722
|
+
<div
|
|
723
|
+
className="file-card-header"
|
|
724
|
+
style={`border-left: 4px solid ${statusColor}`}
|
|
725
|
+
>
|
|
726
|
+
<div
|
|
727
|
+
className={`file-icon ${iconClass}`}
|
|
728
|
+
dangerouslySetInnerHTML={{ __html: iconSvg }}
|
|
729
|
+
/>
|
|
730
|
+
<span className="file-name">{file.name}</span>
|
|
731
|
+
<span
|
|
732
|
+
className="file-status"
|
|
733
|
+
style={`background: ${statusColor}20; color: ${statusColor}; font-size: 11px; padding: 2px 8px; border-radius: 4px; font-weight: 600;`}
|
|
734
|
+
>
|
|
735
|
+
{statusLabel}
|
|
736
|
+
</span>
|
|
737
|
+
<span style="font-size: 10px; color: var(--text-muted); margin-left: auto;">
|
|
738
|
+
{metaInfo}
|
|
739
|
+
</span>
|
|
740
|
+
</div>
|
|
741
|
+
<div className="file-card-body">
|
|
742
|
+
{file.oldPath ? (
|
|
743
|
+
<div className="file-rename-path">
|
|
744
|
+
{file.oldPath} → {file.path}
|
|
745
|
+
{file.similarity ? (
|
|
746
|
+
<span className="rename-similarity">{file.similarity}%</span>
|
|
747
|
+
) : null}
|
|
748
|
+
</div>
|
|
749
|
+
) : (
|
|
750
|
+
<div className="file-path">{file.path}</div>
|
|
751
|
+
)}
|
|
752
|
+
<FileCardContent file={file} />
|
|
753
|
+
</div>
|
|
754
|
+
</>,
|
|
755
|
+
card,
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
cardFileData.set(card, file);
|
|
759
|
+
|
|
760
|
+
// When managed by CardManager, skip legacy drag/resize/z-order setup
|
|
761
|
+
// but ALWAYS attach context menu, double-click, and click-to-select
|
|
762
|
+
if (!skipInteraction) {
|
|
763
|
+
setupCardInteraction(ctx, card, commitHash);
|
|
764
|
+
} else {
|
|
765
|
+
card.addEventListener("contextmenu", (e) => {
|
|
766
|
+
e.preventDefault();
|
|
767
|
+
e.stopPropagation();
|
|
768
|
+
showCardContextMenu(ctx, card, e.clientX, e.clientY);
|
|
769
|
+
});
|
|
770
|
+
card.addEventListener("dblclick", (e) => {
|
|
771
|
+
if (
|
|
772
|
+
(e.target as HTMLElement).tagName === "BUTTON" ||
|
|
773
|
+
(e.target as HTMLElement).closest("button")
|
|
774
|
+
)
|
|
775
|
+
return;
|
|
776
|
+
e.preventDefault();
|
|
777
|
+
e.stopPropagation();
|
|
778
|
+
const filePath = card.dataset.path;
|
|
779
|
+
if (filePath) {
|
|
780
|
+
const file = ctx.allFilesData?.find((f) => f.path === filePath) || {
|
|
781
|
+
path: filePath,
|
|
782
|
+
name: filePath.split("/").pop(),
|
|
783
|
+
lines: 0,
|
|
784
|
+
};
|
|
785
|
+
import("./file-modal").then(({ openFileModal }) =>
|
|
786
|
+
openFileModal(ctx, file),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
// Smart selection: don't deselect others on mousedown if card is already selected
|
|
791
|
+
// This allows multi-drag to work. Deselection is deferred to mouseup (click without drag).
|
|
792
|
+
let _dragOccurred = false;
|
|
793
|
+
card.addEventListener("mousedown", (e) => {
|
|
794
|
+
if (e.button !== 0) return;
|
|
795
|
+
if (
|
|
796
|
+
(e.target as HTMLElement).closest("button") ||
|
|
797
|
+
(e.target as HTMLElement).closest(".connect-btn")
|
|
798
|
+
)
|
|
799
|
+
return;
|
|
800
|
+
const filePath = card.dataset.path || "";
|
|
801
|
+
const multi = e.shiftKey || e.ctrlKey;
|
|
802
|
+
const selected = ctx.snap().context.selectedCards;
|
|
803
|
+
const alreadySelected = selected.includes(filePath);
|
|
804
|
+
_dragOccurred = false;
|
|
805
|
+
|
|
806
|
+
if (multi) {
|
|
807
|
+
// Shift/Ctrl: toggle selection
|
|
808
|
+
ctx.actor.send({ type: "SELECT_CARD", path: filePath, shift: true });
|
|
809
|
+
try {
|
|
810
|
+
const { getCardManager } = require("./xydraw-bridge");
|
|
811
|
+
const cm = getCardManager();
|
|
812
|
+
if (cm) {
|
|
813
|
+
if (alreadySelected) cm.deselect(filePath);
|
|
814
|
+
else cm.select(filePath, true);
|
|
815
|
+
}
|
|
816
|
+
} catch {}
|
|
817
|
+
} else if (!alreadySelected) {
|
|
818
|
+
// Not selected yet → replace selection with this card
|
|
819
|
+
ctx.actor.send({ type: "SELECT_CARD", path: filePath, shift: false });
|
|
820
|
+
try {
|
|
821
|
+
const { getCardManager } = require("./xydraw-bridge");
|
|
822
|
+
const cm = getCardManager();
|
|
823
|
+
if (cm) cm.select(filePath, false);
|
|
824
|
+
} catch {}
|
|
825
|
+
}
|
|
826
|
+
// If already selected without shift → do nothing on mousedown (allow multi-drag)
|
|
827
|
+
// Deselection of others happens on mouseup below
|
|
828
|
+
|
|
829
|
+
updateSelectionHighlights(ctx);
|
|
830
|
+
updateArrangeToolbar(ctx);
|
|
831
|
+
});
|
|
832
|
+
card.addEventListener("mouseup", (e) => {
|
|
833
|
+
if (e.button !== 0) return;
|
|
834
|
+
if (
|
|
835
|
+
(e.target as HTMLElement).closest("button") ||
|
|
836
|
+
(e.target as HTMLElement).closest(".connect-btn")
|
|
837
|
+
)
|
|
838
|
+
return;
|
|
839
|
+
// Only deselect others if: no shift, card was already selected, and no drag happened
|
|
840
|
+
const filePath = card.dataset.path || "";
|
|
841
|
+
const multi = e.shiftKey || e.ctrlKey;
|
|
842
|
+
if (multi) return; // shift-click handled in mousedown
|
|
843
|
+
const selected = ctx.snap().context.selectedCards;
|
|
844
|
+
if (
|
|
845
|
+
selected.length > 1 &&
|
|
846
|
+
selected.includes(filePath) &&
|
|
847
|
+
!_dragOccurred
|
|
848
|
+
) {
|
|
849
|
+
ctx.actor.send({ type: "SELECT_CARD", path: filePath, shift: false });
|
|
850
|
+
try {
|
|
851
|
+
const { getCardManager } = require("./xydraw-bridge");
|
|
852
|
+
const cm = getCardManager();
|
|
853
|
+
if (cm) cm.select(filePath, false);
|
|
854
|
+
} catch {}
|
|
855
|
+
updateSelectionHighlights(ctx);
|
|
856
|
+
updateArrangeToolbar(ctx);
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
// Track drag state from engine for mouseup deselection logic
|
|
860
|
+
card.addEventListener("mousemove", () => {
|
|
861
|
+
_dragOccurred = true;
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
setupConnectionDrag(ctx, card, file.path);
|
|
865
|
+
|
|
866
|
+
// Expand button → open modal
|
|
867
|
+
const expandBtn = card.querySelector(".expand-btn");
|
|
868
|
+
if (expandBtn) {
|
|
869
|
+
expandBtn.addEventListener("click", (e) => {
|
|
870
|
+
e.stopPropagation();
|
|
871
|
+
openFileModal(ctx, file);
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// AI button → open chat
|
|
876
|
+
const aiBtn = card.querySelector(".ai-btn");
|
|
877
|
+
if (aiBtn) {
|
|
878
|
+
aiBtn.addEventListener("click", (e) => {
|
|
879
|
+
e.stopPropagation();
|
|
880
|
+
_handleChatClick(ctx, file);
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Scroll listener
|
|
885
|
+
const body = card.querySelector(".file-card-body");
|
|
886
|
+
if (body) {
|
|
887
|
+
body.addEventListener("scroll", () => {
|
|
888
|
+
scheduleRenderConnections(ctx);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Listen for resize from indicator drag
|
|
893
|
+
card.addEventListener("card-resized", ((e: CustomEvent) => {
|
|
894
|
+
const { path: p, width: w, height: h } = e.detail;
|
|
895
|
+
const state = ctx.snap().context;
|
|
896
|
+
const ch = state.currentCommitHash || "allfiles";
|
|
897
|
+
ctx.actor.send({ type: "RESIZE_CARD", path: p, width: w, height: h });
|
|
898
|
+
savePosition(
|
|
899
|
+
ctx,
|
|
900
|
+
ch,
|
|
901
|
+
p,
|
|
902
|
+
parseInt(card.style.left) || 0,
|
|
903
|
+
parseInt(card.style.top) || 0,
|
|
904
|
+
w,
|
|
905
|
+
h,
|
|
906
|
+
);
|
|
907
|
+
renderConnections(ctx);
|
|
908
|
+
}) as EventListener);
|
|
909
|
+
|
|
910
|
+
return card;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ─── Build file content HTML with optional line limiting ─
|
|
914
|
+
// When isExpanded=false, only render VISIBLE_LINE_LIMIT lines to keep DOM small.
|
|
915
|
+
// When isExpanded=true (F key), render all lines for full scrolling.
|
|
916
|
+
export function _buildFileContentHTML(
|
|
917
|
+
content: string,
|
|
918
|
+
layerSections: any,
|
|
919
|
+
addedLines: Set<number>,
|
|
920
|
+
deletedBeforeLine: Map<number, string[]>,
|
|
921
|
+
isAllAdded: boolean,
|
|
922
|
+
isAllDeleted: boolean,
|
|
923
|
+
isExpanded: boolean,
|
|
924
|
+
totalFileLines: number,
|
|
925
|
+
): string {
|
|
926
|
+
const { filteredContent, visibleLineIndices } = filterFileContentByLayer(
|
|
927
|
+
content,
|
|
928
|
+
layerSections,
|
|
929
|
+
);
|
|
930
|
+
const lines = content.split("\n");
|
|
931
|
+
const totalVisible = Array.from(visibleLineIndices).length;
|
|
932
|
+
const limit = isExpanded ? Infinity : VISIBLE_LINE_LIMIT;
|
|
933
|
+
let code = "";
|
|
934
|
+
let renderedCount = 0;
|
|
935
|
+
|
|
936
|
+
for (let i = 0; i < lines.length; i++) {
|
|
937
|
+
if (!visibleLineIndices.has(i)) continue;
|
|
938
|
+
if (renderedCount >= limit) break;
|
|
939
|
+
|
|
940
|
+
const line = lines[i];
|
|
941
|
+
const lineNum = i + 1;
|
|
942
|
+
const lineClass = isAllAdded
|
|
943
|
+
? "diff-add"
|
|
944
|
+
: isAllDeleted
|
|
945
|
+
? "diff-del"
|
|
946
|
+
: addedLines.has(lineNum)
|
|
947
|
+
? "diff-add"
|
|
948
|
+
: "diff-ctx";
|
|
949
|
+
const hasDel = deletedBeforeLine.has(lineNum);
|
|
950
|
+
const delCount = hasDel ? deletedBeforeLine.get(lineNum)!.length : 0;
|
|
951
|
+
const delAttr = hasDel ? ` data-del-count="${delCount}"` : "";
|
|
952
|
+
const delLines = hasDel
|
|
953
|
+
? ` data-del-lines="${encodeURIComponent(JSON.stringify(deletedBeforeLine.get(lineNum)))}"`
|
|
954
|
+
: "";
|
|
955
|
+
code += `<span class="diff-line ${lineClass}${hasDel ? " has-deleted" : ""}" data-line="${lineNum}"${delAttr}${delLines}><span class="line-num">${String(lineNum).padStart(4, " ")}</span>${escapeHtml(line)}</span>\n`;
|
|
956
|
+
renderedCount++;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const hiddenCount = totalVisible - renderedCount;
|
|
960
|
+
// Invisible sentinel for IntersectionObserver auto-loading (no visible text)
|
|
961
|
+
const truncNote =
|
|
962
|
+
hiddenCount > 0
|
|
963
|
+
? `<span class="more-lines" data-auto-expand="true" style="display:block;height:1px;"></span>`
|
|
964
|
+
: "";
|
|
965
|
+
return `<div class="file-content-preview"><pre><code>${code}</code></pre>${truncNote}</div>`;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// ─── Create all-file card (working tree) ────────────────
|
|
969
|
+
export function createAllFileCard(
|
|
970
|
+
ctx: CanvasContext,
|
|
971
|
+
file: any,
|
|
972
|
+
x: number,
|
|
973
|
+
y: number,
|
|
974
|
+
savedSize: any,
|
|
975
|
+
skipInteraction = false,
|
|
976
|
+
): HTMLElement {
|
|
977
|
+
const card = document.createElement("div");
|
|
978
|
+
card.className = "file-card";
|
|
979
|
+
// Guard against NaN/undefined positions (corrupted position records)
|
|
980
|
+
const safeX = isNaN(x) ? 0 : x;
|
|
981
|
+
const safeY = isNaN(y) ? 0 : y;
|
|
982
|
+
card.style.left = `${safeX}px`;
|
|
983
|
+
card.style.top = `${safeY}px`;
|
|
984
|
+
card.dataset.path = file.path;
|
|
985
|
+
|
|
986
|
+
if (savedSize) {
|
|
987
|
+
card.style.width = `${savedSize.width}px`;
|
|
988
|
+
card.style.height = `${savedSize.height}px`;
|
|
989
|
+
card.style.maxHeight = `${savedSize.height}px`;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const ext = file.ext || "";
|
|
993
|
+
const iconClass = getFileIconClass(ext);
|
|
994
|
+
const addedLines: Set<number> = file.addedLines || new Set();
|
|
995
|
+
const isAllAdded = file.status === "added";
|
|
996
|
+
const isAllDeleted = file.status === "deleted";
|
|
997
|
+
|
|
998
|
+
const deletedBeforeLine: Map<number, string[]> =
|
|
999
|
+
file.deletedBeforeLine || new Map();
|
|
1000
|
+
|
|
1001
|
+
// All files are now same fixed size - no expand persistence
|
|
1002
|
+
|
|
1003
|
+
let contentHTML = "";
|
|
1004
|
+
let canvasOptions: any = null;
|
|
1005
|
+
const useAdvancedRenderer = ctx.textRendererMode === 'canvas' || ctx.textRendererMode === 'webgl';
|
|
1006
|
+
|
|
1007
|
+
const IMAGE_EXTS = new Set([
|
|
1008
|
+
"png",
|
|
1009
|
+
"jpg",
|
|
1010
|
+
"jpeg",
|
|
1011
|
+
"gif",
|
|
1012
|
+
"webp",
|
|
1013
|
+
"svg",
|
|
1014
|
+
"bmp",
|
|
1015
|
+
"ico",
|
|
1016
|
+
]);
|
|
1017
|
+
const PDF_EXTS = new Set(["pdf"]);
|
|
1018
|
+
const isImage = IMAGE_EXTS.has(ext);
|
|
1019
|
+
const isPdf = PDF_EXTS.has(ext);
|
|
1020
|
+
|
|
1021
|
+
if (isImage) {
|
|
1022
|
+
contentHTML = `<div class="file-content-preview file-image-preview" style="display:flex;align-items:center;justify-content:center;height:100%;background:var(--bg-card);overflow:hidden;">
|
|
1023
|
+
<img src="/api/repo/file-content?path=${encodeURIComponent(ctx.snap().context.repoPath || "")}&file=${encodeURIComponent(file.path)}"
|
|
1024
|
+
alt="${escapeHtml(file.name)}"
|
|
1025
|
+
style="max-width:100%;max-height:100%;object-fit:contain;"
|
|
1026
|
+
loading="lazy" />
|
|
1027
|
+
</div>`;
|
|
1028
|
+
} else if (isPdf) {
|
|
1029
|
+
contentHTML = `<div class="file-content-preview file-image-preview" style="display:flex;align-items:center;justify-content:center;height:100%;background:var(--bg-card);overflow:hidden;">
|
|
1030
|
+
<img src="/api/repo/pdf-thumb?path=${encodeURIComponent(ctx.snap().context.repoPath || "")}&file=${encodeURIComponent(file.path)}"
|
|
1031
|
+
alt="${escapeHtml(file.name)}"
|
|
1032
|
+
style="max-width:100%;max-height:100%;object-fit:contain;"
|
|
1033
|
+
loading="lazy"
|
|
1034
|
+
onerror="this.parentElement.innerHTML='<pre><code><span class=\\'error-notice\\'>PDF preview unavailable</span></code></pre>'" />
|
|
1035
|
+
</div>`;
|
|
1036
|
+
} else if (file.isBinary) {
|
|
1037
|
+
contentHTML = `<div class="file-content-preview"><pre><code><span class="error-notice">Binary file</span></code></pre></div>`;
|
|
1038
|
+
} else if (file.content) {
|
|
1039
|
+
if (useAdvancedRenderer) {
|
|
1040
|
+
canvasOptions = {
|
|
1041
|
+
content: file.content,
|
|
1042
|
+
addedLines,
|
|
1043
|
+
deletedBeforeLine,
|
|
1044
|
+
isAllAdded,
|
|
1045
|
+
isAllDeleted,
|
|
1046
|
+
visibleLineIndices: filterFileContentByLayer(
|
|
1047
|
+
file.content,
|
|
1048
|
+
file.layerSections,
|
|
1049
|
+
).visibleLineIndices,
|
|
1050
|
+
filePath: file.path,
|
|
1051
|
+
};
|
|
1052
|
+
contentHTML = `<div class="file-content-preview canvas-container" style="position:relative; height: 100%; overflow: auto; background: var(--bg-card);"></div>`;
|
|
1053
|
+
} else {
|
|
1054
|
+
contentHTML = _buildFileContentHTML(
|
|
1055
|
+
file.content,
|
|
1056
|
+
file.layerSections,
|
|
1057
|
+
addedLines,
|
|
1058
|
+
deletedBeforeLine,
|
|
1059
|
+
isAllAdded,
|
|
1060
|
+
isAllDeleted,
|
|
1061
|
+
false,
|
|
1062
|
+
file.lines,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
} else {
|
|
1066
|
+
contentHTML = `<div class="file-content-preview"><pre><code><span class="error-notice">Could not read file</span></code></pre></div>`;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const dir = file.path.includes("/")
|
|
1070
|
+
? file.path.split("/").slice(0, -1).join("/")
|
|
1071
|
+
: "";
|
|
1072
|
+
|
|
1073
|
+
// Status badge for changed files
|
|
1074
|
+
const statusColors: Record<string, string> = {
|
|
1075
|
+
added: "#22c55e",
|
|
1076
|
+
modified: "#eab308",
|
|
1077
|
+
deleted: "#ef4444",
|
|
1078
|
+
renamed: "#60a5fa",
|
|
1079
|
+
copied: "#a78bfa",
|
|
1080
|
+
};
|
|
1081
|
+
const statusBadge =
|
|
1082
|
+
file.status && file.status !== "unmodified"
|
|
1083
|
+
? `<span style="font-size: 9px; color: ${statusColors[file.status] || "var(--text-muted)"}; margin-left: 4px; text-transform: uppercase; letter-spacing: 0.05em;">${escapeHtml(file.status)}${addedLines.size > 0 ? ` <span style="color:#22c55e">+${addedLines.size}</span>` : ""}${deletedBeforeLine.size > 0 ? ` <span style="color:#f87171">-${Array.from(deletedBeforeLine.values()).reduce((s, a) => s + a.length, 0)}</span>` : ""}</span>`
|
|
1084
|
+
: "";
|
|
1085
|
+
const metaInfo = file.status
|
|
1086
|
+
? statusBadge
|
|
1087
|
+
: `<span style="font-size: 10px; color: var(--text-muted); margin-left: auto;">${file.lines} lines</span>`;
|
|
1088
|
+
|
|
1089
|
+
card.innerHTML = `
|
|
1090
|
+
<div class="file-card-header">
|
|
1091
|
+
<div class="file-icon ${iconClass}">
|
|
1092
|
+
${getFileIcon(file.type, ext)}
|
|
1093
|
+
</div>
|
|
1094
|
+
<span class="file-name">${escapeHtml(file.name)}</span>
|
|
1095
|
+
${metaInfo}
|
|
1096
|
+
|
|
1097
|
+
</div>
|
|
1098
|
+
<div class="file-card-body">
|
|
1099
|
+
<div class="file-path">${escapeHtml(dir)}</div>
|
|
1100
|
+
${contentHTML}
|
|
1101
|
+
</div>
|
|
1102
|
+
`;
|
|
1103
|
+
|
|
1104
|
+
// Store file data for re-rendering on expand/collapse
|
|
1105
|
+
cardFileData.set(card, file);
|
|
1106
|
+
|
|
1107
|
+
setupConnectionDrag(ctx, card, file.path);
|
|
1108
|
+
// When managed by CardManager, skip legacy drag/resize/z-order setup
|
|
1109
|
+
// but ALWAYS attach context menu, double-click, and click-to-select
|
|
1110
|
+
if (!skipInteraction) {
|
|
1111
|
+
setupCardInteraction(ctx, card, "allfiles");
|
|
1112
|
+
} else {
|
|
1113
|
+
// Context menu (right-click)
|
|
1114
|
+
card.addEventListener("contextmenu", (e) => {
|
|
1115
|
+
e.preventDefault();
|
|
1116
|
+
e.stopPropagation();
|
|
1117
|
+
showCardContextMenu(ctx, card, e.clientX, e.clientY);
|
|
1118
|
+
});
|
|
1119
|
+
// Double-click to open in editor modal
|
|
1120
|
+
card.addEventListener("dblclick", (e) => {
|
|
1121
|
+
if (
|
|
1122
|
+
(e.target as HTMLElement).tagName === "BUTTON" ||
|
|
1123
|
+
(e.target as HTMLElement).closest("button")
|
|
1124
|
+
)
|
|
1125
|
+
return;
|
|
1126
|
+
e.preventDefault();
|
|
1127
|
+
e.stopPropagation();
|
|
1128
|
+
const filePath = card.dataset.path;
|
|
1129
|
+
if (filePath) {
|
|
1130
|
+
const file = ctx.allFilesData?.find((f) => f.path === filePath) || {
|
|
1131
|
+
path: filePath,
|
|
1132
|
+
name: filePath.split("/").pop(),
|
|
1133
|
+
lines: 0,
|
|
1134
|
+
};
|
|
1135
|
+
import("./file-modal").then(({ openFileModal }) =>
|
|
1136
|
+
openFileModal(ctx, file),
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
// Click to select (sync both XState and CardManager)
|
|
1141
|
+
card.addEventListener("mousedown", (e) => {
|
|
1142
|
+
if (e.button !== 0) return;
|
|
1143
|
+
if (
|
|
1144
|
+
(e.target as HTMLElement).closest("button") ||
|
|
1145
|
+
(e.target as HTMLElement).closest(".connect-btn")
|
|
1146
|
+
)
|
|
1147
|
+
return;
|
|
1148
|
+
const filePath = card.dataset.path || "";
|
|
1149
|
+
const multi = e.shiftKey || e.ctrlKey;
|
|
1150
|
+
|
|
1151
|
+
try {
|
|
1152
|
+
const { getCardManager } = require("./xydraw-bridge");
|
|
1153
|
+
const cm = getCardManager();
|
|
1154
|
+
if (cm) {
|
|
1155
|
+
const alreadySelected = cm.selected.has(filePath);
|
|
1156
|
+
|
|
1157
|
+
if (multi) {
|
|
1158
|
+
// Shift/Ctrl click: toggle selection
|
|
1159
|
+
if (alreadySelected) {
|
|
1160
|
+
cm.deselect(filePath);
|
|
1161
|
+
} else {
|
|
1162
|
+
cm.select(filePath, true);
|
|
1163
|
+
}
|
|
1164
|
+
ctx.actor.send({
|
|
1165
|
+
type: "SELECT_CARD",
|
|
1166
|
+
path: filePath,
|
|
1167
|
+
shift: true,
|
|
1168
|
+
});
|
|
1169
|
+
} else if (alreadySelected && cm.selected.size > 1) {
|
|
1170
|
+
// Clicking already-selected card in a multi-selection:
|
|
1171
|
+
// Don't deselect yet — user might be starting a multi-drag.
|
|
1172
|
+
// Deselect on mouseup if no drag occurred.
|
|
1173
|
+
let dragged = false;
|
|
1174
|
+
const onMove = () => {
|
|
1175
|
+
dragged = true;
|
|
1176
|
+
};
|
|
1177
|
+
const onUp = () => {
|
|
1178
|
+
window.removeEventListener("mousemove", onMove);
|
|
1179
|
+
window.removeEventListener("mouseup", onUp);
|
|
1180
|
+
if (!dragged) {
|
|
1181
|
+
// No drag happened — deselect all others
|
|
1182
|
+
cm.deselectAll();
|
|
1183
|
+
cm.select(filePath, false);
|
|
1184
|
+
ctx.actor.send({
|
|
1185
|
+
type: "SELECT_CARD",
|
|
1186
|
+
path: filePath,
|
|
1187
|
+
shift: false,
|
|
1188
|
+
});
|
|
1189
|
+
updateSelectionHighlights(ctx);
|
|
1190
|
+
updateArrangeToolbar(ctx);
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
window.addEventListener("mousemove", onMove);
|
|
1194
|
+
window.addEventListener("mouseup", onUp);
|
|
1195
|
+
} else {
|
|
1196
|
+
// Normal click: deselect all, select this one
|
|
1197
|
+
cm.select(filePath, false);
|
|
1198
|
+
ctx.actor.send({
|
|
1199
|
+
type: "SELECT_CARD",
|
|
1200
|
+
path: filePath,
|
|
1201
|
+
shift: false,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
} catch {}
|
|
1206
|
+
updateSelectionHighlights(ctx);
|
|
1207
|
+
updateArrangeToolbar(ctx);
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (canvasOptions) {
|
|
1212
|
+
const previewEl = card.querySelector(".canvas-container") as HTMLElement;
|
|
1213
|
+
if (previewEl) {
|
|
1214
|
+
const mode = ctx.textRendererMode || 'dom';
|
|
1215
|
+
|
|
1216
|
+
if (mode === 'webgl') {
|
|
1217
|
+
// Use WebGL renderer (Pixi.js)
|
|
1218
|
+
import("./webgl-text").then(({ WebGLTextRenderer }) => {
|
|
1219
|
+
try {
|
|
1220
|
+
const renderer = new WebGLTextRenderer(previewEl, canvasOptions);
|
|
1221
|
+
(card as any)._webglTextRenderer = renderer;
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
console.error('[cards] WebGL text renderer failed, falling back to canvas:', e);
|
|
1224
|
+
import("./canvas-text").then(({ CanvasTextRenderer }) => {
|
|
1225
|
+
const renderer = new CanvasTextRenderer(previewEl, canvasOptions);
|
|
1226
|
+
(card as any)._canvasTextRenderer = renderer;
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
} else if (mode === 'canvas') {
|
|
1231
|
+
// Use Canvas 2D renderer
|
|
1232
|
+
import("./canvas-text").then(({ CanvasTextRenderer }) => {
|
|
1233
|
+
const renderer = new CanvasTextRenderer(previewEl, canvasOptions);
|
|
1234
|
+
(card as any)._canvasTextRenderer = renderer;
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
// else: DOM mode (default) - no special renderer needed
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const expandBtn = card.querySelector(".expand-btn");
|
|
1242
|
+
if (expandBtn) {
|
|
1243
|
+
expandBtn.addEventListener("click", (e) => {
|
|
1244
|
+
e.stopPropagation();
|
|
1245
|
+
openFileModal(ctx, file);
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// AI button → open chat
|
|
1250
|
+
const aiBtn = card.querySelector(".ai-btn");
|
|
1251
|
+
if (aiBtn) {
|
|
1252
|
+
aiBtn.addEventListener("click", (e) => {
|
|
1253
|
+
e.stopPropagation();
|
|
1254
|
+
_handleChatClick(ctx, file);
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const body = card.querySelector(".file-card-body") as HTMLElement;
|
|
1259
|
+
if (body) {
|
|
1260
|
+
body.addEventListener("scroll", () => {
|
|
1261
|
+
debounceSaveScroll(ctx, file.path, body.scrollTop);
|
|
1262
|
+
scheduleRenderConnections(ctx);
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// ── Auto-load truncated lines when scrolled into view ──
|
|
1267
|
+
const moreLinesEl = card.querySelector(
|
|
1268
|
+
".more-lines[data-auto-expand]",
|
|
1269
|
+
) as HTMLElement;
|
|
1270
|
+
if (moreLinesEl && file.content && !file.isBinary) {
|
|
1271
|
+
const pre = card.querySelector(".file-content-preview pre") as HTMLElement;
|
|
1272
|
+
if (pre) {
|
|
1273
|
+
const observer = new IntersectionObserver(
|
|
1274
|
+
(entries) => {
|
|
1275
|
+
for (const entry of entries) {
|
|
1276
|
+
if (entry.isIntersecting) {
|
|
1277
|
+
observer.disconnect();
|
|
1278
|
+
// Re-render with all lines (expanded)
|
|
1279
|
+
const newHTML = _buildFileContentHTML(
|
|
1280
|
+
file.content,
|
|
1281
|
+
file.layerSections,
|
|
1282
|
+
addedLines,
|
|
1283
|
+
deletedBeforeLine,
|
|
1284
|
+
isAllAdded,
|
|
1285
|
+
isAllDeleted,
|
|
1286
|
+
true,
|
|
1287
|
+
file.lines,
|
|
1288
|
+
);
|
|
1289
|
+
const preview = card.querySelector(".file-content-preview");
|
|
1290
|
+
if (preview) preview.outerHTML = newHTML;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
},
|
|
1294
|
+
{ root: pre, rootMargin: "200px" },
|
|
1295
|
+
);
|
|
1296
|
+
observer.observe(moreLinesEl);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// ── Diff marker strip (scrollbar annotations for changed lines) ──
|
|
1301
|
+
// Skip when advanced renderer mode is active — renders its own gutter
|
|
1302
|
+
if (
|
|
1303
|
+
(addedLines.size > 0 || deletedBeforeLine.size > 0) &&
|
|
1304
|
+
!isAllAdded &&
|
|
1305
|
+
file.content &&
|
|
1306
|
+
!useAdvancedRenderer
|
|
1307
|
+
) {
|
|
1308
|
+
const totalLines = file.content.split("\n").length;
|
|
1309
|
+
_buildDiffMarkerStrip(
|
|
1310
|
+
card,
|
|
1311
|
+
body,
|
|
1312
|
+
addedLines,
|
|
1313
|
+
totalLines,
|
|
1314
|
+
deletedBeforeLine,
|
|
1315
|
+
file.hunks,
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// ── Deleted lines hover overlay ──
|
|
1320
|
+
if (deletedBeforeLine.size > 0) {
|
|
1321
|
+
_setupDeletedLinesOverlay(card);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Listen for resize from indicator drag
|
|
1325
|
+
card.addEventListener("card-resized", ((e: CustomEvent) => {
|
|
1326
|
+
const { path: p, width: w, height: h } = e.detail;
|
|
1327
|
+
const state = ctx.snap().context;
|
|
1328
|
+
const ch = state.currentCommitHash || "allfiles";
|
|
1329
|
+
ctx.actor.send({ type: "RESIZE_CARD", path: p, width: w, height: h });
|
|
1330
|
+
savePosition(
|
|
1331
|
+
ctx,
|
|
1332
|
+
ch,
|
|
1333
|
+
p,
|
|
1334
|
+
parseInt(card.style.left) || 0,
|
|
1335
|
+
parseInt(card.style.top) || 0,
|
|
1336
|
+
w,
|
|
1337
|
+
h,
|
|
1338
|
+
);
|
|
1339
|
+
renderConnections(ctx);
|
|
1340
|
+
}) as EventListener);
|
|
1341
|
+
|
|
1342
|
+
return card;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// ─── File expand modal (extracted to file-modal.tsx) ─────
|
|
1346
|
+
import { openFileModal } from "./file-modal";
|
|
1347
|
+
export { openFileModal };
|
|
1348
|
+
|
|
1349
|
+
// ─── Diff markers & card expand (extracted modules) ─────
|
|
1350
|
+
export {
|
|
1351
|
+
buildDiffMarkerStrip,
|
|
1352
|
+
scrollToLine,
|
|
1353
|
+
setupDeletedLinesOverlay,
|
|
1354
|
+
} from "./card-diff-markers";
|
|
1355
|
+
export {
|
|
1356
|
+
changeCardsFontSize,
|
|
1357
|
+
toggleCardExpand,
|
|
1358
|
+
expandCardByPath,
|
|
1359
|
+
fitScreenSize,
|
|
1360
|
+
updateHiddenLinesIndicator,
|
|
1361
|
+
} from "./card-expand";
|