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
package/app/lib/loading.tsx
CHANGED
|
@@ -1,45 +1,160 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* Loading progress overlay.
|
|
4
|
-
* Uses melina/client JSX + render.
|
|
5
|
-
*/
|
|
6
|
-
import { render } from
|
|
7
|
-
import type { CanvasContext } from
|
|
8
|
-
|
|
9
|
-
function LoadingOverlayContent({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Loading progress overlay.
|
|
4
|
+
* Uses melina/client JSX + render.
|
|
5
|
+
*/
|
|
6
|
+
import { render } from "melina/client";
|
|
7
|
+
import type { CanvasContext } from "./context";
|
|
8
|
+
|
|
9
|
+
function LoadingOverlayContent({
|
|
10
|
+
message,
|
|
11
|
+
sub,
|
|
12
|
+
progress,
|
|
13
|
+
loaded,
|
|
14
|
+
total,
|
|
15
|
+
}: {
|
|
16
|
+
message: string;
|
|
17
|
+
sub: string;
|
|
18
|
+
progress?: number;
|
|
19
|
+
loaded?: number;
|
|
20
|
+
total?: number;
|
|
21
|
+
}) {
|
|
22
|
+
const hasFileCount = loaded !== undefined && total !== undefined && total > 0;
|
|
23
|
+
const safeLoaded = hasFileCount ? Math.min(loaded ?? 0, total ?? 0) : undefined;
|
|
24
|
+
const remaining = hasFileCount ? Math.max((total ?? 0) - (safeLoaded ?? 0), 0) : undefined;
|
|
25
|
+
const pct = hasFileCount ? Math.round(((safeLoaded ?? 0) / (total ?? 1)) * 100) : progress;
|
|
26
|
+
|
|
27
|
+
const fileSummary = hasFileCount
|
|
28
|
+
? `${total} total • ${safeLoaded} loaded • ${remaining} remaining`
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="loading-content">
|
|
33
|
+
<div className="loading-spinner"></div>
|
|
34
|
+
<div className="loading-message">{message}</div>
|
|
35
|
+
{(sub || hasFileCount) && (
|
|
36
|
+
<div className="loading-sub">
|
|
37
|
+
{sub}
|
|
38
|
+
{sub && hasFileCount ? " · " : ""}
|
|
39
|
+
{fileSummary}
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
{hasFileCount && (
|
|
43
|
+
<div className="loading-stats">
|
|
44
|
+
<div className="loading-stat">
|
|
45
|
+
<span className="loading-stat-label">Total</span>
|
|
46
|
+
<span className="loading-stat-value">{total}</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="loading-stat">
|
|
49
|
+
<span className="loading-stat-label">Loaded</span>
|
|
50
|
+
<span className="loading-stat-value">{safeLoaded}</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="loading-stat">
|
|
53
|
+
<span className="loading-stat-label">Remaining</span>
|
|
54
|
+
<span className="loading-stat-value">{remaining}</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
{pct !== undefined && (
|
|
59
|
+
<div className="loading-progress-container">
|
|
60
|
+
<div className="loading-progress-bar">
|
|
61
|
+
<div
|
|
62
|
+
className="loading-progress-fill"
|
|
63
|
+
style={{ width: `${pct}%` }}
|
|
64
|
+
></div>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="loading-progress-text">
|
|
67
|
+
{hasFileCount
|
|
68
|
+
? `${pct}% • ${remaining} remaining`
|
|
69
|
+
: `${Math.round(pct)}%`}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let currentMessage = "";
|
|
78
|
+
let currentSub = "";
|
|
79
|
+
let currentProgress: number | undefined;
|
|
80
|
+
let currentLoaded: number | undefined;
|
|
81
|
+
let currentTotal: number | undefined;
|
|
82
|
+
|
|
83
|
+
function rerender(ctx: CanvasContext) {
|
|
84
|
+
if (!ctx.loadingOverlay) return;
|
|
85
|
+
render(
|
|
86
|
+
<LoadingOverlayContent
|
|
87
|
+
message={currentMessage}
|
|
88
|
+
sub={currentSub}
|
|
89
|
+
progress={currentProgress}
|
|
90
|
+
loaded={currentLoaded}
|
|
91
|
+
total={currentTotal}
|
|
92
|
+
/>,
|
|
93
|
+
ctx.loadingOverlay,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function showLoadingProgress(
|
|
98
|
+
ctx: CanvasContext,
|
|
99
|
+
message: string,
|
|
100
|
+
progress?: number,
|
|
101
|
+
) {
|
|
102
|
+
if (!ctx.loadingOverlay) {
|
|
103
|
+
ctx.loadingOverlay = document.createElement("div");
|
|
104
|
+
ctx.loadingOverlay.className = "loading-overlay";
|
|
105
|
+
document.body.appendChild(ctx.loadingOverlay);
|
|
106
|
+
}
|
|
107
|
+
currentMessage = message;
|
|
108
|
+
currentSub = "";
|
|
109
|
+
currentProgress = progress;
|
|
110
|
+
currentLoaded = undefined;
|
|
111
|
+
currentTotal = undefined;
|
|
112
|
+
rerender(ctx);
|
|
113
|
+
ctx.loadingOverlay.classList.add("active");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function updateLoadingProgress(
|
|
117
|
+
ctx: CanvasContext,
|
|
118
|
+
sub: string,
|
|
119
|
+
progress?: number,
|
|
120
|
+
) {
|
|
121
|
+
if (ctx.loadingOverlay) {
|
|
122
|
+
currentSub = sub;
|
|
123
|
+
if (progress !== undefined) currentProgress = progress;
|
|
124
|
+
rerender(ctx);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function updateLoadingMessage(
|
|
129
|
+
ctx: CanvasContext,
|
|
130
|
+
message: string,
|
|
131
|
+
progress?: number,
|
|
132
|
+
) {
|
|
133
|
+
if (ctx.loadingOverlay) {
|
|
134
|
+
currentMessage = message;
|
|
135
|
+
if (progress !== undefined) currentProgress = progress;
|
|
136
|
+
rerender(ctx);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function updateLoadingFileCount(
|
|
141
|
+
ctx: CanvasContext,
|
|
142
|
+
loaded: number,
|
|
143
|
+
total: number,
|
|
144
|
+
sub?: string,
|
|
145
|
+
) {
|
|
146
|
+
|
|
147
|
+
if (ctx.loadingOverlay) {
|
|
148
|
+
currentLoaded = loaded;
|
|
149
|
+
currentTotal = total;
|
|
150
|
+
if (sub !== undefined) currentSub = sub;
|
|
151
|
+
rerender(ctx);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function hideLoadingProgress(ctx: CanvasContext) {
|
|
156
|
+
if (ctx.loadingOverlay) {
|
|
157
|
+
ctx.loadingOverlay.classList.remove("active");
|
|
158
|
+
}
|
|
159
|
+
document.body.classList.remove("repo-loading");
|
|
160
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { cleanupMount } from './mount-cleanup';
|
|
3
|
+
import { setupDomTest } from './test-dom';
|
|
4
|
+
|
|
5
|
+
describe('mount cleanup helper', () => {
|
|
6
|
+
test('runs cleanup steps in order and tears down preview when viewport exists', async () => {
|
|
7
|
+
const handle = setupDomTest({ html: '<div id="viewport"></div>' });
|
|
8
|
+
try {
|
|
9
|
+
const calls: string[] = [];
|
|
10
|
+
const viewport = document.getElementById('viewport') as HTMLElement;
|
|
11
|
+
const ctx = { canvasViewport: viewport } as any;
|
|
12
|
+
const actor = { stop: mock(() => { calls.push('stopActor'); }) };
|
|
13
|
+
|
|
14
|
+
await cleanupMount(ctx, actor, {
|
|
15
|
+
markDisposed: () => { calls.push('markDisposed'); },
|
|
16
|
+
clearMount: () => { calls.push('clearMount'); },
|
|
17
|
+
stopActor: () => { calls.push('stopActor'); },
|
|
18
|
+
destroyPreview: (el) => { calls.push(`destroyPreview:${el.id}`); },
|
|
19
|
+
clearCanvasUi: () => { calls.push('clearCanvas'); },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(calls).toEqual([
|
|
23
|
+
'markDisposed',
|
|
24
|
+
'clearMount',
|
|
25
|
+
'stopActor',
|
|
26
|
+
'destroyPreview:viewport',
|
|
27
|
+
'clearCanvas',
|
|
28
|
+
]);
|
|
29
|
+
} finally {
|
|
30
|
+
handle.cleanup();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('continues cleanup when actor stop throws and skips preview teardown without viewport', async () => {
|
|
35
|
+
const handle = setupDomTest();
|
|
36
|
+
try {
|
|
37
|
+
const destroyPreview = mock(() => undefined);
|
|
38
|
+
const clearCanvasUi = mock(() => undefined);
|
|
39
|
+
|
|
40
|
+
cleanupMount({ canvasViewport: null } as any, { stop: mock(() => { throw new Error('boom'); }) }, {
|
|
41
|
+
stopActor: () => { throw new Error('boom'); },
|
|
42
|
+
destroyPreview,
|
|
43
|
+
clearCanvasUi,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(destroyPreview).not.toHaveBeenCalled();
|
|
47
|
+
expect(clearCanvasUi).toHaveBeenCalledTimes(1);
|
|
48
|
+
} finally {
|
|
49
|
+
handle.cleanup();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CanvasContext } from './context';
|
|
2
|
+
import { clearCanvas } from './canvas';
|
|
3
|
+
import { clearCanvasMount } from './mount-lifecycle';
|
|
4
|
+
|
|
5
|
+
export async function cleanupMount(
|
|
6
|
+
ctx: CanvasContext,
|
|
7
|
+
actor: { stop: () => void },
|
|
8
|
+
options?: {
|
|
9
|
+
markDisposed?: () => void;
|
|
10
|
+
clearMount?: () => void;
|
|
11
|
+
stopActor?: () => void;
|
|
12
|
+
destroyPreview?: (viewport: HTMLElement) => void | Promise<void>;
|
|
13
|
+
clearCanvasUi?: (ctx: CanvasContext) => void;
|
|
14
|
+
},
|
|
15
|
+
) {
|
|
16
|
+
const markDisposed = options?.markDisposed || (() => {});
|
|
17
|
+
const clearMount = options?.clearMount || clearCanvasMount;
|
|
18
|
+
const stopActor = options?.stopActor || (() => actor.stop());
|
|
19
|
+
const destroyPreview = options?.destroyPreview || (async (viewport: HTMLElement) => {
|
|
20
|
+
const mod = await import('./file-preview');
|
|
21
|
+
mod.destroyFilePreview(viewport);
|
|
22
|
+
});
|
|
23
|
+
const clearCanvasUi = options?.clearCanvasUi || clearCanvas;
|
|
24
|
+
|
|
25
|
+
markDisposed();
|
|
26
|
+
clearMount();
|
|
27
|
+
try {
|
|
28
|
+
stopActor();
|
|
29
|
+
} catch {}
|
|
30
|
+
if (ctx.canvasViewport) {
|
|
31
|
+
await destroyPreview(ctx.canvasViewport);
|
|
32
|
+
}
|
|
33
|
+
clearCanvasUi(ctx);
|
|
34
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { ensureSvgOverlay, initializeMountUi } from './mount-init';
|
|
3
|
+
import { setupDomTest } from './test-dom';
|
|
4
|
+
|
|
5
|
+
describe('mount init helper', () => {
|
|
6
|
+
test('ensureSvgOverlay reuses existing overlay or creates one on demand', () => {
|
|
7
|
+
const handle = setupDomTest({ html: '<div id="canvasContent"></div><svg id="connectionsOverlay"></svg>' });
|
|
8
|
+
try {
|
|
9
|
+
const existing = document.getElementById('connectionsOverlay') as unknown as SVGSVGElement;
|
|
10
|
+
const ctx = { canvas: document.getElementById('canvasContent'), svgOverlay: null } as any;
|
|
11
|
+
ensureSvgOverlay(ctx);
|
|
12
|
+
expect(ctx.svgOverlay).toBe(existing);
|
|
13
|
+
|
|
14
|
+
existing.remove();
|
|
15
|
+
ctx.svgOverlay = null;
|
|
16
|
+
ensureSvgOverlay(ctx);
|
|
17
|
+
expect(ctx.svgOverlay?.id).toBe('connectionsOverlay');
|
|
18
|
+
expect(document.getElementById('canvasContent')?.querySelector('#connectionsOverlay')).toBeTruthy();
|
|
19
|
+
} finally {
|
|
20
|
+
handle.cleanup();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('initializes mount modules in the expected order', async () => {
|
|
25
|
+
const handle = setupDomTest({ html: '<div id="canvasViewport"></div>' });
|
|
26
|
+
try {
|
|
27
|
+
const calls: string[] = [];
|
|
28
|
+
const ctx = { canvasViewport: document.getElementById('canvasViewport') } as any;
|
|
29
|
+
const actor = { start: mock(() => { calls.push('actor.start'); }) };
|
|
30
|
+
|
|
31
|
+
await initializeMountUi(ctx, actor, {
|
|
32
|
+
isDisposed: () => false,
|
|
33
|
+
initDrawState: mock(() => { calls.push('initDrawState'); }),
|
|
34
|
+
initCards: mock(() => { calls.push('initCards'); }),
|
|
35
|
+
setupCanvasUi: mock(() => { calls.push('setupCanvasUi'); }),
|
|
36
|
+
setupEvents: mock(() => { calls.push('setupEvents'); }),
|
|
37
|
+
setupPills: mock(() => { calls.push('setupPills'); }),
|
|
38
|
+
setupPerf: mock(() => { calls.push('setupPerf'); }),
|
|
39
|
+
initPreview: mock(() => { calls.push('initPreview'); }),
|
|
40
|
+
initBranches: mock(() => { calls.push('initBranches'); }),
|
|
41
|
+
initCommands: mock(() => { calls.push('initCommands'); }),
|
|
42
|
+
initShortcuts: mock(() => { calls.push('initShortcuts'); }),
|
|
43
|
+
initStatus: mock(() => { calls.push('initStatus'); }),
|
|
44
|
+
initSnapshots: mock(() => { calls.push('initSnapshots'); }),
|
|
45
|
+
loadPositions: mock(async () => { calls.push('loadPositions'); }),
|
|
46
|
+
loadHidden: mock(() => { calls.push('loadHidden'); }),
|
|
47
|
+
updateHidden: mock(() => { calls.push('updateHidden'); }),
|
|
48
|
+
loadSavedConnections: mock(() => { calls.push('loadConnections'); }),
|
|
49
|
+
setupAuthUi: mock(() => { calls.push('setupAuth'); }),
|
|
50
|
+
renderRole: mock(() => { calls.push('renderRole'); }),
|
|
51
|
+
renderSync: mock(() => { calls.push('renderSync'); }),
|
|
52
|
+
renderVersion: mock(async () => { calls.push('renderVersion'); }),
|
|
53
|
+
renderRecents: mock(() => { calls.push('renderRecents'); }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(calls).toEqual([
|
|
57
|
+
'initDrawState',
|
|
58
|
+
'initCards',
|
|
59
|
+
'actor.start',
|
|
60
|
+
'setupCanvasUi',
|
|
61
|
+
'setupEvents',
|
|
62
|
+
'setupPills',
|
|
63
|
+
'setupPerf',
|
|
64
|
+
'initPreview',
|
|
65
|
+
'initBranches',
|
|
66
|
+
'initCommands',
|
|
67
|
+
'initShortcuts',
|
|
68
|
+
'initStatus',
|
|
69
|
+
'initSnapshots',
|
|
70
|
+
'loadPositions',
|
|
71
|
+
'loadHidden',
|
|
72
|
+
'updateHidden',
|
|
73
|
+
'loadConnections',
|
|
74
|
+
'setupAuth',
|
|
75
|
+
'renderRole',
|
|
76
|
+
'renderSync',
|
|
77
|
+
'renderVersion',
|
|
78
|
+
'renderRecents',
|
|
79
|
+
]);
|
|
80
|
+
} finally {
|
|
81
|
+
handle.cleanup();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('stops after awaited checkpoints when disposed', async () => {
|
|
86
|
+
const handle = setupDomTest({ html: '<div id="canvasViewport"></div>' });
|
|
87
|
+
try {
|
|
88
|
+
let disposed = false;
|
|
89
|
+
const loadHidden = mock(() => undefined);
|
|
90
|
+
const setupAuthUi = mock(() => undefined);
|
|
91
|
+
|
|
92
|
+
await initializeMountUi({ canvasViewport: document.getElementById('canvasViewport') } as any, { start: mock(() => undefined) }, {
|
|
93
|
+
isDisposed: () => disposed,
|
|
94
|
+
initDrawState: mock(() => undefined),
|
|
95
|
+
initCards: mock(() => undefined),
|
|
96
|
+
setupCanvasUi: mock(() => undefined),
|
|
97
|
+
setupEvents: mock(() => undefined),
|
|
98
|
+
setupPills: mock(() => undefined),
|
|
99
|
+
setupPerf: mock(() => undefined),
|
|
100
|
+
initPreview: mock(() => undefined),
|
|
101
|
+
initBranches: mock(() => undefined),
|
|
102
|
+
initCommands: mock(() => undefined),
|
|
103
|
+
initShortcuts: mock(() => undefined),
|
|
104
|
+
initStatus: mock(() => undefined),
|
|
105
|
+
initSnapshots: mock(() => undefined),
|
|
106
|
+
loadPositions: mock(async () => { disposed = true; }),
|
|
107
|
+
loadHidden,
|
|
108
|
+
updateHidden: mock(() => undefined),
|
|
109
|
+
loadSavedConnections: mock(() => undefined),
|
|
110
|
+
setupAuthUi,
|
|
111
|
+
renderRole: mock(() => undefined),
|
|
112
|
+
renderSync: mock(() => undefined),
|
|
113
|
+
renderVersion: mock(async () => undefined),
|
|
114
|
+
renderRecents: mock(() => undefined),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(loadHidden).not.toHaveBeenCalled();
|
|
118
|
+
expect(setupAuthUi).not.toHaveBeenCalled();
|
|
119
|
+
} finally {
|
|
120
|
+
handle.cleanup();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { CanvasContext } from './context';
|
|
2
|
+
import { loadSavedPositions } from './positions';
|
|
3
|
+
import { loadHiddenFiles, updateHiddenUI } from './hidden-files';
|
|
4
|
+
import { setupCanvasInteraction, setupEventListeners } from './events';
|
|
5
|
+
import { loadConnections } from './connections';
|
|
6
|
+
import { setupPillInteraction } from './viewport-culling';
|
|
7
|
+
import { setupAuth } from './user';
|
|
8
|
+
import { setupPerfOverlay } from './perf-overlay';
|
|
9
|
+
import { initGalaxyDrawState, initCardManager } from './xydraw-bridge';
|
|
10
|
+
import { initBranchCompare } from './branch-compare';
|
|
11
|
+
import { initCommandPalette } from './command-palette';
|
|
12
|
+
import { initShortcutsPanel } from './shortcuts-panel';
|
|
13
|
+
import { initStatusBar } from './status-bar';
|
|
14
|
+
import { initLayoutSnapshots } from './layout-snapshots';
|
|
15
|
+
import { renderSyncControls } from './sync-controls';
|
|
16
|
+
import { renderVersionBadge } from './version';
|
|
17
|
+
import { renderRoleBadge } from './role';
|
|
18
|
+
import { renderRecentCommitsUI } from './recent-commits';
|
|
19
|
+
|
|
20
|
+
export function ensureSvgOverlay(ctx: CanvasContext) {
|
|
21
|
+
ctx.svgOverlay = document.getElementById('connectionsOverlay') as unknown as SVGSVGElement;
|
|
22
|
+
if (!ctx.svgOverlay && ctx.canvas) {
|
|
23
|
+
ctx.svgOverlay = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement;
|
|
24
|
+
ctx.svgOverlay.id = 'connectionsOverlay';
|
|
25
|
+
ctx.svgOverlay.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1;overflow:visible;';
|
|
26
|
+
ctx.canvas.appendChild(ctx.svgOverlay);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function initializeMountUi(
|
|
31
|
+
ctx: CanvasContext,
|
|
32
|
+
actor: { start: () => void },
|
|
33
|
+
options: {
|
|
34
|
+
isDisposed: () => boolean;
|
|
35
|
+
initDrawState?: (ctx: CanvasContext) => void;
|
|
36
|
+
initCards?: (ctx: CanvasContext) => void;
|
|
37
|
+
setupCanvasUi?: (ctx: CanvasContext) => void;
|
|
38
|
+
setupEvents?: (ctx: CanvasContext) => void;
|
|
39
|
+
setupPills?: (ctx: CanvasContext) => void;
|
|
40
|
+
setupPerf?: (ctx: CanvasContext) => void;
|
|
41
|
+
initPreview?: (viewport: HTMLElement, ctx: CanvasContext) => void | Promise<void>;
|
|
42
|
+
initBranches?: (ctx: CanvasContext) => void;
|
|
43
|
+
initCommands?: (ctx: CanvasContext) => void;
|
|
44
|
+
initShortcuts?: () => void;
|
|
45
|
+
initStatus?: (ctx: CanvasContext) => void;
|
|
46
|
+
initSnapshots?: (ctx: CanvasContext) => void;
|
|
47
|
+
loadPositions?: (ctx: CanvasContext) => Promise<void>;
|
|
48
|
+
loadHidden?: (ctx: CanvasContext) => void;
|
|
49
|
+
updateHidden?: (ctx: CanvasContext) => void;
|
|
50
|
+
loadSavedConnections?: (ctx: CanvasContext) => void;
|
|
51
|
+
setupAuthUi?: () => void;
|
|
52
|
+
renderRole?: () => void;
|
|
53
|
+
renderSync?: () => void;
|
|
54
|
+
renderVersion?: () => Promise<void>;
|
|
55
|
+
renderRecents?: () => void;
|
|
56
|
+
},
|
|
57
|
+
) {
|
|
58
|
+
const initDrawState = options.initDrawState || initGalaxyDrawState;
|
|
59
|
+
const initCards = options.initCards || initCardManager;
|
|
60
|
+
const setupCanvasUi = options.setupCanvasUi || setupCanvasInteraction;
|
|
61
|
+
const setupEvents = options.setupEvents || setupEventListeners;
|
|
62
|
+
const setupPills = options.setupPills || setupPillInteraction;
|
|
63
|
+
const setupPerf = options.setupPerf || setupPerfOverlay;
|
|
64
|
+
const initPreview = options.initPreview || (async (viewport: HTMLElement, ctx: CanvasContext) => {
|
|
65
|
+
const mod = await import('./file-preview');
|
|
66
|
+
mod.initFilePreview(viewport, ctx);
|
|
67
|
+
});
|
|
68
|
+
const initBranches = options.initBranches || initBranchCompare;
|
|
69
|
+
const initCommands = options.initCommands || initCommandPalette;
|
|
70
|
+
const initShortcuts = options.initShortcuts || initShortcutsPanel;
|
|
71
|
+
const initStatus = options.initStatus || initStatusBar;
|
|
72
|
+
const initSnapshots = options.initSnapshots || initLayoutSnapshots;
|
|
73
|
+
const loadPositions = options.loadPositions || loadSavedPositions;
|
|
74
|
+
const loadHidden = options.loadHidden || loadHiddenFiles;
|
|
75
|
+
const updateHidden = options.updateHidden || updateHiddenUI;
|
|
76
|
+
const loadSavedConnections = options.loadSavedConnections || loadConnections;
|
|
77
|
+
const setupAuthUi = options.setupAuthUi || setupAuth;
|
|
78
|
+
const renderRole = options.renderRole || renderRoleBadge;
|
|
79
|
+
const renderSync = options.renderSync || renderSyncControls;
|
|
80
|
+
const renderVersion = options.renderVersion || renderVersionBadge;
|
|
81
|
+
const renderRecents = options.renderRecents || renderRecentCommitsUI;
|
|
82
|
+
|
|
83
|
+
initDrawState(ctx);
|
|
84
|
+
initCards(ctx);
|
|
85
|
+
actor.start();
|
|
86
|
+
setupCanvasUi(ctx);
|
|
87
|
+
setupEvents(ctx);
|
|
88
|
+
setupPills(ctx);
|
|
89
|
+
setupPerf(ctx);
|
|
90
|
+
if (ctx.canvasViewport) await initPreview(ctx.canvasViewport, ctx);
|
|
91
|
+
initBranches(ctx);
|
|
92
|
+
initCommands(ctx);
|
|
93
|
+
initShortcuts();
|
|
94
|
+
initStatus(ctx);
|
|
95
|
+
initSnapshots(ctx);
|
|
96
|
+
await loadPositions(ctx);
|
|
97
|
+
if (options.isDisposed()) return;
|
|
98
|
+
loadHidden(ctx);
|
|
99
|
+
updateHidden(ctx);
|
|
100
|
+
loadSavedConnections(ctx);
|
|
101
|
+
if (options.isDisposed()) return;
|
|
102
|
+
setupAuthUi();
|
|
103
|
+
renderRole();
|
|
104
|
+
renderSync();
|
|
105
|
+
await renderVersion();
|
|
106
|
+
renderRecents();
|
|
107
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { getCanvasContext } from './context';
|
|
3
|
+
import { clearCanvasMount, registerCanvasMount } from './mount-lifecycle';
|
|
4
|
+
import { setupDomTest } from './test-dom';
|
|
5
|
+
|
|
6
|
+
describe('mount lifecycle helper', () => {
|
|
7
|
+
test('registerCanvasMount stores cleanup and shared context together', () => {
|
|
8
|
+
const handle = setupDomTest();
|
|
9
|
+
try {
|
|
10
|
+
const ctx = { snap: () => ({ context: {} }) } as any;
|
|
11
|
+
const cleanup = () => {};
|
|
12
|
+
|
|
13
|
+
registerCanvasMount(ctx, cleanup);
|
|
14
|
+
|
|
15
|
+
expect(getCanvasContext()).toBe(ctx);
|
|
16
|
+
expect((window as any).__gitcanvas_cleanup__).toBe(cleanup);
|
|
17
|
+
} finally {
|
|
18
|
+
clearCanvasMount();
|
|
19
|
+
handle.cleanup();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('clearCanvasMount clears both cleanup and shared context', () => {
|
|
24
|
+
const handle = setupDomTest();
|
|
25
|
+
try {
|
|
26
|
+
registerCanvasMount({ snap: () => ({ context: {} }) } as any, () => {});
|
|
27
|
+
expect(getCanvasContext()).not.toBeNull();
|
|
28
|
+
expect((window as any).__gitcanvas_cleanup__).toBeTruthy();
|
|
29
|
+
|
|
30
|
+
clearCanvasMount();
|
|
31
|
+
|
|
32
|
+
expect(getCanvasContext()).toBeNull();
|
|
33
|
+
expect((window as any).__gitcanvas_cleanup__).toBeNull();
|
|
34
|
+
} finally {
|
|
35
|
+
clearCanvasMount();
|
|
36
|
+
handle.cleanup();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { setCanvasContext } from './context';
|
|
2
|
+
import type { CanvasContext } from './context';
|
|
3
|
+
|
|
4
|
+
export function registerCanvasMount(ctx: CanvasContext, cleanup: () => void): void {
|
|
5
|
+
setCanvasContext(ctx);
|
|
6
|
+
(window as any).__gitcanvas_cleanup__ = cleanup;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function clearCanvasMount(): void {
|
|
10
|
+
(window as any).__gitcanvas_cleanup__ = null;
|
|
11
|
+
setCanvasContext(null);
|
|
12
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { bindMountPopstate, wireMountRoutes } from './mount-route-wiring';
|
|
3
|
+
|
|
4
|
+
describe('mount route wiring helper', () => {
|
|
5
|
+
test('wires initial hydration through shared route helpers', async () => {
|
|
6
|
+
const calls: string[] = [];
|
|
7
|
+
const ctx = { snap: () => ({ context: { repoPath: 'C:/Code/gitmaps' } }) } as any;
|
|
8
|
+
const hydrateRoutes = mock(async (_ctx: any, options: any) => {
|
|
9
|
+
calls.push('hydrateRoutes');
|
|
10
|
+
await options.resolveRepoPath('7flash/gitmaps');
|
|
11
|
+
await options.bootstrapRepoUi('C:/Code/gitmaps');
|
|
12
|
+
});
|
|
13
|
+
const resolveRepoPath = mock(async () => {
|
|
14
|
+
calls.push('resolveRepoPath');
|
|
15
|
+
return 'C:/Code/gitmaps';
|
|
16
|
+
});
|
|
17
|
+
const bootstrapRepoUi = mock(async () => {
|
|
18
|
+
calls.push('bootstrapRepoUi');
|
|
19
|
+
});
|
|
20
|
+
const bindPopstate = mock(() => {
|
|
21
|
+
calls.push('bindPopstate');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await wireMountRoutes(ctx, {
|
|
25
|
+
isDisposed: () => false,
|
|
26
|
+
showLandingPlaceholder: mock(() => undefined),
|
|
27
|
+
updateFavoriteStar: mock(() => undefined),
|
|
28
|
+
applySharedLayout: mock(async () => undefined),
|
|
29
|
+
hydrateRoutes: hydrateRoutes as any,
|
|
30
|
+
resolveRepoPath: resolveRepoPath as any,
|
|
31
|
+
bootstrapRepoUi: bootstrapRepoUi as any,
|
|
32
|
+
bindPopstate: bindPopstate as any,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(calls).toEqual([
|
|
36
|
+
'hydrateRoutes',
|
|
37
|
+
'resolveRepoPath',
|
|
38
|
+
'bootstrapRepoUi',
|
|
39
|
+
'bindPopstate',
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('falls back to route error handler when initial resolution throws', async () => {
|
|
44
|
+
const handleRouteError = mock(async () => null);
|
|
45
|
+
const hydrateRoutes = mock(async (_ctx: any, options: any) => {
|
|
46
|
+
await options.resolveRepoPath('7flash/gitmaps');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await wireMountRoutes({} as any, {
|
|
50
|
+
isDisposed: () => false,
|
|
51
|
+
showLandingPlaceholder: mock(() => undefined),
|
|
52
|
+
updateFavoriteStar: mock(() => undefined),
|
|
53
|
+
applySharedLayout: mock(async () => undefined),
|
|
54
|
+
hydrateRoutes: hydrateRoutes as any,
|
|
55
|
+
resolveRepoPath: mock(async () => {
|
|
56
|
+
throw new Error('boom');
|
|
57
|
+
}) as any,
|
|
58
|
+
handleRouteError: handleRouteError as any,
|
|
59
|
+
bindPopstate: mock(() => undefined) as any,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(handleRouteError).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('binds popstate to the shared route-entry helper with current repo state', () => {
|
|
66
|
+
let popHandler: (() => void) | undefined;
|
|
67
|
+
const addListener = mock((_type: string, handler: () => void) => {
|
|
68
|
+
popHandler = handler;
|
|
69
|
+
});
|
|
70
|
+
const showLandingPlaceholder = mock(() => undefined);
|
|
71
|
+
const updateFavoriteStar = mock(() => undefined);
|
|
72
|
+
const ctx = {
|
|
73
|
+
snap: () => ({ context: { repoPath: 'C:/Code/gitmaps' } }),
|
|
74
|
+
onRepoReady: mock(() => undefined),
|
|
75
|
+
} as any;
|
|
76
|
+
|
|
77
|
+
bindMountPopstate(ctx, {
|
|
78
|
+
isDisposed: () => false,
|
|
79
|
+
showLandingPlaceholder,
|
|
80
|
+
updateFavoriteStar,
|
|
81
|
+
addListener,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(addListener).toHaveBeenCalledWith('popstate', expect.any(Function));
|
|
85
|
+
expect(popHandler).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
});
|