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
@@ -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
+ });