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.
Files changed (121) hide show
  1. package/README.md +267 -118
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/analytics.db +0 -0
  5. package/app/api/analytics/route.ts +64 -0
  6. package/app/api/auth/positions/route.ts +95 -33
  7. package/app/api/build-info/route.ts +19 -0
  8. package/app/api/chat/route.ts +13 -2
  9. package/app/api/og-image/route.ts +14 -0
  10. package/app/api/repo/file-content/route.ts +73 -20
  11. package/app/api/repo/load/route.test.ts +62 -0
  12. package/app/api/repo/load/route.ts +41 -1
  13. package/app/api/repo/pdf-thumb/route.ts +127 -0
  14. package/app/api/repo/resolve-slug/route.ts +51 -0
  15. package/app/api/repo/tree/route.ts +188 -104
  16. package/app/api/version/route.ts +26 -0
  17. package/app/globals.css +5706 -4938
  18. package/app/layout.tsx +1279 -490
  19. package/app/lib/auto-arrange.test.ts +158 -0
  20. package/app/lib/auto-arrange.ts +147 -0
  21. package/app/lib/canvas-export.ts +358 -358
  22. package/app/lib/canvas.ts +625 -564
  23. package/app/lib/cards.tsx +1361 -916
  24. package/app/lib/chat.tsx +65 -9
  25. package/app/lib/code-editor.ts +86 -2
  26. package/app/lib/context.test.ts +32 -0
  27. package/app/lib/context.ts +19 -3
  28. package/app/lib/cursor-sharing.ts +34 -0
  29. package/app/lib/events.tsx +71 -93
  30. package/app/lib/export-canvas.ts +287 -0
  31. package/app/lib/file-card-plugin.ts +148 -148
  32. package/app/lib/file-modal.tsx +49 -0
  33. package/app/lib/file-preview.ts +486 -427
  34. package/app/lib/github-import.test.ts +424 -0
  35. package/app/lib/initial-route-hydration.test.ts +283 -0
  36. package/app/lib/initial-route-hydration.ts +202 -0
  37. package/app/lib/landing-reset.test.ts +99 -0
  38. package/app/lib/landing-reset.ts +106 -0
  39. package/app/lib/landing-shell.test.ts +75 -0
  40. package/app/lib/large-repo-optimization.ts +37 -0
  41. package/app/lib/layout-snapshots.ts +320 -0
  42. package/app/lib/loading.test.ts +69 -0
  43. package/app/lib/loading.tsx +160 -45
  44. package/app/lib/mount-cleanup.test.ts +52 -0
  45. package/app/lib/mount-cleanup.ts +34 -0
  46. package/app/lib/mount-init.test.ts +123 -0
  47. package/app/lib/mount-init.ts +107 -0
  48. package/app/lib/mount-lifecycle.test.ts +39 -0
  49. package/app/lib/mount-lifecycle.ts +12 -0
  50. package/app/lib/mount-route-wiring.test.ts +87 -0
  51. package/app/lib/mount-route-wiring.ts +84 -0
  52. package/app/lib/multi-repo.ts +14 -0
  53. package/app/lib/onboarding-tutorial.ts +278 -0
  54. package/app/lib/positions.ts +190 -121
  55. package/app/lib/recent-commits.test.ts +947 -0
  56. package/app/lib/recent-commits.ts +227 -0
  57. package/app/lib/repo-handoff.test.ts +23 -0
  58. package/app/lib/repo-handoff.ts +16 -0
  59. package/app/lib/repo-progressive.ts +119 -0
  60. package/app/lib/repo-select.test.ts +61 -0
  61. package/app/lib/repo-select.ts +74 -0
  62. package/app/lib/repo.tsx +1383 -987
  63. package/app/lib/role.ts +228 -0
  64. package/app/lib/route-catchall.test.ts +27 -0
  65. package/app/lib/route-repo-entry.test.ts +95 -0
  66. package/app/lib/route-repo-entry.ts +36 -0
  67. package/app/lib/router-contract.test.ts +22 -0
  68. package/app/lib/router-contract.ts +19 -0
  69. package/app/lib/shared-layout.test.ts +86 -0
  70. package/app/lib/shared-layout.ts +82 -0
  71. package/app/lib/status-bar.test.ts +118 -0
  72. package/app/lib/status-bar.ts +365 -128
  73. package/app/lib/sync-controls.test.ts +43 -0
  74. package/app/lib/sync-controls.tsx +303 -0
  75. package/app/lib/test-dom.ts +145 -0
  76. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  77. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  78. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  79. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  80. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  81. package/app/lib/transclusion-smoke.test.ts +163 -0
  82. package/app/lib/tutorial.ts +301 -0
  83. package/app/lib/version.ts +93 -0
  84. package/app/lib/viewport-culling.ts +740 -735
  85. package/app/lib/virtual-files.ts +456 -0
  86. package/app/lib/webgl-text.ts +189 -0
  87. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
  88. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  89. package/app/og-image.png +0 -0
  90. package/app/page.client.tsx +70 -269
  91. package/app/page.tsx +15 -16
  92. package/app/state/machine.js +13 -0
  93. package/package.json +84 -75
  94. package/server.ts +10 -0
  95. package/app/[owner]/[repo]/page.tsx +0 -6
  96. package/app/[slug]/page.tsx +0 -6
  97. package/packages/galaxydraw/README.md +0 -296
  98. package/packages/galaxydraw/banner.png +0 -0
  99. package/packages/galaxydraw/demo/build-static.ts +0 -100
  100. package/packages/galaxydraw/demo/client.ts +0 -154
  101. package/packages/galaxydraw/demo/dist/client.js +0 -8
  102. package/packages/galaxydraw/demo/index.html +0 -256
  103. package/packages/galaxydraw/demo/server.ts +0 -96
  104. package/packages/galaxydraw/dist/index.js +0 -984
  105. package/packages/galaxydraw/dist/index.js.map +0 -16
  106. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  109. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  110. package/packages/galaxydraw/package.json +0 -49
  111. package/packages/galaxydraw/perf.test.ts +0 -284
  112. package/packages/galaxydraw/src/core/cards.ts +0 -435
  113. package/packages/galaxydraw/src/core/engine.ts +0 -339
  114. package/packages/galaxydraw/src/core/events.ts +0 -81
  115. package/packages/galaxydraw/src/core/layout.ts +0 -136
  116. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  117. package/packages/galaxydraw/src/core/state.ts +0 -177
  118. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  119. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  120. package/packages/galaxydraw/src/index.ts +0 -40
  121. package/packages/galaxydraw/tsconfig.json +0 -30
@@ -1,482 +1,485 @@
1
- // @ts-nocheck
2
- /**
3
- * GalaxyDraw Bridge — Adapter between GitMaps and the galaxydraw engine.
4
- *
5
- * Wires galaxydraw's CanvasState + CardManager into the existing
6
- * server-rendered DOM and XState persistence layer.
7
- *
8
- * Architecture:
9
- * - CanvasState manages zoom/pan/transform (replaces manual math)
10
- * - CardManager creates/defers cards via FileCardPlugin + DiffCardPlugin
11
- * - XState actor remains source-of-truth for persistence
12
- * - Server-rendered DOM (#canvasViewport, #canvasContent) stays intact
13
- */
14
-
15
- import { CanvasState } from '../../packages/galaxydraw/src/core/state';
16
- import { CardManager } from '../../packages/galaxydraw/src/core/cards';
17
- import { EventBus } from '../../packages/galaxydraw/src/core/events';
18
- import { createFileCardPlugin, createDiffCardPlugin } from './file-card-plugin';
19
- import type { CanvasContext } from './context';
20
-
21
- /**
22
- * Shared galaxydraw state instance.
23
- * Replaces manual `ctx.canvas.style.transform = ...` calls.
24
- */
25
- let _gdState: CanvasState | null = null;
26
- let _cardManager: CardManager | null = null;
27
- let _eventBus: EventBus | null = null;
28
-
29
- /**
30
- * Initialize the galaxydraw state engine and bind to existing DOM.
31
- * Call this after ctx.canvas and ctx.canvasViewport are set.
32
- */
33
- export function initGalaxyDrawState(ctx: CanvasContext): CanvasState {
34
- _gdState = new CanvasState();
35
-
36
- if (ctx.canvasViewport && ctx.canvas) {
37
- _gdState.bind(ctx.canvasViewport, ctx.canvas);
38
- }
39
-
40
- // Sync initial state from XState
41
- const state = ctx.snap().context;
42
- if (state.zoom) _gdState.zoom = state.zoom;
43
- if (state.offsetX) _gdState.offsetX = state.offsetX;
44
- if (state.offsetY) _gdState.offsetY = state.offsetY;
45
- _gdState.applyTransform();
46
-
47
- return _gdState;
48
- }
49
-
50
- /**
51
- * Get the shared CanvasState instance.
52
- */
53
- export function getGalaxyDrawState(): CanvasState | null {
54
- return _gdState;
55
- }
56
-
57
- /**
58
- * Zoom toward a screen point using galaxydraw's engine,
59
- * then sync the computed state back to XState for persistence.
60
- *
61
- * @returns The new zoom/offset values (for callers that need them)
62
- */
63
- export function zoomTowardScreen(
64
- ctx: CanvasContext,
65
- screenX: number,
66
- screenY: number,
67
- factor: number,
68
- ): { zoom: number; offsetX: number; offsetY: number } {
69
- const gd = _gdState;
70
-
71
- if (gd) {
72
- // Delegate to galaxydraw engine
73
- gd.zoomToward(screenX, screenY, factor);
74
- // Sync back to XState for persistence
75
- ctx.actor.send({ type: 'SET_ZOOM', zoom: gd.zoom });
76
- ctx.actor.send({ type: 'SET_OFFSET', x: gd.offsetX, y: gd.offsetY });
77
- return { zoom: gd.zoom, offsetX: gd.offsetX, offsetY: gd.offsetY };
78
- }
79
-
80
- // Fallback: manual math (pre-bridge init)
81
- const state = ctx.snap().context;
82
- const rect = ctx.canvasViewport?.getBoundingClientRect();
83
- const mouseX = screenX - (rect?.left ?? 0);
84
- const mouseY = screenY - (rect?.top ?? 0);
85
- const newZoom = Math.min(3, Math.max(0.1, state.zoom * factor));
86
- const scale = newZoom / state.zoom;
87
- const newOffsetX = mouseX - (mouseX - state.offsetX) * scale;
88
- const newOffsetY = mouseY - (mouseY - state.offsetY) * scale;
89
- ctx.actor.send({ type: 'SET_ZOOM', zoom: newZoom });
90
- ctx.actor.send({ type: 'SET_OFFSET', x: newOffsetX, y: newOffsetY });
91
- return { zoom: newZoom, offsetX: newOffsetX, offsetY: newOffsetY };
92
- }
93
-
94
- /**
95
- * Pan by pixel delta via galaxydraw's engine.
96
- * Syncs back to XState for persistence.
97
- */
98
- export function panByDelta(
99
- ctx: CanvasContext,
100
- dx: number,
101
- dy: number,
102
- ): void {
103
- const gd = _gdState;
104
-
105
- if (gd) {
106
- gd.pan(dx, dy);
107
- ctx.actor.send({ type: 'SET_OFFSET', x: gd.offsetX, y: gd.offsetY });
108
- return;
109
- }
110
-
111
- // Fallback
112
- const state = ctx.snap().context;
113
- ctx.actor.send({ type: 'SET_OFFSET', x: state.offsetX + dx, y: state.offsetY + dy });
114
- }
115
-
116
- /**
117
- * Convert screen coordinates to world coordinates.
118
- * Delegates to CanvasState.screenToWorld() when available.
119
- */
120
- export function screenToWorld(
121
- ctx: CanvasContext,
122
- screenX: number,
123
- screenY: number,
124
- ): { x: number; y: number } {
125
- const gd = _gdState;
126
-
127
- if (gd) {
128
- return gd.screenToWorld(screenX, screenY);
129
- }
130
-
131
- // Fallback
132
- const state = ctx.snap().context;
133
- const rect = ctx.canvasViewport?.getBoundingClientRect();
134
- return {
135
- x: (screenX - (rect?.left ?? 0) - state.offsetX) / state.zoom,
136
- y: (screenY - (rect?.top ?? 0) - state.offsetY) / state.zoom,
137
- };
138
- }
139
-
140
- /**
141
- * Center the viewport on a world coordinate.
142
- * Delegates to CanvasState.panTo() when available.
143
- */
144
- export function panToWorld(
145
- ctx: CanvasContext,
146
- worldX: number,
147
- worldY: number,
148
- ): void {
149
- const gd = _gdState;
150
-
151
- if (gd) {
152
- gd.panTo(worldX, worldY);
153
- ctx.actor.send({ type: 'SET_OFFSET', x: gd.offsetX, y: gd.offsetY });
154
- return;
155
- }
156
-
157
- // Fallback
158
- const state = ctx.snap().context;
159
- const vp = ctx.canvasViewport;
160
- if (vp) {
161
- const vpW = vp.clientWidth;
162
- const vpH = vp.clientHeight;
163
- const newOffsetX = vpW / 2 - worldX * state.zoom;
164
- const newOffsetY = vpH / 2 - worldY * state.zoom;
165
- ctx.actor.send({ type: 'SET_OFFSET', x: newOffsetX, y: newOffsetY });
166
- }
167
- }
168
-
169
- // ─── Card Manager ───────────────────────────────────────
170
-
171
- /**
172
- * Initialize the CardManager with file card plugins.
173
- * Call after initGalaxyDrawState() when ctx.canvas is available.
174
- *
175
- * The CardManager handles:
176
- * - Card creation via plugins (FileCardPlugin, DiffCardPlugin)
177
- * - Drag, resize, z-order management
178
- * - Selection (single, multi)
179
- * - Deferred rendering (virtualization)
180
- */
181
- import { scheduleRenderConnections } from './connections';
182
-
183
- export function initCardManager(ctx: CanvasContext): CardManager | null {
184
- if (!_gdState || !ctx.canvas) {
185
- console.warn('[galaxydraw-bridge] Cannot init CardManager: state or canvas not ready');
186
- return null;
187
- }
188
-
189
- _eventBus = new EventBus();
190
- _cardManager = new CardManager(_gdState, _eventBus, ctx.canvas, {
191
- defaultWidth: 580,
192
- defaultHeight: 700,
193
- minWidth: 280,
194
- minHeight: 200,
195
- gridSize: 0,
196
- cornerSize: 40,
197
- });
198
-
199
- // Register plugins
200
- _cardManager.registerPlugin(createFileCardPlugin());
201
- _cardManager.registerPlugin(createDiffCardPlugin());
202
-
203
- // Sync card events back to XState for persistence
204
- _eventBus.on('card:move', (ev) => {
205
- const { id, x, y } = ev;
206
- ctx.actor.send({ type: 'SAVE_POSITION', path: id, x, y });
207
- scheduleRenderConnections(ctx);
208
- });
209
-
210
- _eventBus.on('card:resize', (ev) => {
211
- const { id, width, height } = ev;
212
- ctx.actor.send({ type: 'RESIZE_CARD', path: id, width, height });
213
- scheduleRenderConnections(ctx);
214
- });
215
-
216
- console.log('[galaxydraw-bridge] CardManager initialized with file + diff plugins');
217
- return _cardManager;
218
- }
219
-
220
- /**
221
- * Get the shared CardManager instance.
222
- */
223
- export function getCardManager(): CardManager | null {
224
- return _cardManager;
225
- }
226
-
227
- /**
228
- * Get the shared EventBus instance.
229
- */
230
- export function getEventBus(): EventBus | null {
231
- return _eventBus;
232
- }
233
-
234
- // ─── Card Creation via CardManager ──────────────────────
235
-
236
- import { FILE_CARD_TYPE, DIFF_CARD_TYPE } from './file-card-plugin';
237
- import { getActiveLayer } from './layers';
238
- import { updateHiddenUI } from './hidden-files';
239
- import type { ViewportRect } from '../../packages/galaxydraw/src/core/state';
240
-
241
- /**
242
- * Render all files on canvas using CardManager instead of direct DOM.
243
- *
244
- * This replaces the viewport culling logic in renderAllFilesOnCanvas():
245
- * - Cards in/near viewport CardManager.create() (immediate DOM)
246
- * - Cards outside viewport → CardManager.defer() (lazy materialization)
247
- * - On scroll/zoom materializeViewport() creates deferred cards
248
- *
249
- * Benefits over the legacy approach:
250
- * - Drag/resize/z-order handled uniformly by CardManager
251
- * - EventBus emits card:create/card:move/card:resize for persistence
252
- * - Cleaner separation between rendering and interaction
253
- */
254
- export function renderAllFilesViaCardManager(ctx: CanvasContext, files: any[]) {
255
- if (!_cardManager || !_gdState) {
256
- // Fallback to legacy if CardManager not initialized
257
- console.warn('[galaxydraw-bridge] CardManager not ready, falling back to legacy render');
258
- return false; // Signal caller to use legacy path
259
- }
260
-
261
- _cardManager.clear();
262
-
263
- // Also clear existing DOM cards, pills, and deferred state
264
- // Without this, layer switching leaves orphaned elements
265
- ctx.fileCards.forEach(card => card.remove());
266
- ctx.fileCards.clear();
267
- ctx.deferredCards.clear();
268
- ctx.canvas?.querySelectorAll('.dir-label').forEach(el => el.remove());
269
- ctx.canvas?.querySelectorAll('.file-pill').forEach(el => el.remove());
270
- // Clear pill tracking Map
271
- const { clearAllPills } = require('./viewport-culling');
272
- clearAllPills(ctx);
273
- if (ctx.svgOverlay) ctx.svgOverlay.innerHTML = '';
274
-
275
- const { resetCardGroups, restoreCollapsedDirs } = require('./card-groups');
276
- resetCardGroups();
277
-
278
- const visibleFiles = files.filter(f => !ctx.hiddenFiles.has(f.path));
279
- updateHiddenUI(ctx);
280
-
281
- // Build changed file data map
282
- const changedFileDataMap = new Map<string, any>();
283
- if (ctx.commitFilesData) {
284
- ctx.commitFilesData.forEach(f => changedFileDataMap.set(f.path, f));
285
- }
286
-
287
- let layerFiles = visibleFiles;
288
- const activeLayer = getActiveLayer();
289
- if (activeLayer) {
290
- layerFiles = visibleFiles.filter(f => !!activeLayer.files[f.path]);
291
- } else {
292
- // Default layer: exclude files that have been moved to other layers
293
- const { isFileMovedFromDefault } = require('./layers');
294
- layerFiles = visibleFiles.filter(f => !isFileMovedFromDefault(f.path));
295
- }
296
-
297
- // Grid layout: square-ish
298
- const count = layerFiles.length;
299
- const cols = Math.max(1, Math.ceil(Math.sqrt(count)));
300
- const defaultCardWidth = 580;
301
- const defaultCardHeight = 700;
302
- const gap = 20;
303
- const cellW = defaultCardWidth + gap;
304
- const cellH = defaultCardHeight + gap;
305
-
306
- // Viewport rect for initial visibility check
307
- const MARGIN = 800;
308
- const state = _gdState.snapshot();
309
- const vpEl = ctx.canvasViewport;
310
- const vpW = vpEl?.clientWidth || window.innerWidth;
311
- const vpH = vpEl?.clientHeight || window.innerHeight;
312
- const zoom = state.zoom || 1;
313
- const offsetX = state.offsetX || 0;
314
- const offsetY = state.offsetY || 0;
315
- const worldLeft = (-offsetX - MARGIN) / zoom;
316
- const worldTop = (-offsetY - MARGIN) / zoom;
317
- const worldRight = (vpW - offsetX + MARGIN) / zoom;
318
- const worldBottom = (vpH - offsetY + MARGIN) / zoom;
319
-
320
- let createdCount = 0;
321
- let deferredCount = 0;
322
-
323
- // Cache XState state once outside the loop — avoids N snapshots for N files
324
- const cachedCardSizes = ctx.snap().context.cardSizes || {};
325
-
326
- layerFiles.forEach((f, index) => {
327
- const posKey = `allfiles:${f.path}`;
328
- let x: number, y: number;
329
-
330
- if (ctx.positions.has(posKey)) {
331
- const pos = ctx.positions.get(posKey);
332
- x = pos.x; y = pos.y;
333
- } else {
334
- const col = index % cols;
335
- const row = Math.floor(index / cols);
336
- x = 50 + col * cellW;
337
- y = 50 + row * cellH;
338
- }
339
-
340
- // Get saved size (from cached snapshot — no per-file ctx.snap() call)
341
- let size = cachedCardSizes[f.path];
342
- if (!size && ctx.positions.has(posKey)) {
343
- const pos = ctx.positions.get(posKey);
344
- if (pos.width) size = { width: pos.width, height: pos.height };
345
- }
346
-
347
- // Merge diff/layer data
348
- let fileWithDiff = { ...f };
349
- if (activeLayer && activeLayer.files[fileWithDiff.path]) {
350
- fileWithDiff.layerSections = activeLayer.files[fileWithDiff.path].sections;
351
- }
352
-
353
- const isChanged = ctx.changedFilePaths.has(f.path);
354
- if (isChanged && changedFileDataMap.has(fileWithDiff.path)) {
355
- const diffData = changedFileDataMap.get(fileWithDiff.path);
356
- if (diffData.content) {
357
- fileWithDiff.content = diffData.content;
358
- fileWithDiff.lines = diffData.content.split('\n').length;
359
- }
360
- fileWithDiff.status = diffData.status;
361
- fileWithDiff.hunks = diffData.hunks;
362
-
363
- if (diffData.hunks?.length > 0) {
364
- const addedLines = new Set<number>();
365
- const deletedBeforeLine = new Map<number, string[]>();
366
- for (const hunk of diffData.hunks) {
367
- let newLine = hunk.newStart;
368
- let pendingDeleted: string[] = [];
369
- for (const l of hunk.lines) {
370
- if (l.type === 'add') {
371
- addedLines.add(newLine);
372
- if (pendingDeleted.length > 0) {
373
- const existing = deletedBeforeLine.get(newLine) || [];
374
- deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
375
- pendingDeleted = [];
376
- }
377
- newLine++;
378
- } else if (l.type === 'del') {
379
- pendingDeleted.push(l.content);
380
- } else {
381
- if (pendingDeleted.length > 0) {
382
- const existing = deletedBeforeLine.get(newLine) || [];
383
- deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
384
- pendingDeleted = [];
385
- }
386
- newLine++;
387
- }
388
- }
389
- if (pendingDeleted.length > 0) {
390
- const existing = deletedBeforeLine.get(newLine) || [];
391
- deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
392
- }
393
- }
394
- fileWithDiff.addedLines = addedLines;
395
- fileWithDiff.deletedBeforeLine = deletedBeforeLine;
396
- }
397
- }
398
-
399
- const cardData = {
400
- id: f.path,
401
- x, y,
402
- width: size?.width || defaultCardWidth,
403
- height: size?.height || defaultCardHeight,
404
- meta: { file: fileWithDiff, ctx, savedSize: size },
405
- };
406
-
407
- // Check if in viewport
408
- const inViewport =
409
- x + (size?.width || defaultCardWidth) > worldLeft &&
410
- x < worldRight &&
411
- y + (size?.height || defaultCardHeight) > worldTop &&
412
- y < worldBottom;
413
-
414
- if (inViewport) {
415
- const card = _cardManager!.create(FILE_CARD_TYPE, cardData);
416
- if (card) {
417
- // Sync to ctx.fileCards so minimap, fitAll, etc. can find it
418
- ctx.fileCards.set(f.path, card);
419
- // Apply change markers for diff highlighting
420
- if (isChanged) {
421
- card.classList.add('file-card--changed');
422
- card.dataset.changed = 'true';
423
- }
424
- }
425
- createdCount++;
426
- } else {
427
- _cardManager!.defer(FILE_CARD_TYPE, cardData);
428
- // Also store in ctx.deferredCards so minimap, fitAll, etc. can see ALL files
429
- ctx.deferredCards.set(f.path, {
430
- file: fileWithDiff, x, y,
431
- size: { width: cardData.width, height: cardData.height },
432
- isChanged,
433
- });
434
- deferredCount++;
435
- }
436
- });
437
-
438
- restoreCollapsedDirs(ctx);
439
-
440
- console.log(`[gd-bridge] ${createdCount} created, ${deferredCount} deferred (${layerFiles.length} total)`);
441
- return true; // Signal: we handled it
442
- }
443
-
444
- /**
445
- * Materialize deferred cards that are now in the viewport.
446
- * Call this on zoom/pan changes.
447
- */
448
- export function materializeViewport(ctx: CanvasContext): number {
449
- if (!_cardManager || !_gdState) return 0;
450
-
451
- const MARGIN = 800;
452
- const state = _gdState.snapshot();
453
- const vpEl = ctx.canvasViewport;
454
- const vpW = vpEl?.clientWidth || window.innerWidth;
455
- const vpH = vpEl?.clientHeight || window.innerHeight;
456
- const zoom = state.zoom || 1;
457
- const offsetX = state.offsetX || 0;
458
- const offsetY = state.offsetY || 0;
459
-
460
- const rect: ViewportRect = {
461
- left: (-offsetX - MARGIN) / zoom,
462
- top: (-offsetY - MARGIN) / zoom,
463
- right: (vpW - offsetX + MARGIN) / zoom,
464
- bottom: (vpH - offsetY + MARGIN) / zoom,
465
- };
466
-
467
- const count = _cardManager.materializeInRect(rect);
468
-
469
- // Sync newly materialized cards to ctx.fileCards for minimap/fitAll
470
- // AND remove from ctx.deferredCards so viewport-culling doesn't re-create them
471
- if (count > 0) {
472
- for (const [id, card] of _cardManager.cards) {
473
- if (!ctx.fileCards.has(id)) {
474
- ctx.fileCards.set(id, card);
475
- }
476
- // Remove from deferredCards to prevent duplicate creation by viewport-culling
477
- ctx.deferredCards.delete(id);
478
- }
479
- }
480
-
481
- return count;
482
- }
1
+ // @ts-nocheck
2
+ /**
3
+ * XyDraw Bridge — Adapter between GitMaps and the xydraw engine.
4
+ *
5
+ * Wires xydraw's CanvasState + CardManager into the existing
6
+ * server-rendered DOM and XState persistence layer.
7
+ *
8
+ * Architecture:
9
+ * - CanvasState manages zoom/pan/transform (replaces manual math)
10
+ * - CardManager creates/defers cards via FileCardPlugin + DiffCardPlugin
11
+ * - XState actor remains source-of-truth for persistence
12
+ * - Server-rendered DOM (#canvasViewport, #canvasContent) stays intact
13
+ */
14
+
15
+ import { CanvasState } from '../../packages/galaxydraw/src/core/state';
16
+ import { CardManager } from '../../packages/galaxydraw/src/core/cards';
17
+ import { EventBus } from '../../packages/galaxydraw/src/core/events';
18
+ import { createFileCardPlugin, createDiffCardPlugin } from './file-card-plugin';
19
+ import type { CanvasContext } from './context';
20
+
21
+ /**
22
+ * Shared xydraw state instance.
23
+ * Replaces manual `ctx.canvas.style.transform = ...` calls.
24
+ */
25
+ let _gdState: CanvasState | null = null;
26
+ let _cardManager: CardManager | null = null;
27
+ let _eventBus: EventBus | null = null;
28
+
29
+ /**
30
+ * Initialize the xydraw state engine and bind to existing DOM.
31
+ * Call this after ctx.canvas and ctx.canvasViewport are set.
32
+ */
33
+ export function initXyDrawState(ctx: CanvasContext): CanvasState {
34
+ _gdState = new CanvasState();
35
+
36
+ if (ctx.canvasViewport && ctx.canvas) {
37
+ _gdState.bind(ctx.canvasViewport, ctx.canvas);
38
+ }
39
+
40
+ // Sync initial state from XState
41
+ const state = ctx.snap().context;
42
+ if (state.zoom) _gdState.zoom = state.zoom;
43
+ if (state.offsetX) _gdState.offsetX = state.offsetX;
44
+ if (state.offsetY) _gdState.offsetY = state.offsetY;
45
+ _gdState.applyTransform();
46
+
47
+ return _gdState;
48
+ }
49
+
50
+ /**
51
+ * Get the shared CanvasState instance.
52
+ */
53
+ export function getXyDrawState(): CanvasState | null {
54
+ return _gdState;
55
+ }
56
+
57
+ export const initGalaxyDrawState = initXyDrawState;
58
+ export const getGalaxyDrawState = getXyDrawState;
59
+
60
+ /**
61
+ * Zoom toward a screen point using xydraw's engine,
62
+ * then sync the computed state back to XState for persistence.
63
+ *
64
+ * @returns The new zoom/offset values (for callers that need them)
65
+ */
66
+ export function zoomTowardScreen(
67
+ ctx: CanvasContext,
68
+ screenX: number,
69
+ screenY: number,
70
+ factor: number,
71
+ ): { zoom: number; offsetX: number; offsetY: number } {
72
+ const gd = _gdState;
73
+
74
+ if (gd) {
75
+ // Delegate to xydraw engine
76
+ gd.zoomToward(screenX, screenY, factor);
77
+ // Sync back to XState for persistence
78
+ ctx.actor.send({ type: 'SET_ZOOM', zoom: gd.zoom });
79
+ ctx.actor.send({ type: 'SET_OFFSET', x: gd.offsetX, y: gd.offsetY });
80
+ return { zoom: gd.zoom, offsetX: gd.offsetX, offsetY: gd.offsetY };
81
+ }
82
+
83
+ // Fallback: manual math (pre-bridge init)
84
+ const state = ctx.snap().context;
85
+ const rect = ctx.canvasViewport?.getBoundingClientRect();
86
+ const mouseX = screenX - (rect?.left ?? 0);
87
+ const mouseY = screenY - (rect?.top ?? 0);
88
+ const newZoom = Math.min(3, Math.max(0.1, state.zoom * factor));
89
+ const scale = newZoom / state.zoom;
90
+ const newOffsetX = mouseX - (mouseX - state.offsetX) * scale;
91
+ const newOffsetY = mouseY - (mouseY - state.offsetY) * scale;
92
+ ctx.actor.send({ type: 'SET_ZOOM', zoom: newZoom });
93
+ ctx.actor.send({ type: 'SET_OFFSET', x: newOffsetX, y: newOffsetY });
94
+ return { zoom: newZoom, offsetX: newOffsetX, offsetY: newOffsetY };
95
+ }
96
+
97
+ /**
98
+ * Pan by pixel delta via xydraw's engine.
99
+ * Syncs back to XState for persistence.
100
+ */
101
+ export function panByDelta(
102
+ ctx: CanvasContext,
103
+ dx: number,
104
+ dy: number,
105
+ ): void {
106
+ const gd = _gdState;
107
+
108
+ if (gd) {
109
+ gd.pan(dx, dy);
110
+ ctx.actor.send({ type: 'SET_OFFSET', x: gd.offsetX, y: gd.offsetY });
111
+ return;
112
+ }
113
+
114
+ // Fallback
115
+ const state = ctx.snap().context;
116
+ ctx.actor.send({ type: 'SET_OFFSET', x: state.offsetX + dx, y: state.offsetY + dy });
117
+ }
118
+
119
+ /**
120
+ * Convert screen coordinates to world coordinates.
121
+ * Delegates to CanvasState.screenToWorld() when available.
122
+ */
123
+ export function screenToWorld(
124
+ ctx: CanvasContext,
125
+ screenX: number,
126
+ screenY: number,
127
+ ): { x: number; y: number } {
128
+ const gd = _gdState;
129
+
130
+ if (gd) {
131
+ return gd.screenToWorld(screenX, screenY);
132
+ }
133
+
134
+ // Fallback
135
+ const state = ctx.snap().context;
136
+ const rect = ctx.canvasViewport?.getBoundingClientRect();
137
+ return {
138
+ x: (screenX - (rect?.left ?? 0) - state.offsetX) / state.zoom,
139
+ y: (screenY - (rect?.top ?? 0) - state.offsetY) / state.zoom,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Center the viewport on a world coordinate.
145
+ * Delegates to CanvasState.panTo() when available.
146
+ */
147
+ export function panToWorld(
148
+ ctx: CanvasContext,
149
+ worldX: number,
150
+ worldY: number,
151
+ ): void {
152
+ const gd = _gdState;
153
+
154
+ if (gd) {
155
+ gd.panTo(worldX, worldY);
156
+ ctx.actor.send({ type: 'SET_OFFSET', x: gd.offsetX, y: gd.offsetY });
157
+ return;
158
+ }
159
+
160
+ // Fallback
161
+ const state = ctx.snap().context;
162
+ const vp = ctx.canvasViewport;
163
+ if (vp) {
164
+ const vpW = vp.clientWidth;
165
+ const vpH = vp.clientHeight;
166
+ const newOffsetX = vpW / 2 - worldX * state.zoom;
167
+ const newOffsetY = vpH / 2 - worldY * state.zoom;
168
+ ctx.actor.send({ type: 'SET_OFFSET', x: newOffsetX, y: newOffsetY });
169
+ }
170
+ }
171
+
172
+ // ─── Card Manager ───────────────────────────────────────
173
+
174
+ /**
175
+ * Initialize the CardManager with file card plugins.
176
+ * Call after initXyDrawState() when ctx.canvas is available.
177
+ *
178
+ * The CardManager handles:
179
+ * - Card creation via plugins (FileCardPlugin, DiffCardPlugin)
180
+ * - Drag, resize, z-order management
181
+ * - Selection (single, multi)
182
+ * - Deferred rendering (virtualization)
183
+ */
184
+ import { scheduleRenderConnections } from './connections';
185
+
186
+ export function initCardManager(ctx: CanvasContext): CardManager | null {
187
+ if (!_gdState || !ctx.canvas) {
188
+ console.warn('[xydraw-bridge] Cannot init CardManager: state or canvas not ready');
189
+ return null;
190
+ }
191
+
192
+ _eventBus = new EventBus();
193
+ _cardManager = new CardManager(_gdState, _eventBus, ctx.canvas, {
194
+ defaultWidth: 580,
195
+ defaultHeight: 700,
196
+ minWidth: 280,
197
+ minHeight: 200,
198
+ gridSize: 0,
199
+ cornerSize: 40,
200
+ });
201
+
202
+ // Register plugins
203
+ _cardManager.registerPlugin(createFileCardPlugin());
204
+ _cardManager.registerPlugin(createDiffCardPlugin());
205
+
206
+ // Sync card events back to XState for persistence
207
+ _eventBus.on('card:move', (ev) => {
208
+ const { id, x, y } = ev;
209
+ ctx.actor.send({ type: 'SAVE_POSITION', path: id, x, y });
210
+ scheduleRenderConnections(ctx);
211
+ });
212
+
213
+ _eventBus.on('card:resize', (ev) => {
214
+ const { id, width, height } = ev;
215
+ ctx.actor.send({ type: 'RESIZE_CARD', path: id, width, height });
216
+ scheduleRenderConnections(ctx);
217
+ });
218
+
219
+ console.log('[xydraw-bridge] CardManager initialized with file + diff plugins');
220
+ return _cardManager;
221
+ }
222
+
223
+ /**
224
+ * Get the shared CardManager instance.
225
+ */
226
+ export function getCardManager(): CardManager | null {
227
+ return _cardManager;
228
+ }
229
+
230
+ /**
231
+ * Get the shared EventBus instance.
232
+ */
233
+ export function getEventBus(): EventBus | null {
234
+ return _eventBus;
235
+ }
236
+
237
+ // ─── Card Creation via CardManager ──────────────────────
238
+
239
+ import { FILE_CARD_TYPE, DIFF_CARD_TYPE } from './file-card-plugin';
240
+ import { getActiveLayer } from './layers';
241
+ import { updateHiddenUI } from './hidden-files';
242
+ import type { ViewportRect } from '../../packages/xydraw/src/core/state';
243
+
244
+ /**
245
+ * Render all files on canvas using CardManager instead of direct DOM.
246
+ *
247
+ * This replaces the viewport culling logic in renderAllFilesOnCanvas():
248
+ * - Cards in/near viewport → CardManager.create() (immediate DOM)
249
+ * - Cards outside viewport → CardManager.defer() (lazy materialization)
250
+ * - On scroll/zoom materializeViewport() creates deferred cards
251
+ *
252
+ * Benefits over the legacy approach:
253
+ * - Drag/resize/z-order handled uniformly by CardManager
254
+ * - EventBus emits card:create/card:move/card:resize for persistence
255
+ * - Cleaner separation between rendering and interaction
256
+ */
257
+ export function renderAllFilesViaCardManager(ctx: CanvasContext, files: any[]) {
258
+ if (!_cardManager || !_gdState) {
259
+ // Fallback to legacy if CardManager not initialized
260
+ console.warn('[xydraw-bridge] CardManager not ready, falling back to legacy render');
261
+ return false; // Signal caller to use legacy path
262
+ }
263
+
264
+ _cardManager.clear();
265
+
266
+ // Also clear existing DOM cards, pills, and deferred state
267
+ // Without this, layer switching leaves orphaned elements
268
+ ctx.fileCards.forEach(card => card.remove());
269
+ ctx.fileCards.clear();
270
+ ctx.deferredCards.clear();
271
+ ctx.canvas?.querySelectorAll('.dir-label').forEach(el => el.remove());
272
+ ctx.canvas?.querySelectorAll('.file-pill').forEach(el => el.remove());
273
+ // Clear pill tracking Map
274
+ const { clearAllPills } = require('./viewport-culling');
275
+ clearAllPills(ctx);
276
+ if (ctx.svgOverlay) ctx.svgOverlay.innerHTML = '';
277
+
278
+ const { resetCardGroups, restoreCollapsedDirs } = require('./card-groups');
279
+ resetCardGroups();
280
+
281
+ const visibleFiles = files.filter(f => !ctx.hiddenFiles.has(f.path));
282
+ updateHiddenUI(ctx);
283
+
284
+ // Build changed file data map
285
+ const changedFileDataMap = new Map<string, any>();
286
+ if (ctx.commitFilesData) {
287
+ ctx.commitFilesData.forEach(f => changedFileDataMap.set(f.path, f));
288
+ }
289
+
290
+ let layerFiles = visibleFiles;
291
+ const activeLayer = getActiveLayer();
292
+ if (activeLayer) {
293
+ layerFiles = visibleFiles.filter(f => !!activeLayer.files[f.path]);
294
+ } else {
295
+ // Default layer: exclude files that have been moved to other layers
296
+ const { isFileMovedFromDefault } = require('./layers');
297
+ layerFiles = visibleFiles.filter(f => !isFileMovedFromDefault(f.path));
298
+ }
299
+
300
+ // Grid layout: square-ish
301
+ const count = layerFiles.length;
302
+ const cols = Math.max(1, Math.ceil(Math.sqrt(count)));
303
+ const defaultCardWidth = 580;
304
+ const defaultCardHeight = 700;
305
+ const gap = 20;
306
+ const cellW = defaultCardWidth + gap;
307
+ const cellH = defaultCardHeight + gap;
308
+
309
+ // Viewport rect for initial visibility check
310
+ const MARGIN = 800;
311
+ const state = _gdState.snapshot();
312
+ const vpEl = ctx.canvasViewport;
313
+ const vpW = vpEl?.clientWidth || window.innerWidth;
314
+ const vpH = vpEl?.clientHeight || window.innerHeight;
315
+ const zoom = state.zoom || 1;
316
+ const offsetX = state.offsetX || 0;
317
+ const offsetY = state.offsetY || 0;
318
+ const worldLeft = (-offsetX - MARGIN) / zoom;
319
+ const worldTop = (-offsetY - MARGIN) / zoom;
320
+ const worldRight = (vpW - offsetX + MARGIN) / zoom;
321
+ const worldBottom = (vpH - offsetY + MARGIN) / zoom;
322
+
323
+ let createdCount = 0;
324
+ let deferredCount = 0;
325
+
326
+ // Cache XState state once outside the loop — avoids N snapshots for N files
327
+ const cachedCardSizes = ctx.snap().context.cardSizes || {};
328
+
329
+ layerFiles.forEach((f, index) => {
330
+ const posKey = `allfiles:${f.path}`;
331
+ let x: number, y: number;
332
+
333
+ if (ctx.positions.has(posKey)) {
334
+ const pos = ctx.positions.get(posKey);
335
+ x = pos.x; y = pos.y;
336
+ } else {
337
+ const col = index % cols;
338
+ const row = Math.floor(index / cols);
339
+ x = 50 + col * cellW;
340
+ y = 50 + row * cellH;
341
+ }
342
+
343
+ // Get saved size (from cached snapshot — no per-file ctx.snap() call)
344
+ let size = cachedCardSizes[f.path];
345
+ if (!size && ctx.positions.has(posKey)) {
346
+ const pos = ctx.positions.get(posKey);
347
+ if (pos.width) size = { width: pos.width, height: pos.height };
348
+ }
349
+
350
+ // Merge diff/layer data
351
+ let fileWithDiff = { ...f };
352
+ if (activeLayer && activeLayer.files[fileWithDiff.path]) {
353
+ fileWithDiff.layerSections = activeLayer.files[fileWithDiff.path].sections;
354
+ }
355
+
356
+ const isChanged = ctx.changedFilePaths.has(f.path);
357
+ if (isChanged && changedFileDataMap.has(fileWithDiff.path)) {
358
+ const diffData = changedFileDataMap.get(fileWithDiff.path);
359
+ if (diffData.content) {
360
+ fileWithDiff.content = diffData.content;
361
+ fileWithDiff.lines = diffData.content.split('\n').length;
362
+ }
363
+ fileWithDiff.status = diffData.status;
364
+ fileWithDiff.hunks = diffData.hunks;
365
+
366
+ if (diffData.hunks?.length > 0) {
367
+ const addedLines = new Set<number>();
368
+ const deletedBeforeLine = new Map<number, string[]>();
369
+ for (const hunk of diffData.hunks) {
370
+ let newLine = hunk.newStart;
371
+ let pendingDeleted: string[] = [];
372
+ for (const l of hunk.lines) {
373
+ if (l.type === 'add') {
374
+ addedLines.add(newLine);
375
+ if (pendingDeleted.length > 0) {
376
+ const existing = deletedBeforeLine.get(newLine) || [];
377
+ deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
378
+ pendingDeleted = [];
379
+ }
380
+ newLine++;
381
+ } else if (l.type === 'del') {
382
+ pendingDeleted.push(l.content);
383
+ } else {
384
+ if (pendingDeleted.length > 0) {
385
+ const existing = deletedBeforeLine.get(newLine) || [];
386
+ deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
387
+ pendingDeleted = [];
388
+ }
389
+ newLine++;
390
+ }
391
+ }
392
+ if (pendingDeleted.length > 0) {
393
+ const existing = deletedBeforeLine.get(newLine) || [];
394
+ deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
395
+ }
396
+ }
397
+ fileWithDiff.addedLines = addedLines;
398
+ fileWithDiff.deletedBeforeLine = deletedBeforeLine;
399
+ }
400
+ }
401
+
402
+ const cardData = {
403
+ id: f.path,
404
+ x, y,
405
+ width: size?.width || defaultCardWidth,
406
+ height: size?.height || defaultCardHeight,
407
+ meta: { file: fileWithDiff, ctx, savedSize: size },
408
+ };
409
+
410
+ // Check if in viewport
411
+ const inViewport =
412
+ x + (size?.width || defaultCardWidth) > worldLeft &&
413
+ x < worldRight &&
414
+ y + (size?.height || defaultCardHeight) > worldTop &&
415
+ y < worldBottom;
416
+
417
+ if (inViewport) {
418
+ const card = _cardManager!.create(FILE_CARD_TYPE, cardData);
419
+ if (card) {
420
+ // Sync to ctx.fileCards so minimap, fitAll, etc. can find it
421
+ ctx.fileCards.set(f.path, card);
422
+ // Apply change markers for diff highlighting
423
+ if (isChanged) {
424
+ card.classList.add('file-card--changed');
425
+ card.dataset.changed = 'true';
426
+ }
427
+ }
428
+ createdCount++;
429
+ } else {
430
+ _cardManager!.defer(FILE_CARD_TYPE, cardData);
431
+ // Also store in ctx.deferredCards so minimap, fitAll, etc. can see ALL files
432
+ ctx.deferredCards.set(f.path, {
433
+ file: fileWithDiff, x, y,
434
+ size: { width: cardData.width, height: cardData.height },
435
+ isChanged,
436
+ });
437
+ deferredCount++;
438
+ }
439
+ });
440
+
441
+ restoreCollapsedDirs(ctx);
442
+
443
+ console.log(`[gd-bridge] ${createdCount} created, ${deferredCount} deferred (${layerFiles.length} total)`);
444
+ return true; // Signal: we handled it
445
+ }
446
+
447
+ /**
448
+ * Materialize deferred cards that are now in the viewport.
449
+ * Call this on zoom/pan changes.
450
+ */
451
+ export function materializeViewport(ctx: CanvasContext): number {
452
+ if (!_cardManager || !_gdState) return 0;
453
+
454
+ const MARGIN = 800;
455
+ const state = _gdState.snapshot();
456
+ const vpEl = ctx.canvasViewport;
457
+ const vpW = vpEl?.clientWidth || window.innerWidth;
458
+ const vpH = vpEl?.clientHeight || window.innerHeight;
459
+ const zoom = state.zoom || 1;
460
+ const offsetX = state.offsetX || 0;
461
+ const offsetY = state.offsetY || 0;
462
+
463
+ const rect: ViewportRect = {
464
+ left: (-offsetX - MARGIN) / zoom,
465
+ top: (-offsetY - MARGIN) / zoom,
466
+ right: (vpW - offsetX + MARGIN) / zoom,
467
+ bottom: (vpH - offsetY + MARGIN) / zoom,
468
+ };
469
+
470
+ const count = _cardManager.materializeInRect(rect);
471
+
472
+ // Sync newly materialized cards to ctx.fileCards for minimap/fitAll
473
+ // AND remove from ctx.deferredCards so viewport-culling doesn't re-create them
474
+ if (count > 0) {
475
+ for (const [id, card] of _cardManager.cards) {
476
+ if (!ctx.fileCards.has(id)) {
477
+ ctx.fileCards.set(id, card);
478
+ }
479
+ // Remove from deferredCards to prevent duplicate creation by viewport-culling
480
+ ctx.deferredCards.delete(id);
481
+ }
482
+ }
483
+
484
+ return count;
485
+ }