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,128 @@
1
+ /**
2
+ * Status Bar — VS Code-style bottom bar for GitMaps
3
+ *
4
+ * Shows: zoom %, file count, selected count, repo name, mode.
5
+ * Updates reactively via exported update functions.
6
+ */
7
+
8
+ import type { CanvasContext } from './context';
9
+
10
+ let bar: HTMLElement | null = null;
11
+ let ctx: CanvasContext | null = null;
12
+
13
+ // Cached state for efficient updates
14
+ let _zoom = 1;
15
+ let _fileCount = 0;
16
+ let _selectedCount = 0;
17
+ let _repoName = '';
18
+ let _mode = 'Simple';
19
+ let _commitHash = '';
20
+
21
+ function createBar(): HTMLElement {
22
+ const el = document.createElement('div');
23
+ el.id = 'status-bar';
24
+ el.innerHTML = `
25
+ <div class="sb-left">
26
+ <span class="sb-item sb-repo" id="sbRepo" title="Current repository"></span>
27
+ <span class="sb-item sb-commit" id="sbCommit" title="Current commit"></span>
28
+ </div>
29
+ <div class="sb-right">
30
+ <span class="sb-item sb-mode" id="sbMode" title="Interaction mode"></span>
31
+ <span class="sb-item sb-selected" id="sbSelected" title="Selected cards"></span>
32
+ <span class="sb-item sb-files" id="sbFiles" title="Total files on canvas"></span>
33
+ <span class="sb-item sb-zoom" id="sbZoom" title="Zoom level (scroll to zoom)"></span>
34
+ </div>
35
+ `;
36
+ return el;
37
+ }
38
+
39
+ function render() {
40
+ if (!bar) return;
41
+
42
+ const repoEl = bar.querySelector('#sbRepo') as HTMLElement;
43
+ const commitEl = bar.querySelector('#sbCommit') as HTMLElement;
44
+ const modeEl = bar.querySelector('#sbMode') as HTMLElement;
45
+ const selectedEl = bar.querySelector('#sbSelected') as HTMLElement;
46
+ const filesEl = bar.querySelector('#sbFiles') as HTMLElement;
47
+ const zoomEl = bar.querySelector('#sbZoom') as HTMLElement;
48
+
49
+ if (repoEl) repoEl.textContent = _repoName ? `📂 ${_repoName}` : '';
50
+ if (commitEl) commitEl.textContent = _commitHash ? `⊙ ${_commitHash.substring(0, 7)}` : '';
51
+ if (modeEl) {
52
+ modeEl.textContent = `${_mode === 'Advanced' ? '🎯' : '✋'} ${_mode}`;
53
+ modeEl.className = `sb-item sb-mode sb-mode--${_mode.toLowerCase()}`;
54
+ }
55
+ if (selectedEl) {
56
+ selectedEl.textContent = _selectedCount > 0 ? `☑ ${_selectedCount} selected` : '';
57
+ selectedEl.style.display = _selectedCount > 0 ? '' : 'none';
58
+ }
59
+ if (filesEl) filesEl.textContent = `📄 ${_fileCount} files`;
60
+ if (zoomEl) zoomEl.textContent = `🔍 ${Math.round(_zoom * 100)}%`;
61
+ }
62
+
63
+ // ─── Public API ──────────────────────────────────────────
64
+
65
+ export function initStatusBar(context: CanvasContext) {
66
+ ctx = context;
67
+ bar = createBar();
68
+
69
+ // Insert after canvas-area
70
+ const canvasArea = document.querySelector('.canvas-area');
71
+ if (canvasArea) {
72
+ canvasArea.parentElement?.insertBefore(bar, canvasArea.nextSibling);
73
+ } else {
74
+ document.body.appendChild(bar);
75
+ }
76
+
77
+ // Initial sync
78
+ const state = ctx.snap().context;
79
+ _zoom = state.zoom || 1;
80
+ _repoName = (state.repoPath || '').split('/').pop() || '';
81
+ _fileCount = ctx.fileCards.size;
82
+ _mode = state.mode === 'advanced' ? 'Advanced' : 'Simple';
83
+ _commitHash = state.currentCommitHash || '';
84
+ render();
85
+ }
86
+
87
+ export function updateStatusBarZoom(zoom: number) {
88
+ if (Math.round(zoom * 100) === Math.round(_zoom * 100)) return;
89
+ _zoom = zoom;
90
+ const el = bar?.querySelector('#sbZoom') as HTMLElement;
91
+ if (el) el.textContent = `🔍 ${Math.round(zoom * 100)}%`;
92
+ }
93
+
94
+ export function updateStatusBarFiles(count: number) {
95
+ _fileCount = count;
96
+ const el = bar?.querySelector('#sbFiles') as HTMLElement;
97
+ if (el) el.textContent = `📄 ${count} files`;
98
+ }
99
+
100
+ export function updateStatusBarSelected(count: number) {
101
+ _selectedCount = count;
102
+ const el = bar?.querySelector('#sbSelected') as HTMLElement;
103
+ if (el) {
104
+ el.textContent = count > 0 ? `☑ ${count} selected` : '';
105
+ el.style.display = count > 0 ? '' : 'none';
106
+ }
107
+ }
108
+
109
+ export function updateStatusBarRepo(repoPath: string) {
110
+ _repoName = repoPath.split('/').pop() || repoPath.split('\\').pop() || '';
111
+ const el = bar?.querySelector('#sbRepo') as HTMLElement;
112
+ if (el) el.textContent = `📂 ${_repoName}`;
113
+ }
114
+
115
+ export function updateStatusBarCommit(hash: string) {
116
+ _commitHash = hash;
117
+ const el = bar?.querySelector('#sbCommit') as HTMLElement;
118
+ if (el) el.textContent = hash ? `⊙ ${hash.substring(0, 7)}` : '';
119
+ }
120
+
121
+ export function updateStatusBarMode(mode: string) {
122
+ _mode = mode;
123
+ const el = bar?.querySelector('#sbMode') as HTMLElement;
124
+ if (el) {
125
+ el.textContent = `${mode === 'Advanced' ? '🎯' : '✋'} ${mode}`;
126
+ el.className = `sb-item sb-mode sb-mode--${mode.toLowerCase()}`;
127
+ }
128
+ }
@@ -0,0 +1,212 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Symbol outline panel — extracts functions, classes, interfaces, types,
4
+ * and exports from file content and renders them as a navigable list.
5
+ */
6
+
7
+ export interface SymbolEntry {
8
+ name: string;
9
+ kind: 'function' | 'class' | 'interface' | 'type' | 'const' | 'variable' | 'enum' | 'method' | 'export';
10
+ line: number;
11
+ indent: number; // nesting depth
12
+ }
13
+
14
+ // ─── Symbol extraction patterns ─────────────────────
15
+ const PATTERNS: Array<{ regex: RegExp; kind: SymbolEntry['kind'] }> = [
16
+ // JS/TS: export function / async function / function
17
+ { regex: /^(\s*)(?:export\s+)?(?:async\s+)?function\s+(\w+)/gm, kind: 'function' },
18
+ // JS/TS: arrow function assigned to const/let/var
19
+ { regex: /^(\s*)(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/gm, kind: 'function' },
20
+ // JS/TS: class
21
+ { regex: /^(\s*)(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/gm, kind: 'class' },
22
+ // TS: interface
23
+ { regex: /^(\s*)(?:export\s+)?interface\s+(\w+)/gm, kind: 'interface' },
24
+ // TS: type alias
25
+ { regex: /^(\s*)(?:export\s+)?type\s+(\w+)\s*[=<]/gm, kind: 'type' },
26
+ // TS: enum
27
+ { regex: /^(\s*)(?:export\s+)?enum\s+(\w+)/gm, kind: 'enum' },
28
+ // JS/TS: exported const (non-arrow)
29
+ { regex: /^(\s*)export\s+(?:const|let|var)\s+(\w+)\s*[=:]/gm, kind: 'const' },
30
+ // Class methods
31
+ { regex: /^(\s+)(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/gm, kind: 'method' },
32
+ // Python: def / class
33
+ { regex: /^(\s*)def\s+(\w+)/gm, kind: 'function' },
34
+ { regex: /^(\s*)class\s+(\w+)/gm, kind: 'class' },
35
+ ];
36
+
37
+ /**
38
+ * Extract symbols from file content.
39
+ */
40
+ export function extractSymbols(content: string, fileName: string): SymbolEntry[] {
41
+ const ext = fileName.split('.').pop()?.toLowerCase() || '';
42
+ const isPython = ext === 'py';
43
+ const isCSS = ext === 'css' || ext === 'scss' || ext === 'less';
44
+ const isJSON = ext === 'json';
45
+ const isMarkdown = ext === 'md';
46
+
47
+ // CSS: extract selectors/rules
48
+ if (isCSS) return extractCSSSymbols(content);
49
+ // JSON: extract top-level keys
50
+ if (isJSON) return extractJSONSymbols(content);
51
+ // Markdown: extract headings
52
+ if (isMarkdown) return extractMarkdownSymbols(content);
53
+
54
+ const symbols: SymbolEntry[] = [];
55
+ const lines = content.split('\n');
56
+
57
+ for (const pattern of PATTERNS) {
58
+ // Skip Python-specific patterns for non-Python, and vice versa
59
+ if (isPython && pattern.kind === 'interface') continue;
60
+ if (isPython && pattern.kind === 'type') continue;
61
+ if (isPython && pattern.kind === 'enum') continue;
62
+
63
+ pattern.regex.lastIndex = 0;
64
+ let match;
65
+ while ((match = pattern.regex.exec(content)) !== null) {
66
+ const indent = (match[1] || '').length;
67
+ const name = match[2];
68
+ if (!name || name === 'if' || name === 'for' || name === 'while' || name === 'switch' || name === 'catch' || name === 'return') continue;
69
+
70
+ // Calculate line number
71
+ const beforeMatch = content.slice(0, match.index);
72
+ const line = beforeMatch.split('\n').length;
73
+
74
+ // Skip methods that are too deeply nested (likely control flow)
75
+ if (pattern.kind === 'method' && indent < 2) continue;
76
+
77
+ symbols.push({ name, kind: pattern.kind, line, indent: Math.floor(indent / 2) });
78
+ }
79
+ }
80
+
81
+ // Deduplicate (same name + line)
82
+ const seen = new Set<string>();
83
+ return symbols
84
+ .filter(s => {
85
+ const key = `${s.name}:${s.line}`;
86
+ if (seen.has(key)) return false;
87
+ seen.add(key);
88
+ return true;
89
+ })
90
+ .sort((a, b) => a.line - b.line);
91
+ }
92
+
93
+ function extractCSSSymbols(content: string): SymbolEntry[] {
94
+ const symbols: SymbolEntry[] = [];
95
+ const regex = /^([.#@][^\s{]+)\s*\{/gm;
96
+ let match;
97
+ while ((match = regex.exec(content)) !== null) {
98
+ const beforeMatch = content.slice(0, match.index);
99
+ const line = beforeMatch.split('\n').length;
100
+ symbols.push({ name: match[1], kind: 'class', line, indent: 0 });
101
+ }
102
+ return symbols.sort((a, b) => a.line - b.line);
103
+ }
104
+
105
+ function extractJSONSymbols(content: string): SymbolEntry[] {
106
+ const symbols: SymbolEntry[] = [];
107
+ const regex = /^(\s*)"(\w+)"\s*:/gm;
108
+ let match;
109
+ while ((match = regex.exec(content)) !== null) {
110
+ const indent = (match[1] || '').length;
111
+ if (indent > 4) continue; // Only top-level and one level deep
112
+ const beforeMatch = content.slice(0, match.index);
113
+ const line = beforeMatch.split('\n').length;
114
+ symbols.push({ name: match[2], kind: 'const', line, indent: Math.floor(indent / 2) });
115
+ }
116
+ return symbols.sort((a, b) => a.line - b.line);
117
+ }
118
+
119
+ function extractMarkdownSymbols(content: string): SymbolEntry[] {
120
+ const symbols: SymbolEntry[] = [];
121
+ const regex = /^(#{1,6})\s+(.+)$/gm;
122
+ let match;
123
+ while ((match = regex.exec(content)) !== null) {
124
+ const level = match[1].length;
125
+ const beforeMatch = content.slice(0, match.index);
126
+ const line = beforeMatch.split('\n').length;
127
+ symbols.push({ name: match[2].trim(), kind: 'export', line, indent: level - 1 });
128
+ }
129
+ return symbols.sort((a, b) => a.line - b.line);
130
+ }
131
+
132
+ // ─── Symbol icons ───────────────────────────────────
133
+ const SYMBOL_ICONS: Record<SymbolEntry['kind'], string> = {
134
+ function: 'ƒ',
135
+ class: '◆',
136
+ interface: '◇',
137
+ type: 'T',
138
+ const: '●',
139
+ variable: '○',
140
+ enum: 'E',
141
+ method: 'μ',
142
+ export: '→',
143
+ };
144
+
145
+ const SYMBOL_COLORS: Record<SymbolEntry['kind'], string> = {
146
+ function: '#c4b5fd',
147
+ class: '#86efac',
148
+ interface: '#67e8f9',
149
+ type: '#fde68a',
150
+ const: '#93c5fd',
151
+ variable: '#9ca3af',
152
+ enum: '#f9a8d4',
153
+ method: '#a5b4fc',
154
+ export: '#fdba74',
155
+ };
156
+
157
+ /**
158
+ * Render the symbol outline panel.
159
+ */
160
+ export function renderSymbolOutline(
161
+ container: HTMLElement,
162
+ content: string,
163
+ fileName: string,
164
+ onSymbolClick: (line: number) => void
165
+ ) {
166
+ const symbols = extractSymbols(content, fileName);
167
+
168
+ if (symbols.length === 0) {
169
+ container.innerHTML = '<div class="outline-empty">No symbols found</div>';
170
+ return;
171
+ }
172
+
173
+ container.innerHTML = '';
174
+
175
+ // Header
176
+ const header = document.createElement('div');
177
+ header.className = 'outline-header';
178
+ header.innerHTML = `<span class="outline-title">Outline</span><span class="outline-count">${symbols.length}</span>`;
179
+ container.appendChild(header);
180
+
181
+ // Symbol list
182
+ const list = document.createElement('div');
183
+ list.className = 'outline-list';
184
+
185
+ for (const sym of symbols) {
186
+ const item = document.createElement('button');
187
+ item.className = 'outline-item';
188
+ item.style.paddingLeft = `${12 + sym.indent * 14}px`;
189
+ item.title = `${sym.kind}: ${sym.name} (line ${sym.line})`;
190
+
191
+ const icon = document.createElement('span');
192
+ icon.className = 'outline-icon';
193
+ icon.textContent = SYMBOL_ICONS[sym.kind];
194
+ icon.style.color = SYMBOL_COLORS[sym.kind];
195
+ item.appendChild(icon);
196
+
197
+ const name = document.createElement('span');
198
+ name.className = 'outline-name';
199
+ name.textContent = sym.name;
200
+ item.appendChild(name);
201
+
202
+ const lineNo = document.createElement('span');
203
+ lineNo.className = 'outline-lineno';
204
+ lineNo.textContent = `:${sym.line}`;
205
+ item.appendChild(lineNo);
206
+
207
+ item.addEventListener('click', () => onSymbolClick(sym.line));
208
+ list.appendChild(item);
209
+ }
210
+
211
+ container.appendChild(list);
212
+ }
@@ -0,0 +1,177 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Syntax highlighting & diff HTML builders.
4
+ */
5
+ import { escapeHtml } from './utils';
6
+
7
+ // ─── Check if position is inside a string literal ────────
8
+ export function isInsideString(line: string, idx: number): boolean {
9
+ let inSingle = false, inDouble = false;
10
+ for (let i = 0; i < idx; i++) {
11
+ if (line[i] === "'" && !inDouble) inSingle = !inSingle;
12
+ if (line[i] === '"' && !inSingle) inDouble = !inDouble;
13
+ }
14
+ return inSingle || inDouble;
15
+ }
16
+
17
+ // ─── Syntax-highlighted HTML from file content ───────────
18
+ export function highlightSyntax(content: string, ext: string): string {
19
+ const lines = content.split('\n');
20
+ const isJSLike = ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'].includes(ext);
21
+ const isPyLike = ['py', 'pyw'].includes(ext);
22
+ const isCSSLike = ['css', 'scss', 'less', 'sass'].includes(ext);
23
+ const isHTMLLike = ['html', 'htm', 'xml', 'svg', 'jsx', 'tsx'].includes(ext);
24
+ const isRustGo = ['rs', 'go'].includes(ext);
25
+ const isJSON = ext === 'json';
26
+ const isMD = ['md', 'mdx'].includes(ext);
27
+ const isYAML = ['yml', 'yaml', 'toml'].includes(ext);
28
+ const isShell = ['sh', 'bash', 'zsh', 'fish', 'bat', 'cmd', 'ps1'].includes(ext);
29
+ const isSQL = ext === 'sql';
30
+
31
+ return lines.map((line, i) => {
32
+ const lineNum = `<span class="line-num">${String(i + 1).padStart(4, ' ')}</span>`;
33
+ let highlighted = escapeHtml(line);
34
+
35
+ if (isJSON) {
36
+ highlighted = highlighted
37
+ .replace(/(&quot;[^&]*?&quot;)\s*:/g, '<span style="color:#7dd3fc">$1</span>:')
38
+ .replace(/:(\s*)(&quot;[^&]*?&quot;)/g, ':$1<span style="color:#86efac">$2</span>')
39
+ .replace(/\b(\d+(?:\.\d+)?)\b/g, '<span style="color:#fbbf24">$1</span>')
40
+ .replace(/\b(true|false|null)\b/g, '<span style="color:#c084fc">$1</span>');
41
+ } else if (isCSSLike) {
42
+ highlighted = highlighted
43
+ .replace(/(\/\*.*?\*\/)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
44
+ .replace(/(\/\/.*$)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
45
+ .replace(/([.#][\w-]+)/g, '<span style="color:#7dd3fc">$1</span>')
46
+ .replace(/(:\s*)([^;{}]+)(;)/g, '$1<span style="color:#86efac">$2</span>$3')
47
+ .replace(/\b(\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|s|ms)?)\b/g, '<span style="color:#fbbf24">$1</span>');
48
+ } else if (isMD) {
49
+ highlighted = highlighted
50
+ .replace(/^(#{1,6}\s.*)$/, '<span style="color:#c084fc;font-weight:600">$1</span>')
51
+ .replace(/(\*\*[^*]+\*\*)/g, '<span style="color:#f0abfc;font-weight:600">$1</span>')
52
+ .replace(/(\*[^*]+\*)/g, '<span style="color:#f0abfc;font-style:italic">$1</span>')
53
+ .replace(/(`[^`]+`)/g, '<span style="color:#86efac;background:rgba(134,239,172,0.08);padding:1px 3px;border-radius:2px">$1</span>')
54
+ .replace(/(\[[^\]]+\]\([^)]+\))/g, '<span style="color:#7dd3fc;text-decoration:underline">$1</span>')
55
+ .replace(/^(\s*[-*+]\s)/g, '<span style="color:#fbbf24">$1</span>')
56
+ .replace(/^(\s*\d+\.\s)/g, '<span style="color:#fbbf24">$1</span>')
57
+ .replace(/^(&gt;\s.*)/g, '<span style="color:#6b7280;font-style:italic;border-left:2px solid #6b7280;padding-left:8px">$1</span>');
58
+ } else if (isYAML) {
59
+ highlighted = highlighted
60
+ .replace(/(#.*$)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
61
+ .replace(/^(\s*[\w.-]+)(\s*[:=])/g, '<span style="color:#7dd3fc">$1</span>$2')
62
+ .replace(/:\s*(&quot;[^&]*?&quot;)/g, ': <span style="color:#86efac">$1</span>')
63
+ .replace(/:\s*(&amp;#x27;[^&]*?&amp;#x27;)/g, ': <span style="color:#86efac">$1</span>')
64
+ .replace(/\b(true|false|yes|no|null|~)\b/gi, '<span style="color:#c084fc">$1</span>')
65
+ .replace(/\b(\d+(?:\.\d+)?)\b/g, '<span style="color:#fbbf24">$1</span>')
66
+ .replace(/^(\s*-\s)/g, '<span style="color:#fbbf24">$1</span>')
67
+ .replace(/([\w.-]+\])/g, '<span style="color:#c084fc;font-weight:500">$1</span>');
68
+ } else if (isShell) {
69
+ highlighted = highlighted
70
+ .replace(/(#.*$)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
71
+ .replace(/(\$[\w{][^}\s]*}?)/g, '<span style="color:#7dd3fc">$1</span>')
72
+ .replace(/(&quot;[^&]*?&quot;)/g, '<span style="color:#86efac">$1</span>')
73
+ .replace(/(&amp;#x27;[^&]*?&amp;#x27;)/g, '<span style="color:#86efac">$1</span>');
74
+ const shKeywords = 'if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|echo|export|source|local|set|unset|cd|mkdir|rm|cp|mv|cat|grep|sed|awk|chmod|chown';
75
+ highlighted = highlighted.replace(
76
+ new RegExp(`\\b(${shKeywords})\\b`, 'g'),
77
+ '<span style="color:#c084fc">$1</span>'
78
+ );
79
+ } else if (isSQL) {
80
+ const sqlKeywords = 'SELECT|FROM|WHERE|INSERT|INTO|UPDATE|SET|DELETE|CREATE|DROP|ALTER|TABLE|INDEX|VIEW|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AND|OR|NOT|IN|EXISTS|BETWEEN|LIKE|ORDER|BY|GROUP|HAVING|LIMIT|OFFSET|UNION|ALL|AS|IS|NULL|PRIMARY|KEY|FOREIGN|REFERENCES|UNIQUE|DEFAULT|CHECK|CONSTRAINT|VALUES|COUNT|SUM|AVG|MIN|MAX|DISTINCT';
81
+ highlighted = highlighted
82
+ .replace(/(--.*$)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
83
+ .replace(/(&amp;#x27;[^&]*?&amp;#x27;)/g, '<span style="color:#86efac">$1</span>');
84
+ highlighted = highlighted.replace(
85
+ new RegExp(`\\b(${sqlKeywords})\\b`, 'gi'),
86
+ '<span style="color:#c084fc">$1</span>'
87
+ );
88
+ highlighted = highlighted.replace(/\b(\d+(?:\.\d+)?)\b/g, '<span style="color:#fbbf24">$1</span>');
89
+ } else if (isJSLike || isPyLike || isRustGo) {
90
+ const commentIdx = highlighted.indexOf('//');
91
+ const hashIdx = isPyLike ? highlighted.indexOf('#') : -1;
92
+ if (commentIdx >= 0 && !isInsideString(line, line.indexOf('//'))) {
93
+ const before = highlighted.substring(0, commentIdx);
94
+ const comment = highlighted.substring(commentIdx);
95
+ highlighted = before + `<span style="color:#6b7280;font-style:italic">${comment}</span>`;
96
+ } else if (hashIdx >= 0 && isPyLike && !isInsideString(line, line.indexOf('#'))) {
97
+ const before = highlighted.substring(0, hashIdx);
98
+ const comment = highlighted.substring(hashIdx);
99
+ highlighted = before + `<span style="color:#6b7280;font-style:italic">${comment}</span>`;
100
+ }
101
+
102
+ highlighted = highlighted
103
+ .replace(/(&quot;(?:[^&]|&(?!quot;))*?&quot;)/g, '<span style="color:#86efac">$1</span>')
104
+ .replace(/(&amp;#x27;(?:[^&]|&(?!#x27;))*?&amp;#x27;)/g, '<span style="color:#86efac">$1</span>')
105
+ .replace(/(`[^`]*?`)/g, '<span style="color:#86efac">$1</span>');
106
+
107
+ if (isJSLike || isPyLike) {
108
+ highlighted = highlighted.replace(
109
+ /(@[\w.]+)/g,
110
+ '<span style="color:#fb923c">$1</span>'
111
+ );
112
+ }
113
+
114
+ const keywords = isJSLike
115
+ ? 'const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|try|catch|finally|throw|new|delete|typeof|instanceof|in|of|class|extends|super|import|export|from|default|async|await|yield|this|null|undefined|true|false|void|static|get|set|type|interface|enum|as|is'
116
+ : isPyLike
117
+ ? 'def|class|if|elif|else|for|while|return|import|from|as|try|except|finally|raise|with|yield|lambda|pass|break|continue|and|or|not|in|is|True|False|None|self|async|await'
118
+ : 'fn|let|mut|const|if|else|for|while|loop|match|return|struct|enum|impl|trait|use|mod|pub|self|Self|true|false|async|await|type|func|go|defer|chan|select|range|package|import|var';
119
+ highlighted = highlighted.replace(
120
+ new RegExp(`\\b(${keywords})\\b`, 'g'),
121
+ '<span style="color:#c084fc">$1</span>'
122
+ );
123
+
124
+ highlighted = highlighted.replace(/\b(\d+(?:\.\d+)?(?:e[+-]?\d+)?)\b/gi, '<span style="color:#fbbf24">$1</span>');
125
+ highlighted = highlighted.replace(/\b([A-Z][a-zA-Z0-9_]*)\b/g, '<span style="color:#7dd3fc">$1</span>');
126
+ } else if (isHTMLLike) {
127
+ highlighted = highlighted
128
+ .replace(/(&lt;\/?[\w-]+)/g, '<span style="color:#c084fc">$1</span>')
129
+ .replace(/([\w-]+)(=)/g, '<span style="color:#7dd3fc">$1</span>$2')
130
+ .replace(/(&quot;[^&]*?&quot;)/g, '<span style="color:#86efac">$1</span>');
131
+ }
132
+
133
+ return `<span class="diff-line">${lineNum}${highlighted}</span>`;
134
+ }).join('\n');
135
+ }
136
+
137
+ // ─── Build diff HTML for the modal view ──────────────────
138
+ export function buildModalDiffHTML(file: any): string {
139
+ let html = '';
140
+
141
+ if (file.status === 'added' && file.content) {
142
+ const lines = file.content.split('\n');
143
+ html = lines.map((line: string, i: number) =>
144
+ `<span class="diff-line diff-add" data-line="${i + 1}"><span class="line-num">${String(i + 1).padStart(4, ' ')}</span>+ ${escapeHtml(line)}</span>`
145
+ ).join('\n');
146
+ } else if (file.status === 'deleted' && file.content) {
147
+ const lines = file.content.split('\n');
148
+ html = lines.map((line: string, i: number) =>
149
+ `<span class="diff-line diff-del" data-line="${i + 1}"><span class="line-num">${String(i + 1).padStart(4, ' ')}</span>- ${escapeHtml(line)}</span>`
150
+ ).join('\n');
151
+ } else if (file.status === 'modified' && file.hunks?.length > 0) {
152
+ const hunkSections = file.hunks.map((hunk: any) => {
153
+ const header = `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@${hunk.context ? ' ' + escapeHtml(hunk.context) : ''}`;
154
+ let oldLine = hunk.oldStart;
155
+ let newLine = hunk.newStart;
156
+
157
+ const lineItems = hunk.lines.map((l: any) => {
158
+ if (l.type === 'add') {
159
+ const ln = newLine++;
160
+ return `<span class="diff-line diff-add" data-line="${ln}"><span class="line-num">${String(ln).padStart(4, ' ')}</span>+ ${escapeHtml(l.content)}</span>`;
161
+ } else if (l.type === 'del') {
162
+ const ln = oldLine++;
163
+ return `<span class="diff-line diff-del" data-line="${ln}"><span class="line-num">${String(ln).padStart(4, ' ')}</span>- ${escapeHtml(l.content)}</span>`;
164
+ } else {
165
+ oldLine++; newLine++;
166
+ const ln = newLine - 1;
167
+ return `<span class="diff-line diff-ctx" data-line="${ln}"><span class="line-num">${String(ln).padStart(4, ' ')}</span> ${escapeHtml(l.content)}</span>`;
168
+ }
169
+ }).join('\n');
170
+
171
+ return `<span class="diff-line diff-hunk-header-line" style="color:var(--accent-tertiary);background:rgba(124,58,237,0.08);font-weight:500;">${header}</span>\n${lineItems}`;
172
+ });
173
+ html = hunkSections.join('\n\n');
174
+ }
175
+
176
+ return html || '<span style="color: var(--text-muted); font-style: italic;">No diff available</span>';
177
+ }