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
@@ -1,45 +1,160 @@
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({ message, sub }: { message: string; sub: string }) {
10
- return (
11
- <div className="loading-content">
12
- <div className="loading-spinner"></div>
13
- <div className="loading-message">{message}</div>
14
- <div className="loading-sub">{sub}</div>
15
- </div>
16
- );
17
- }
18
-
19
- let currentMessage = '';
20
- let currentSub = '';
21
-
22
- export function showLoadingProgress(ctx: CanvasContext, message: string) {
23
- if (!ctx.loadingOverlay) {
24
- ctx.loadingOverlay = document.createElement('div');
25
- ctx.loadingOverlay.className = 'loading-overlay';
26
- document.body.appendChild(ctx.loadingOverlay);
27
- }
28
- currentMessage = message;
29
- currentSub = '';
30
- render(<LoadingOverlayContent message={currentMessage} sub={currentSub} />, ctx.loadingOverlay);
31
- ctx.loadingOverlay.classList.add('active');
32
- }
33
-
34
- export function updateLoadingProgress(ctx: CanvasContext, sub: string) {
35
- if (ctx.loadingOverlay) {
36
- currentSub = sub;
37
- render(<LoadingOverlayContent message={currentMessage} sub={currentSub} />, ctx.loadingOverlay);
38
- }
39
- }
40
-
41
- export function hideLoadingProgress(ctx: CanvasContext) {
42
- if (ctx.loadingOverlay) {
43
- ctx.loadingOverlay.classList.remove('active');
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
+ });