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,286 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Multi-repo workspace — load multiple repos on the same canvas.
|
|
4
|
+
*
|
|
5
|
+
* Each repo gets its own zone (bounding box region) on the canvas.
|
|
6
|
+
* Files from different repos are offset horizontally with a gap.
|
|
7
|
+
* A floating repo zone label appears at the top of each repo's area.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* - `loadedRepos` tracks all loaded repos with their file data and bounds
|
|
11
|
+
* - When a second repo is loaded, its grid starts to the right of the first
|
|
12
|
+
* - Repo zone labels are DOM elements inside the canvas (world-space)
|
|
13
|
+
* - Sidebar commit timeline switches between repos via tab clicks
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { CanvasContext } from './context';
|
|
17
|
+
import { showToast } from './utils';
|
|
18
|
+
|
|
19
|
+
export interface LoadedRepo {
|
|
20
|
+
path: string;
|
|
21
|
+
name: string; // Display name (last folder segment)
|
|
22
|
+
commits: any[];
|
|
23
|
+
files: any[]; // allFilesData
|
|
24
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
25
|
+
zoneLabel: HTMLElement | null;
|
|
26
|
+
color: string; // Accent color for the zone
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── State ────────────────────────────────────────────
|
|
30
|
+
const loadedRepos = new Map<string, LoadedRepo>();
|
|
31
|
+
let _activeRepoPath: string | null = null;
|
|
32
|
+
|
|
33
|
+
const REPO_COLORS = [
|
|
34
|
+
'rgba(124, 58, 237, 0.6)', // Purple (primary)
|
|
35
|
+
'rgba(59, 130, 246, 0.6)', // Blue
|
|
36
|
+
'rgba(16, 185, 129, 0.6)', // Emerald
|
|
37
|
+
'rgba(245, 158, 11, 0.6)', // Amber
|
|
38
|
+
'rgba(239, 68, 68, 0.6)', // Red
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const REPO_GAP = 800; // World-space gap between repos
|
|
42
|
+
|
|
43
|
+
// ── Public API ───────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export function getLoadedRepos() { return loadedRepos; }
|
|
46
|
+
export function getActiveRepoPath() { return _activeRepoPath; }
|
|
47
|
+
export function setActiveRepoPath(path: string) { _activeRepoPath = path; }
|
|
48
|
+
|
|
49
|
+
export function getRepoDisplayName(path: string): string {
|
|
50
|
+
const parts = path.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
51
|
+
return parts[parts.length - 1] || path;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Register a repo that has just been loaded.
|
|
56
|
+
* Called from loadRepository after files are rendered.
|
|
57
|
+
*/
|
|
58
|
+
export function registerRepo(ctx: CanvasContext, repoPath: string, commits: any[], files: any[]) {
|
|
59
|
+
const name = getRepoDisplayName(repoPath);
|
|
60
|
+
const colorIdx = loadedRepos.size % REPO_COLORS.length;
|
|
61
|
+
|
|
62
|
+
// Calculate bounds from existing cards + deferred cards
|
|
63
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
64
|
+
|
|
65
|
+
for (const [path, card] of ctx.fileCards) {
|
|
66
|
+
if (!repoMatchesPath(repoPath, path)) continue;
|
|
67
|
+
const x = parseFloat(card.style.left) || 0;
|
|
68
|
+
const y = parseFloat(card.style.top) || 0;
|
|
69
|
+
const w = card.offsetWidth || 580;
|
|
70
|
+
const h = card.offsetHeight || 700;
|
|
71
|
+
if (x < minX) minX = x;
|
|
72
|
+
if (y < minY) minY = y;
|
|
73
|
+
if (x + w > maxX) maxX = x + w;
|
|
74
|
+
if (y + h > maxY) maxY = y + h;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const [path, entry] of ctx.deferredCards) {
|
|
78
|
+
if (!repoMatchesPath(repoPath, path)) continue;
|
|
79
|
+
const { x, y, size } = entry;
|
|
80
|
+
const w = size?.width || 580;
|
|
81
|
+
const h = size?.height || 700;
|
|
82
|
+
if (x < minX) minX = x;
|
|
83
|
+
if (y < minY) minY = y;
|
|
84
|
+
if (x + w > maxX) maxX = x + w;
|
|
85
|
+
if (y + h > maxY) maxY = y + h;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (minX === Infinity) {
|
|
89
|
+
minX = 50; minY = 50; maxX = 650; maxY = 750;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const repo: LoadedRepo = {
|
|
93
|
+
path: repoPath,
|
|
94
|
+
name,
|
|
95
|
+
commits,
|
|
96
|
+
files,
|
|
97
|
+
bounds: { x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
98
|
+
zoneLabel: null,
|
|
99
|
+
color: REPO_COLORS[colorIdx],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
loadedRepos.set(repoPath, repo);
|
|
103
|
+
_activeRepoPath = repoPath;
|
|
104
|
+
|
|
105
|
+
createZoneLabel(ctx, repo);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the X offset for a new repo being added to the canvas.
|
|
110
|
+
* Returns the right edge of the rightmost existing repo + gap.
|
|
111
|
+
*/
|
|
112
|
+
export function getNextRepoOffset(): number {
|
|
113
|
+
if (loadedRepos.size === 0) return 50;
|
|
114
|
+
|
|
115
|
+
let maxRight = 0;
|
|
116
|
+
for (const [, repo] of loadedRepos) {
|
|
117
|
+
const right = repo.bounds.x + repo.bounds.width;
|
|
118
|
+
if (right > maxRight) maxRight = right;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return maxRight + REPO_GAP;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if this is an additional repo load (not the first one).
|
|
126
|
+
*/
|
|
127
|
+
export function isMultiRepoLoad(): boolean {
|
|
128
|
+
return loadedRepos.size > 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Remove a repo from the workspace.
|
|
133
|
+
*/
|
|
134
|
+
export function unloadRepo(ctx: CanvasContext, repoPath: string) {
|
|
135
|
+
const repo = loadedRepos.get(repoPath);
|
|
136
|
+
if (!repo) return;
|
|
137
|
+
|
|
138
|
+
// Remove zone label
|
|
139
|
+
if (repo.zoneLabel) repo.zoneLabel.remove();
|
|
140
|
+
|
|
141
|
+
// Remove cards belonging to this repo
|
|
142
|
+
for (const [path, card] of ctx.fileCards) {
|
|
143
|
+
if (repoMatchesPath(repoPath, path)) {
|
|
144
|
+
card.remove();
|
|
145
|
+
ctx.fileCards.delete(path);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Remove deferred cards
|
|
150
|
+
for (const [path] of ctx.deferredCards) {
|
|
151
|
+
if (repoMatchesPath(repoPath, path)) {
|
|
152
|
+
ctx.deferredCards.delete(path);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
loadedRepos.delete(repoPath);
|
|
157
|
+
|
|
158
|
+
// Switch active to first remaining repo
|
|
159
|
+
if (_activeRepoPath === repoPath) {
|
|
160
|
+
const first = loadedRepos.keys().next();
|
|
161
|
+
_activeRepoPath = first.done ? null : first.value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create repo zone tabs in the sidebar for switching between repos.
|
|
167
|
+
*/
|
|
168
|
+
export function renderRepoTabs(ctx: CanvasContext) {
|
|
169
|
+
const container = document.getElementById('repoTabs');
|
|
170
|
+
if (!container) return;
|
|
171
|
+
|
|
172
|
+
if (loadedRepos.size <= 1) {
|
|
173
|
+
container.style.display = 'none';
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
container.style.display = 'flex';
|
|
178
|
+
container.innerHTML = '';
|
|
179
|
+
|
|
180
|
+
for (const [path, repo] of loadedRepos) {
|
|
181
|
+
const tab = document.createElement('button');
|
|
182
|
+
tab.className = `repo-tab ${path === _activeRepoPath ? 'repo-tab--active' : ''}`;
|
|
183
|
+
tab.textContent = repo.name;
|
|
184
|
+
tab.style.cssText = `
|
|
185
|
+
padding: 6px 14px;
|
|
186
|
+
font-size: 11px;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
border: 1px solid ${path === _activeRepoPath ? repo.color : 'rgba(255,255,255,0.08)'};
|
|
189
|
+
background: ${path === _activeRepoPath ? repo.color.replace('0.6', '0.15') : 'rgba(255,255,255,0.03)'};
|
|
190
|
+
color: ${path === _activeRepoPath ? '#e2e8f0' : 'rgba(255,255,255,0.4)'};
|
|
191
|
+
border-radius: 6px;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
transition: all 0.15s;
|
|
194
|
+
white-space: nowrap;
|
|
195
|
+
font-family: 'JetBrains Mono', monospace;
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
tab.addEventListener('click', () => {
|
|
199
|
+
_activeRepoPath = path;
|
|
200
|
+
renderRepoTabs(ctx);
|
|
201
|
+
// Re-render commit timeline for this repo
|
|
202
|
+
import('./repo').then(m => {
|
|
203
|
+
// Update XState with this repo's commits
|
|
204
|
+
ctx.actor.send({ type: 'REPO_LOADED', commits: repo.commits });
|
|
205
|
+
ctx.snap().context.repoPath = path;
|
|
206
|
+
m.renderCommitTimeline(ctx);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
tab.addEventListener('mouseenter', () => {
|
|
211
|
+
if (path !== _activeRepoPath) {
|
|
212
|
+
tab.style.borderColor = repo.color;
|
|
213
|
+
tab.style.color = '#e2e8f0';
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
tab.addEventListener('mouseleave', () => {
|
|
217
|
+
if (path !== _activeRepoPath) {
|
|
218
|
+
tab.style.borderColor = 'rgba(255,255,255,0.08)';
|
|
219
|
+
tab.style.color = 'rgba(255,255,255,0.4)';
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
container.appendChild(tab);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Zone Label ───────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
function createZoneLabel(ctx: CanvasContext, repo: LoadedRepo) {
|
|
230
|
+
if (!ctx.canvas) return;
|
|
231
|
+
|
|
232
|
+
// Remove old label if exists
|
|
233
|
+
if (repo.zoneLabel) repo.zoneLabel.remove();
|
|
234
|
+
|
|
235
|
+
const label = document.createElement('div');
|
|
236
|
+
label.className = 'repo-zone-label';
|
|
237
|
+
label.dataset.repo = repo.path;
|
|
238
|
+
label.innerHTML = `
|
|
239
|
+
<span style="
|
|
240
|
+
display: inline-flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: 8px;
|
|
243
|
+
padding: 8px 20px;
|
|
244
|
+
background: ${repo.color.replace('0.6', '0.12')};
|
|
245
|
+
border: 1px solid ${repo.color.replace('0.6', '0.3')};
|
|
246
|
+
border-radius: 10px;
|
|
247
|
+
color: #e2e8f0;
|
|
248
|
+
font-size: 32px;
|
|
249
|
+
font-weight: 700;
|
|
250
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
251
|
+
backdrop-filter: blur(8px);
|
|
252
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
253
|
+
letter-spacing: -0.02em;
|
|
254
|
+
pointer-events: auto;
|
|
255
|
+
user-select: none;
|
|
256
|
+
">
|
|
257
|
+
<span style="
|
|
258
|
+
width: 10px; height: 10px; border-radius: 50%;
|
|
259
|
+
background: ${repo.color};
|
|
260
|
+
box-shadow: 0 0 8px ${repo.color};
|
|
261
|
+
"></span>
|
|
262
|
+
${repo.name}
|
|
263
|
+
</span>
|
|
264
|
+
`;
|
|
265
|
+
|
|
266
|
+
label.style.cssText = `
|
|
267
|
+
position: absolute;
|
|
268
|
+
left: ${repo.bounds.x}px;
|
|
269
|
+
top: ${repo.bounds.y - 70}px;
|
|
270
|
+
z-index: 1;
|
|
271
|
+
pointer-events: none;
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
ctx.canvas.appendChild(label);
|
|
275
|
+
repo.zoneLabel = label;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Helpers ──────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
function repoMatchesPath(repoPath: string, filePath: string): boolean {
|
|
281
|
+
// In multi-repo mode, file paths are prefixed with the repo name
|
|
282
|
+
// or we match by checking all paths registered for this repo
|
|
283
|
+
const repo = loadedRepos.get(repoPath);
|
|
284
|
+
if (!repo || !repo.files) return false;
|
|
285
|
+
return repo.files.some(f => f.path === filePath);
|
|
286
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* New file dialog — create files directly from the canvas.
|
|
4
|
+
* Opens a dialog to enter the file path, creates the file via API,
|
|
5
|
+
* then adds a card and opens it in edit mode.
|
|
6
|
+
*/
|
|
7
|
+
import { render } from 'melina/client';
|
|
8
|
+
import type { CanvasContext } from './context';
|
|
9
|
+
import { showToast, escapeHtml } from './utils';
|
|
10
|
+
|
|
11
|
+
// ─── New File Dialog JSX ────────────────────────────────
|
|
12
|
+
function NewFileDialog({ onSubmit, onCancel, repoPath }: {
|
|
13
|
+
onSubmit: (filePath: string) => void;
|
|
14
|
+
onCancel: () => void;
|
|
15
|
+
repoPath: string;
|
|
16
|
+
}) {
|
|
17
|
+
const repoName = repoPath.replace(/\\/g, '/').split('/').filter(Boolean).pop() || repoPath;
|
|
18
|
+
return (
|
|
19
|
+
<div className="new-file-overlay" onClick={(e) => { if (e.target === e.currentTarget) onCancel(); }}>
|
|
20
|
+
<div className="new-file-dialog">
|
|
21
|
+
<div className="new-file-header">
|
|
22
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2">
|
|
23
|
+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
|
24
|
+
<polyline points="14 2 14 8 20 8" />
|
|
25
|
+
<line x1="12" y1="18" x2="12" y2="12" />
|
|
26
|
+
<line x1="9" y1="15" x2="15" y2="15" />
|
|
27
|
+
</svg>
|
|
28
|
+
<h3>Create New File</h3>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="new-file-body">
|
|
31
|
+
<label className="new-file-label">File path relative to <code>{repoName}</code></label>
|
|
32
|
+
<input
|
|
33
|
+
type="text"
|
|
34
|
+
id="newFilePathInput"
|
|
35
|
+
className="new-file-input"
|
|
36
|
+
placeholder="src/components/Button.tsx"
|
|
37
|
+
autoComplete="off"
|
|
38
|
+
spellCheck={false}
|
|
39
|
+
autofocus
|
|
40
|
+
/>
|
|
41
|
+
<div className="new-file-hint">
|
|
42
|
+
Directories will be created automatically. Use <code>/</code> as separator.
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="new-file-actions">
|
|
46
|
+
<button className="btn-ghost btn-sm" onClick={onCancel}>Cancel</button>
|
|
47
|
+
<button className="btn-primary btn-sm new-file-create-btn" id="newFileCreateBtn">
|
|
48
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
|
|
49
|
+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
|
50
|
+
<polyline points="14 2 14 8 20 8" />
|
|
51
|
+
<line x1="12" y1="18" x2="12" y2="12" />
|
|
52
|
+
<line x1="9" y1="15" x2="15" y2="15" />
|
|
53
|
+
</svg>
|
|
54
|
+
Create File
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Show New File Dialog ───────────────────────────────
|
|
63
|
+
export function showNewFileDialog(ctx: CanvasContext) {
|
|
64
|
+
const state = ctx.snap().context;
|
|
65
|
+
if (!state.repoPath) {
|
|
66
|
+
showToast('Load a repository first', 'error');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Remove existing dialog if open
|
|
71
|
+
document.getElementById('newFileOverlay')?.remove();
|
|
72
|
+
|
|
73
|
+
const container = document.createElement('div');
|
|
74
|
+
container.id = 'newFileOverlay';
|
|
75
|
+
document.body.appendChild(container);
|
|
76
|
+
|
|
77
|
+
function close() {
|
|
78
|
+
render(null, container);
|
|
79
|
+
container.remove();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function submit(filePath: string) {
|
|
83
|
+
const normalizedPath = filePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
84
|
+
if (!normalizedPath) {
|
|
85
|
+
showToast('Please enter a file path', 'error');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check for dangerous paths
|
|
90
|
+
if (normalizedPath.includes('..') || normalizedPath.startsWith('/')) {
|
|
91
|
+
showToast('Invalid path — cannot use .. or start with /', 'error');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const createBtn = document.getElementById('newFileCreateBtn');
|
|
96
|
+
if (createBtn) { createBtn.textContent = 'Creating...'; createBtn.setAttribute('disabled', 'true'); }
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Determine initial template content based on extension
|
|
100
|
+
const ext = normalizedPath.split('.').pop()?.toLowerCase() || '';
|
|
101
|
+
const fileName = normalizedPath.split('/').pop() || normalizedPath;
|
|
102
|
+
const content = getTemplateContent(fileName, ext);
|
|
103
|
+
|
|
104
|
+
const res = await fetch('/api/repo/file-save', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
path: state.repoPath,
|
|
109
|
+
filePath: normalizedPath,
|
|
110
|
+
content,
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const err = await res.text();
|
|
116
|
+
showToast(`Failed to create file: ${err}`, 'error');
|
|
117
|
+
if (createBtn) { createBtn.textContent = 'Create File'; createBtn.removeAttribute('disabled'); }
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const data = await res.json();
|
|
122
|
+
close();
|
|
123
|
+
|
|
124
|
+
showToast(`Created ${normalizedPath}`, 'success');
|
|
125
|
+
|
|
126
|
+
// Create file object and open in the modal for editing
|
|
127
|
+
const newFile = {
|
|
128
|
+
path: normalizedPath,
|
|
129
|
+
name: fileName,
|
|
130
|
+
lines: data.lines || 1,
|
|
131
|
+
content,
|
|
132
|
+
status: 'added',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Open the file modal in edit mode
|
|
136
|
+
const { openFileModal } = await import('./file-modal');
|
|
137
|
+
openFileModal(ctx, newFile);
|
|
138
|
+
|
|
139
|
+
// Switch to edit tab after a brief delay to let modal render
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
const editTab = document.querySelector('.modal-tab[data-view="edit"]') as HTMLElement;
|
|
142
|
+
if (editTab) editTab.click();
|
|
143
|
+
}, 200);
|
|
144
|
+
|
|
145
|
+
} catch (err: any) {
|
|
146
|
+
showToast(`Error: ${err.message}`, 'error');
|
|
147
|
+
if (createBtn) { createBtn.textContent = 'Create File'; createBtn.removeAttribute('disabled'); }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
render(
|
|
152
|
+
<NewFileDialog
|
|
153
|
+
onSubmit={submit}
|
|
154
|
+
onCancel={close}
|
|
155
|
+
repoPath={state.repoPath}
|
|
156
|
+
/>,
|
|
157
|
+
container
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Focus and wire up the input
|
|
161
|
+
requestAnimationFrame(() => {
|
|
162
|
+
const input = document.getElementById('newFilePathInput') as HTMLInputElement;
|
|
163
|
+
if (input) {
|
|
164
|
+
input.focus();
|
|
165
|
+
input.addEventListener('keydown', (e) => {
|
|
166
|
+
if (e.key === 'Enter') {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
submit(input.value.trim());
|
|
169
|
+
}
|
|
170
|
+
if (e.key === 'Escape') {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
close();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Also wire create button click
|
|
177
|
+
const createBtn = document.getElementById('newFileCreateBtn');
|
|
178
|
+
if (createBtn) {
|
|
179
|
+
createBtn.addEventListener('click', () => {
|
|
180
|
+
const input = document.getElementById('newFilePathInput') as HTMLInputElement;
|
|
181
|
+
if (input) submit(input.value.trim());
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Template content for new files ─────────────────────
|
|
188
|
+
function getTemplateContent(fileName: string, ext: string): string {
|
|
189
|
+
const baseName = fileName.replace(/\.[^.]+$/, '');
|
|
190
|
+
|
|
191
|
+
switch (ext) {
|
|
192
|
+
case 'ts':
|
|
193
|
+
case 'tsx':
|
|
194
|
+
return `/**\n * ${baseName}\n */\n\nexport function ${toCamelCase(baseName)}() {\n // TODO: implement\n}\n`;
|
|
195
|
+
case 'js':
|
|
196
|
+
case 'jsx':
|
|
197
|
+
return `/**\n * ${baseName}\n */\n\nexport function ${toCamelCase(baseName)}() {\n // TODO: implement\n}\n`;
|
|
198
|
+
case 'py':
|
|
199
|
+
return `"""${baseName}"""\n\n\ndef ${toSnakeCase(baseName)}():\n \"\"\"TODO: implement\"\"\"\n pass\n`;
|
|
200
|
+
case 'css':
|
|
201
|
+
return `/* ${baseName} styles */\n\n`;
|
|
202
|
+
case 'html':
|
|
203
|
+
return `<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>${baseName}</title>\n</head>\n<body>\n \n</body>\n</html>\n`;
|
|
204
|
+
case 'json':
|
|
205
|
+
return `{\n \n}\n`;
|
|
206
|
+
case 'md':
|
|
207
|
+
return `# ${baseName}\n\n`;
|
|
208
|
+
case 'yaml':
|
|
209
|
+
case 'yml':
|
|
210
|
+
return `# ${baseName}\n\n`;
|
|
211
|
+
case 'toml':
|
|
212
|
+
return `# ${baseName}\n\n`;
|
|
213
|
+
default:
|
|
214
|
+
return ``;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function toCamelCase(str: string): string {
|
|
219
|
+
return str
|
|
220
|
+
.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
|
|
221
|
+
.replace(/^[A-Z]/, c => c.toLowerCase());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function toSnakeCase(str: string): string {
|
|
225
|
+
return str
|
|
226
|
+
.replace(/([A-Z])/g, '_$1')
|
|
227
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
228
|
+
.replace(/^_+|_+$/g, '')
|
|
229
|
+
.toLowerCase();
|
|
230
|
+
}
|