gitmaps 1.0.0 → 1.1.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 (46) hide show
  1. package/README.md +5 -11
  2. package/app/[owner]/[repo]/page.client.tsx +5 -0
  3. package/app/[owner]/[repo]/page.tsx +6 -0
  4. package/app/[slug]/page.client.tsx +5 -0
  5. package/app/[slug]/page.tsx +6 -0
  6. package/app/api/manifest.json/route.ts +20 -0
  7. package/app/api/pwa-icon/route.ts +14 -0
  8. package/app/api/repo/clone-stream/route.ts +20 -12
  9. package/app/api/repo/imports/route.ts +21 -3
  10. package/app/api/repo/list/route.ts +30 -0
  11. package/app/api/repo/upload/route.ts +6 -9
  12. package/app/api/sw.js/route.ts +70 -0
  13. package/app/galaxy-canvas/page.client.tsx +2 -0
  14. package/app/galaxy-canvas/page.tsx +5 -0
  15. package/app/globals.css +477 -95
  16. package/app/icon.png +0 -0
  17. package/app/layout.tsx +30 -2
  18. package/app/lib/canvas-text.ts +4 -72
  19. package/app/lib/canvas.ts +1 -1
  20. package/app/lib/card-arrangement.ts +21 -7
  21. package/app/lib/card-context-menu.tsx +2 -2
  22. package/app/lib/card-groups.ts +9 -2
  23. package/app/lib/cards.tsx +3 -1
  24. package/app/lib/connections.tsx +34 -43
  25. package/app/lib/events.tsx +25 -0
  26. package/app/lib/file-card-plugin.ts +14 -0
  27. package/app/lib/file-preview.ts +68 -41
  28. package/app/lib/galaxydraw-bridge.ts +5 -0
  29. package/app/lib/global-search.ts +48 -27
  30. package/app/lib/layers.tsx +17 -18
  31. package/app/lib/perf-overlay.ts +78 -0
  32. package/app/lib/positions.ts +1 -1
  33. package/app/lib/repo.tsx +18 -8
  34. package/app/lib/shortcuts-panel.ts +2 -0
  35. package/app/lib/viewport-culling.ts +7 -0
  36. package/app/page.client.tsx +72 -18
  37. package/app/page.tsx +22 -86
  38. package/banner.png +0 -0
  39. package/package.json +2 -2
  40. package/packages/galaxydraw/README.md +2 -2
  41. package/packages/galaxydraw/package.json +1 -1
  42. package/server.ts +1 -1
  43. package/app/api/connections/route.ts +0 -72
  44. package/app/api/positions/route.ts +0 -80
  45. package/app/api/repo/browse/route.ts +0 -55
  46. package/app/lib/pr-review.ts +0 -374
@@ -1,374 +0,0 @@
1
- /**
2
- * PR Review — Inline Comments on Diff Lines
3
- *
4
- * Enables code review directly on the canvas:
5
- * - Click the gutter area of any line to add a comment
6
- * - Comments stored per file+line in localStorage
7
- * - Visual markers (purple dot) rendered in the canvas gutter
8
- * - Comment thread popup with input field and existing comments
9
- * - Optional WebSocket sync for collaborative review
10
- *
11
- * Storage key: `gitcanvas:reviews:{repoSlug}`
12
- * Data format: { [filePath]: { [lineNum]: ReviewComment[] } }
13
- */
14
-
15
- // ─── Types ──────────────────────────────────────────────
16
-
17
- export interface ReviewComment {
18
- id: string;
19
- author: string;
20
- text: string;
21
- lineNum: number;
22
- filePath: string;
23
- createdAt: number;
24
- resolved?: boolean;
25
- }
26
-
27
- export interface ReviewStore {
28
- [filePath: string]: {
29
- [lineNum: string]: ReviewComment[];
30
- };
31
- }
32
-
33
- // ─── Storage ────────────────────────────────────────────
34
-
35
- const STORAGE_PREFIX = 'gitcanvas:reviews:';
36
- let currentRepo = '';
37
- let store: ReviewStore = {};
38
- let changeListeners: Array<() => void> = [];
39
-
40
- export function initReviewStore(repoSlug: string) {
41
- currentRepo = repoSlug;
42
- try {
43
- const saved = localStorage.getItem(STORAGE_PREFIX + repoSlug);
44
- store = saved ? JSON.parse(saved) : {};
45
- } catch {
46
- store = {};
47
- }
48
- }
49
-
50
- function persist() {
51
- if (!currentRepo) return;
52
- try {
53
- localStorage.setItem(STORAGE_PREFIX + currentRepo, JSON.stringify(store));
54
- } catch { /* quota exceeded — non-fatal */ }
55
- changeListeners.forEach(fn => fn());
56
- }
57
-
58
- export function onReviewChange(fn: () => void): () => void {
59
- changeListeners.push(fn);
60
- return () => { changeListeners = changeListeners.filter(f => f !== fn); };
61
- }
62
-
63
- // ─── CRUD ───────────────────────────────────────────────
64
-
65
- export function addComment(filePath: string, lineNum: number, text: string, author = 'You'): ReviewComment {
66
- if (!store[filePath]) store[filePath] = {};
67
- const key = String(lineNum);
68
- if (!store[filePath][key]) store[filePath][key] = [];
69
-
70
- const comment: ReviewComment = {
71
- id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
72
- author,
73
- text,
74
- lineNum,
75
- filePath,
76
- createdAt: Date.now(),
77
- };
78
-
79
- store[filePath][key].push(comment);
80
- persist();
81
- return comment;
82
- }
83
-
84
- export function getComments(filePath: string, lineNum: number): ReviewComment[] {
85
- return store[filePath]?.[String(lineNum)] || [];
86
- }
87
-
88
- export function getAllFileComments(filePath: string): Map<number, ReviewComment[]> {
89
- const map = new Map<number, ReviewComment[]>();
90
- const fileStore = store[filePath];
91
- if (!fileStore) return map;
92
- for (const [key, comments] of Object.entries(fileStore)) {
93
- const lineNum = parseInt(key, 10);
94
- if (!isNaN(lineNum) && comments.length > 0) {
95
- map.set(lineNum, comments);
96
- }
97
- }
98
- return map;
99
- }
100
-
101
- export function resolveComment(filePath: string, lineNum: number, commentId: string) {
102
- const comments = store[filePath]?.[String(lineNum)];
103
- if (!comments) return;
104
- const comment = comments.find(c => c.id === commentId);
105
- if (comment) {
106
- comment.resolved = true;
107
- persist();
108
- }
109
- }
110
-
111
- export function deleteComment(filePath: string, lineNum: number, commentId: string) {
112
- const key = String(lineNum);
113
- const comments = store[filePath]?.[key];
114
- if (!comments) return;
115
- store[filePath][key] = comments.filter(c => c.id !== commentId);
116
- if (store[filePath][key].length === 0) delete store[filePath][key];
117
- if (Object.keys(store[filePath]).length === 0) delete store[filePath];
118
- persist();
119
- }
120
-
121
- export function getReviewStats(): { totalComments: number; totalFiles: number; unresolvedCount: number } {
122
- let totalComments = 0;
123
- let unresolvedCount = 0;
124
- let totalFiles = 0;
125
- for (const filePath of Object.keys(store)) {
126
- let hasComments = false;
127
- for (const comments of Object.values(store[filePath])) {
128
- totalComments += comments.length;
129
- unresolvedCount += comments.filter(c => !c.resolved).length;
130
- if (comments.length > 0) hasComments = true;
131
- }
132
- if (hasComments) totalFiles++;
133
- }
134
- return { totalComments, totalFiles, unresolvedCount };
135
- }
136
-
137
- // ─── Comment Thread Popup ───────────────────────────────
138
-
139
- let activePopup: HTMLElement | null = null;
140
-
141
- export function showCommentPopup(
142
- filePath: string,
143
- lineNum: number,
144
- anchorX: number,
145
- anchorY: number,
146
- onSubmit?: (comment: ReviewComment) => void
147
- ) {
148
- hideCommentPopup();
149
-
150
- const comments = getComments(filePath, lineNum);
151
- const popup = document.createElement('div');
152
- popup.className = 'review-comment-popup';
153
- popup.setAttribute('data-line', String(lineNum));
154
- popup.style.cssText = `
155
- position: fixed;
156
- z-index: 10000;
157
- background: rgba(22, 22, 35, 0.98);
158
- border: 1px solid rgba(124, 58, 237, 0.5);
159
- border-radius: 12px;
160
- padding: 0;
161
- min-width: 320px;
162
- max-width: 420px;
163
- max-height: 360px;
164
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 16px rgba(124, 58, 237, 0.2);
165
- font-family: 'Inter', -apple-system, sans-serif;
166
- font-size: 13px;
167
- color: #c9d1d9;
168
- overflow: hidden;
169
- backdrop-filter: blur(16px);
170
- `;
171
-
172
- // Header
173
- const header = document.createElement('div');
174
- header.style.cssText = `
175
- padding: 10px 14px;
176
- border-bottom: 1px solid rgba(255,255,255,0.08);
177
- display: flex;
178
- justify-content: space-between;
179
- align-items: center;
180
- background: rgba(124, 58, 237, 0.1);
181
- `;
182
- header.innerHTML = `
183
- <span style="font-weight: 600; color: #a78bfa;">
184
- 💬 Line ${lineNum}
185
- </span>
186
- <span style="color: #6e7681; font-size: 11px;">
187
- ${comments.length} comment${comments.length !== 1 ? 's' : ''}
188
- </span>
189
- `;
190
- popup.appendChild(header);
191
-
192
- // Existing comments
193
- if (comments.length > 0) {
194
- const list = document.createElement('div');
195
- list.style.cssText = `
196
- max-height: 180px;
197
- overflow-y: auto;
198
- padding: 8px 14px;
199
- `;
200
- for (const c of comments) {
201
- const item = document.createElement('div');
202
- item.style.cssText = `
203
- padding: 8px 0;
204
- border-bottom: 1px solid rgba(255,255,255,0.04);
205
- ${c.resolved ? 'opacity: 0.5;' : ''}
206
- `;
207
- const ago = timeAgo(c.createdAt);
208
- item.innerHTML = `
209
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
210
- <span style="font-weight: 500; color: #a78bfa; font-size: 12px;">${escapeHtml(c.author)}</span>
211
- <span style="color: #484f58; font-size: 11px;">${ago}</span>
212
- </div>
213
- <div style="line-height: 1.5; ${c.resolved ? 'text-decoration: line-through;' : ''}">${escapeHtml(c.text)}</div>
214
- `;
215
-
216
- // Resolve/delete buttons
217
- const actions = document.createElement('div');
218
- actions.style.cssText = 'display: flex; gap: 8px; margin-top: 4px;';
219
-
220
- if (!c.resolved) {
221
- const resolveBtn = document.createElement('button');
222
- resolveBtn.textContent = '✓ Resolve';
223
- resolveBtn.style.cssText = btnStyle('#238636');
224
- resolveBtn.onclick = (e) => {
225
- e.stopPropagation();
226
- resolveComment(filePath, lineNum, c.id);
227
- showCommentPopup(filePath, lineNum, anchorX, anchorY, onSubmit);
228
- };
229
- actions.appendChild(resolveBtn);
230
- }
231
-
232
- const delBtn = document.createElement('button');
233
- delBtn.textContent = '✕';
234
- delBtn.style.cssText = btnStyle('#da3633');
235
- delBtn.onclick = (e) => {
236
- e.stopPropagation();
237
- deleteComment(filePath, lineNum, c.id);
238
- showCommentPopup(filePath, lineNum, anchorX, anchorY, onSubmit);
239
- };
240
- actions.appendChild(delBtn);
241
-
242
- item.appendChild(actions);
243
- list.appendChild(item);
244
- }
245
- popup.appendChild(list);
246
- }
247
-
248
- // Input area
249
- const inputArea = document.createElement('div');
250
- inputArea.style.cssText = `
251
- padding: 10px 14px;
252
- border-top: 1px solid rgba(255,255,255,0.06);
253
- display: flex;
254
- gap: 8px;
255
- `;
256
-
257
- const input = document.createElement('input');
258
- input.type = 'text';
259
- input.placeholder = 'Add a comment...';
260
- input.style.cssText = `
261
- flex: 1;
262
- background: rgba(255,255,255,0.06);
263
- border: 1px solid rgba(255,255,255,0.1);
264
- border-radius: 8px;
265
- padding: 8px 12px;
266
- color: #c9d1d9;
267
- font-size: 13px;
268
- outline: none;
269
- font-family: inherit;
270
- `;
271
-
272
- const submitBtn = document.createElement('button');
273
- submitBtn.textContent = '↵';
274
- submitBtn.style.cssText = `
275
- background: rgba(124, 58, 237, 0.8);
276
- border: none;
277
- border-radius: 8px;
278
- padding: 8px 14px;
279
- color: white;
280
- font-size: 14px;
281
- cursor: pointer;
282
- transition: background 0.15s;
283
- `;
284
- submitBtn.onmouseenter = () => submitBtn.style.background = 'rgba(124, 58, 237, 1)';
285
- submitBtn.onmouseleave = () => submitBtn.style.background = 'rgba(124, 58, 237, 0.8)';
286
-
287
- const submit = () => {
288
- const text = input.value.trim();
289
- if (!text) return;
290
- const comment = addComment(filePath, lineNum, text);
291
- onSubmit?.(comment);
292
- // Refresh popup to show new comment
293
- showCommentPopup(filePath, lineNum, anchorX, anchorY, onSubmit);
294
- };
295
-
296
- input.onkeydown = (e) => {
297
- if (e.key === 'Enter') submit();
298
- if (e.key === 'Escape') hideCommentPopup();
299
- e.stopPropagation(); // Don't trigger canvas shortcuts
300
- };
301
- submitBtn.onclick = submit;
302
-
303
- inputArea.appendChild(input);
304
- inputArea.appendChild(submitBtn);
305
- popup.appendChild(inputArea);
306
-
307
- // Position: prefer above the anchor, fall below if near top
308
- document.body.appendChild(popup);
309
- const popupRect = popup.getBoundingClientRect();
310
- let px = anchorX;
311
- let py = anchorY - popupRect.height - 8;
312
- if (py < 10) py = anchorY + 24;
313
- if (px + popupRect.width > window.innerWidth - 10) px = window.innerWidth - popupRect.width - 10;
314
- popup.style.left = `${px}px`;
315
- popup.style.top = `${py}px`;
316
-
317
- activePopup = popup;
318
-
319
- // Focus input
320
- requestAnimationFrame(() => input.focus());
321
-
322
- // Close on outside click (delayed to avoid immediate close)
323
- setTimeout(() => {
324
- const closeHandler = (e: MouseEvent) => {
325
- if (!popup.contains(e.target as Node)) {
326
- hideCommentPopup();
327
- document.removeEventListener('mousedown', closeHandler);
328
- }
329
- };
330
- document.addEventListener('mousedown', closeHandler);
331
- }, 50);
332
- }
333
-
334
- export function hideCommentPopup() {
335
- if (activePopup) {
336
- activePopup.remove();
337
- activePopup = null;
338
- }
339
- }
340
-
341
- // ─── Helpers ────────────────────────────────────────────
342
-
343
- function btnStyle(color: string): string {
344
- return `
345
- background: none;
346
- border: none;
347
- color: ${color};
348
- cursor: pointer;
349
- font-size: 11px;
350
- padding: 2px 4px;
351
- border-radius: 4px;
352
- opacity: 0.7;
353
- transition: opacity 0.15s;
354
- `;
355
- }
356
-
357
- function escapeHtml(str: string): string {
358
- return str
359
- .replace(/&/g, '&amp;')
360
- .replace(/</g, '&lt;')
361
- .replace(/>/g, '&gt;')
362
- .replace(/"/g, '&quot;');
363
- }
364
-
365
- function timeAgo(ts: number): string {
366
- const diff = Date.now() - ts;
367
- const mins = Math.floor(diff / 60000);
368
- if (mins < 1) return 'just now';
369
- if (mins < 60) return `${mins}m ago`;
370
- const hours = Math.floor(mins / 60);
371
- if (hours < 24) return `${hours}h ago`;
372
- const days = Math.floor(hours / 24);
373
- return `${days}d ago`;
374
- }