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,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
+ }