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,222 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Hidden files management — hide/restore/modal with folder bulk-hide.
4
+ * Uses melina/client JSX + render instead of innerHTML.
5
+ */
6
+ import { measure } from 'measure-fn';
7
+ import { render } from 'melina/client';
8
+ import type { CanvasContext } from './context';
9
+
10
+
11
+ // ─── Load hidden files from localStorage ────────────────
12
+ export function loadHiddenFiles(ctx: CanvasContext) {
13
+ try {
14
+ const saved = localStorage.getItem('gitcanvas:hiddenFiles');
15
+ if (saved) {
16
+ const arr = JSON.parse(saved);
17
+ arr.forEach((f: string) => ctx.hiddenFiles.add(f));
18
+ }
19
+ } catch (e) { /* ignore */ }
20
+ }
21
+
22
+ // ─── Persist hidden files to localStorage ───────────────
23
+ export function saveHiddenFiles(ctx: CanvasContext) {
24
+ localStorage.setItem('gitcanvas:hiddenFiles', JSON.stringify([...ctx.hiddenFiles]));
25
+ }
26
+
27
+ // ─── Update hidden button badge ─────────────────────────
28
+ export function updateHiddenUI(ctx: CanvasContext) {
29
+ const btn = document.getElementById('showHidden');
30
+ const badge = document.getElementById('hiddenCount');
31
+ if (ctx.hiddenFiles.size > 0) {
32
+ if (btn) btn.style.display = 'inline-flex';
33
+ if (badge) badge.textContent = String(ctx.hiddenFiles.size);
34
+ } else {
35
+ if (btn) btn.style.display = 'none';
36
+ }
37
+ }
38
+
39
+ // ─── Hide selected file paths ───────────────────────────
40
+ export function hideSelectedFiles(ctx: CanvasContext, paths: string[]) {
41
+ measure('files:hide', () => {
42
+ paths.forEach(p => ctx.hiddenFiles.add(p));
43
+ saveHiddenFiles(ctx);
44
+ ctx.actor.send({ type: 'DESELECT_ALL' });
45
+
46
+ paths.forEach(p => {
47
+ const card = ctx.fileCards.get(p);
48
+ if (card) {
49
+ card.remove();
50
+ ctx.fileCards.delete(p);
51
+ }
52
+ // Also remove from deferred so viewport-culling doesn't re-create
53
+ ctx.deferredCards.delete(p);
54
+ });
55
+
56
+ updateHiddenUI(ctx);
57
+ });
58
+ }
59
+
60
+ // ─── Restore a single hidden file ───────────────────────
61
+ export function restoreFile(ctx: CanvasContext, filePath: string) {
62
+ ctx.hiddenFiles.delete(filePath);
63
+ saveHiddenFiles(ctx);
64
+ updateHiddenUI(ctx);
65
+ }
66
+
67
+ // ─── Restore all hidden files ───────────────────────────
68
+ export function restoreAllHidden(ctx: CanvasContext) {
69
+ ctx.hiddenFiles.clear();
70
+ saveHiddenFiles(ctx);
71
+ updateHiddenUI(ctx);
72
+ }
73
+
74
+ // ─── Get unique folder paths from file list ─────────────
75
+ function getFolders(allFiles: string[]): string[] {
76
+ const folders = new Set<string>();
77
+ for (const f of allFiles) {
78
+ const parts = f.split('/');
79
+ // Build all ancestor folder paths
80
+ for (let i = 1; i < parts.length; i++) {
81
+ folders.add(parts.slice(0, i).join('/'));
82
+ }
83
+ }
84
+ return [...folders].sort();
85
+ }
86
+
87
+ // ─── Hidden files modal (JSX) ───────────────────────────
88
+ function HiddenFilesModalContent({
89
+ hiddenFiles, allFiles, onRestore, onRestoreAll, onHideFolder, onClose
90
+ }: {
91
+ hiddenFiles: string[];
92
+ allFiles: string[];
93
+ onRestore: (path: string) => void;
94
+ onRestoreAll: () => void;
95
+ onHideFolder: (folder: string) => void;
96
+ onClose: () => void;
97
+ }) {
98
+ const folders = getFolders(allFiles);
99
+
100
+ return (
101
+ <>
102
+ <div className="hidden-modal-backdrop" onClick={onClose}></div>
103
+ <div className="hidden-modal-content">
104
+ <div className="hidden-modal-header">
105
+ <h3>Hidden Files ({hiddenFiles.length})</h3>
106
+ <div className="hidden-modal-actions">
107
+ <button className="btn-secondary btn-sm" onClick={onRestoreAll}>Restore All</button>
108
+ <button className="hidden-modal-close" onClick={onClose}>&times;</button>
109
+ </div>
110
+ </div>
111
+ <div className="hidden-modal-body">
112
+ {/* Folder bulk-hide section */}
113
+ {folders.length > 0 && (
114
+ <div className="hidden-folder-section">
115
+ <div className="hidden-section-label">Hide by folder</div>
116
+ <div className="hidden-folder-list">
117
+ {folders.map(folder => {
118
+ const filesInFolder = allFiles.filter(f => f.startsWith(folder + '/'));
119
+ const hiddenInFolder = filesInFolder.filter(f => hiddenFiles.includes(f));
120
+ const allHidden = hiddenInFolder.length === filesInFolder.length;
121
+ return (
122
+ <div key={folder} className="hidden-folder-row">
123
+ <span className="hidden-folder-path">
124
+ 📁 {folder}/
125
+ <span className="hidden-folder-count">
126
+ {hiddenInFolder.length}/{filesInFolder.length}
127
+ </span>
128
+ </span>
129
+ <button
130
+ className={`btn-hide-folder ${allHidden ? 'btn-disabled' : ''}`}
131
+ title={allHidden ? 'All files already hidden' : `Hide ${filesInFolder.length - hiddenInFolder.length} files in ${folder}/`}
132
+ disabled={allHidden}
133
+ onClick={() => onHideFolder(folder)}
134
+ >
135
+ {allHidden ? '✓' : '🙈'}
136
+ </button>
137
+ </div>
138
+ );
139
+ })}
140
+ </div>
141
+ </div>
142
+ )}
143
+
144
+ {/* Individual hidden files */}
145
+ {hiddenFiles.length > 0 && (
146
+ <div className="hidden-section-label" style={{ marginTop: '12px' }}>
147
+ Currently hidden
148
+ </div>
149
+ )}
150
+ {hiddenFiles.map(f => (
151
+ <div key={f} className="hidden-file-row" data-path={f}>
152
+ <span className="hidden-file-path">{f}</span>
153
+ <button className="btn-restore" title="Restore this file" onClick={() => onRestore(f)}>
154
+ 👁
155
+ </button>
156
+ </div>
157
+ ))}
158
+ </div>
159
+ </div>
160
+ </>
161
+ );
162
+ }
163
+
164
+ // ─── Show the hidden files modal ────────────────────────
165
+ export function showHiddenFilesModal(ctx: CanvasContext, rerenderCurrentView: () => void) {
166
+ measure('modal:hiddenFiles', () => {
167
+ // Allow opening even with 0 hidden files (to use folder bulk-hide)
168
+ let modal = document.getElementById('hiddenFilesModal');
169
+ if (modal) modal.remove();
170
+
171
+ modal = document.createElement('div');
172
+ modal.id = 'hiddenFilesModal';
173
+ modal.className = 'hidden-files-modal';
174
+ document.body.appendChild(modal);
175
+
176
+ // Get all file paths from fileCards + deferredCards + hiddenFiles
177
+ const allFiles = [
178
+ ...ctx.fileCards.keys(),
179
+ ...ctx.deferredCards.keys(),
180
+ ...ctx.hiddenFiles,
181
+ ];
182
+ // Deduplicate
183
+ const uniqueFiles = [...new Set(allFiles)];
184
+
185
+ function rerender() {
186
+ const hiddenFiles = [...ctx.hiddenFiles];
187
+ render(
188
+ <HiddenFilesModalContent
189
+ hiddenFiles={hiddenFiles}
190
+ allFiles={uniqueFiles}
191
+ onRestore={(path) => {
192
+ restoreFile(ctx, path);
193
+ rerenderCurrentView();
194
+ rerender();
195
+ }}
196
+ onRestoreAll={() => {
197
+ restoreAllHidden(ctx);
198
+ render(null, modal);
199
+ modal.remove();
200
+ rerenderCurrentView();
201
+ }}
202
+ onHideFolder={(folder) => {
203
+ const toHide = uniqueFiles
204
+ .filter(f => f.startsWith(folder + '/'))
205
+ .filter(f => !ctx.hiddenFiles.has(f));
206
+ if (toHide.length > 0) {
207
+ hideSelectedFiles(ctx, toHide);
208
+ rerender();
209
+ }
210
+ }}
211
+ onClose={() => {
212
+ render(null, modal);
213
+ modal.remove();
214
+ }}
215
+ />,
216
+ modal
217
+ );
218
+ }
219
+
220
+ rerender();
221
+ });
222
+ }
File without changes
@@ -0,0 +1,365 @@
1
+ // @ts-nocheck
2
+ import { render } from 'melina/client';
3
+ import type { CanvasContext } from './context';
4
+ import { renderAllFilesOnCanvas } from './repo';
5
+
6
+ export interface FileSection {
7
+ startString: string;
8
+ endString: string;
9
+ }
10
+
11
+ export interface LayerData {
12
+ id: string;
13
+ name: string;
14
+ files: Record<string, { sections: FileSection[] }>;
15
+ }
16
+
17
+ export const layerState = {
18
+ layers: [] as LayerData[],
19
+ activeLayerId: 'default' as string
20
+ };
21
+
22
+ const DEFAULT_LAYER: LayerData = { id: 'default', name: 'All Files (Default)', files: {} };
23
+
24
+ export function initLayers(ctx: CanvasContext) {
25
+ // Load from local storage for now or maybe an API? Let's use localStorage to persist across commits.
26
+ try {
27
+ const stored = localStorage.getItem(`gitcanvas:layers:${ctx.snap().context.repoPath}`);
28
+ if (stored) {
29
+ layerState.layers = JSON.parse(stored);
30
+ } else {
31
+ layerState.layers = [DEFAULT_LAYER];
32
+ }
33
+ } catch {
34
+ layerState.layers = [DEFAULT_LAYER];
35
+ }
36
+
37
+ // Ensure default exists
38
+ if (!layerState.layers.find(l => l.id === 'default')) {
39
+ layerState.layers.unshift(DEFAULT_LAYER);
40
+ }
41
+
42
+ const savedActive = localStorage.getItem(`gitcanvas:activeLayer:${ctx.snap().context.repoPath}`);
43
+ if (savedActive && layerState.layers.find(l => l.id === savedActive)) {
44
+ layerState.activeLayerId = savedActive;
45
+ } else {
46
+ layerState.activeLayerId = layerState.layers[0].id;
47
+ }
48
+ }
49
+
50
+ export function saveLayers(ctx: CanvasContext) {
51
+ if (!ctx.snap().context.repoPath) return;
52
+ localStorage.setItem(`gitcanvas:layers:${ctx.snap().context.repoPath}`, JSON.stringify(layerState.layers));
53
+ }
54
+
55
+ export function createLayer(ctx: CanvasContext, name: string) {
56
+ const newLayer: LayerData = {
57
+ id: `layer_${Date.now()}`,
58
+ name,
59
+ files: {}
60
+ };
61
+ layerState.layers.push(newLayer);
62
+ // Don't auto-switch to the new layer — stay on the current one
63
+ saveLayers(ctx);
64
+ renderLayersUI(ctx);
65
+ }
66
+
67
+ export function renameLayer(ctx: CanvasContext, id: string, newName: string) {
68
+ const layer = layerState.layers.find(l => l.id === id);
69
+ if (!layer || layer.id === 'default') return;
70
+ layer.name = newName;
71
+ saveLayers(ctx);
72
+ renderLayersUI(ctx);
73
+ }
74
+
75
+ export function deleteLayer(ctx: CanvasContext, id: string) {
76
+ if (id === 'default') return;
77
+
78
+ // Return files from deleted layer back to default visibility
79
+ const layer = layerState.layers.find(l => l.id === id);
80
+ if (layer) {
81
+ const defaultLayer = layerState.layers.find(l => l.id === 'default');
82
+ if (defaultLayer) {
83
+ // Remove exclusions for files that were in this layer
84
+ for (const path of Object.keys(layer.files)) {
85
+ delete defaultLayer.files[path]; // Remove from moved-out tracking
86
+ }
87
+ }
88
+ }
89
+
90
+ layerState.layers = layerState.layers.filter(l => l.id !== id);
91
+ if (layerState.activeLayerId === id) {
92
+ setActiveLayer(ctx, 'default');
93
+ } else {
94
+ saveLayers(ctx);
95
+ renderLayersUI(ctx);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Move a file to a layer — this REMOVES it from the default layer's visible set.
101
+ * When a file is moved to a non-default layer, the default layer tracks it as "moved out"
102
+ * so it won't show on the default canvas.
103
+ */
104
+ export function moveFileToLayer(ctx: CanvasContext, layerId: string, path: string) {
105
+ if (layerId === 'default') return; // Can't "move" to default — use removeFileFromLayer instead
106
+
107
+ const layer = layerState.layers.find(l => l.id === layerId);
108
+ if (!layer) return;
109
+
110
+ // Add file to target layer
111
+ if (!layer.files[path]) {
112
+ layer.files[path] = { sections: [] };
113
+ }
114
+
115
+ // Track in default layer that this file has been moved out
116
+ const defaultLayer = layerState.layers.find(l => l.id === 'default');
117
+ if (defaultLayer) {
118
+ if (!defaultLayer.files[path]) {
119
+ defaultLayer.files[path] = { sections: [] };
120
+ }
121
+ // Mark as moved-out by adding a special marker
122
+ (defaultLayer.files[path] as any).__movedTo = layerId;
123
+ }
124
+
125
+ saveLayers(ctx);
126
+ renderLayersUI(ctx);
127
+ // Re-render current layer to hide the moved file
128
+ if (layerState.activeLayerId === 'default') {
129
+ applyLayer(ctx);
130
+ }
131
+ }
132
+
133
+ /** Backwards-compatible addFileToLayer that now delegates to moveFileToLayer */
134
+ export function addFileToLayer(ctx: CanvasContext, layerId: string, path: string) {
135
+ moveFileToLayer(ctx, layerId, path);
136
+ }
137
+
138
+ export function removeFileFromLayer(ctx: CanvasContext, layerId: string, path: string) {
139
+ const layer = layerState.layers.find(l => l.id === layerId);
140
+ if (!layer || layer.id === 'default') return;
141
+ if (layer.files[path]) {
142
+ delete layer.files[path];
143
+
144
+ // Remove the moved-out tracking from default layer
145
+ const defaultLayer = layerState.layers.find(l => l.id === 'default');
146
+ if (defaultLayer && defaultLayer.files[path]) {
147
+ delete defaultLayer.files[path];
148
+ }
149
+
150
+ saveLayers(ctx);
151
+ renderLayersUI(ctx);
152
+ if (layer.id === layerState.activeLayerId) applyLayer(ctx);
153
+ }
154
+ }
155
+
156
+ export function addSectionToLayer(ctx: CanvasContext, layerId: string, path: string, startString: string, endString: string) {
157
+ const layer = layerState.layers.find(l => l.id === layerId);
158
+ if (!layer || layer.id === 'default') return;
159
+ if (!layer.files[path]) {
160
+ layer.files[path] = { sections: [] };
161
+ }
162
+ layer.files[path].sections.push({ startString, endString });
163
+ saveLayers(ctx);
164
+ if (layer.id === layerState.activeLayerId) applyLayer(ctx);
165
+ }
166
+
167
+ export function setActiveLayer(ctx: CanvasContext, id: string) {
168
+ layerState.activeLayerId = id;
169
+ localStorage.setItem(`gitcanvas:activeLayer:${ctx.snap().context.repoPath}`, id);
170
+ renderLayersUI(ctx);
171
+ applyLayer(ctx);
172
+
173
+ // User feedback
174
+ const layer = layerState.layers.find(l => l.id === id);
175
+ if (layer && id !== 'default') {
176
+ const fileCount = Object.keys(layer.files).length;
177
+ if (fileCount === 0) {
178
+ import('./utils').then(m => m.showToast(
179
+ `Layer "${layer.name}" is empty — right-click cards to move them here`,
180
+ 'info'
181
+ ));
182
+ } else {
183
+ import('./utils').then(m => m.showToast(
184
+ `Switched to "${layer.name}" (${fileCount} files)`,
185
+ 'info'
186
+ ));
187
+ }
188
+ } else if (id === 'default') {
189
+ import('./utils').then(m => m.showToast('Switched to All Files', 'info'));
190
+ }
191
+ }
192
+
193
+ export function getActiveLayer(): LayerData | null {
194
+ if (layerState.activeLayerId === 'default') return null;
195
+ return layerState.layers.find(l => l.id === layerState.activeLayerId) || null;
196
+ }
197
+
198
+ /**
199
+ * Get which layer a file belongs to (returns null if it's only in default/visible everywhere)
200
+ */
201
+ export function getFileLayer(path: string): { layerId: string; layerName: string } | null {
202
+ for (const layer of layerState.layers) {
203
+ if (layer.id === 'default') continue;
204
+ if (layer.files[path]) {
205
+ return { layerId: layer.id, layerName: layer.name };
206
+ }
207
+ }
208
+ return null;
209
+ }
210
+
211
+ /**
212
+ * Check if a file has been moved out of the default layer to another layer
213
+ */
214
+ export function isFileMovedFromDefault(path: string): boolean {
215
+ const defaultLayer = layerState.layers.find(l => l.id === 'default');
216
+ if (!defaultLayer) return false;
217
+ return !!(defaultLayer.files[path] && (defaultLayer.files[path] as any).__movedTo);
218
+ }
219
+
220
+ /**
221
+ * Navigate to a file that might be in another layer.
222
+ * Switches to the correct layer and returns true if found.
223
+ */
224
+ export function navigateToFileInLayer(ctx: CanvasContext, path: string): boolean {
225
+ const fileLayer = getFileLayer(path);
226
+ if (fileLayer && fileLayer.layerId !== layerState.activeLayerId) {
227
+ setActiveLayer(ctx, fileLayer.layerId);
228
+ return true; // Layer was switched
229
+ }
230
+ return false; // No layer switch needed
231
+ }
232
+
233
+ export function applyLayer(ctx: CanvasContext) {
234
+ const state = ctx.snap().context;
235
+ const commitHash = state.currentCommitHash || 'allfiles';
236
+ import('./repo').then(({ selectCommit, renderAllFilesOnCanvas, populateChangedFilesPanel }) => {
237
+ if (commitHash === 'allfiles' && ctx.allFilesData) {
238
+ renderAllFilesOnCanvas(ctx, ctx.allFilesData);
239
+ // Also repopulate the changed files panel with the new layer filter
240
+ if (ctx.commitFilesData) {
241
+ populateChangedFilesPanel(ctx.commitFilesData);
242
+ }
243
+ } else if (commitHash && commitHash !== 'allfiles') {
244
+ selectCommit(ctx, commitHash, true);
245
+ }
246
+ });
247
+ }
248
+
249
+ function LayerItem({ layer, activeId, ctx }: { layer: LayerData; activeId: string; ctx: CanvasContext }) {
250
+ const isActive = layer.id === activeId;
251
+ const fileCount = Object.keys(layer.files).filter(k => {
252
+ // Don't count moved-out tracking entries in default layer
253
+ if (layer.id === 'default') return !(layer.files[k] as any).__movedTo;
254
+ return true;
255
+ }).length;
256
+
257
+ return (
258
+ <div
259
+ className={`layers-bar-item ${isActive ? 'active' : ''}`}
260
+ onClick={() => setActiveLayer(ctx, layer.id)}
261
+ onContextMenu={(e) => {
262
+ e.preventDefault();
263
+ if (layer.id === 'default') return;
264
+ if (confirm(`Delete layer "${layer.name}"?`)) {
265
+ deleteLayer(ctx, layer.id);
266
+ }
267
+ }}
268
+ onDoubleClick={(e) => {
269
+ e.preventDefault();
270
+ if (layer.id === 'default') return;
271
+ const newName = prompt('Rename layer:', layer.name);
272
+ if (newName) {
273
+ renameLayer(ctx, layer.id, newName);
274
+ }
275
+ }}
276
+ title={layer.id === 'default' ? 'Default Layer' : 'Double-click to rename, Right-click to delete'}
277
+ >
278
+ <span className="layer-name">{layer.name}</span>
279
+ {layer.id !== 'default' && fileCount > 0 && (
280
+ <span className="layer-badge">{fileCount}</span>
281
+ )}
282
+ </div>
283
+ );
284
+ }
285
+
286
+ export function renderLayersUI(ctx: CanvasContext) {
287
+ const container = document.getElementById('layersBarContainer');
288
+ if (!container) return;
289
+
290
+ render(
291
+ <div className="layers-bar">
292
+ {layerState.layers.map(l => (
293
+ <LayerItem key={`${l.id}_${Object.keys(l.files).length}`} layer={l} activeId={layerState.activeLayerId} ctx={ctx} />
294
+ ))}
295
+ <button
296
+ className="layers-bar-add"
297
+ id="newLayerBtn"
298
+ title="Create a new Layer"
299
+ >
300
+ + New Layer
301
+ </button>
302
+ </div>,
303
+ container
304
+ );
305
+
306
+ // Attach click handler via DOM (Melina onClick doesn't reliably bind here)
307
+ const btn = document.getElementById('newLayerBtn');
308
+ if (btn) {
309
+ btn.onclick = () => {
310
+ const name = prompt('Enter a name for the new layer:');
311
+ if (name) createLayer(ctx, name);
312
+ };
313
+ }
314
+ }
315
+
316
+ // UI to configure section extraction
317
+ export function promptAddSection(ctx: CanvasContext, path: string, layerId?: string) {
318
+ const layer = layerId ? layerState.layers.find(l => l.id === layerId) : getActiveLayer();
319
+ if (!layer || layer.id === 'default') {
320
+ alert("Please select a valid layer to add sections to it.");
321
+ return;
322
+ }
323
+ const startStr = prompt(`Extracting ${path.split('/').pop()} into "${layer.name}"\nEnter starting string for the section (leave blank to include whole file):`);
324
+ // If user presses Cancel, startStr is null. If they just hit Enter, startStr is ''.
325
+ if (startStr === null) return;
326
+ const endStr = prompt("Enter ending string for the section (leave blank for end of file):");
327
+ if (endStr === null) return;
328
+
329
+ addSectionToLayer(ctx, layer.id, path, startStr, endStr);
330
+ }
331
+
332
+ export function filterFileContentByLayer(content: string, sections: FileSection[]): { filteredContent: string; visibleLineIndices: Set<number> } {
333
+ if (!content) return { filteredContent: '', visibleLineIndices: new Set() };
334
+ if (!sections || sections.length === 0) {
335
+ return { filteredContent: content, visibleLineIndices: new Set(content.split('\n').map((_, i) => i)) };
336
+ }
337
+
338
+ const lines = content.split('\n');
339
+ const visibleLines = new Set<number>();
340
+
341
+ for (const sec of sections) {
342
+ let inSection = false;
343
+ let startConditionMet = false;
344
+
345
+ for (let i = 0; i < lines.length; i++) {
346
+ const line = lines[i];
347
+
348
+ if (!inSection && line.includes(sec.startString)) {
349
+ inSection = true;
350
+ startConditionMet = true;
351
+ }
352
+
353
+ if (inSection) {
354
+ visibleLines.add(i);
355
+
356
+ // End after we add the closing line
357
+ if (line.includes(sec.endString) && (!sec.startString || line !== sec.startString || !startConditionMet)) {
358
+ inSection = false;
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ return { filteredContent: content, visibleLineIndices: visibleLines };
365
+ }
@@ -0,0 +1,45 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Loading progress overlay.
4
+ * Uses melina/client JSX + render.
5
+ */
6
+ import { render } from 'melina/client';
7
+ import type { CanvasContext } from './context';
8
+
9
+ function LoadingOverlayContent({ message, sub }: { message: string; sub: string }) {
10
+ return (
11
+ <div className="loading-content">
12
+ <div className="loading-spinner"></div>
13
+ <div className="loading-message">{message}</div>
14
+ <div className="loading-sub">{sub}</div>
15
+ </div>
16
+ );
17
+ }
18
+
19
+ let currentMessage = '';
20
+ let currentSub = '';
21
+
22
+ export function showLoadingProgress(ctx: CanvasContext, message: string) {
23
+ if (!ctx.loadingOverlay) {
24
+ ctx.loadingOverlay = document.createElement('div');
25
+ ctx.loadingOverlay.className = 'loading-overlay';
26
+ document.body.appendChild(ctx.loadingOverlay);
27
+ }
28
+ currentMessage = message;
29
+ currentSub = '';
30
+ render(<LoadingOverlayContent message={currentMessage} sub={currentSub} />, ctx.loadingOverlay);
31
+ ctx.loadingOverlay.classList.add('active');
32
+ }
33
+
34
+ export function updateLoadingProgress(ctx: CanvasContext, sub: string) {
35
+ if (ctx.loadingOverlay) {
36
+ currentSub = sub;
37
+ render(<LoadingOverlayContent message={currentMessage} sub={currentSub} />, ctx.loadingOverlay);
38
+ }
39
+ }
40
+
41
+ export function hideLoadingProgress(ctx: CanvasContext) {
42
+ if (ctx.loadingOverlay) {
43
+ ctx.loadingOverlay.classList.remove('active');
44
+ }
45
+ }