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,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, '&')
|
|
360
|
+
.replace(/</g, '<')
|
|
361
|
+
.replace(/>/g, '>')
|
|
362
|
+
.replace(/"/g, '"');
|
|
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
|
+
}
|