gitmaps 1.1.0 → 1.1.2

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