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,238 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Tab diff — side-by-side diff viewer for comparing two open tabs.
4
+ * Uses a simple LCS-based diff algorithm to highlight additions/removals.
5
+ */
6
+ import { getOpenTabs, getActiveTabIndex } from './file-tabs';
7
+
8
+ // ─── Simple LCS diff algorithm ──────────────────────
9
+
10
+ interface DiffLine {
11
+ type: 'same' | 'add' | 'remove';
12
+ content: string;
13
+ lineNo: number;
14
+ }
15
+
16
+ function computeDiff(a: string[], b: string[]): { left: DiffLine[]; right: DiffLine[] } {
17
+ // Build LCS table
18
+ const m = a.length, n = b.length;
19
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
20
+
21
+ for (let i = 1; i <= m; i++) {
22
+ for (let j = 1; j <= n; j++) {
23
+ dp[i][j] = a[i - 1] === b[j - 1]
24
+ ? dp[i - 1][j - 1] + 1
25
+ : Math.max(dp[i - 1][j], dp[i][j - 1]);
26
+ }
27
+ }
28
+
29
+ // Backtrack to find diff
30
+ const left: DiffLine[] = [];
31
+ const right: DiffLine[] = [];
32
+ let i = m, j = n;
33
+
34
+ const resultPairs: Array<{ type: 'same' | 'add' | 'remove'; aLine?: string; bLine?: string; aNo?: number; bNo?: number }> = [];
35
+
36
+ while (i > 0 || j > 0) {
37
+ if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
38
+ resultPairs.unshift({ type: 'same', aLine: a[i - 1], bLine: b[j - 1], aNo: i, bNo: j });
39
+ i--; j--;
40
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
41
+ resultPairs.unshift({ type: 'add', bLine: b[j - 1], bNo: j });
42
+ j--;
43
+ } else {
44
+ resultPairs.unshift({ type: 'remove', aLine: a[i - 1], aNo: i });
45
+ i--;
46
+ }
47
+ }
48
+
49
+ for (const pair of resultPairs) {
50
+ if (pair.type === 'same') {
51
+ left.push({ type: 'same', content: pair.aLine!, lineNo: pair.aNo! });
52
+ right.push({ type: 'same', content: pair.bLine!, lineNo: pair.bNo! });
53
+ } else if (pair.type === 'remove') {
54
+ left.push({ type: 'remove', content: pair.aLine!, lineNo: pair.aNo! });
55
+ right.push({ type: 'same', content: '', lineNo: 0 }); // spacer
56
+ } else {
57
+ left.push({ type: 'same', content: '', lineNo: 0 }); // spacer
58
+ right.push({ type: 'add', content: pair.bLine!, lineNo: pair.bNo! });
59
+ }
60
+ }
61
+
62
+ return { left, right };
63
+ }
64
+
65
+ // ─── Diff panel rendering ───────────────────────────
66
+
67
+ let diffOverlay: HTMLElement | null = null;
68
+
69
+ function escapeHtml(s: string): string {
70
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
71
+ }
72
+
73
+ function renderDiffPanel(leftContent: string, rightContent: string, leftName: string, rightName: string) {
74
+ closeDiffPanel();
75
+
76
+ const leftLines = leftContent.split('\n');
77
+ const rightLines = rightContent.split('\n');
78
+ const { left, right } = computeDiff(leftLines, rightLines);
79
+
80
+ // Count changes
81
+ const adds = right.filter(l => l.type === 'add').length;
82
+ const removes = left.filter(l => l.type === 'remove').length;
83
+
84
+ const overlay = document.createElement('div');
85
+ overlay.className = 'tab-diff-overlay';
86
+ overlay.innerHTML = `
87
+ <div class="tab-diff-container">
88
+ <div class="tab-diff-header">
89
+ <div class="tab-diff-title">
90
+ <span class="tab-diff-icon">⇄</span>
91
+ Comparing Files
92
+ <span class="tab-diff-stats">
93
+ <span class="tab-diff-stat-add">+${adds}</span>
94
+ <span class="tab-diff-stat-rm">−${removes}</span>
95
+ </span>
96
+ </div>
97
+ <button class="tab-diff-close" title="Close diff (Esc)">✕</button>
98
+ </div>
99
+ <div class="tab-diff-names">
100
+ <div class="tab-diff-name tab-diff-name-left">📄 ${escapeHtml(leftName)}</div>
101
+ <div class="tab-diff-name tab-diff-name-right">📄 ${escapeHtml(rightName)}</div>
102
+ </div>
103
+ <div class="tab-diff-body">
104
+ <div class="tab-diff-pane tab-diff-left"></div>
105
+ <div class="tab-diff-divider"></div>
106
+ <div class="tab-diff-pane tab-diff-right"></div>
107
+ </div>
108
+ </div>
109
+ `;
110
+
111
+ document.body.appendChild(overlay);
112
+ diffOverlay = overlay;
113
+
114
+ const leftPane = overlay.querySelector('.tab-diff-left')!;
115
+ const rightPane = overlay.querySelector('.tab-diff-right')!;
116
+
117
+ // Render lines
118
+ const renderLines = (pane: Element, lines: DiffLine[]) => {
119
+ const frag = document.createDocumentFragment();
120
+ for (const line of lines) {
121
+ const el = document.createElement('div');
122
+ el.className = `tab-diff-line tab-diff-${line.type}`;
123
+
124
+ const gutter = document.createElement('span');
125
+ gutter.className = 'tab-diff-gutter';
126
+ gutter.textContent = line.lineNo > 0 ? String(line.lineNo) : ' ';
127
+ el.appendChild(gutter);
128
+
129
+ const marker = document.createElement('span');
130
+ marker.className = 'tab-diff-marker';
131
+ marker.textContent = line.type === 'add' ? '+' : line.type === 'remove' ? '−' : ' ';
132
+ el.appendChild(marker);
133
+
134
+ const code = document.createElement('span');
135
+ code.className = 'tab-diff-code';
136
+ code.textContent = line.content;
137
+ el.appendChild(code);
138
+
139
+ frag.appendChild(el);
140
+ }
141
+ pane.appendChild(frag);
142
+ };
143
+
144
+ renderLines(leftPane, left);
145
+ renderLines(rightPane, right);
146
+
147
+ // Sync scroll between panes
148
+ let syncing = false;
149
+ const syncScroll = (source: Element, target: Element) => {
150
+ if (syncing) return;
151
+ syncing = true;
152
+ target.scrollTop = source.scrollTop;
153
+ syncing = false;
154
+ };
155
+ leftPane.addEventListener('scroll', () => syncScroll(leftPane, rightPane));
156
+ rightPane.addEventListener('scroll', () => syncScroll(rightPane, leftPane));
157
+
158
+ // Close on button/overlay/Esc
159
+ overlay.querySelector('.tab-diff-close')!.addEventListener('click', closeDiffPanel);
160
+ overlay.addEventListener('click', (e) => {
161
+ if ((e.target as HTMLElement).classList.contains('tab-diff-overlay')) closeDiffPanel();
162
+ });
163
+
164
+ const onEsc = (e: KeyboardEvent) => {
165
+ if (e.key === 'Escape') { closeDiffPanel(); document.removeEventListener('keydown', onEsc); }
166
+ };
167
+ document.addEventListener('keydown', onEsc);
168
+ }
169
+
170
+ function closeDiffPanel() {
171
+ if (diffOverlay) {
172
+ diffOverlay.remove();
173
+ diffOverlay = null;
174
+ }
175
+ }
176
+
177
+ // ─── Tab selection UI ───────────────────────────────
178
+
179
+ export function openTabDiffSelector() {
180
+ const tabs = getOpenTabs();
181
+ if (tabs.length < 2) {
182
+ alert('Open at least 2 files in tabs to compare them.');
183
+ return;
184
+ }
185
+
186
+ const activeIdx = getActiveTabIndex();
187
+
188
+ // If exactly 2 tabs, diff them immediately
189
+ if (tabs.length === 2) {
190
+ renderDiffPanel(
191
+ tabs[0].originalContent || tabs[0].rendered.full_raw || '',
192
+ tabs[1].originalContent || tabs[1].rendered.full_raw || '',
193
+ tabs[0].name,
194
+ tabs[1].name
195
+ );
196
+ return;
197
+ }
198
+
199
+ // More than 2 tabs — show picker for the second file
200
+ const modal = document.createElement('div');
201
+ modal.className = 'tab-diff-picker-overlay';
202
+ modal.innerHTML = `
203
+ <div class="tab-diff-picker">
204
+ <div class="tab-diff-picker-title">Compare "${escapeHtml(tabs[activeIdx].name)}" with:</div>
205
+ <div class="tab-diff-picker-list"></div>
206
+ </div>
207
+ `;
208
+
209
+ const list = modal.querySelector('.tab-diff-picker-list')!;
210
+ for (let i = 0; i < tabs.length; i++) {
211
+ if (i === activeIdx) continue;
212
+ const btn = document.createElement('button');
213
+ btn.className = 'tab-diff-picker-item';
214
+ btn.textContent = `📄 ${tabs[i].name}`;
215
+ btn.title = tabs[i].path;
216
+ btn.addEventListener('click', () => {
217
+ modal.remove();
218
+ renderDiffPanel(
219
+ tabs[activeIdx].originalContent || tabs[activeIdx].rendered.full_raw || '',
220
+ tabs[i].originalContent || tabs[i].rendered.full_raw || '',
221
+ tabs[activeIdx].name,
222
+ tabs[i].name
223
+ );
224
+ });
225
+ list.appendChild(btn);
226
+ }
227
+
228
+ document.body.appendChild(modal);
229
+
230
+ // Close on overlay click or Esc
231
+ modal.addEventListener('click', (e) => {
232
+ if ((e.target as HTMLElement).classList.contains('tab-diff-picker-overlay')) modal.remove();
233
+ });
234
+ const onEsc = (e: KeyboardEvent) => {
235
+ if (e.key === 'Escape') { modal.remove(); document.removeEventListener('keydown', onEsc); }
236
+ };
237
+ document.addEventListener('keydown', onEsc);
238
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Client-side auth — fetches user session, manages login/logout UI, favorites
3
+ */
4
+
5
+ interface UserData {
6
+ id: number;
7
+ username: string;
8
+ displayName: string;
9
+ avatarUrl: string;
10
+ }
11
+
12
+ let currentUser: UserData | null = null;
13
+ let userFavorites: { repoUrl: string; repoName: string }[] = [];
14
+
15
+ /** Initialize auth UI — call once on page load */
16
+ export async function setupAuth() {
17
+ try {
18
+ const res = await fetch('/api/auth/me');
19
+ const data = await res.json();
20
+
21
+ if (data.authenticated && data.user) {
22
+ currentUser = data.user;
23
+ userFavorites = data.favorites || [];
24
+ showLoggedIn(data.user);
25
+ } else {
26
+ showLoggedOut();
27
+ }
28
+ } catch {
29
+ showLoggedOut();
30
+ }
31
+
32
+ // Wire up logout button
33
+ const logoutBtn = document.getElementById('logoutBtn');
34
+ if (logoutBtn) {
35
+ logoutBtn.addEventListener('click', async () => {
36
+ try {
37
+ await fetch('/api/auth/me', { method: 'POST' });
38
+ currentUser = null;
39
+ userFavorites = [];
40
+ showLoggedOut();
41
+ } catch { }
42
+ });
43
+ }
44
+
45
+ // Wire up favorite toggle
46
+ const favBtn = document.getElementById('favToggle');
47
+ if (favBtn) {
48
+ favBtn.addEventListener('click', toggleFavorite);
49
+ }
50
+ }
51
+
52
+ function showLoggedIn(user: UserData) {
53
+ const loggedOut = document.getElementById('userLoggedOut');
54
+ const loggedIn = document.getElementById('userLoggedIn');
55
+ const avatar = document.getElementById('userAvatar') as HTMLImageElement;
56
+ const name = document.getElementById('userName');
57
+
58
+ if (loggedOut) loggedOut.style.display = 'none';
59
+ if (loggedIn) loggedIn.style.display = 'flex';
60
+ if (avatar) avatar.src = user.avatarUrl;
61
+ if (name) name.textContent = user.displayName || user.username;
62
+ }
63
+
64
+ function showLoggedOut() {
65
+ const loggedOut = document.getElementById('userLoggedOut');
66
+ const loggedIn = document.getElementById('userLoggedIn');
67
+
68
+ if (loggedOut) loggedOut.style.display = '';
69
+ if (loggedIn) loggedIn.style.display = 'none';
70
+ }
71
+
72
+ /** Update the favorite star for the currently loaded repo */
73
+ export function updateFavoriteStar(repoUrl?: string) {
74
+ const favBtn = document.getElementById('favToggle');
75
+ if (!favBtn || !currentUser) return;
76
+
77
+ const url = repoUrl || getCurrentRepoUrl();
78
+ if (!url) {
79
+ favBtn.style.display = 'none';
80
+ return;
81
+ }
82
+
83
+ favBtn.style.display = '';
84
+ const isFav = userFavorites.some(f => f.repoUrl === url);
85
+ favBtn.classList.toggle('favorited', isFav);
86
+ }
87
+
88
+ async function toggleFavorite() {
89
+ if (!currentUser) return;
90
+
91
+ const repoUrl = getCurrentRepoUrl();
92
+ if (!repoUrl) return;
93
+
94
+ const isFav = userFavorites.some(f => f.repoUrl === repoUrl);
95
+ const action = isFav ? 'remove' : 'add';
96
+ const repoName = document.querySelector('.commit-hash-label')?.textContent || '';
97
+
98
+ try {
99
+ const res = await fetch('/api/auth/favorites', {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ action, repoUrl, repoName }),
103
+ });
104
+
105
+ const data = await res.json();
106
+ if (data.ok) {
107
+ if (action === 'add') {
108
+ userFavorites.push({ repoUrl, repoName });
109
+ } else {
110
+ userFavorites = userFavorites.filter(f => f.repoUrl !== repoUrl);
111
+ }
112
+ updateFavoriteStar(repoUrl);
113
+ }
114
+ } catch { }
115
+ }
116
+
117
+ function getCurrentRepoUrl(): string {
118
+ // Get from hash (e.g. #C:\Code\project or #https://github.com/user/repo)
119
+ const hash = location.hash.slice(1);
120
+ if (hash) return hash;
121
+
122
+ return '';
123
+ }
124
+
125
+ /** Get the current user for other modules */
126
+ export function getUser() {
127
+ return currentUser;
128
+ }
129
+
130
+ /** Get user favorites list */
131
+ export function getFavorites() {
132
+ return userFavorites;
133
+ }
@@ -0,0 +1,78 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * General-purpose utilities — escaping, formatting, icons, toast.
4
+ */
5
+ import { measure } from 'measure-fn';
6
+
7
+ // ─── HTML escaping ───────────────────────────────────────
8
+ export function escapeHtml(text: string): string {
9
+ const div = document.createElement('div');
10
+ div.textContent = text;
11
+ return div.innerHTML;
12
+ }
13
+
14
+ // ─── Date formatting ─────────────────────────────────────
15
+ export function formatDate(dateStr: string): string {
16
+ const date = new Date(dateStr);
17
+ const now = new Date();
18
+ const diff = (now as any) - (date as any);
19
+ if (diff < 60000) return 'just now';
20
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
21
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
22
+ if (diff < 604800000) return `${Math.floor(diff / 86400000)}d ago`;
23
+ return date.toLocaleDateString();
24
+ }
25
+
26
+ // ─── File size formatting ────────────────────────────────
27
+ export function formatSize(bytes: number): string {
28
+ if (bytes < 1024) return `${bytes} B`;
29
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
30
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
31
+ }
32
+
33
+ // ─── File icon class from extension ──────────────────────
34
+ export function getFileIconClass(ext: string): string {
35
+ const extMap: Record<string, string> = {
36
+ 'js': 'js', 'jsx': 'js', 'mjs': 'js',
37
+ 'ts': 'ts', 'tsx': 'ts',
38
+ 'html': 'html', 'htm': 'html',
39
+ 'css': 'css', 'scss': 'css', 'sass': 'css', 'less': 'css',
40
+ 'json': 'json',
41
+ 'md': 'md', 'markdown': 'md',
42
+ 'py': 'py',
43
+ 'go': 'go',
44
+ 'rs': 'rs'
45
+ };
46
+ return extMap[ext] || '';
47
+ }
48
+
49
+ // ─── SVG file icon ───────────────────────────────────────
50
+ export function getFileIcon(type: string, ext: string): string {
51
+ if (type === 'folder') {
52
+ return `<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
53
+ <path d="M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z"/>
54
+ </svg>`;
55
+ }
56
+ return `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
57
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
58
+ <polyline points="14 2 14 8 20 8"/>
59
+ </svg>`;
60
+ }
61
+
62
+ // ─── Toast notifications ─────────────────────────────────
63
+ export function showToast(message: string, type = 'info') {
64
+ measure('toast:show', () => {
65
+ let container = document.querySelector('.toast-container') as HTMLElement;
66
+ if (!container) {
67
+ container = document.createElement('div');
68
+ container.className = 'toast-container';
69
+ document.body.appendChild(container);
70
+ }
71
+
72
+ const toast = document.createElement('div');
73
+ toast.className = `toast ${type}`;
74
+ toast.textContent = message;
75
+ container.appendChild(toast);
76
+ setTimeout(() => toast.remove(), 3000);
77
+ });
78
+ }