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.
- package/README.md +167 -0
- package/app/api/auth/favorites/route.ts +56 -0
- package/app/api/auth/github/callback/route.ts +103 -0
- package/app/api/auth/github/route.ts +32 -0
- package/app/api/auth/me/route.ts +52 -0
- package/app/api/auth/positions/route.ts +50 -0
- package/app/api/chat/route.ts +101 -0
- package/app/api/connections/route.ts +72 -0
- package/app/api/github/repos/route.ts +111 -0
- package/app/api/positions/route.ts +80 -0
- package/app/api/repo/branch-diff/route.ts +201 -0
- package/app/api/repo/branches/route.ts +53 -0
- package/app/api/repo/browse/route.ts +55 -0
- package/app/api/repo/clone/route.ts +78 -0
- package/app/api/repo/clone-stream/route.ts +131 -0
- package/app/api/repo/file-content/route.ts +28 -0
- package/app/api/repo/file-delete/route.ts +62 -0
- package/app/api/repo/file-history/route.ts +45 -0
- package/app/api/repo/file-rename/route.ts +83 -0
- package/app/api/repo/file-save/route.ts +45 -0
- package/app/api/repo/files/route.ts +169 -0
- package/app/api/repo/git-blame/route.ts +86 -0
- package/app/api/repo/git-commit/route.ts +40 -0
- package/app/api/repo/git-heatmap/route.ts +55 -0
- package/app/api/repo/imports/route.ts +154 -0
- package/app/api/repo/load/route.ts +56 -0
- package/app/api/repo/mode/route.ts +14 -0
- package/app/api/repo/search/route.ts +127 -0
- package/app/api/repo/tree/route.ts +104 -0
- package/app/api/repo/upload/route.ts +53 -0
- package/app/api/repo/validate-path.ts +53 -0
- package/app/canvas_users.db +0 -0
- package/app/canvas_users.db-shm +0 -0
- package/app/canvas_users.db-wal +0 -0
- package/app/globals.css +7899 -0
- package/app/layout.tsx +493 -0
- package/app/lib/auth.ts +193 -0
- package/app/lib/auto-save.ts +137 -0
- package/app/lib/branch-compare.ts +443 -0
- package/app/lib/breadcrumbs.ts +170 -0
- package/app/lib/canvas-export.ts +358 -0
- package/app/lib/canvas-text.ts +912 -0
- package/app/lib/canvas.ts +564 -0
- package/app/lib/card-arrangement.ts +188 -0
- package/app/lib/card-context-menu.tsx +453 -0
- package/app/lib/card-diff-markers.ts +270 -0
- package/app/lib/card-expand.ts +189 -0
- package/app/lib/card-groups.ts +246 -0
- package/app/lib/cards.tsx +914 -0
- package/app/lib/chat.tsx +308 -0
- package/app/lib/code-editor.ts +508 -0
- package/app/lib/command-palette.ts +262 -0
- package/app/lib/connections.tsx +1037 -0
- package/app/lib/context.ts +94 -0
- package/app/lib/cursor-sharing.ts +281 -0
- package/app/lib/dependency-graph.ts +438 -0
- package/app/lib/events.tsx +1747 -0
- package/app/lib/file-card-plugin.ts +134 -0
- package/app/lib/file-modal.tsx +849 -0
- package/app/lib/file-preview.ts +400 -0
- package/app/lib/file-tabs.ts +318 -0
- package/app/lib/galaxydraw-bridge.ts +477 -0
- package/app/lib/galaxydraw.test.ts +229 -0
- package/app/lib/global-search.ts +264 -0
- package/app/lib/goto-definition.ts +224 -0
- package/app/lib/heatmap.ts +178 -0
- package/app/lib/hidden-files.tsx +222 -0
- package/app/lib/layers.ts +0 -0
- package/app/lib/layers.tsx +365 -0
- package/app/lib/loading.tsx +45 -0
- package/app/lib/multi-repo.ts +286 -0
- package/app/lib/new-file-dialog.tsx +230 -0
- package/app/lib/onboarding.tsx +213 -0
- package/app/lib/perf-overlay.ts +360 -0
- package/app/lib/positions.ts +176 -0
- package/app/lib/pr-review.ts +374 -0
- package/app/lib/production-mode.ts +47 -0
- package/app/lib/repo.tsx +977 -0
- package/app/lib/settings-modal.tsx +374 -0
- package/app/lib/settings.ts +97 -0
- package/app/lib/shortcuts-panel.ts +141 -0
- package/app/lib/status-bar.ts +128 -0
- package/app/lib/symbol-outline.ts +212 -0
- package/app/lib/syntax.ts +177 -0
- package/app/lib/tab-diff.ts +238 -0
- package/app/lib/user.tsx +133 -0
- package/app/lib/utils.ts +78 -0
- package/app/lib/viewport-culling.ts +728 -0
- package/app/page.client.tsx +215 -0
- package/app/page.tsx +291 -0
- package/app/state/machine.js +196 -0
- package/app/styles/main.css +2168 -0
- package/banner.png +0 -0
- package/cli.ts +44 -0
- package/package.json +75 -0
- package/packages/galaxydraw/README.md +296 -0
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +100 -0
- package/packages/galaxydraw/demo/client.ts +154 -0
- package/packages/galaxydraw/demo/dist/client.js +8 -0
- package/packages/galaxydraw/demo/index.html +256 -0
- package/packages/galaxydraw/demo/server.ts +96 -0
- package/packages/galaxydraw/dist/index.js +984 -0
- package/packages/galaxydraw/dist/index.js.map +16 -0
- 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 +49 -0
- package/packages/galaxydraw/perf.test.ts +284 -0
- package/packages/galaxydraw/src/core/cards.ts +435 -0
- package/packages/galaxydraw/src/core/engine.ts +339 -0
- package/packages/galaxydraw/src/core/events.ts +81 -0
- package/packages/galaxydraw/src/core/layout.ts +136 -0
- package/packages/galaxydraw/src/core/minimap.ts +216 -0
- package/packages/galaxydraw/src/core/state.ts +177 -0
- package/packages/galaxydraw/src/core/viewport.ts +106 -0
- package/packages/galaxydraw/src/galaxydraw.css +166 -0
- package/packages/galaxydraw/src/index.ts +40 -0
- package/packages/galaxydraw/tsconfig.json +30 -0
- package/server.ts +62 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Branch Comparison — UI and logic for comparing two branches.
|
|
4
|
+
*
|
|
5
|
+
* Adds a "Compare" button to the canvas header. When clicked, opens
|
|
6
|
+
* a branch picker drawer where the user selects base and compare branches.
|
|
7
|
+
* On submit, fetches the diff from /api/repo/branch-diff and renders
|
|
8
|
+
* the changed files as diff cards on the canvas.
|
|
9
|
+
*
|
|
10
|
+
* Architecture:
|
|
11
|
+
* - Reuses the existing commit diff rendering (renderFilesOnCanvas)
|
|
12
|
+
* - Branch list is fetched from the API response
|
|
13
|
+
* - UI is a slide-out drawer with glassmorphism styling
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { CanvasContext } from './context';
|
|
17
|
+
import { showToast } from './utils';
|
|
18
|
+
|
|
19
|
+
let _drawer: HTMLElement | null = null;
|
|
20
|
+
let _isOpen = false;
|
|
21
|
+
let _branches: string[] = [];
|
|
22
|
+
let _currentBase = '';
|
|
23
|
+
let _currentCompare = '';
|
|
24
|
+
let _currentBranch = '';
|
|
25
|
+
|
|
26
|
+
// ─── UI: Compare Button ──────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the branch comparison feature.
|
|
30
|
+
* Adds a "Compare" button to the canvas header.
|
|
31
|
+
*/
|
|
32
|
+
export function initBranchCompare(ctx: CanvasContext) {
|
|
33
|
+
const headerActions = document.querySelector('.header-actions');
|
|
34
|
+
if (!headerActions) return;
|
|
35
|
+
|
|
36
|
+
const btn = document.createElement('button');
|
|
37
|
+
btn.className = 'btn-secondary btn-sm';
|
|
38
|
+
btn.id = 'branchCompareBtn';
|
|
39
|
+
btn.innerHTML = `
|
|
40
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
41
|
+
<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/>
|
|
42
|
+
<path d="M13 6h3a2 2 0 0 1 2 2v7"/>
|
|
43
|
+
<path d="M6 9v12"/>
|
|
44
|
+
</svg>
|
|
45
|
+
Compare
|
|
46
|
+
`;
|
|
47
|
+
btn.onclick = () => toggleDrawer(ctx);
|
|
48
|
+
headerActions.insertBefore(btn, headerActions.firstChild);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── UI: Branch Picker Drawer ────────────────────────────
|
|
52
|
+
|
|
53
|
+
function ensureDrawer(): HTMLElement {
|
|
54
|
+
if (_drawer) return _drawer;
|
|
55
|
+
|
|
56
|
+
_drawer = document.createElement('div');
|
|
57
|
+
_drawer.id = 'branchCompareDrawer';
|
|
58
|
+
_drawer.style.cssText = `
|
|
59
|
+
position: fixed;
|
|
60
|
+
top: 0;
|
|
61
|
+
right: -400px;
|
|
62
|
+
width: 380px;
|
|
63
|
+
height: 100vh;
|
|
64
|
+
z-index: 999;
|
|
65
|
+
background: rgba(12, 12, 20, 0.95);
|
|
66
|
+
backdrop-filter: blur(20px);
|
|
67
|
+
border-left: 1px solid rgba(124, 58, 237, 0.25);
|
|
68
|
+
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.5), -2px 0 12px rgba(124, 58, 237, 0.1);
|
|
69
|
+
transition: right 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
73
|
+
`;
|
|
74
|
+
document.body.appendChild(_drawer);
|
|
75
|
+
return _drawer;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function toggleDrawer(ctx: CanvasContext) {
|
|
79
|
+
const drawer = ensureDrawer();
|
|
80
|
+
_isOpen = !_isOpen;
|
|
81
|
+
|
|
82
|
+
if (_isOpen) {
|
|
83
|
+
renderDrawerContent(ctx, drawer);
|
|
84
|
+
fetchBranches(ctx);
|
|
85
|
+
requestAnimationFrame(() => {
|
|
86
|
+
drawer.style.right = '0px';
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
drawer.style.right = '-400px';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchBranches(ctx: CanvasContext) {
|
|
94
|
+
const state = ctx.snap().context;
|
|
95
|
+
if (!state.repoPath) return;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Try dedicated branches endpoint first (lightweight, no diff)
|
|
99
|
+
let branches: string[] = [];
|
|
100
|
+
let currentBranch = '';
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const resp = await fetch('/api/repo/branches', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify({ path: state.repoPath }),
|
|
107
|
+
});
|
|
108
|
+
if (resp.ok) {
|
|
109
|
+
const data = await resp.json();
|
|
110
|
+
branches = data.branches || [];
|
|
111
|
+
currentBranch = data.current || '';
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Fall back to branch-diff API which also returns branches
|
|
115
|
+
const resp = await fetch('/api/repo/branch-diff', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
path: state.repoPath,
|
|
120
|
+
base: 'HEAD',
|
|
121
|
+
compare: 'HEAD',
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
const data = await resp.json();
|
|
125
|
+
branches = data.branches || [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (branches.length > 0) {
|
|
129
|
+
_branches = branches;
|
|
130
|
+
_currentBranch = currentBranch;
|
|
131
|
+
updateBranchSelects();
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.error('[branch-compare] fetch branches error:', e);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function updateBranchSelects() {
|
|
139
|
+
const baseSelect = document.getElementById('branchBase') as HTMLSelectElement;
|
|
140
|
+
const compareSelect = document.getElementById('branchCompare') as HTMLSelectElement;
|
|
141
|
+
if (!baseSelect || !compareSelect) return;
|
|
142
|
+
|
|
143
|
+
const buildOptions = (select: HTMLSelectElement, defaultVal: string) => {
|
|
144
|
+
select.innerHTML = '';
|
|
145
|
+
for (const b of _branches) {
|
|
146
|
+
const opt = document.createElement('option');
|
|
147
|
+
opt.value = b;
|
|
148
|
+
opt.textContent = b;
|
|
149
|
+
if (b === defaultVal || (!defaultVal && b.includes('main') || b.includes('master'))) {
|
|
150
|
+
opt.selected = true;
|
|
151
|
+
}
|
|
152
|
+
select.appendChild(opt);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Default: base = main/master, compare = current branch (or first non-main)
|
|
157
|
+
const mainBranch = _branches.find(b => b === 'main' || b === 'master') || _branches[0] || '';
|
|
158
|
+
const compareBranch = _currentBranch && _currentBranch !== mainBranch
|
|
159
|
+
? _currentBranch
|
|
160
|
+
: _branches.find(b => b !== mainBranch)
|
|
161
|
+
|| _branches[0] || '';
|
|
162
|
+
|
|
163
|
+
buildOptions(baseSelect, _currentBase || mainBranch);
|
|
164
|
+
buildOptions(compareSelect, _currentCompare || compareBranch);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderDrawerContent(ctx: CanvasContext, drawer: HTMLElement) {
|
|
168
|
+
drawer.innerHTML = `
|
|
169
|
+
<div style="padding:16px 20px;border-bottom:1px solid rgba(124,58,237,0.15);display:flex;align-items:center;justify-content:space-between;">
|
|
170
|
+
<div style="display:flex;align-items:center;gap:10px;">
|
|
171
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="rgba(124,58,237,0.8)" stroke-width="2">
|
|
172
|
+
<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/>
|
|
173
|
+
<path d="M13 6h3a2 2 0 0 1 2 2v7"/>
|
|
174
|
+
<path d="M6 9v12"/>
|
|
175
|
+
</svg>
|
|
176
|
+
<span style="font-size:14px;font-weight:600;color:#e2e8f0;">Branch Comparison</span>
|
|
177
|
+
</div>
|
|
178
|
+
<button id="branchCompareClose" style="background:none;border:none;color:rgba(255,255,255,0.4);cursor:pointer;font-size:18px;padding:4px 8px;border-radius:4px;"
|
|
179
|
+
onmouseover="this.style.color='#fff';this.style.background='rgba(255,255,255,0.08)'"
|
|
180
|
+
onmouseout="this.style.color='rgba(255,255,255,0.4)';this.style.background='none'">✕</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div style="padding:20px;display:flex;flex-direction:column;gap:16px;">
|
|
184
|
+
<div style="display:flex;flex-direction:column;gap:6px;">
|
|
185
|
+
<label style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:rgba(255,255,255,0.4);">Base Branch</label>
|
|
186
|
+
<select id="branchBase" style="
|
|
187
|
+
padding:9px 12px;
|
|
188
|
+
background:rgba(255,255,255,0.05);
|
|
189
|
+
border:1px solid rgba(124,58,237,0.2);
|
|
190
|
+
border-radius:8px;
|
|
191
|
+
color:#e2e8f0;
|
|
192
|
+
font-size:13px;
|
|
193
|
+
font-family:'JetBrains Mono',monospace;
|
|
194
|
+
cursor:pointer;
|
|
195
|
+
outline:none;
|
|
196
|
+
transition:border-color 0.15s;
|
|
197
|
+
" onfocus="this.style.borderColor='rgba(124,58,237,0.5)'" onblur="this.style.borderColor='rgba(124,58,237,0.2)'">
|
|
198
|
+
<option value="">Loading branches…</option>
|
|
199
|
+
</select>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
|
|
203
|
+
<div style="flex:1;height:1px;background:rgba(124,58,237,0.15);"></div>
|
|
204
|
+
<button id="branchSwap" style="
|
|
205
|
+
background:rgba(124,58,237,0.1);
|
|
206
|
+
border:1px solid rgba(124,58,237,0.2);
|
|
207
|
+
border-radius:50%;
|
|
208
|
+
width:32px;height:32px;
|
|
209
|
+
display:flex;align-items:center;justify-content:center;
|
|
210
|
+
cursor:pointer;color:rgba(124,58,237,0.6);
|
|
211
|
+
transition:all 0.15s;
|
|
212
|
+
" title="Swap branches"
|
|
213
|
+
onmouseover="this.style.background='rgba(124,58,237,0.2)';this.style.color='rgba(124,58,237,0.9)'"
|
|
214
|
+
onmouseout="this.style.background='rgba(124,58,237,0.1)';this.style.color='rgba(124,58,237,0.6)'">
|
|
215
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
216
|
+
<path d="M7 16V4m0 0L3 8m4-4l4 4M17 8v12m0 0l4-4m-4 4l-4-4"/>
|
|
217
|
+
</svg>
|
|
218
|
+
</button>
|
|
219
|
+
<div style="flex:1;height:1px;background:rgba(124,58,237,0.15);"></div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div style="display:flex;flex-direction:column;gap:6px;">
|
|
223
|
+
<label style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:rgba(255,255,255,0.4);">Compare Branch</label>
|
|
224
|
+
<select id="branchCompare" style="
|
|
225
|
+
padding:9px 12px;
|
|
226
|
+
background:rgba(255,255,255,0.05);
|
|
227
|
+
border:1px solid rgba(124,58,237,0.2);
|
|
228
|
+
border-radius:8px;
|
|
229
|
+
color:#e2e8f0;
|
|
230
|
+
font-size:13px;
|
|
231
|
+
font-family:'JetBrains Mono',monospace;
|
|
232
|
+
cursor:pointer;
|
|
233
|
+
outline:none;
|
|
234
|
+
transition:border-color 0.15s;
|
|
235
|
+
" onfocus="this.style.borderColor='rgba(124,58,237,0.5)'" onblur="this.style.borderColor='rgba(124,58,237,0.2)'">
|
|
236
|
+
<option value="">Loading branches…</option>
|
|
237
|
+
</select>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<button id="branchDiffRun" style="
|
|
241
|
+
padding:11px 16px;
|
|
242
|
+
background:linear-gradient(135deg, rgba(124,58,237,0.8), rgba(168,85,247,0.8));
|
|
243
|
+
border:1px solid rgba(124,58,237,0.4);
|
|
244
|
+
border-radius:8px;
|
|
245
|
+
color:white;
|
|
246
|
+
font-size:13px;
|
|
247
|
+
font-weight:600;
|
|
248
|
+
cursor:pointer;
|
|
249
|
+
transition:all 0.2s;
|
|
250
|
+
box-shadow:0 2px 12px rgba(124,58,237,0.25);
|
|
251
|
+
letter-spacing:0.02em;
|
|
252
|
+
"
|
|
253
|
+
onmouseover="this.style.boxShadow='0 4px 20px rgba(124,58,237,0.4)';this.style.transform='translateY(-1px)'"
|
|
254
|
+
onmouseout="this.style.boxShadow='0 2px 12px rgba(124,58,237,0.25)';this.style.transform='none'">
|
|
255
|
+
Compare Branches
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div id="branchDiffResult" style="flex:1;overflow-y:auto;padding:0 20px 20px;"></div>
|
|
260
|
+
`;
|
|
261
|
+
|
|
262
|
+
// Wire up events
|
|
263
|
+
drawer.querySelector('#branchCompareClose')!.addEventListener('click', () => toggleDrawer(ctx));
|
|
264
|
+
|
|
265
|
+
drawer.querySelector('#branchSwap')!.addEventListener('click', () => {
|
|
266
|
+
const baseSelect = document.getElementById('branchBase') as HTMLSelectElement;
|
|
267
|
+
const compareSelect = document.getElementById('branchCompare') as HTMLSelectElement;
|
|
268
|
+
const tmp = baseSelect.value;
|
|
269
|
+
baseSelect.value = compareSelect.value;
|
|
270
|
+
compareSelect.value = tmp;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
drawer.querySelector('#branchDiffRun')!.addEventListener('click', () => runComparison(ctx));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Run Comparison ──────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
async function runComparison(ctx: CanvasContext) {
|
|
279
|
+
const baseSelect = document.getElementById('branchBase') as HTMLSelectElement;
|
|
280
|
+
const compareSelect = document.getElementById('branchCompare') as HTMLSelectElement;
|
|
281
|
+
const resultDiv = document.getElementById('branchDiffResult')!;
|
|
282
|
+
const runBtn = document.getElementById('branchDiffRun') as HTMLButtonElement;
|
|
283
|
+
|
|
284
|
+
const base = baseSelect.value;
|
|
285
|
+
const compare = compareSelect.value;
|
|
286
|
+
|
|
287
|
+
if (!base || !compare) {
|
|
288
|
+
showToast('Select both branches', 'error');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (base === compare) {
|
|
293
|
+
showToast('Select different branches', 'error');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_currentBase = base;
|
|
298
|
+
_currentCompare = compare;
|
|
299
|
+
|
|
300
|
+
// Loading state
|
|
301
|
+
runBtn.disabled = true;
|
|
302
|
+
runBtn.textContent = 'Loading…';
|
|
303
|
+
resultDiv.innerHTML = `
|
|
304
|
+
<div style="display:flex;align-items:center;justify-content:center;padding:40px 0;color:rgba(255,255,255,0.3);">
|
|
305
|
+
<div style="width:24px;height:24px;border:2px solid rgba(124,58,237,0.3);border-top-color:rgba(124,58,237,0.8);border-radius:50%;animation:spin 0.8s linear infinite;"></div>
|
|
306
|
+
<style>@keyframes spin { to { transform: rotate(360deg); } }</style>
|
|
307
|
+
</div>
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const state = ctx.snap().context;
|
|
312
|
+
const resp = await fetch('/api/repo/branch-diff', {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
315
|
+
body: JSON.stringify({ path: state.repoPath, base, compare }),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (!resp.ok) throw new Error(await resp.text());
|
|
319
|
+
|
|
320
|
+
const data = await resp.json();
|
|
321
|
+
|
|
322
|
+
// Render result summary in the drawer
|
|
323
|
+
renderDiffSummary(ctx, resultDiv, data);
|
|
324
|
+
|
|
325
|
+
// Render diff files on the canvas
|
|
326
|
+
if (data.files && data.files.length > 0) {
|
|
327
|
+
ctx.commitFilesData = data.files;
|
|
328
|
+
ctx.changedFilePaths = new Set(data.files.map((f: any) => f.path));
|
|
329
|
+
|
|
330
|
+
// Import renderFilesOnCanvas dynamically to avoid circular deps
|
|
331
|
+
const { renderFilesOnCanvas, populateChangedFilesPanel } = await import('./repo');
|
|
332
|
+
renderFilesOnCanvas(ctx, data.files, `${base}...${compare}`);
|
|
333
|
+
populateChangedFilesPanel(data.files);
|
|
334
|
+
|
|
335
|
+
// Update header info
|
|
336
|
+
const commitInfo = document.getElementById('commitInfo');
|
|
337
|
+
if (commitInfo) {
|
|
338
|
+
commitInfo.innerHTML = `
|
|
339
|
+
<span style="font-family:var(--font-mono);font-size:0.8rem;color:var(--accent-tertiary);background:var(--bg-elevated);padding:4px 10px;border-radius:4px;">
|
|
340
|
+
${base.substring(0, 12)} → ${compare.substring(0, 12)}
|
|
341
|
+
</span>
|
|
342
|
+
<span style="font-size:0.85rem;color:var(--text-primary);">
|
|
343
|
+
${data.totalChanged} files changed
|
|
344
|
+
</span>
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
showToast(`Comparing ${base} → ${compare}: ${data.totalChanged} files`, 'success');
|
|
349
|
+
} else {
|
|
350
|
+
showToast('No differences found between branches', 'info');
|
|
351
|
+
}
|
|
352
|
+
} catch (err: any) {
|
|
353
|
+
resultDiv.innerHTML = `
|
|
354
|
+
<div style="padding:16px;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);border-radius:8px;color:#f87171;font-size:12px;">
|
|
355
|
+
${err.message}
|
|
356
|
+
</div>
|
|
357
|
+
`;
|
|
358
|
+
showToast(`Branch diff failed: ${err.message}`, 'error');
|
|
359
|
+
} finally {
|
|
360
|
+
runBtn.disabled = false;
|
|
361
|
+
runBtn.textContent = 'Compare Branches';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Diff Summary in Drawer ──────────────────────────────
|
|
366
|
+
|
|
367
|
+
function renderDiffSummary(ctx: CanvasContext, container: HTMLElement, data: any) {
|
|
368
|
+
const { files, totalChanged, stats, base, compare } = data;
|
|
369
|
+
|
|
370
|
+
const statusCounts: Record<string, number> = {};
|
|
371
|
+
for (const f of files) {
|
|
372
|
+
statusCounts[f.status] = (statusCounts[f.status] || 0) + 1;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const statusColors: Record<string, string> = {
|
|
376
|
+
added: '#22c55e',
|
|
377
|
+
modified: '#eab308',
|
|
378
|
+
deleted: '#ef4444',
|
|
379
|
+
renamed: '#a78bfa',
|
|
380
|
+
copied: '#60a5fa',
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const statusBadges = Object.entries(statusCounts)
|
|
384
|
+
.map(([status, count]) => `
|
|
385
|
+
<span style="
|
|
386
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
387
|
+
padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600;
|
|
388
|
+
background:${statusColors[status] || '#888'}18;
|
|
389
|
+
color:${statusColors[status] || '#888'};
|
|
390
|
+
border:1px solid ${statusColors[status] || '#888'}33;
|
|
391
|
+
">${count} ${status}</span>
|
|
392
|
+
`).join('');
|
|
393
|
+
|
|
394
|
+
container.innerHTML = `
|
|
395
|
+
<div style="margin-top:4px;display:flex;flex-direction:column;gap:12px;">
|
|
396
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
|
397
|
+
<span style="font-size:18px;font-weight:700;color:#e2e8f0;">${totalChanged}</span>
|
|
398
|
+
<span style="font-size:12px;color:rgba(255,255,255,0.4);">files changed</span>
|
|
399
|
+
${stats ? `
|
|
400
|
+
<span style="font-size:11px;color:#4ade80;font-family:monospace;">+${stats.totalAdd}</span>
|
|
401
|
+
<span style="font-size:11px;color:#f87171;font-family:monospace;">-${stats.totalDel}</span>
|
|
402
|
+
` : ''}
|
|
403
|
+
</div>
|
|
404
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;">${statusBadges}</div>
|
|
405
|
+
<div style="max-height:calc(100vh - 400px);overflow-y:auto;">
|
|
406
|
+
${files.map((f: any) => `
|
|
407
|
+
<div style="
|
|
408
|
+
display:flex;align-items:center;gap:8px;
|
|
409
|
+
padding:7px 10px;margin-bottom:2px;
|
|
410
|
+
border-radius:6px;cursor:pointer;
|
|
411
|
+
transition:background 0.15s;
|
|
412
|
+
"
|
|
413
|
+
onmouseover="this.style.background='rgba(255,255,255,0.04)'"
|
|
414
|
+
onmouseout="this.style.background='none'"
|
|
415
|
+
data-path="${f.path}">
|
|
416
|
+
<span style="
|
|
417
|
+
width:6px;height:6px;border-radius:50%;flex-shrink:0;
|
|
418
|
+
background:${statusColors[f.status] || '#888'};
|
|
419
|
+
"></span>
|
|
420
|
+
<span style="font-size:12px;color:#e2e8f0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;font-family:'JetBrains Mono',monospace;">
|
|
421
|
+
${f.name}
|
|
422
|
+
</span>
|
|
423
|
+
<span style="font-size:10px;color:rgba(255,255,255,0.25);flex-shrink:0;">${f.status}</span>
|
|
424
|
+
</div>
|
|
425
|
+
`).join('')}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
`;
|
|
429
|
+
|
|
430
|
+
// File click → scroll canvas to that card
|
|
431
|
+
container.querySelectorAll('[data-path]').forEach(el => {
|
|
432
|
+
el.addEventListener('click', () => {
|
|
433
|
+
const path = el.getAttribute('data-path');
|
|
434
|
+
if (!path) return;
|
|
435
|
+
const card = document.querySelector(`.file-card[data-path="${path}"]`) as HTMLElement;
|
|
436
|
+
if (card) {
|
|
437
|
+
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
438
|
+
card.style.boxShadow = '0 0 0 3px rgba(124,58,237,0.6), var(--shadow-lg)';
|
|
439
|
+
setTimeout(() => { card.style.boxShadow = ''; }, 2000);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Breadcrumb navigation for the file modal header.
|
|
4
|
+
* Renders directory path as clickable segments with a dropdown
|
|
5
|
+
* showing sibling files when a directory segment is clicked.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CanvasContext } from './context';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Render breadcrumbs into the path element.
|
|
12
|
+
* Each segment is clickable to show sibling files/dirs at that level.
|
|
13
|
+
*/
|
|
14
|
+
export function renderBreadcrumbs(ctx: CanvasContext, pathEl: HTMLElement, filePath: string) {
|
|
15
|
+
pathEl.innerHTML = '';
|
|
16
|
+
pathEl.className = 'file-path breadcrumb-bar';
|
|
17
|
+
|
|
18
|
+
const parts = filePath.split('/');
|
|
19
|
+
const fileName = parts.pop() || filePath;
|
|
20
|
+
|
|
21
|
+
// Directory segments
|
|
22
|
+
for (let i = 0; i < parts.length; i++) {
|
|
23
|
+
const segment = parts[i];
|
|
24
|
+
const dirPath = parts.slice(0, i + 1).join('/');
|
|
25
|
+
|
|
26
|
+
const segBtn = document.createElement('button');
|
|
27
|
+
segBtn.className = 'breadcrumb-segment';
|
|
28
|
+
segBtn.textContent = segment;
|
|
29
|
+
segBtn.title = dirPath;
|
|
30
|
+
segBtn.addEventListener('click', (e) => {
|
|
31
|
+
e.stopPropagation();
|
|
32
|
+
showDirectoryDropdown(ctx, segBtn, dirPath, filePath);
|
|
33
|
+
});
|
|
34
|
+
pathEl.appendChild(segBtn);
|
|
35
|
+
|
|
36
|
+
// Separator
|
|
37
|
+
const sep = document.createElement('span');
|
|
38
|
+
sep.className = 'breadcrumb-sep';
|
|
39
|
+
sep.textContent = '/';
|
|
40
|
+
pathEl.appendChild(sep);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// File name (no dropdown, just styled differently)
|
|
44
|
+
const fileSpan = document.createElement('span');
|
|
45
|
+
fileSpan.className = 'breadcrumb-file';
|
|
46
|
+
fileSpan.textContent = fileName;
|
|
47
|
+
pathEl.appendChild(fileSpan);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Directory dropdown ────────────────────────────
|
|
51
|
+
let activeDropdown: HTMLElement | null = null;
|
|
52
|
+
|
|
53
|
+
function closeDropdown() {
|
|
54
|
+
if (activeDropdown) {
|
|
55
|
+
activeDropdown.remove();
|
|
56
|
+
activeDropdown = null;
|
|
57
|
+
}
|
|
58
|
+
document.removeEventListener('click', closeDropdown);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function showDirectoryDropdown(ctx: CanvasContext, anchor: HTMLElement, dirPath: string, currentFilePath: string) {
|
|
62
|
+
closeDropdown();
|
|
63
|
+
|
|
64
|
+
// Gather all files in this directory
|
|
65
|
+
const allFiles: string[] = [];
|
|
66
|
+
if (ctx.allFilesData) {
|
|
67
|
+
for (const f of ctx.allFilesData) {
|
|
68
|
+
if (f.path) allFiles.push(f.path);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const [p] of ctx.fileCards) {
|
|
72
|
+
if (!allFiles.includes(p)) allFiles.push(p);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Find siblings (files and immediate subdirectories)
|
|
76
|
+
const prefix = dirPath + '/';
|
|
77
|
+
const siblings: Array<{ name: string; path: string; isDir: boolean }> = [];
|
|
78
|
+
const seenDirs = new Set<string>();
|
|
79
|
+
|
|
80
|
+
for (const fp of allFiles) {
|
|
81
|
+
if (!fp.startsWith(prefix)) continue;
|
|
82
|
+
const rest = fp.slice(prefix.length);
|
|
83
|
+
const slashIdx = rest.indexOf('/');
|
|
84
|
+
|
|
85
|
+
if (slashIdx === -1) {
|
|
86
|
+
// Direct file
|
|
87
|
+
siblings.push({ name: rest, path: fp, isDir: false });
|
|
88
|
+
} else {
|
|
89
|
+
// Subdirectory
|
|
90
|
+
const subDir = rest.slice(0, slashIdx);
|
|
91
|
+
if (!seenDirs.has(subDir)) {
|
|
92
|
+
seenDirs.add(subDir);
|
|
93
|
+
siblings.push({ name: subDir + '/', path: prefix + subDir, isDir: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Sort: dirs first, then files, alphabetically
|
|
99
|
+
siblings.sort((a, b) => {
|
|
100
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
101
|
+
return a.name.localeCompare(b.name);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (siblings.length === 0) return;
|
|
105
|
+
|
|
106
|
+
// Create dropdown
|
|
107
|
+
const dropdown = document.createElement('div');
|
|
108
|
+
dropdown.className = 'breadcrumb-dropdown';
|
|
109
|
+
|
|
110
|
+
// Position relative to anchor
|
|
111
|
+
const rect = anchor.getBoundingClientRect();
|
|
112
|
+
dropdown.style.top = `${rect.bottom + 4}px`;
|
|
113
|
+
dropdown.style.left = `${rect.left}px`;
|
|
114
|
+
|
|
115
|
+
for (const item of siblings) {
|
|
116
|
+
const btn = document.createElement('button');
|
|
117
|
+
btn.className = 'breadcrumb-dropdown-item';
|
|
118
|
+
if (item.path === currentFilePath) btn.classList.add('active');
|
|
119
|
+
|
|
120
|
+
const icon = document.createElement('span');
|
|
121
|
+
icon.className = 'breadcrumb-dropdown-icon';
|
|
122
|
+
icon.textContent = item.isDir ? '📁' : getFileIcon(item.name);
|
|
123
|
+
btn.appendChild(icon);
|
|
124
|
+
|
|
125
|
+
const label = document.createElement('span');
|
|
126
|
+
label.textContent = item.name;
|
|
127
|
+
btn.appendChild(label);
|
|
128
|
+
|
|
129
|
+
btn.addEventListener('click', (e) => {
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
closeDropdown();
|
|
132
|
+
|
|
133
|
+
if (item.isDir) {
|
|
134
|
+
// Show sub-directory contents
|
|
135
|
+
showDirectoryDropdown(ctx, anchor, item.path, currentFilePath);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Open the file
|
|
140
|
+
const fileData = ctx.allFilesData?.find(f => f.path === item.path);
|
|
141
|
+
if (fileData) {
|
|
142
|
+
import('./file-modal').then(({ openFileModal }) => {
|
|
143
|
+
openFileModal(ctx, fileData);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
dropdown.appendChild(btn);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
document.body.appendChild(dropdown);
|
|
152
|
+
activeDropdown = dropdown;
|
|
153
|
+
|
|
154
|
+
// Close on click outside (defer to avoid immediate close)
|
|
155
|
+
requestAnimationFrame(() => {
|
|
156
|
+
document.addEventListener('click', closeDropdown);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getFileIcon(name: string): string {
|
|
161
|
+
const ext = name.split('.').pop()?.toLowerCase() || '';
|
|
162
|
+
const icons: Record<string, string> = {
|
|
163
|
+
ts: '🔷', tsx: '⚛️', js: '🟨', jsx: '⚛️',
|
|
164
|
+
py: '🐍', css: '🎨', html: '🌐', json: '📋',
|
|
165
|
+
md: '📝', yaml: '⚙️', yml: '⚙️', toml: '⚙️',
|
|
166
|
+
sh: '🐚', sql: '🗃️', rs: '🦀', go: '🐹',
|
|
167
|
+
svg: '🖼️', png: '🖼️', jpg: '🖼️',
|
|
168
|
+
};
|
|
169
|
+
return icons[ext] || '📄';
|
|
170
|
+
}
|