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,176 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Positions — load/save card positions with dual storage:
4
+ * - Server-side (SQLite via /api/auth/positions) when logged in
5
+ * - localStorage fallback when not authenticated
6
+ *
7
+ * Enables shared repositories: each user has their own card layout.
8
+ */
9
+ import { measure } from 'measure-fn';
10
+ import type { CanvasContext } from './context';
11
+ import { getUser } from './user';
12
+
13
+ const STORAGE_PREFIX = 'gitcanvas:positions:';
14
+
15
+ /** Debounce timer for batched saves */
16
+ let _saveTimer: any = null;
17
+ const SAVE_DEBOUNCE_MS = 300;
18
+
19
+ // ─── Get the localStorage key for the current repo ───────
20
+ function getStorageKey(ctx: CanvasContext): string | null {
21
+ const repoPath = ctx.snap?.()?.context?.repoPath;
22
+ if (!repoPath) return null;
23
+ return `${STORAGE_PREFIX}${repoPath}`;
24
+ }
25
+
26
+ function getRepoPath(ctx: CanvasContext): string | null {
27
+ return ctx.snap?.()?.context?.repoPath || null;
28
+ }
29
+
30
+ // ─── Load all saved positions ────────────────────────────
31
+ export async function loadSavedPositions(ctx: CanvasContext) {
32
+ return measure('positions:load', async () => {
33
+ try {
34
+ const repoPath = getRepoPath(ctx);
35
+ if (!repoPath) return;
36
+
37
+ // Clear old positions first to prevent stale data from
38
+ // a previously loaded repo bleeding into the new one
39
+ ctx.positions = new Map();
40
+
41
+ // Try server-side first (if logged in)
42
+ const user = getUser();
43
+ if (user) {
44
+ try {
45
+ const res = await fetch(`/api/auth/positions?repo=${encodeURIComponent(repoPath)}`);
46
+ const data = await res.json();
47
+ if (data.positions) {
48
+ ctx.positions = new Map(Object.entries(data.positions));
49
+ // Migrate legacy expanded state from separate localStorage key
50
+ _migrateLegacyExpanded(ctx, repoPath);
51
+ return;
52
+ }
53
+ } catch { /* fall through to localStorage */ }
54
+ }
55
+
56
+ // Fallback: localStorage
57
+ const key = getStorageKey(ctx);
58
+ if (!key) return;
59
+ const raw = localStorage.getItem(key);
60
+ if (raw) {
61
+ const data = JSON.parse(raw);
62
+ ctx.positions = new Map(Object.entries(data));
63
+ }
64
+
65
+ // Migrate legacy expanded state from separate localStorage key
66
+ _migrateLegacyExpanded(ctx, repoPath);
67
+ } catch (e) {
68
+ measure('positions:loadError', () => e);
69
+ }
70
+ });
71
+ }
72
+
73
+ // ─── Persist all positions (debounced) ───────────────────
74
+ function flushPositions(ctx: CanvasContext) {
75
+ const repoPath = getRepoPath(ctx);
76
+ if (!repoPath) return;
77
+
78
+ const obj: Record<string, any> = {};
79
+ for (const [k, v] of ctx.positions) {
80
+ obj[k] = v;
81
+ }
82
+
83
+ // Always save to localStorage (instant)
84
+ try {
85
+ const key = getStorageKey(ctx);
86
+ if (key) localStorage.setItem(key, JSON.stringify(obj));
87
+ } catch { }
88
+
89
+ // Also sync to server if logged in (async, fire-and-forget)
90
+ const user = getUser();
91
+ if (user) {
92
+ fetch('/api/auth/positions', {
93
+ method: 'POST',
94
+ headers: { 'Content-Type': 'application/json' },
95
+ body: JSON.stringify({ repoUrl: repoPath, positions: obj }),
96
+ }).catch(() => { /* silent — localStorage is the safety net */ });
97
+ }
98
+ }
99
+
100
+ // ─── Save a single card position (debounced) ─────────────
101
+ export async function savePosition(ctx: CanvasContext, commitHash: string, filePath: string, x?: number, y?: number, width?: number, height?: number) {
102
+ return measure('positions:save', async () => {
103
+ try {
104
+ const posKey = `${commitHash}:${filePath}`;
105
+ const existing = ctx.positions.get(posKey) || {};
106
+ const newPos = {
107
+ x: x !== undefined ? x : existing.x,
108
+ y: y !== undefined ? y : existing.y,
109
+ width: width !== undefined ? width : existing.width,
110
+ height: height !== undefined ? height : existing.height,
111
+ expanded: existing.expanded, // preserve expanded flag
112
+ };
113
+ ctx.positions.set(posKey, newPos);
114
+
115
+ // Debounced persist — avoid hammering storage on every drag frame
116
+ if (_saveTimer) clearTimeout(_saveTimer);
117
+ _saveTimer = setTimeout(() => flushPositions(ctx), SAVE_DEBOUNCE_MS);
118
+ } catch (e) {
119
+ measure('positions:saveError', () => e);
120
+ }
121
+ });
122
+ }
123
+
124
+ // ─── Position key helper ─────────────────────────────────
125
+ export function getPositionKey(filePath: string, commitHash: string): string {
126
+ return `${commitHash}:${filePath}`;
127
+ }
128
+
129
+ // ─── Expanded state (unified with positions) ─────────────
130
+
131
+ /** Check if a file path is saved as expanded in positions */
132
+ export function isPathExpandedInPositions(ctx: CanvasContext, filePath: string): boolean {
133
+ // Check allfiles key (primary)
134
+ const key = `allfiles:${filePath}`;
135
+ const pos = ctx.positions.get(key);
136
+ return !!(pos && pos.expanded);
137
+ }
138
+
139
+ /** Mark a file path as expanded or collapsed in positions */
140
+ export function setPathExpandedInPositions(ctx: CanvasContext, filePath: string, expanded: boolean) {
141
+ const key = `allfiles:${filePath}`;
142
+ const existing = ctx.positions.get(key) || {};
143
+ ctx.positions.set(key, { ...existing, expanded });
144
+ // Trigger debounced persist
145
+ if (_saveTimer) clearTimeout(_saveTimer);
146
+ _saveTimer = setTimeout(() => flushPositions(ctx), SAVE_DEBOUNCE_MS);
147
+ }
148
+
149
+ /** Migrate legacy expanded state from separate localStorage key into positions */
150
+ function _migrateLegacyExpanded(ctx: CanvasContext, repoPath: string) {
151
+ const legacyKey = `gitcanvas:expanded:${repoPath}`;
152
+ try {
153
+ const raw = localStorage.getItem(legacyKey);
154
+ if (!raw) return;
155
+ const paths: string[] = JSON.parse(raw);
156
+ if (!Array.isArray(paths) || paths.length === 0) return;
157
+
158
+ let migrated = 0;
159
+ for (const filePath of paths) {
160
+ const posKey = `allfiles:${filePath}`;
161
+ const existing = ctx.positions.get(posKey) || {};
162
+ if (!existing.expanded) {
163
+ ctx.positions.set(posKey, { ...existing, expanded: true });
164
+ migrated++;
165
+ }
166
+ }
167
+
168
+ if (migrated > 0) {
169
+ // Persist immediately to save the migration
170
+ flushPositions(ctx);
171
+ }
172
+
173
+ // Remove the legacy key
174
+ localStorage.removeItem(legacyKey);
175
+ } catch { }
176
+ }
@@ -0,0 +1,374 @@
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
+ }
@@ -0,0 +1,47 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Production mode detection — checks if we're running on the SaaS deploy.
4
+ *
5
+ * When in production (gitmaps.xyz), certain features are restricted:
6
+ * - File editing/saving is disabled
7
+ * - Git commits are disabled
8
+ * - Local repo paths are hidden
9
+ *
10
+ * These restrictions exist because the SaaS version works with
11
+ * cloned repos, not local file systems.
12
+ */
13
+
14
+ const PRODUCTION_HOSTS = ['gitmaps.xyz', 'www.gitmaps.xyz'];
15
+
16
+ /** Check if the app is running in production SaaS mode */
17
+ export function isProductionMode(): boolean {
18
+ if (typeof window === 'undefined') return false;
19
+ return PRODUCTION_HOSTS.includes(window.location.hostname);
20
+ }
21
+
22
+ /** Check if editing is allowed (false in production) */
23
+ export function isEditingAllowed(): boolean {
24
+ return !isProductionMode();
25
+ }
26
+
27
+ /** Get the production notice HTML for the editor area */
28
+ export function getProductionEditorNotice(): string {
29
+ return `
30
+ <div class="production-notice">
31
+ <div class="production-notice-icon">
32
+ <svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5">
33
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
34
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
35
+ </svg>
36
+ </div>
37
+ <h3 class="production-notice-title">Editing is view-only on GitMaps.xyz</h3>
38
+ <p class="production-notice-desc">
39
+ To edit files, save changes, and commit directly — install GitMaps locally:
40
+ </p>
41
+ <code class="production-notice-cmd">npx gitmaps</code>
42
+ <p class="production-notice-sub">
43
+ Local mode gives you full read-write access to your repositories.
44
+ </p>
45
+ </div>
46
+ `;
47
+ }