gitmaps 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -118
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/og-image/route.ts +14 -0
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/version/route.ts +26 -0
- package/app/globals.css +5706 -4938
- package/app/layout.tsx +1279 -490
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas.ts +625 -564
- package/app/lib/cards.tsx +1361 -916
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +71 -93
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -148
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -427
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/positions.ts +190 -121
- package/app/lib/recent-commits.test.ts +947 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -987
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -735
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -269
- package/app/page.tsx +15 -16
- package/app/state/machine.js +13 -0
- package/package.json +84 -75
- package/server.ts +10 -0
- package/app/[owner]/[repo]/page.tsx +0 -6
- package/app/[slug]/page.tsx +0 -6
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
|
@@ -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,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
|
+
});
|