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.
Files changed (145) hide show
  1. package/README.md +265 -122
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/[owner]/[repo]/page.client.tsx +5 -0
  5. package/app/[slug]/page.client.tsx +5 -0
  6. package/app/analytics.db +0 -0
  7. package/app/api/analytics/route.ts +64 -0
  8. package/app/api/auth/positions/route.ts +95 -33
  9. package/app/api/build-info/route.ts +19 -0
  10. package/app/api/chat/route.ts +13 -2
  11. package/app/api/manifest.json/route.ts +20 -0
  12. package/app/api/og-image/route.ts +14 -0
  13. package/app/api/pwa-icon/route.ts +14 -0
  14. package/app/api/repo/clone-stream/route.ts +20 -12
  15. package/app/api/repo/file-content/route.ts +73 -20
  16. package/app/api/repo/imports/route.ts +21 -3
  17. package/app/api/repo/list/route.ts +30 -0
  18. package/app/api/repo/load/route.test.ts +62 -0
  19. package/app/api/repo/load/route.ts +41 -1
  20. package/app/api/repo/pdf-thumb/route.ts +127 -0
  21. package/app/api/repo/resolve-slug/route.ts +51 -0
  22. package/app/api/repo/tree/route.ts +188 -104
  23. package/app/api/repo/upload/route.ts +6 -9
  24. package/app/api/sw.js/route.ts +70 -0
  25. package/app/api/version/route.ts +26 -0
  26. package/app/galaxy-canvas/page.client.tsx +2 -0
  27. package/app/galaxy-canvas/page.tsx +5 -0
  28. package/app/globals.css +5844 -4694
  29. package/app/icon.png +0 -0
  30. package/app/layout.tsx +1284 -467
  31. package/app/lib/auto-arrange.test.ts +158 -0
  32. package/app/lib/auto-arrange.ts +147 -0
  33. package/app/lib/canvas-export.ts +358 -358
  34. package/app/lib/canvas-text.ts +4 -72
  35. package/app/lib/canvas.ts +625 -564
  36. package/app/lib/card-arrangement.ts +21 -7
  37. package/app/lib/card-context-menu.tsx +2 -2
  38. package/app/lib/card-groups.ts +9 -2
  39. package/app/lib/cards.tsx +1361 -914
  40. package/app/lib/chat.tsx +65 -9
  41. package/app/lib/code-editor.ts +86 -2
  42. package/app/lib/connections.tsx +34 -43
  43. package/app/lib/context.test.ts +32 -0
  44. package/app/lib/context.ts +19 -3
  45. package/app/lib/cursor-sharing.ts +34 -0
  46. package/app/lib/events.tsx +76 -73
  47. package/app/lib/export-canvas.ts +287 -0
  48. package/app/lib/file-card-plugin.ts +148 -134
  49. package/app/lib/file-modal.tsx +49 -0
  50. package/app/lib/file-preview.ts +486 -400
  51. package/app/lib/github-import.test.ts +424 -0
  52. package/app/lib/global-search.ts +48 -27
  53. package/app/lib/initial-route-hydration.test.ts +283 -0
  54. package/app/lib/initial-route-hydration.ts +202 -0
  55. package/app/lib/landing-reset.test.ts +99 -0
  56. package/app/lib/landing-reset.ts +106 -0
  57. package/app/lib/landing-shell.test.ts +75 -0
  58. package/app/lib/large-repo-optimization.ts +37 -0
  59. package/app/lib/layers.tsx +17 -18
  60. package/app/lib/layout-snapshots.ts +320 -0
  61. package/app/lib/loading.test.ts +69 -0
  62. package/app/lib/loading.tsx +160 -45
  63. package/app/lib/mount-cleanup.test.ts +52 -0
  64. package/app/lib/mount-cleanup.ts +34 -0
  65. package/app/lib/mount-init.test.ts +123 -0
  66. package/app/lib/mount-init.ts +107 -0
  67. package/app/lib/mount-lifecycle.test.ts +39 -0
  68. package/app/lib/mount-lifecycle.ts +12 -0
  69. package/app/lib/mount-route-wiring.test.ts +87 -0
  70. package/app/lib/mount-route-wiring.ts +84 -0
  71. package/app/lib/multi-repo.ts +14 -0
  72. package/app/lib/onboarding-tutorial.ts +278 -0
  73. package/app/lib/perf-overlay.ts +78 -0
  74. package/app/lib/positions.ts +191 -122
  75. package/app/lib/recent-commits.test.ts +869 -0
  76. package/app/lib/recent-commits.ts +227 -0
  77. package/app/lib/repo-handoff.test.ts +23 -0
  78. package/app/lib/repo-handoff.ts +16 -0
  79. package/app/lib/repo-progressive.ts +119 -0
  80. package/app/lib/repo-select.test.ts +61 -0
  81. package/app/lib/repo-select.ts +74 -0
  82. package/app/lib/repo.tsx +1383 -977
  83. package/app/lib/role.ts +228 -0
  84. package/app/lib/route-catchall.test.ts +27 -0
  85. package/app/lib/route-repo-entry.test.ts +95 -0
  86. package/app/lib/route-repo-entry.ts +36 -0
  87. package/app/lib/router-contract.test.ts +22 -0
  88. package/app/lib/router-contract.ts +19 -0
  89. package/app/lib/shared-layout.test.ts +86 -0
  90. package/app/lib/shared-layout.ts +82 -0
  91. package/app/lib/shortcuts-panel.ts +2 -0
  92. package/app/lib/status-bar.test.ts +118 -0
  93. package/app/lib/status-bar.ts +365 -128
  94. package/app/lib/sync-controls.test.ts +43 -0
  95. package/app/lib/sync-controls.tsx +303 -0
  96. package/app/lib/test-dom.ts +145 -0
  97. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  98. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  99. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  100. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  101. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  102. package/app/lib/transclusion-smoke.test.ts +163 -0
  103. package/app/lib/tutorial.ts +301 -0
  104. package/app/lib/version.ts +93 -0
  105. package/app/lib/viewport-culling.ts +740 -728
  106. package/app/lib/virtual-files.ts +456 -0
  107. package/app/lib/webgl-text.ts +189 -0
  108. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -477
  109. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  110. package/app/og-image.png +0 -0
  111. package/app/page.client.tsx +70 -215
  112. package/app/page.tsx +27 -92
  113. package/app/state/machine.js +13 -0
  114. package/banner.png +0 -0
  115. package/package.json +17 -8
  116. package/server.ts +11 -1
  117. package/app/api/connections/route.ts +0 -72
  118. package/app/api/positions/route.ts +0 -80
  119. package/app/api/repo/browse/route.ts +0 -55
  120. package/app/lib/pr-review.ts +0 -374
  121. package/packages/galaxydraw/README.md +0 -296
  122. package/packages/galaxydraw/banner.png +0 -0
  123. package/packages/galaxydraw/demo/build-static.ts +0 -100
  124. package/packages/galaxydraw/demo/client.ts +0 -154
  125. package/packages/galaxydraw/demo/dist/client.js +0 -8
  126. package/packages/galaxydraw/demo/index.html +0 -256
  127. package/packages/galaxydraw/demo/server.ts +0 -96
  128. package/packages/galaxydraw/dist/index.js +0 -984
  129. package/packages/galaxydraw/dist/index.js.map +0 -16
  130. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  131. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  132. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  133. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  134. package/packages/galaxydraw/package.json +0 -49
  135. package/packages/galaxydraw/perf.test.ts +0 -284
  136. package/packages/galaxydraw/src/core/cards.ts +0 -435
  137. package/packages/galaxydraw/src/core/engine.ts +0 -339
  138. package/packages/galaxydraw/src/core/events.ts +0 -81
  139. package/packages/galaxydraw/src/core/layout.ts +0 -136
  140. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  141. package/packages/galaxydraw/src/core/state.ts +0 -177
  142. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  143. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  144. package/packages/galaxydraw/src/index.ts +0 -40
  145. package/packages/galaxydraw/tsconfig.json +0 -30
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Sync Controls — Leader-only UI for pushing canvas state to remote servers
3
+ *
4
+ * Features:
5
+ * - Server dropdown (select remote to push to)
6
+ * - Auto-sync toggle
7
+ * - Manual push/pull buttons
8
+ * - Last sync status indicator
9
+ */
10
+
11
+ import { isLeader } from "./role";
12
+
13
+ const DEFAULT_SERVERS = [
14
+ { url: "https://gitmaps.xyz", name: "gitmaps.xyz (Production)" },
15
+ { url: "http://localhost:3336", name: "Local Dev Server" },
16
+ ];
17
+
18
+ let _customServers: string[] = [];
19
+ let _selectedServer = DEFAULT_SERVERS[0].url;
20
+ let _autoSync = false;
21
+ let _lastSyncTime: number | null = null;
22
+ let _syncing = false;
23
+
24
+ try {
25
+ const stored = localStorage.getItem("gitcanvas:servers");
26
+ if (stored) _customServers = JSON.parse(stored);
27
+ const selected = localStorage.getItem("gitcanvas:selectedServer");
28
+ if (selected) _selectedServer = selected;
29
+ const auto = localStorage.getItem("gitcanvas:autoSync");
30
+ if (auto) _autoSync = JSON.parse(auto);
31
+ } catch {}
32
+
33
+ export function getSelectedServer(): string {
34
+ return _selectedServer;
35
+ }
36
+
37
+ export function isAutoSyncEnabled(): boolean {
38
+ return _autoSync;
39
+ }
40
+
41
+ export function getLastSyncTime(): number | null {
42
+ return _lastSyncTime;
43
+ }
44
+
45
+ export function isSyncing(): boolean {
46
+ return _syncing;
47
+ }
48
+
49
+ export function toggleAutoSync(): boolean {
50
+ _autoSync = !_autoSync;
51
+ localStorage.setItem("gitcanvas:autoSync", JSON.stringify(_autoSync));
52
+ console.log(`[sync] Auto-sync ${_autoSync ? "enabled" : "disabled"}`);
53
+ return _autoSync;
54
+ }
55
+
56
+ export function setSelectedServer(url: string): void {
57
+ _selectedServer = url;
58
+ localStorage.setItem("gitcanvas:selectedServer", url);
59
+ console.log(`[sync] Selected server: ${url}`);
60
+ }
61
+
62
+ export function getAvailableServers() {
63
+ return [
64
+ ...DEFAULT_SERVERS,
65
+ ..._customServers.map((url) => ({ url, name: `Custom: ${url}` })),
66
+ ];
67
+ }
68
+
69
+ export function addCustomServer(url: string): void {
70
+ if (!_customServers.includes(url)) {
71
+ _customServers.push(url);
72
+ localStorage.setItem("gitcanvas:servers", JSON.stringify(_customServers));
73
+ }
74
+ }
75
+
76
+ export async function pushToServer(
77
+ repoPath: string,
78
+ positions: Record<string, any>,
79
+ ): Promise<boolean> {
80
+ if (!isLeader()) {
81
+ console.warn("[sync] Cannot push - not in leader mode");
82
+ return false;
83
+ }
84
+
85
+ if (_syncing) {
86
+ console.log("[sync] Already syncing, skipping");
87
+ return false;
88
+ }
89
+
90
+ _syncing = true;
91
+ const startTime = Date.now();
92
+
93
+ try {
94
+ const serverUrl = _selectedServer;
95
+ const endpoint = `${serverUrl}/api/auth/positions`;
96
+
97
+ console.log(`[sync] Pushing to ${endpoint}`);
98
+
99
+ const response = await fetch(endpoint, {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify({
103
+ repoUrl: repoPath,
104
+ positions,
105
+ syncedAt: new Date().toISOString(),
106
+ }),
107
+ });
108
+
109
+ if (!response.ok) {
110
+ const error = await response.text();
111
+ throw new Error(`Server returned ${response.status}: ${error}`);
112
+ }
113
+
114
+ _lastSyncTime = Date.now();
115
+ console.log(`[sync] Push successful (${Date.now() - startTime}ms)`);
116
+ return true;
117
+ } catch (error: any) {
118
+ console.error("[sync] Push failed:", error.message);
119
+ return false;
120
+ } finally {
121
+ _syncing = false;
122
+ }
123
+ }
124
+
125
+ export async function pullFromServer(
126
+ repoPath: string,
127
+ ): Promise<Record<string, any> | null> {
128
+ if (_syncing) {
129
+ console.log("[sync] Already syncing, skipping");
130
+ return null;
131
+ }
132
+
133
+ _syncing = true;
134
+ const startTime = Date.now();
135
+
136
+ try {
137
+ const serverUrl = _selectedServer;
138
+ const endpoint = `${serverUrl}/api/auth/positions?repo=${encodeURIComponent(repoPath)}`;
139
+
140
+ console.log(`[sync] Pulling from ${endpoint}`);
141
+
142
+ const response = await fetch(endpoint);
143
+
144
+ if (!response.ok) {
145
+ const error = await response.text();
146
+ throw new Error(`Server returned ${response.status}: ${error}`);
147
+ }
148
+
149
+ const data = await response.json();
150
+ console.log(`[sync] Pull successful (${Date.now() - startTime}ms)`);
151
+ return data.positions || null;
152
+ } catch (error: any) {
153
+ console.error("[sync] Pull failed:", error.message);
154
+ return null;
155
+ } finally {
156
+ _syncing = false;
157
+ }
158
+ }
159
+
160
+ export function formatLastSync(time: number | null): string {
161
+ if (!time) return "Never";
162
+ const diff = Date.now() - time;
163
+ if (diff < 60000) return "Just now";
164
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
165
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
166
+ return `${Math.floor(diff / 86400000)}d ago`;
167
+ }
168
+
169
+ export function createSyncControlsUI(): HTMLElement {
170
+ const container = document.createElement("div");
171
+ container.className = "sync-controls";
172
+ container.id = "syncControls";
173
+
174
+ const servers = getAvailableServers();
175
+ const lastSync = formatLastSync(_lastSyncTime);
176
+
177
+ container.innerHTML = `
178
+ <div class="sync-controls-inner">
179
+ <div class="sync-server-select">
180
+ <label for="syncServer">Server:</label>
181
+ <select id="syncServer">
182
+ ${servers.map((s) => `<option value="${s.url}" ${s.url === _selectedServer ? "selected" : ""}>${s.name}</option>`).join("")}
183
+ </select>
184
+ <button id="addServerBtn" class="sync-btn-icon" title="Add custom server">+</button>
185
+ </div>
186
+
187
+ <div class="sync-auto-toggle">
188
+ <input type="checkbox" id="autoSyncToggle" ${_autoSync ? "checked" : ""} />
189
+ <label for="autoSyncToggle">Auto-sync</label>
190
+ </div>
191
+
192
+ <div class="sync-buttons">
193
+ <button id="pushBtn" class="sync-btn sync-btn-push" ${_syncing ? "disabled" : ""}>
194
+ ${_syncing ? "⏳" : "📤"} Push
195
+ </button>
196
+ <button id="pullBtn" class="sync-btn sync-btn-pull" ${_syncing ? "disabled" : ""}>
197
+ ${_syncing ? "⏳" : "📥"} Pull
198
+ </button>
199
+ </div>
200
+
201
+ <div class="sync-status">
202
+ <span class="sync-status-dot ${_lastSyncTime ? "synced" : ""}"></span>
203
+ <span class="sync-status-text">Last sync: ${lastSync}</span>
204
+ </div>
205
+ </div>
206
+ `;
207
+
208
+ // Wire up event listeners
209
+ const serverSelect = container.querySelector(
210
+ "#syncServer",
211
+ ) as HTMLSelectElement;
212
+ serverSelect?.addEventListener("change", (e) => {
213
+ setSelectedServer((e.target as HTMLSelectElement).value);
214
+ });
215
+
216
+ const addServerBtn = container.querySelector(
217
+ "#addServerBtn",
218
+ ) as HTMLButtonElement;
219
+ addServerBtn?.addEventListener("click", () => {
220
+ const url = prompt("Enter custom server URL (e.g., https://myserver.com):");
221
+ if (url && url.startsWith("http")) {
222
+ addCustomServer(url);
223
+ location.reload(); // Refresh to show new option
224
+ }
225
+ });
226
+
227
+ const autoSyncToggle = container.querySelector(
228
+ "#autoSyncToggle",
229
+ ) as HTMLInputElement;
230
+ autoSyncToggle?.addEventListener("change", () => {
231
+ toggleAutoSync();
232
+ });
233
+
234
+ const pushBtn = container.querySelector("#pushBtn") as HTMLButtonElement;
235
+ pushBtn?.addEventListener("click", async () => {
236
+ const ctx = getCanvasContext();
237
+ if (!ctx) return;
238
+
239
+ const repoPath = ctx.snap()?.context?.repoPath;
240
+ if (!repoPath) return;
241
+
242
+ pushBtn.disabled = true;
243
+ pushBtn.textContent = "⏳ Pushing...";
244
+
245
+ const positions: Record<string, any> = {};
246
+ for (const [k, v] of ctx.positions) {
247
+ positions[k] = v;
248
+ }
249
+
250
+ const success = await pushToServer(repoPath, positions);
251
+
252
+ pushBtn.disabled = false;
253
+ pushBtn.textContent = success ? "✅ Pushed!" : "❌ Failed";
254
+ setTimeout(() => {
255
+ pushBtn.textContent = "📤 Push";
256
+ location.reload(); // Refresh to show updated status
257
+ }, 2000);
258
+ });
259
+
260
+ const pullBtn = container.querySelector("#pullBtn") as HTMLButtonElement;
261
+ pullBtn?.addEventListener("click", async () => {
262
+ const ctx = getCanvasContext();
263
+ if (!ctx) return;
264
+
265
+ const repoPath = ctx.snap()?.context?.repoPath;
266
+ if (!repoPath) return;
267
+
268
+ pullBtn.disabled = true;
269
+ pullBtn.textContent = "⏳ Pulling...";
270
+
271
+ const positions = await pullFromServer(repoPath);
272
+
273
+ pullBtn.disabled = false;
274
+ pullBtn.textContent = positions ? "✅ Pulled!" : "❌ Failed";
275
+ setTimeout(() => {
276
+ pullBtn.textContent = "📥 Pull";
277
+ if (positions) {
278
+ // Merge pulled positions
279
+ for (const [k, v] of Object.entries(positions)) {
280
+ ctx.positions.set(k, v);
281
+ }
282
+ import("./repo").then((m) =>
283
+ m.renderAllFilesOnCanvas(ctx, ctx.allFilesData || []),
284
+ );
285
+ }
286
+ }, 2000);
287
+ });
288
+
289
+ return container;
290
+ }
291
+
292
+ export function renderSyncControls(container?: HTMLElement) {
293
+ if (!isLeader()) return; // Only leaders see sync controls
294
+
295
+ const target = container || document.querySelector(".toolbar-right");
296
+ if (!target) return;
297
+
298
+ const existing = document.getElementById("syncControls");
299
+ if (existing) existing.remove();
300
+
301
+ const ui = createSyncControlsUI();
302
+ target.appendChild(ui);
303
+ }
@@ -0,0 +1,145 @@
1
+ import { Window } from 'happy-dom';
2
+
3
+ export interface SetupDomTestOptions {
4
+ url?: string;
5
+ html?: string;
6
+ clipboard?: { writeText: (text: string) => Promise<void> };
7
+ raf?: boolean;
8
+ }
9
+
10
+ export interface DomTestHandle {
11
+ window: Window;
12
+ cleanup: () => void;
13
+ }
14
+
15
+ export function setupDomTest(options: SetupDomTestOptions = {}): DomTestHandle {
16
+ const {
17
+ url = 'http://localhost:3335/',
18
+ html = '',
19
+ clipboard,
20
+ raf = false,
21
+ } = options;
22
+
23
+ const window = new Window({ url });
24
+ (window as any).SyntaxError = SyntaxError;
25
+
26
+ Object.assign(globalThis, {
27
+ window,
28
+ document: window.document,
29
+ navigator: window.navigator,
30
+ localStorage: window.localStorage,
31
+ Element: window.Element,
32
+ HTMLElement: window.HTMLElement,
33
+ HTMLButtonElement: window.HTMLButtonElement,
34
+ HTMLInputElement: window.HTMLInputElement,
35
+ HTMLSelectElement: window.HTMLSelectElement,
36
+ HTMLTextAreaElement: window.HTMLTextAreaElement,
37
+ SVGElement: window.SVGElement,
38
+ DocumentFragment: window.DocumentFragment,
39
+ Node: window.Node,
40
+ Text: window.Text,
41
+ DOMRect: window.DOMRect,
42
+ Event: window.Event,
43
+ CustomEvent: window.CustomEvent,
44
+ MouseEvent: window.MouseEvent,
45
+ KeyboardEvent: window.KeyboardEvent,
46
+ MutationObserver: window.MutationObserver,
47
+ ResizeObserver: window.ResizeObserver,
48
+ getComputedStyle: window.getComputedStyle.bind(window),
49
+ });
50
+
51
+ if (raf) {
52
+ Object.assign(globalThis, {
53
+ requestAnimationFrame: (cb: FrameRequestCallback) => setTimeout(() => cb(Date.now()), 0),
54
+ cancelAnimationFrame: (id: any) => clearTimeout(id),
55
+ });
56
+ }
57
+
58
+ if (html) {
59
+ document.body.innerHTML = html;
60
+ }
61
+
62
+ if (clipboard) {
63
+ Object.defineProperty(navigator, 'clipboard', {
64
+ value: clipboard,
65
+ configurable: true,
66
+ });
67
+ }
68
+
69
+ const cleanup = () => {
70
+ window?.close();
71
+ document.body.innerHTML = '';
72
+ try {
73
+ localStorage.clear();
74
+ } catch {}
75
+ try {
76
+ delete (globalThis as any).requestAnimationFrame;
77
+ delete (globalThis as any).cancelAnimationFrame;
78
+ } catch {}
79
+ try {
80
+ delete (globalThis as any).window;
81
+ delete (globalThis as any).document;
82
+ delete (globalThis as any).navigator;
83
+ delete (globalThis as any).localStorage;
84
+ delete (globalThis as any).Element;
85
+ delete (globalThis as any).HTMLElement;
86
+ delete (globalThis as any).HTMLButtonElement;
87
+ delete (globalThis as any).HTMLInputElement;
88
+ delete (globalThis as any).HTMLSelectElement;
89
+ delete (globalThis as any).HTMLTextAreaElement;
90
+ delete (globalThis as any).SVGElement;
91
+ delete (globalThis as any).DocumentFragment;
92
+ delete (globalThis as any).Node;
93
+ delete (globalThis as any).Text;
94
+ delete (globalThis as any).DOMRect;
95
+ delete (globalThis as any).Event;
96
+ delete (globalThis as any).CustomEvent;
97
+ delete (globalThis as any).MouseEvent;
98
+ delete (globalThis as any).KeyboardEvent;
99
+ delete (globalThis as any).MutationObserver;
100
+ delete (globalThis as any).ResizeObserver;
101
+ delete (globalThis as any).getComputedStyle;
102
+ } catch {}
103
+ };
104
+
105
+ return { window, cleanup };
106
+ }
107
+
108
+ export function installFetchMock(fetchImpl: typeof globalThis.fetch): RestoreHandle {
109
+ const originalFetch = globalThis.fetch;
110
+ const originalWindowFetch = window.fetch;
111
+ (globalThis as any).fetch = fetchImpl;
112
+ (window as any).fetch = fetchImpl;
113
+ return {
114
+ restore() {
115
+ (globalThis as any).fetch = originalFetch;
116
+ (window as any).fetch = originalWindowFetch;
117
+ },
118
+ };
119
+ }
120
+
121
+ export function installWindowOpenMock(openImpl: typeof window.open): RestoreHandle {
122
+ const originalOpen = window.open;
123
+ (window as any).open = openImpl;
124
+ return {
125
+ restore() {
126
+ (window as any).open = originalOpen;
127
+ },
128
+ };
129
+ }
130
+
131
+ export function setElementRect(el: HTMLElement, width: number, height: number) {
132
+ Object.defineProperty(el, 'offsetWidth', { value: width, configurable: true });
133
+ Object.defineProperty(el, 'offsetHeight', { value: height, configurable: true });
134
+ (el as any).getBoundingClientRect = () => ({
135
+ left: 0,
136
+ top: 0,
137
+ width,
138
+ height,
139
+ right: width,
140
+ bottom: height,
141
+ x: 0,
142
+ y: 0,
143
+ toJSON() {},
144
+ });
145
+ }
@@ -0,0 +1,3 @@
1
+ export default function CatchAllPage() {
2
+ return <div>catch-all</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export function GET() {
2
+ return Response.json({ ok: true });
3
+ }
@@ -0,0 +1,3 @@
1
+ export function GET() {
2
+ return Response.json({ version: 'test' });
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function GalaxyCanvasPage() {
2
+ return <div>galaxy-canvas</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function RootPage() {
2
+ return <div>root</div>;
3
+ }
@@ -0,0 +1,163 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import type { Window } from 'happy-dom';
3
+ import { processVirtualFileSet } from './virtual-files';
4
+ import { setElementRect, setupDomTest } from './test-dom';
5
+
6
+ function makeActor() {
7
+ const state = {
8
+ context: {
9
+ zoom: 1,
10
+ offsetX: 0,
11
+ offsetY: 0,
12
+ repoPath: 'C:/Code/gitmaps',
13
+ selectedCards: [],
14
+ currentCommitHash: '',
15
+ cardSizes: {},
16
+ },
17
+ } as any;
18
+
19
+ return {
20
+ getSnapshot() {
21
+ return state;
22
+ },
23
+ send(event: any) {
24
+ if (event?.type === 'SET_ZOOM') state.context.zoom = event.zoom;
25
+ if (event?.type === 'SET_OFFSET') {
26
+ state.context.offsetX = event.x;
27
+ state.context.offsetY = event.y;
28
+ }
29
+ },
30
+ };
31
+ }
32
+
33
+ function makeRepeatingContent() {
34
+ const repeated = [
35
+ 'export function renderWidget(ctx) {',
36
+ ' const node = document.createElement("div");',
37
+ ' node.className = "widget-row";',
38
+ ' return node;',
39
+ '}',
40
+ ].join('\n');
41
+
42
+ const chunks: string[] = [];
43
+ for (let i = 0; i < 40; i++) {
44
+ chunks.push(`INFO widget-${i} start`);
45
+ chunks.push(repeated);
46
+ chunks.push(`INFO widget-${i} end`);
47
+ }
48
+ return chunks.join('\n');
49
+ }
50
+
51
+ describe('transclusion smoke', () => {
52
+ let window: Window;
53
+ let actor: any;
54
+ let ctx: any;
55
+ let sourceCard: HTMLElement;
56
+
57
+ let cleanup: (() => void) | undefined;
58
+
59
+ beforeEach(() => {
60
+ const handle = setupDomTest({
61
+ url: 'http://localhost:3335/gitmaps',
62
+ raf: true,
63
+ html: `
64
+ <div class="canvas-area"></div>
65
+ <div id="canvasViewport"><div id="canvasContent"><svg id="connectionsOverlay"></svg></div></div>
66
+ <input id="zoomSlider" />
67
+ <span id="zoomValue"></span>
68
+ <input id="stickyZoomSlider" />
69
+ <span id="stickyZoomValue"></span>
70
+ <div id="minimap"></div>
71
+ <div id="minimapViewport"></div>
72
+ `,
73
+ });
74
+ window = handle.window;
75
+ cleanup = handle.cleanup;
76
+
77
+ const viewport = document.getElementById('canvasViewport') as HTMLElement;
78
+ const canvas = document.getElementById('canvasContent') as HTMLElement;
79
+ const overlay = document.getElementById('connectionsOverlay') as unknown as SVGSVGElement;
80
+ setElementRect(viewport, 1400, 900);
81
+ setElementRect(canvas, 4000, 3000);
82
+ setElementRect(overlay as unknown as HTMLElement, 1400, 900);
83
+
84
+ actor = makeActor();
85
+ ctx = {
86
+ actor,
87
+ snap: () => actor.getSnapshot(),
88
+ canvas,
89
+ canvasViewport: viewport,
90
+ svgOverlay: overlay,
91
+ fileCards: new Map(),
92
+ positions: new Map(),
93
+ hiddenFiles: new Set(),
94
+ changedFilePaths: new Set(),
95
+ deferredCards: new Map(),
96
+ allFilesData: null,
97
+ commitFilesData: null,
98
+ isDragging: false,
99
+ spaceHeld: false,
100
+ CORNER_SIZE: 40,
101
+ scrollTimers: {},
102
+ connectionDragState: null,
103
+ loadingOverlay: null,
104
+ textRendererMode: 'dom',
105
+ allFilesActive: true,
106
+ controlMode: 'advanced',
107
+ };
108
+
109
+ sourceCard = document.createElement('div');
110
+ sourceCard.className = 'file-card';
111
+ sourceCard.dataset.path = 'app/lib/events.tsx';
112
+ sourceCard.style.left = '900px';
113
+ sourceCard.style.top = '600px';
114
+ setElementRect(sourceCard, 580, 700);
115
+ canvas.appendChild(sourceCard);
116
+ ctx.fileCards.set('app/lib/events.tsx', sourceCard);
117
+ });
118
+
119
+ afterEach(() => {
120
+ cleanup?.();
121
+ });
122
+
123
+ test('preserves slug-route bootstrap assumptions for transclusion flows', () => {
124
+ expect(window.location.pathname).toBe('/gitmaps');
125
+ expect(ctx.snap().context.repoPath).toBe('C:/Code/gitmaps');
126
+ expect(document.getElementById('canvasViewport')).toBeTruthy();
127
+ expect(document.getElementById('canvasContent')).toBeTruthy();
128
+ });
129
+
130
+ test('creates transclusion cards and clicking one highlights the source card', async () => {
131
+ const files = [
132
+ {
133
+ path: 'app/lib/events.tsx',
134
+ name: 'events.tsx',
135
+ ext: 'tsx',
136
+ type: 'file',
137
+ isBinary: false,
138
+ lines: 240,
139
+ size: 20000,
140
+ content: makeRepeatingContent(),
141
+ },
142
+ ];
143
+
144
+ const created = await processVirtualFileSet(ctx, files as any);
145
+ expect(created).toBeGreaterThan(0);
146
+ expect(document.querySelectorAll('.virtual-card').length).toBe(created);
147
+
148
+ const candidates = (window as any).__virtualCandidates;
149
+ expect(Array.isArray(candidates)).toBe(true);
150
+ expect(candidates[0]?.path).toBe('app/lib/events.tsx');
151
+
152
+ const virtualCard = document.querySelector('.virtual-card') as HTMLElement;
153
+ expect(virtualCard).toBeTruthy();
154
+ expect(virtualCard.title).toContain('app/lib/events.tsx');
155
+ expect(virtualCard.textContent || '').toContain('click to jump to source');
156
+
157
+ virtualCard.click();
158
+ await new Promise((resolve) => setTimeout(resolve, 500));
159
+
160
+ expect(sourceCard.style.outline).toContain('var(--accent-primary)');
161
+ expect(sourceCard.style.outlineOffset).toBe('4px');
162
+ });
163
+ });