gitmaps 1.0.0

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 +167 -0
  2. package/app/api/auth/favorites/route.ts +56 -0
  3. package/app/api/auth/github/callback/route.ts +103 -0
  4. package/app/api/auth/github/route.ts +32 -0
  5. package/app/api/auth/me/route.ts +52 -0
  6. package/app/api/auth/positions/route.ts +50 -0
  7. package/app/api/chat/route.ts +101 -0
  8. package/app/api/connections/route.ts +72 -0
  9. package/app/api/github/repos/route.ts +111 -0
  10. package/app/api/positions/route.ts +80 -0
  11. package/app/api/repo/branch-diff/route.ts +201 -0
  12. package/app/api/repo/branches/route.ts +53 -0
  13. package/app/api/repo/browse/route.ts +55 -0
  14. package/app/api/repo/clone/route.ts +78 -0
  15. package/app/api/repo/clone-stream/route.ts +131 -0
  16. package/app/api/repo/file-content/route.ts +28 -0
  17. package/app/api/repo/file-delete/route.ts +62 -0
  18. package/app/api/repo/file-history/route.ts +45 -0
  19. package/app/api/repo/file-rename/route.ts +83 -0
  20. package/app/api/repo/file-save/route.ts +45 -0
  21. package/app/api/repo/files/route.ts +169 -0
  22. package/app/api/repo/git-blame/route.ts +86 -0
  23. package/app/api/repo/git-commit/route.ts +40 -0
  24. package/app/api/repo/git-heatmap/route.ts +55 -0
  25. package/app/api/repo/imports/route.ts +154 -0
  26. package/app/api/repo/load/route.ts +56 -0
  27. package/app/api/repo/mode/route.ts +14 -0
  28. package/app/api/repo/search/route.ts +127 -0
  29. package/app/api/repo/tree/route.ts +104 -0
  30. package/app/api/repo/upload/route.ts +53 -0
  31. package/app/api/repo/validate-path.ts +53 -0
  32. package/app/canvas_users.db +0 -0
  33. package/app/canvas_users.db-shm +0 -0
  34. package/app/canvas_users.db-wal +0 -0
  35. package/app/globals.css +7899 -0
  36. package/app/layout.tsx +493 -0
  37. package/app/lib/auth.ts +193 -0
  38. package/app/lib/auto-save.ts +137 -0
  39. package/app/lib/branch-compare.ts +443 -0
  40. package/app/lib/breadcrumbs.ts +170 -0
  41. package/app/lib/canvas-export.ts +358 -0
  42. package/app/lib/canvas-text.ts +912 -0
  43. package/app/lib/canvas.ts +564 -0
  44. package/app/lib/card-arrangement.ts +188 -0
  45. package/app/lib/card-context-menu.tsx +453 -0
  46. package/app/lib/card-diff-markers.ts +270 -0
  47. package/app/lib/card-expand.ts +189 -0
  48. package/app/lib/card-groups.ts +246 -0
  49. package/app/lib/cards.tsx +914 -0
  50. package/app/lib/chat.tsx +308 -0
  51. package/app/lib/code-editor.ts +508 -0
  52. package/app/lib/command-palette.ts +262 -0
  53. package/app/lib/connections.tsx +1037 -0
  54. package/app/lib/context.ts +94 -0
  55. package/app/lib/cursor-sharing.ts +281 -0
  56. package/app/lib/dependency-graph.ts +438 -0
  57. package/app/lib/events.tsx +1747 -0
  58. package/app/lib/file-card-plugin.ts +134 -0
  59. package/app/lib/file-modal.tsx +849 -0
  60. package/app/lib/file-preview.ts +400 -0
  61. package/app/lib/file-tabs.ts +318 -0
  62. package/app/lib/galaxydraw-bridge.ts +477 -0
  63. package/app/lib/galaxydraw.test.ts +229 -0
  64. package/app/lib/global-search.ts +264 -0
  65. package/app/lib/goto-definition.ts +224 -0
  66. package/app/lib/heatmap.ts +178 -0
  67. package/app/lib/hidden-files.tsx +222 -0
  68. package/app/lib/layers.ts +0 -0
  69. package/app/lib/layers.tsx +365 -0
  70. package/app/lib/loading.tsx +45 -0
  71. package/app/lib/multi-repo.ts +286 -0
  72. package/app/lib/new-file-dialog.tsx +230 -0
  73. package/app/lib/onboarding.tsx +213 -0
  74. package/app/lib/perf-overlay.ts +360 -0
  75. package/app/lib/positions.ts +176 -0
  76. package/app/lib/pr-review.ts +374 -0
  77. package/app/lib/production-mode.ts +47 -0
  78. package/app/lib/repo.tsx +977 -0
  79. package/app/lib/settings-modal.tsx +374 -0
  80. package/app/lib/settings.ts +97 -0
  81. package/app/lib/shortcuts-panel.ts +141 -0
  82. package/app/lib/status-bar.ts +128 -0
  83. package/app/lib/symbol-outline.ts +212 -0
  84. package/app/lib/syntax.ts +177 -0
  85. package/app/lib/tab-diff.ts +238 -0
  86. package/app/lib/user.tsx +133 -0
  87. package/app/lib/utils.ts +78 -0
  88. package/app/lib/viewport-culling.ts +728 -0
  89. package/app/page.client.tsx +215 -0
  90. package/app/page.tsx +291 -0
  91. package/app/state/machine.js +196 -0
  92. package/app/styles/main.css +2168 -0
  93. package/banner.png +0 -0
  94. package/cli.ts +44 -0
  95. package/package.json +75 -0
  96. package/packages/galaxydraw/README.md +296 -0
  97. package/packages/galaxydraw/banner.png +0 -0
  98. package/packages/galaxydraw/demo/build-static.ts +100 -0
  99. package/packages/galaxydraw/demo/client.ts +154 -0
  100. package/packages/galaxydraw/demo/dist/client.js +8 -0
  101. package/packages/galaxydraw/demo/index.html +256 -0
  102. package/packages/galaxydraw/demo/server.ts +96 -0
  103. package/packages/galaxydraw/dist/index.js +984 -0
  104. package/packages/galaxydraw/dist/index.js.map +16 -0
  105. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  106. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  109. package/packages/galaxydraw/package.json +49 -0
  110. package/packages/galaxydraw/perf.test.ts +284 -0
  111. package/packages/galaxydraw/src/core/cards.ts +435 -0
  112. package/packages/galaxydraw/src/core/engine.ts +339 -0
  113. package/packages/galaxydraw/src/core/events.ts +81 -0
  114. package/packages/galaxydraw/src/core/layout.ts +136 -0
  115. package/packages/galaxydraw/src/core/minimap.ts +216 -0
  116. package/packages/galaxydraw/src/core/state.ts +177 -0
  117. package/packages/galaxydraw/src/core/viewport.ts +106 -0
  118. package/packages/galaxydraw/src/galaxydraw.css +166 -0
  119. package/packages/galaxydraw/src/index.ts +40 -0
  120. package/packages/galaxydraw/tsconfig.json +30 -0
  121. package/server.ts +62 -0
@@ -0,0 +1,270 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Card diff markers — scrollbar annotations, deleted-lines overlay,
4
+ * and scroll-to-line helper.
5
+ *
6
+ * Extracted from cards.tsx to reduce file size.
7
+ * These are internal helpers called from createFileCard / createAllFileCard.
8
+ */
9
+ import { escapeHtml } from './utils';
10
+
11
+ // ─── Scroll to line helper ──────────────────────────────
12
+ export function scrollToLine(body: HTMLElement, lineNum: number, totalLines: number) {
13
+ // Canvas-text mode: container has .canvas-container class, no .diff-line elements
14
+ const canvasContainer = body.querySelector('.canvas-container') as HTMLElement;
15
+ if (canvasContainer) {
16
+ // Read font size from settings for accurate line height calculation
17
+ let lineHeight = 20; // default
18
+ try {
19
+ const stored = localStorage.getItem('gitcanvas:settings');
20
+ if (stored) {
21
+ const parsed = JSON.parse(stored);
22
+ if (parsed.fontSize) lineHeight = parsed.fontSize + 8;
23
+ }
24
+ } catch { }
25
+ const targetScroll = (lineNum - 1) * lineHeight - canvasContainer.clientHeight / 4;
26
+ canvasContainer.scrollTop = Math.max(0, targetScroll);
27
+ return;
28
+ }
29
+
30
+ // DOM mode: find the actual line element
31
+ const lineEl = body.querySelector(`.diff-line[data-line="${lineNum}"]`) as HTMLElement;
32
+ const pre = body.querySelector('.file-content-preview pre') as HTMLElement;
33
+ if (!pre) return;
34
+
35
+ if (lineEl) {
36
+ // getBoundingClientRect() returns viewport coordinates (affected by CSS transform/zoom).
37
+ // pre.scrollTop works in LOCAL content coordinates (unaffected by zoom).
38
+ // Compute the effective zoom from rendered vs logical dimensions.
39
+ const preRect = pre.getBoundingClientRect();
40
+ const zoom = preRect.height / pre.clientHeight || 1;
41
+ const lineRect = lineEl.getBoundingClientRect();
42
+ // Convert viewport delta to content delta by dividing by zoom
43
+ pre.scrollTop += (lineRect.top - preRect.top) / zoom;
44
+ } else {
45
+ // Fallback to percentage-based scroll
46
+ const pct = (lineNum - 1) / totalLines;
47
+ pre.scrollTop = pct * pre.scrollHeight;
48
+ }
49
+ }
50
+
51
+ // ─── Merge line numbers into contiguous regions ─────────
52
+ function mergeIntoRegions(lineNums: number[], gap = 4): { start: number; end: number }[] {
53
+ const sorted = lineNums.sort((a, b) => a - b);
54
+ const regions: { start: number; end: number }[] = [];
55
+ for (const line of sorted) {
56
+ const last = regions[regions.length - 1];
57
+ if (last && line <= last.end + gap) {
58
+ last.end = line;
59
+ } else {
60
+ regions.push({ start: line, end: line });
61
+ }
62
+ }
63
+ return regions;
64
+ }
65
+
66
+ // ─── Diff marker strip (scrollbar annotations) ─────────
67
+ export function buildDiffMarkerStrip(card: HTMLElement, body: HTMLElement, addedLines: Set<number>, totalLines: number, deletedBeforeLine?: Map<number, string[]>, fileHunks?: any[]) {
68
+ if (!body || totalLines === 0) return;
69
+
70
+ const strip = document.createElement('div');
71
+ strip.className = 'diff-marker-strip';
72
+
73
+ // Green markers for added lines
74
+ const addedRegions = mergeIntoRegions(Array.from(addedLines));
75
+ for (const region of addedRegions) {
76
+ const topPct = ((region.start - 1) / totalLines) * 100;
77
+ const heightPct = Math.max(0.5, ((region.end - region.start + 1) / totalLines) * 100);
78
+
79
+ const marker = document.createElement('div');
80
+ marker.className = 'diff-marker diff-marker--add';
81
+ marker.style.top = `${topPct}%`;
82
+ marker.style.height = `${heightPct}%`;
83
+ marker.title = region.start === region.end
84
+ ? `Added: line ${region.start}`
85
+ : `Added: lines ${region.start}–${region.end}`;
86
+
87
+ marker.addEventListener('click', (e) => {
88
+ e.stopPropagation();
89
+ scrollToLine(body, region.start, totalLines);
90
+ });
91
+
92
+ strip.appendChild(marker);
93
+ }
94
+
95
+ // Red markers for deleted line locations
96
+ if (deletedBeforeLine && deletedBeforeLine.size > 0) {
97
+ const deletedRegions = mergeIntoRegions(Array.from(deletedBeforeLine.keys()));
98
+ for (const region of deletedRegions) {
99
+ const topPct = ((region.start - 1) / totalLines) * 100;
100
+ const heightPct = Math.max(0.5, ((region.end - region.start + 1) / totalLines) * 100);
101
+
102
+ const marker = document.createElement('div');
103
+ marker.className = 'diff-marker diff-marker--del';
104
+ marker.style.top = `${topPct}%`;
105
+ marker.style.height = `${heightPct}%`;
106
+ let delCount = 0;
107
+ for (let ln = region.start; ln <= region.end; ln++) {
108
+ delCount += (deletedBeforeLine.get(ln) || []).length;
109
+ }
110
+ marker.title = `${delCount} deleted line${delCount > 1 ? 's' : ''} near line ${region.start}`;
111
+
112
+ marker.addEventListener('click', (e) => {
113
+ e.stopPropagation();
114
+ scrollToLine(body, region.start, totalLines);
115
+ });
116
+
117
+ strip.appendChild(marker);
118
+ }
119
+ }
120
+
121
+ // Build navigation regions
122
+ const allChangedLines = [
123
+ ...Array.from(addedLines),
124
+ ...(deletedBeforeLine ? Array.from(deletedBeforeLine.keys()) : [])
125
+ ];
126
+ const allRegions = mergeIntoRegions(allChangedLines);
127
+
128
+ const navRegions: { start: number; end: number }[] = fileHunks && fileHunks.length > 0
129
+ ? fileHunks.map((h: any) => ({ start: h.newStart, end: h.newStart + (h.newCount || 1) - 1 }))
130
+ : allRegions;
131
+
132
+ // Insert nav buttons inline inside the .file-path element
133
+ if (navRegions.length > 0) {
134
+ let currentIdx = -1;
135
+
136
+ const filePath = body.querySelector('.file-path') as HTMLElement;
137
+ if (filePath) {
138
+ filePath.style.display = 'flex';
139
+ filePath.style.alignItems = 'center';
140
+ filePath.style.justifyContent = 'space-between';
141
+
142
+ const pathText = filePath.textContent || '';
143
+ filePath.textContent = '';
144
+ const pathSpan = document.createElement('span');
145
+ pathSpan.textContent = pathText;
146
+ pathSpan.style.overflow = 'hidden';
147
+ pathSpan.style.textOverflow = 'ellipsis';
148
+ filePath.appendChild(pathSpan);
149
+
150
+ const navGroup = document.createElement('span');
151
+ navGroup.className = 'diff-nav-inline';
152
+ navGroup.title = `${navRegions.length} change${navRegions.length > 1 ? 's' : ''}`;
153
+
154
+ const navLabel = document.createElement('span');
155
+ navLabel.className = 'diff-nav-label';
156
+ navLabel.textContent = `—/${navRegions.length}`;
157
+
158
+ const navUp = document.createElement('button');
159
+ navUp.className = 'diff-nav-btn';
160
+ navUp.textContent = '▲';
161
+ navUp.title = 'Previous change';
162
+ navUp.addEventListener('click', (e) => {
163
+ e.stopPropagation();
164
+ if (currentIdx <= 0) currentIdx = navRegions.length - 1;
165
+ else currentIdx--;
166
+ scrollToLine(body, navRegions[currentIdx].start, totalLines);
167
+ navLabel.textContent = `${currentIdx + 1}/${navRegions.length}`;
168
+ });
169
+
170
+ const navDown = document.createElement('button');
171
+ navDown.className = 'diff-nav-btn';
172
+ navDown.textContent = '▼';
173
+ navDown.title = 'Next change';
174
+ navDown.addEventListener('click', (e) => {
175
+ e.stopPropagation();
176
+ if (currentIdx >= navRegions.length - 1) currentIdx = 0;
177
+ else currentIdx++;
178
+ scrollToLine(body, navRegions[currentIdx].start, totalLines);
179
+ navLabel.textContent = `${currentIdx + 1}/${navRegions.length}`;
180
+ });
181
+
182
+ navGroup.appendChild(navUp);
183
+ navGroup.appendChild(navLabel);
184
+ navGroup.appendChild(navDown);
185
+ filePath.appendChild(navGroup);
186
+ }
187
+ }
188
+
189
+ // Append strip to card (not body) so it doesn't scroll with content
190
+ card.appendChild(strip);
191
+ }
192
+
193
+ // ─── Deleted lines hover overlay ────────────────────────
194
+ export function setupDeletedLinesOverlay(card: HTMLElement) {
195
+ let overlay: HTMLElement | null = null;
196
+ let hideTimeout: any = null;
197
+
198
+ card.addEventListener('mouseover', (e) => {
199
+ const target = e.target as HTMLElement;
200
+ const lineNum = target.closest('.line-num');
201
+ const diffLine = target.closest('.has-deleted') as HTMLElement;
202
+ if (!lineNum || !diffLine) return;
203
+
204
+ const delLinesRaw = diffLine.dataset.delLines;
205
+ if (!delLinesRaw) return;
206
+
207
+ if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; }
208
+
209
+ try {
210
+ const deletedLines: string[] = JSON.parse(decodeURIComponent(delLinesRaw));
211
+ if (deletedLines.length === 0) return;
212
+
213
+ if (overlay) overlay.remove();
214
+
215
+ overlay = document.createElement('div');
216
+ overlay.className = 'deleted-lines-overlay';
217
+
218
+ const header = document.createElement('div');
219
+ header.className = 'deleted-overlay-header';
220
+ header.textContent = `${deletedLines.length} deleted line${deletedLines.length > 1 ? 's' : ''}`;
221
+ overlay.appendChild(header);
222
+
223
+ const pre = document.createElement('pre');
224
+ const code = document.createElement('code');
225
+ code.innerHTML = deletedLines.map((line, i) =>
226
+ `<span class="diff-line diff-del"><span class="line-num del-line-num"> −</span>${escapeHtml(line)}</span>`
227
+ ).join('\n');
228
+ pre.appendChild(code);
229
+ overlay.appendChild(pre);
230
+
231
+ const lineRect = diffLine.getBoundingClientRect();
232
+ const cardRect = card.getBoundingClientRect();
233
+ overlay.style.top = `${lineRect.top - cardRect.top - overlay.offsetHeight}px`;
234
+ overlay.style.left = '50px';
235
+
236
+ card.appendChild(overlay);
237
+
238
+ requestAnimationFrame(() => {
239
+ if (!overlay) return;
240
+ const overlayH = overlay.offsetHeight;
241
+ const yPos = lineRect.top - cardRect.top;
242
+ if (yPos - overlayH > 36) {
243
+ overlay.style.top = `${yPos - overlayH}px`;
244
+ } else {
245
+ overlay.style.top = `${yPos + lineRect.height}px`;
246
+ }
247
+ });
248
+
249
+ overlay.addEventListener('mouseenter', () => {
250
+ if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; }
251
+ });
252
+ overlay.addEventListener('mouseleave', () => {
253
+ hideTimeout = setTimeout(() => {
254
+ if (overlay) { overlay.remove(); overlay = null; }
255
+ }, 200);
256
+ });
257
+ } catch (err) { /* ignore parse errors */ }
258
+ });
259
+
260
+ card.addEventListener('mouseout', (e) => {
261
+ const target = e.target as HTMLElement;
262
+ const lineNum = target.closest('.line-num');
263
+ const diffLine = target.closest('.has-deleted');
264
+ if (!lineNum || !diffLine) return;
265
+
266
+ hideTimeout = setTimeout(() => {
267
+ if (overlay) { overlay.remove(); overlay = null; }
268
+ }, 300);
269
+ });
270
+ }
@@ -0,0 +1,189 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Card expand/collapse — toggle, fit-to-screen, font size, hidden lines indicator.
4
+ *
5
+ * Extracted from cards.tsx to reduce file size.
6
+ */
7
+ import { measure } from 'measure-fn';
8
+ import type { CanvasContext } from './context';
9
+ import { savePosition, setPathExpandedInPositions } from './positions';
10
+ import { updateMinimap } from './canvas';
11
+ import { renderConnections } from './connections';
12
+
13
+ // Re-use card data store and content builder from cards.tsx (lazy import to avoid circular)
14
+ function getCardsDeps() {
15
+ return require('./cards');
16
+ }
17
+
18
+ const DEFAULT_CARD_HEIGHT = 700;
19
+
20
+ // ─── Hidden lines indicator ─────────────────────────────
21
+ // Disabled: all files are same size, no expand/collapse mode.
22
+ // Keeping the export as a no-op for backward compatibility.
23
+ export function updateHiddenLinesIndicator(_card: HTMLElement, _totalLines?: number) {
24
+ // Remove any existing indicator
25
+ const indicator = _card.querySelector('.hidden-lines-indicator') as HTMLElement;
26
+ if (indicator) indicator.remove();
27
+ }
28
+
29
+ // ─── Change card font size (Ctrl +/-) ─────────────────
30
+ export function changeCardsFontSize(ctx: CanvasContext, delta: number) {
31
+ const selected = ctx.snap().context.selectedCards;
32
+ const targets = selected.length > 0 ? selected : Array.from(ctx.fileCards.keys());
33
+
34
+ targets.forEach(path => {
35
+ const card = ctx.fileCards.get(path);
36
+ if (!card) return;
37
+ const pre = card.querySelector('.file-content-preview pre') as HTMLElement;
38
+ if (!pre) return;
39
+ const current = parseFloat(getComputedStyle(pre).fontSize) || 8.5;
40
+ const newSize = Math.max(5, Math.min(24, current + delta));
41
+ pre.style.fontSize = `${newSize}px`;
42
+ pre.style.lineHeight = '1.1';
43
+ updateHiddenLinesIndicator(card, 0);
44
+ });
45
+ }
46
+
47
+ // ─── Toggle card expand/collapse ────────────────────────
48
+ export function toggleCardExpand(ctx: CanvasContext) {
49
+ measure('cards:toggleExpand', () => {
50
+ const selected = ctx.snap().context.selectedCards;
51
+ if (selected.length === 0) return;
52
+
53
+ const firstCard = ctx.fileCards.get(selected[0]);
54
+ if (!firstCard) return;
55
+ const willExpand = firstCard.dataset.expanded !== 'true';
56
+
57
+ const vpRect = ctx.canvasViewport.getBoundingClientRect();
58
+ const expandHeight = Math.max(600, vpRect.height - 40);
59
+
60
+ // Lazy import to get cardFileData and _buildFileContentHTML
61
+ const { _getCardFileData, _buildFileContentHTML } = getCardsDeps();
62
+
63
+ selected.forEach(path => {
64
+ const card = ctx.fileCards.get(path);
65
+ if (!card) return;
66
+
67
+ const body = card.querySelector('.file-card-body') as HTMLElement;
68
+ if (!body) return;
69
+
70
+ if (!willExpand) {
71
+ card.style.height = `${DEFAULT_CARD_HEIGHT}px`;
72
+ card.style.maxHeight = `${DEFAULT_CARD_HEIGHT}px`;
73
+ card.dataset.expanded = 'false';
74
+ setPathExpandedInPositions(ctx, path, false);
75
+ } else {
76
+ card.style.height = `${expandHeight}px`;
77
+ card.style.maxHeight = 'none';
78
+ card.dataset.expanded = 'true';
79
+ setPathExpandedInPositions(ctx, path, true);
80
+ }
81
+
82
+ // Re-render content: expanded shows ALL lines, collapsed shows VISIBLE_LINE_LIMIT
83
+ const file = _getCardFileData(card);
84
+ if (file && file.content && !file.isBinary) {
85
+ if (!ctx.useCanvasText) {
86
+ const addedLines: Set<number> = file.addedLines || new Set();
87
+ const deletedBeforeLine: Map<number, string[]> = file.deletedBeforeLine || new Map();
88
+ const isAllAdded = file.status === 'added';
89
+ const isAllDeleted = file.status === 'deleted';
90
+ const preview = body.querySelector('.file-content-preview');
91
+ if (preview) {
92
+ const newHTML = _buildFileContentHTML(
93
+ file.content, file.layerSections, addedLines, deletedBeforeLine,
94
+ isAllAdded, isAllDeleted, willExpand, file.lines
95
+ );
96
+ preview.outerHTML = newHTML;
97
+ }
98
+ }
99
+ }
100
+
101
+ const state = ctx.snap().context;
102
+ const commitHash = state.currentCommitHash || 'allfiles';
103
+ const newH = card.offsetHeight;
104
+ ctx.actor.send({ type: 'RESIZE_CARD', path, width: card.offsetWidth, height: newH });
105
+ savePosition(ctx, commitHash, path, parseInt(card.style.left) || 0, parseInt(card.style.top) || 0, card.offsetWidth, newH);
106
+
107
+ requestAnimationFrame(() => updateHiddenLinesIndicator(card, 0));
108
+ });
109
+
110
+ updateMinimap(ctx);
111
+ renderConnections(ctx);
112
+ });
113
+ }
114
+
115
+ // ─── Expand a single card by path ───────────────────────
116
+ export function expandCardByPath(ctx: CanvasContext, path: string) {
117
+ const card = ctx.fileCards.get(path);
118
+ if (!card || card.dataset.expanded === 'true') return;
119
+
120
+ const body = card.querySelector('.file-card-body') as HTMLElement;
121
+ if (!body) return;
122
+
123
+ const vpRect = ctx.canvasViewport.getBoundingClientRect();
124
+ const expandHeight = Math.max(600, vpRect.height - 40);
125
+
126
+ card.style.height = `${expandHeight}px`;
127
+ card.style.maxHeight = 'none';
128
+ card.dataset.expanded = 'true';
129
+ setPathExpandedInPositions(ctx, path, true);
130
+
131
+ const { _getCardFileData, _buildFileContentHTML } = getCardsDeps();
132
+ const file = _getCardFileData(card);
133
+ if (file && file.content && !file.isBinary) {
134
+ if (!ctx.useCanvasText) {
135
+ const addedLines: Set<number> = file.addedLines || new Set();
136
+ const deletedBeforeLine: Map<number, string[]> = file.deletedBeforeLine || new Map();
137
+ const isAllAdded = file.status === 'added';
138
+ const isAllDeleted = file.status === 'deleted';
139
+ const preview = body.querySelector('.file-content-preview');
140
+ if (preview) {
141
+ const newHTML = _buildFileContentHTML(
142
+ file.content, file.layerSections, addedLines, deletedBeforeLine,
143
+ isAllAdded, isAllDeleted, true, file.lines
144
+ );
145
+ preview.outerHTML = newHTML;
146
+ }
147
+ }
148
+ }
149
+
150
+ const state = ctx.snap().context;
151
+ const commitHash = state.currentCommitHash || 'allfiles';
152
+ ctx.actor.send({ type: 'RESIZE_CARD', path, width: card.offsetWidth, height: expandHeight });
153
+ savePosition(ctx, commitHash, path, parseInt(card.style.left) || 0, parseInt(card.style.top) || 0, card.offsetWidth, expandHeight);
154
+ requestAnimationFrame(() => updateHiddenLinesIndicator(card, 0));
155
+ }
156
+
157
+ // ─── Fit selected cards to screen viewport ──────────────
158
+ export function fitScreenSize(ctx: CanvasContext) {
159
+ measure('cards:fitScreen', () => {
160
+ const selected = ctx.snap().context.selectedCards;
161
+ if (selected.length === 0) return;
162
+
163
+ const viewport = ctx.canvasViewport;
164
+ if (!viewport) return;
165
+
166
+ const state = ctx.snap().context;
167
+ const vh = viewport.clientHeight / state.zoom;
168
+ const padding = 40;
169
+ const fitH = Math.max(120, vh - padding * 2);
170
+
171
+ selected.forEach(path => {
172
+ const card = ctx.fileCards.get(path);
173
+ if (!card) return;
174
+
175
+ const currentW = card.offsetWidth;
176
+ card.style.height = `${fitH}px`;
177
+ card.style.maxHeight = 'none';
178
+
179
+ const commitHash = state.currentCommitHash || 'allfiles';
180
+ ctx.actor.send({ type: 'RESIZE_CARD', path, width: currentW, height: fitH });
181
+ savePosition(ctx, commitHash, path, parseInt(card.style.left) || 0, parseInt(card.style.top) || 0, currentW, fitH);
182
+
183
+ requestAnimationFrame(() => updateHiddenLinesIndicator(card, 0));
184
+ });
185
+
186
+ updateMinimap(ctx);
187
+ renderConnections(ctx);
188
+ });
189
+ }