gitmaps 1.1.0 → 1.1.2

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 +267 -118
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/analytics.db +0 -0
  5. package/app/api/analytics/route.ts +64 -0
  6. package/app/api/auth/positions/route.ts +95 -33
  7. package/app/api/build-info/route.ts +19 -0
  8. package/app/api/chat/route.ts +13 -2
  9. package/app/api/og-image/route.ts +14 -0
  10. package/app/api/repo/file-content/route.ts +73 -20
  11. package/app/api/repo/load/route.test.ts +62 -0
  12. package/app/api/repo/load/route.ts +41 -1
  13. package/app/api/repo/pdf-thumb/route.ts +127 -0
  14. package/app/api/repo/resolve-slug/route.ts +51 -0
  15. package/app/api/repo/tree/route.ts +188 -104
  16. package/app/api/version/route.ts +26 -0
  17. package/app/globals.css +5706 -4938
  18. package/app/layout.tsx +1279 -490
  19. package/app/lib/auto-arrange.test.ts +158 -0
  20. package/app/lib/auto-arrange.ts +147 -0
  21. package/app/lib/canvas-export.ts +358 -358
  22. package/app/lib/canvas.ts +625 -564
  23. package/app/lib/cards.tsx +1361 -916
  24. package/app/lib/chat.tsx +65 -9
  25. package/app/lib/code-editor.ts +86 -2
  26. package/app/lib/context.test.ts +32 -0
  27. package/app/lib/context.ts +19 -3
  28. package/app/lib/cursor-sharing.ts +34 -0
  29. package/app/lib/events.tsx +71 -93
  30. package/app/lib/export-canvas.ts +287 -0
  31. package/app/lib/file-card-plugin.ts +148 -148
  32. package/app/lib/file-modal.tsx +49 -0
  33. package/app/lib/file-preview.ts +486 -427
  34. package/app/lib/github-import.test.ts +424 -0
  35. package/app/lib/initial-route-hydration.test.ts +283 -0
  36. package/app/lib/initial-route-hydration.ts +202 -0
  37. package/app/lib/landing-reset.test.ts +99 -0
  38. package/app/lib/landing-reset.ts +106 -0
  39. package/app/lib/landing-shell.test.ts +75 -0
  40. package/app/lib/large-repo-optimization.ts +37 -0
  41. package/app/lib/layout-snapshots.ts +320 -0
  42. package/app/lib/loading.test.ts +69 -0
  43. package/app/lib/loading.tsx +160 -45
  44. package/app/lib/mount-cleanup.test.ts +52 -0
  45. package/app/lib/mount-cleanup.ts +34 -0
  46. package/app/lib/mount-init.test.ts +123 -0
  47. package/app/lib/mount-init.ts +107 -0
  48. package/app/lib/mount-lifecycle.test.ts +39 -0
  49. package/app/lib/mount-lifecycle.ts +12 -0
  50. package/app/lib/mount-route-wiring.test.ts +87 -0
  51. package/app/lib/mount-route-wiring.ts +84 -0
  52. package/app/lib/multi-repo.ts +14 -0
  53. package/app/lib/onboarding-tutorial.ts +278 -0
  54. package/app/lib/positions.ts +190 -121
  55. package/app/lib/recent-commits.test.ts +947 -0
  56. package/app/lib/recent-commits.ts +227 -0
  57. package/app/lib/repo-handoff.test.ts +23 -0
  58. package/app/lib/repo-handoff.ts +16 -0
  59. package/app/lib/repo-progressive.ts +119 -0
  60. package/app/lib/repo-select.test.ts +61 -0
  61. package/app/lib/repo-select.ts +74 -0
  62. package/app/lib/repo.tsx +1383 -987
  63. package/app/lib/role.ts +228 -0
  64. package/app/lib/route-catchall.test.ts +27 -0
  65. package/app/lib/route-repo-entry.test.ts +95 -0
  66. package/app/lib/route-repo-entry.ts +36 -0
  67. package/app/lib/router-contract.test.ts +22 -0
  68. package/app/lib/router-contract.ts +19 -0
  69. package/app/lib/shared-layout.test.ts +86 -0
  70. package/app/lib/shared-layout.ts +82 -0
  71. package/app/lib/status-bar.test.ts +118 -0
  72. package/app/lib/status-bar.ts +365 -128
  73. package/app/lib/sync-controls.test.ts +43 -0
  74. package/app/lib/sync-controls.tsx +303 -0
  75. package/app/lib/test-dom.ts +145 -0
  76. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  77. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  78. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  79. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  80. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  81. package/app/lib/transclusion-smoke.test.ts +163 -0
  82. package/app/lib/tutorial.ts +301 -0
  83. package/app/lib/version.ts +93 -0
  84. package/app/lib/viewport-culling.ts +740 -735
  85. package/app/lib/virtual-files.ts +456 -0
  86. package/app/lib/webgl-text.ts +189 -0
  87. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
  88. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  89. package/app/og-image.png +0 -0
  90. package/app/page.client.tsx +70 -269
  91. package/app/page.tsx +15 -16
  92. package/app/state/machine.js +13 -0
  93. package/package.json +84 -75
  94. package/server.ts +10 -0
  95. package/app/[owner]/[repo]/page.tsx +0 -6
  96. package/app/[slug]/page.tsx +0 -6
  97. package/packages/galaxydraw/README.md +0 -296
  98. package/packages/galaxydraw/banner.png +0 -0
  99. package/packages/galaxydraw/demo/build-static.ts +0 -100
  100. package/packages/galaxydraw/demo/client.ts +0 -154
  101. package/packages/galaxydraw/demo/dist/client.js +0 -8
  102. package/packages/galaxydraw/demo/index.html +0 -256
  103. package/packages/galaxydraw/demo/server.ts +0 -96
  104. package/packages/galaxydraw/dist/index.js +0 -984
  105. package/packages/galaxydraw/dist/index.js.map +0 -16
  106. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  109. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  110. package/packages/galaxydraw/package.json +0 -49
  111. package/packages/galaxydraw/perf.test.ts +0 -284
  112. package/packages/galaxydraw/src/core/cards.ts +0 -435
  113. package/packages/galaxydraw/src/core/engine.ts +0 -339
  114. package/packages/galaxydraw/src/core/events.ts +0 -81
  115. package/packages/galaxydraw/src/core/layout.ts +0 -136
  116. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  117. package/packages/galaxydraw/src/core/state.ts +0 -177
  118. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  119. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  120. package/packages/galaxydraw/src/index.ts +0 -40
  121. package/packages/galaxydraw/tsconfig.json +0 -30
@@ -1,16 +1,17 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * Positions — load/save card positions with dual storage:
4
- * - Server-side (SQLite via /api/auth/positions) when logged in
5
- * - localStorage fallback when not authenticated
3
+ * Positions — load/save card positions with role-based storage:
4
+ * - Leader (localhost): SQLite primary + localStorage backup
5
+ * - Follower (remote): Read-only from server
6
6
  *
7
- * Enables shared repositories: each user has their own card layout.
7
+ * Leader can push to remote servers for followers to view.
8
8
  */
9
- import { measure } from 'measure-fn';
10
- import type { CanvasContext } from './context';
11
- import { getUser } from './user';
9
+ import { measure } from "measure-fn";
10
+ import type { CanvasContext } from "./context";
11
+ import { getUser } from "./user";
12
+ import { isLeader, isFollower } from "./role";
12
13
 
13
- const STORAGE_PREFIX = 'gitcanvas:positions:';
14
+ const STORAGE_PREFIX = "gitcanvas:positions:";
14
15
 
15
16
  /** Debounce timer for batched saves */
16
17
  let _saveTimer: any = null;
@@ -18,159 +19,227 @@ const SAVE_DEBOUNCE_MS = 300;
18
19
 
19
20
  // ─── Get the localStorage key for the current repo ───────
20
21
  function getStorageKey(ctx: CanvasContext): string | null {
21
- const repoPath = ctx.snap?.()?.context?.repoPath;
22
- if (!repoPath) return null;
23
- return `${STORAGE_PREFIX}${repoPath}`;
22
+ const repoPath = ctx.snap?.()?.context?.repoPath;
23
+ if (!repoPath) return null;
24
+ return `${STORAGE_PREFIX}${repoPath}`;
24
25
  }
25
26
 
26
27
  function getRepoPath(ctx: CanvasContext): string | null {
27
- return ctx.snap?.()?.context?.repoPath || null;
28
+ return ctx.snap?.()?.context?.repoPath || null;
28
29
  }
29
30
 
30
31
  // ─── Load all saved positions ────────────────────────────
31
32
  export async function loadSavedPositions(ctx: CanvasContext) {
32
- return measure('positions:load', async () => {
33
+ return measure("positions:load", async () => {
34
+ try {
35
+ const repoPath = getRepoPath(ctx);
36
+ if (!repoPath) return;
37
+
38
+ // Clear old positions first to prevent stale data
39
+ ctx.positions = new Map();
40
+
41
+ if (isLeader()) {
42
+ // Leader: Try SQLite first, then localStorage backup
33
43
  try {
34
- const repoPath = getRepoPath(ctx);
35
- if (!repoPath) return;
36
-
37
- // Clear old positions first to prevent stale data from
38
- // a previously loaded repo bleeding into the new one
39
- ctx.positions = new Map();
40
-
41
- // Try server-side first (if logged in)
42
- const user = getUser();
43
- if (user) {
44
- try {
45
- const res = await fetch(`/api/auth/positions?repo=${encodeURIComponent(repoPath)}`);
46
- const data = await res.json();
47
- if (data.positions) {
48
- ctx.positions = new Map(Object.entries(data.positions));
49
- // Migrate legacy expanded state from separate localStorage key
50
- _migrateLegacyExpanded(ctx, repoPath);
51
- return;
52
- }
53
- } catch { /* fall through to localStorage */ }
44
+ const res = await fetch(
45
+ `/api/auth/positions?repo=${encodeURIComponent(repoPath)}`,
46
+ );
47
+ if (res.ok) {
48
+ const data = await res.json();
49
+ if (data.positions) {
50
+ ctx.positions = new Map(Object.entries(data.positions));
51
+ _migrateLegacyExpanded(ctx, repoPath);
52
+ return;
54
53
  }
54
+ }
55
+ } catch {
56
+ /* fall through to localStorage */
57
+ }
55
58
 
56
- // Fallback: localStorage
57
- const key = getStorageKey(ctx);
58
- if (!key) return;
59
- const raw = localStorage.getItem(key);
60
- if (raw) {
61
- const data = JSON.parse(raw);
62
- ctx.positions = new Map(Object.entries(data));
59
+ // Fallback: localStorage
60
+ const key = getStorageKey(ctx);
61
+ if (key) {
62
+ const raw = localStorage.getItem(key);
63
+ if (raw) {
64
+ const data = JSON.parse(raw);
65
+ ctx.positions = new Map(Object.entries(data));
66
+ }
67
+ }
68
+ _migrateLegacyExpanded(ctx, repoPath);
69
+ } else {
70
+ // Follower: Read-only from server
71
+ try {
72
+ const res = await fetch(
73
+ `/api/auth/positions?repo=${encodeURIComponent(repoPath)}&readonly=true`,
74
+ );
75
+ if (res.ok) {
76
+ const data = await res.json();
77
+ if (data.positions) {
78
+ ctx.positions = new Map(Object.entries(data.positions));
63
79
  }
64
-
65
- // Migrate legacy expanded state from separate localStorage key
66
- _migrateLegacyExpanded(ctx, repoPath);
80
+ }
67
81
  } catch (e) {
68
- measure('positions:loadError', () => e);
82
+ console.warn(
83
+ "[positions] Failed to load from server (follower mode)",
84
+ e,
85
+ );
69
86
  }
70
- });
87
+ }
88
+ } catch (e) {
89
+ measure("positions:loadError", () => e);
90
+ }
91
+ });
71
92
  }
72
93
 
73
94
  // ─── Persist all positions (debounced) ───────────────────
74
95
  export function flushPositions(ctx: CanvasContext) {
75
- const repoPath = getRepoPath(ctx);
76
- if (!repoPath) return;
96
+ const repoPath = getRepoPath(ctx);
97
+ if (!repoPath) return;
77
98
 
78
- const obj: Record<string, any> = {};
79
- for (const [k, v] of ctx.positions) {
80
- obj[k] = v;
81
- }
99
+ const obj: Record<string, any> = {};
100
+ for (const [k, v] of ctx.positions) {
101
+ obj[k] = v;
102
+ }
82
103
 
83
- // Always save to localStorage (instant)
104
+ if (isLeader()) {
105
+ // Leader: Save to localStorage + SQLite
84
106
  try {
85
- const key = getStorageKey(ctx);
86
- if (key) localStorage.setItem(key, JSON.stringify(obj));
87
- } catch { }
88
-
89
- // Also sync to server if logged in (async, fire-and-forget)
90
- const user = getUser();
91
- if (user) {
92
- fetch('/api/auth/positions', {
93
- method: 'POST',
94
- headers: { 'Content-Type': 'application/json' },
95
- body: JSON.stringify({ repoUrl: repoPath, positions: obj }),
96
- }).catch(() => { /* silent — localStorage is the safety net */ });
107
+ const key = getStorageKey(ctx);
108
+ if (key) localStorage.setItem(key, JSON.stringify(obj));
109
+ } catch {}
110
+
111
+ // Sync to local server (async, fire-and-forget)
112
+ fetch("/api/auth/positions", {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({ repoUrl: repoPath, positions: obj }),
116
+ }).catch(() => {
117
+ /* silent localStorage is the safety net */
118
+ });
119
+
120
+ // Auto-sync to remote server if enabled
121
+ try {
122
+ const { isAutoSyncEnabled, pushToServer } = require("./sync-controls");
123
+ if (isAutoSyncEnabled()) {
124
+ pushToServer(repoPath, obj).catch(() => {
125
+ /* silent */
126
+ });
127
+ }
128
+ } catch {
129
+ /* sync-controls not loaded or auto-sync disabled */
97
130
  }
131
+ }
132
+ // Follower: No saves allowed (read-only)
98
133
  }
99
134
 
100
135
  // ─── Save a single card position (debounced) ─────────────
101
- export async function savePosition(ctx: CanvasContext, commitHash: string, filePath: string, x?: number, y?: number, width?: number, height?: number) {
102
- return measure('positions:save', async () => {
103
- try {
104
- const posKey = `${commitHash}:${filePath}`;
105
- const existing = ctx.positions.get(posKey) || {};
106
- const newPos = {
107
- x: x !== undefined ? x : existing.x,
108
- y: y !== undefined ? y : existing.y,
109
- width: width !== undefined ? width : existing.width,
110
- height: height !== undefined ? height : existing.height,
111
- expanded: existing.expanded, // preserve expanded flag
112
- };
113
- ctx.positions.set(posKey, newPos);
114
-
115
- // Debounced persist — avoid hammering storage on every drag frame
116
- if (_saveTimer) clearTimeout(_saveTimer);
117
- _saveTimer = setTimeout(() => flushPositions(ctx), SAVE_DEBOUNCE_MS);
118
- } catch (e) {
119
- measure('positions:saveError', () => e);
120
- }
121
- });
136
+ export async function savePosition(
137
+ ctx: CanvasContext,
138
+ commitHash: string,
139
+ filePath: string,
140
+ x?: number,
141
+ y?: number,
142
+ width?: number,
143
+ height?: number,
144
+ immediate = false,
145
+ ) {
146
+ // Follower: No saves allowed
147
+ if (isFollower()) {
148
+ console.warn(
149
+ "[positions] Cannot save positions in follower mode (read-only)",
150
+ );
151
+ return;
152
+ }
153
+
154
+ return measure("positions:save", async () => {
155
+ try {
156
+ const posKey = `${commitHash}:${filePath}`;
157
+ const existing = ctx.positions.get(posKey) || {};
158
+ const newPos = {
159
+ x: x !== undefined ? x : existing.x,
160
+ y: y !== undefined ? y : existing.y,
161
+ width: width !== undefined ? width : existing.width,
162
+ height: height !== undefined ? height : existing.height,
163
+ expanded: existing.expanded,
164
+ };
165
+ ctx.positions.set(posKey, newPos);
166
+
167
+ // Debounced persist (or immediate if requested)
168
+ if (_saveTimer) clearTimeout(_saveTimer);
169
+ if (immediate) {
170
+ flushPositions(ctx);
171
+ } else {
172
+ _saveTimer = setTimeout(() => flushPositions(ctx), SAVE_DEBOUNCE_MS);
173
+ }
174
+ } catch (e) {
175
+ measure("positions:saveError", () => e);
176
+ }
177
+ });
178
+ }
179
+
180
+ // ─── Force immediate save (no debounce) ──────────────────
181
+ export function flushPositionsImmediate(ctx: CanvasContext) {
182
+ if (_saveTimer) clearTimeout(_saveTimer);
183
+ flushPositions(ctx);
122
184
  }
123
185
 
124
186
  // ─── Position key helper ─────────────────────────────────
125
187
  export function getPositionKey(filePath: string, commitHash: string): string {
126
- return `${commitHash}:${filePath}`;
188
+ return `${commitHash}:${filePath}`;
127
189
  }
128
190
 
129
191
  // ─── Expanded state (unified with positions) ─────────────
130
192
 
131
193
  /** Check if a file path is saved as expanded in positions */
132
- export function isPathExpandedInPositions(ctx: CanvasContext, filePath: string): boolean {
133
- // Check allfiles key (primary)
134
- const key = `allfiles:${filePath}`;
135
- const pos = ctx.positions.get(key);
136
- return !!(pos && pos.expanded);
194
+ export function isPathExpandedInPositions(
195
+ ctx: CanvasContext,
196
+ filePath: string,
197
+ ): boolean {
198
+ // Check allfiles key (primary)
199
+ const key = `allfiles:${filePath}`;
200
+ const pos = ctx.positions.get(key);
201
+ return !!(pos && pos.expanded);
137
202
  }
138
203
 
139
204
  /** Mark a file path as expanded or collapsed in positions */
140
- export function setPathExpandedInPositions(ctx: CanvasContext, filePath: string, expanded: boolean) {
141
- const key = `allfiles:${filePath}`;
142
- const existing = ctx.positions.get(key) || {};
143
- ctx.positions.set(key, { ...existing, expanded });
144
- // Trigger debounced persist
145
- if (_saveTimer) clearTimeout(_saveTimer);
146
- _saveTimer = setTimeout(() => flushPositions(ctx), SAVE_DEBOUNCE_MS);
205
+ export function setPathExpandedInPositions(
206
+ ctx: CanvasContext,
207
+ filePath: string,
208
+ expanded: boolean,
209
+ ) {
210
+ const key = `allfiles:${filePath}`;
211
+ const existing = ctx.positions.get(key) || {};
212
+ ctx.positions.set(key, { ...existing, expanded });
213
+ // Trigger debounced persist
214
+ if (_saveTimer) clearTimeout(_saveTimer);
215
+ _saveTimer = setTimeout(() => flushPositions(ctx), SAVE_DEBOUNCE_MS);
147
216
  }
148
217
 
149
218
  /** Migrate legacy expanded state from separate localStorage key into positions */
150
219
  function _migrateLegacyExpanded(ctx: CanvasContext, repoPath: string) {
151
- const legacyKey = `gitcanvas:expanded:${repoPath}`;
152
- try {
153
- const raw = localStorage.getItem(legacyKey);
154
- if (!raw) return;
155
- const paths: string[] = JSON.parse(raw);
156
- if (!Array.isArray(paths) || paths.length === 0) return;
157
-
158
- let migrated = 0;
159
- for (const filePath of paths) {
160
- const posKey = `allfiles:${filePath}`;
161
- const existing = ctx.positions.get(posKey) || {};
162
- if (!existing.expanded) {
163
- ctx.positions.set(posKey, { ...existing, expanded: true });
164
- migrated++;
165
- }
166
- }
220
+ const legacyKey = `gitcanvas:expanded:${repoPath}`;
221
+ try {
222
+ const raw = localStorage.getItem(legacyKey);
223
+ if (!raw) return;
224
+ const paths: string[] = JSON.parse(raw);
225
+ if (!Array.isArray(paths) || paths.length === 0) return;
226
+
227
+ let migrated = 0;
228
+ for (const filePath of paths) {
229
+ const posKey = `allfiles:${filePath}`;
230
+ const existing = ctx.positions.get(posKey) || {};
231
+ if (!existing.expanded) {
232
+ ctx.positions.set(posKey, { ...existing, expanded: true });
233
+ migrated++;
234
+ }
235
+ }
167
236
 
168
- if (migrated > 0) {
169
- // Persist immediately to save the migration
170
- flushPositions(ctx);
171
- }
237
+ if (migrated > 0) {
238
+ // Persist immediately to save the migration
239
+ flushPositions(ctx);
240
+ }
172
241
 
173
- // Remove the legacy key
174
- localStorage.removeItem(legacyKey);
175
- } catch { }
242
+ // Remove the legacy key
243
+ localStorage.removeItem(legacyKey);
244
+ } catch {}
176
245
  }