gitmaps 1.0.0 → 1.1.1

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 (145) hide show
  1. package/README.md +265 -122
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/[owner]/[repo]/page.client.tsx +5 -0
  5. package/app/[slug]/page.client.tsx +5 -0
  6. package/app/analytics.db +0 -0
  7. package/app/api/analytics/route.ts +64 -0
  8. package/app/api/auth/positions/route.ts +95 -33
  9. package/app/api/build-info/route.ts +19 -0
  10. package/app/api/chat/route.ts +13 -2
  11. package/app/api/manifest.json/route.ts +20 -0
  12. package/app/api/og-image/route.ts +14 -0
  13. package/app/api/pwa-icon/route.ts +14 -0
  14. package/app/api/repo/clone-stream/route.ts +20 -12
  15. package/app/api/repo/file-content/route.ts +73 -20
  16. package/app/api/repo/imports/route.ts +21 -3
  17. package/app/api/repo/list/route.ts +30 -0
  18. package/app/api/repo/load/route.test.ts +62 -0
  19. package/app/api/repo/load/route.ts +41 -1
  20. package/app/api/repo/pdf-thumb/route.ts +127 -0
  21. package/app/api/repo/resolve-slug/route.ts +51 -0
  22. package/app/api/repo/tree/route.ts +188 -104
  23. package/app/api/repo/upload/route.ts +6 -9
  24. package/app/api/sw.js/route.ts +70 -0
  25. package/app/api/version/route.ts +26 -0
  26. package/app/galaxy-canvas/page.client.tsx +2 -0
  27. package/app/galaxy-canvas/page.tsx +5 -0
  28. package/app/globals.css +5844 -4694
  29. package/app/icon.png +0 -0
  30. package/app/layout.tsx +1284 -467
  31. package/app/lib/auto-arrange.test.ts +158 -0
  32. package/app/lib/auto-arrange.ts +147 -0
  33. package/app/lib/canvas-export.ts +358 -358
  34. package/app/lib/canvas-text.ts +4 -72
  35. package/app/lib/canvas.ts +625 -564
  36. package/app/lib/card-arrangement.ts +21 -7
  37. package/app/lib/card-context-menu.tsx +2 -2
  38. package/app/lib/card-groups.ts +9 -2
  39. package/app/lib/cards.tsx +1361 -914
  40. package/app/lib/chat.tsx +65 -9
  41. package/app/lib/code-editor.ts +86 -2
  42. package/app/lib/connections.tsx +34 -43
  43. package/app/lib/context.test.ts +32 -0
  44. package/app/lib/context.ts +19 -3
  45. package/app/lib/cursor-sharing.ts +34 -0
  46. package/app/lib/events.tsx +76 -73
  47. package/app/lib/export-canvas.ts +287 -0
  48. package/app/lib/file-card-plugin.ts +148 -134
  49. package/app/lib/file-modal.tsx +49 -0
  50. package/app/lib/file-preview.ts +486 -400
  51. package/app/lib/github-import.test.ts +424 -0
  52. package/app/lib/global-search.ts +48 -27
  53. package/app/lib/initial-route-hydration.test.ts +283 -0
  54. package/app/lib/initial-route-hydration.ts +202 -0
  55. package/app/lib/landing-reset.test.ts +99 -0
  56. package/app/lib/landing-reset.ts +106 -0
  57. package/app/lib/landing-shell.test.ts +75 -0
  58. package/app/lib/large-repo-optimization.ts +37 -0
  59. package/app/lib/layers.tsx +17 -18
  60. package/app/lib/layout-snapshots.ts +320 -0
  61. package/app/lib/loading.test.ts +69 -0
  62. package/app/lib/loading.tsx +160 -45
  63. package/app/lib/mount-cleanup.test.ts +52 -0
  64. package/app/lib/mount-cleanup.ts +34 -0
  65. package/app/lib/mount-init.test.ts +123 -0
  66. package/app/lib/mount-init.ts +107 -0
  67. package/app/lib/mount-lifecycle.test.ts +39 -0
  68. package/app/lib/mount-lifecycle.ts +12 -0
  69. package/app/lib/mount-route-wiring.test.ts +87 -0
  70. package/app/lib/mount-route-wiring.ts +84 -0
  71. package/app/lib/multi-repo.ts +14 -0
  72. package/app/lib/onboarding-tutorial.ts +278 -0
  73. package/app/lib/perf-overlay.ts +78 -0
  74. package/app/lib/positions.ts +191 -122
  75. package/app/lib/recent-commits.test.ts +869 -0
  76. package/app/lib/recent-commits.ts +227 -0
  77. package/app/lib/repo-handoff.test.ts +23 -0
  78. package/app/lib/repo-handoff.ts +16 -0
  79. package/app/lib/repo-progressive.ts +119 -0
  80. package/app/lib/repo-select.test.ts +61 -0
  81. package/app/lib/repo-select.ts +74 -0
  82. package/app/lib/repo.tsx +1383 -977
  83. package/app/lib/role.ts +228 -0
  84. package/app/lib/route-catchall.test.ts +27 -0
  85. package/app/lib/route-repo-entry.test.ts +95 -0
  86. package/app/lib/route-repo-entry.ts +36 -0
  87. package/app/lib/router-contract.test.ts +22 -0
  88. package/app/lib/router-contract.ts +19 -0
  89. package/app/lib/shared-layout.test.ts +86 -0
  90. package/app/lib/shared-layout.ts +82 -0
  91. package/app/lib/shortcuts-panel.ts +2 -0
  92. package/app/lib/status-bar.test.ts +118 -0
  93. package/app/lib/status-bar.ts +365 -128
  94. package/app/lib/sync-controls.test.ts +43 -0
  95. package/app/lib/sync-controls.tsx +303 -0
  96. package/app/lib/test-dom.ts +145 -0
  97. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  98. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  99. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  100. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  101. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  102. package/app/lib/transclusion-smoke.test.ts +163 -0
  103. package/app/lib/tutorial.ts +301 -0
  104. package/app/lib/version.ts +93 -0
  105. package/app/lib/viewport-culling.ts +740 -728
  106. package/app/lib/virtual-files.ts +456 -0
  107. package/app/lib/webgl-text.ts +189 -0
  108. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -477
  109. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  110. package/app/og-image.png +0 -0
  111. package/app/page.client.tsx +70 -215
  112. package/app/page.tsx +27 -92
  113. package/app/state/machine.js +13 -0
  114. package/banner.png +0 -0
  115. package/package.json +17 -8
  116. package/server.ts +11 -1
  117. package/app/api/connections/route.ts +0 -72
  118. package/app/api/positions/route.ts +0 -80
  119. package/app/api/repo/browse/route.ts +0 -55
  120. package/app/lib/pr-review.ts +0 -374
  121. package/packages/galaxydraw/README.md +0 -296
  122. package/packages/galaxydraw/banner.png +0 -0
  123. package/packages/galaxydraw/demo/build-static.ts +0 -100
  124. package/packages/galaxydraw/demo/client.ts +0 -154
  125. package/packages/galaxydraw/demo/dist/client.js +0 -8
  126. package/packages/galaxydraw/demo/index.html +0 -256
  127. package/packages/galaxydraw/demo/server.ts +0 -96
  128. package/packages/galaxydraw/dist/index.js +0 -984
  129. package/packages/galaxydraw/dist/index.js.map +0 -16
  130. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  131. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  132. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  133. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  134. package/packages/galaxydraw/package.json +0 -49
  135. package/packages/galaxydraw/perf.test.ts +0 -284
  136. package/packages/galaxydraw/src/core/cards.ts +0 -435
  137. package/packages/galaxydraw/src/core/engine.ts +0 -339
  138. package/packages/galaxydraw/src/core/events.ts +0 -81
  139. package/packages/galaxydraw/src/core/layout.ts +0 -136
  140. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  141. package/packages/galaxydraw/src/core/state.ts +0 -177
  142. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  143. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  144. package/packages/galaxydraw/src/index.ts +0 -40
  145. package/packages/galaxydraw/tsconfig.json +0 -30
@@ -0,0 +1,84 @@
1
+ import type { CanvasContext } from './context';
2
+ import {
3
+ bootstrapInitialRouteUi,
4
+ handleInitialRouteError,
5
+ hideInitialRouteLanding,
6
+ hydrateInitialRouteRepo,
7
+ migrateLegacyHashRoute,
8
+ resolveInitialRepoPath,
9
+ showInitialRouteCloneStart,
10
+ } from './initial-route-hydration';
11
+ import { handlePopstateRepoEntry } from './route-repo-entry';
12
+
13
+ export async function wireMountRoutes(
14
+ ctx: CanvasContext,
15
+ options: {
16
+ isDisposed: () => boolean;
17
+ showLandingPlaceholder: () => void;
18
+ updateFavoriteStar: (path: string) => void;
19
+ applySharedLayout: () => Promise<void>;
20
+ hydrateRoutes?: typeof hydrateInitialRouteRepo;
21
+ resolveRepoPath?: typeof resolveInitialRepoPath;
22
+ handleRouteError?: typeof handleInitialRouteError;
23
+ bootstrapRepoUi?: typeof bootstrapInitialRouteUi;
24
+ bindPopstate?: typeof bindMountPopstate;
25
+ },
26
+ ) {
27
+ const hydrateRoutes = options.hydrateRoutes || hydrateInitialRouteRepo;
28
+ const resolveRepoPath = options.resolveRepoPath || resolveInitialRepoPath;
29
+ const handleRouteError = options.handleRouteError || handleInitialRouteError;
30
+ const bootstrapRepoUi = options.bootstrapRepoUi || bootstrapInitialRouteUi;
31
+ const bindPopstate = options.bindPopstate || bindMountPopstate;
32
+
33
+ await hydrateRoutes(ctx, {
34
+ disposed: options.isDisposed(),
35
+ showLandingPlaceholder: options.showLandingPlaceholder,
36
+ hideLanding: hideInitialRouteLanding,
37
+ migrateLegacyHashRoute,
38
+ resolveRepoPath: async (slug) => {
39
+ try {
40
+ return await resolveRepoPath(slug, {
41
+ onCloneStart: showInitialRouteCloneStart,
42
+ } as any);
43
+ } catch (err: any) {
44
+ return await handleRouteError(err);
45
+ }
46
+ },
47
+ bootstrapRepoUi: async (resolvedPath) => {
48
+ await bootstrapRepoUi(ctx, resolvedPath, {
49
+ disposed: options.isDisposed(),
50
+ applySharedLayout: options.applySharedLayout,
51
+ } as any);
52
+ },
53
+ updateFavoriteStar: options.updateFavoriteStar,
54
+ });
55
+
56
+ bindPopstate(ctx, {
57
+ isDisposed: options.isDisposed,
58
+ showLandingPlaceholder: options.showLandingPlaceholder,
59
+ updateFavoriteStar: options.updateFavoriteStar,
60
+ });
61
+ }
62
+
63
+ export function bindMountPopstate(
64
+ ctx: CanvasContext,
65
+ options: {
66
+ isDisposed: () => boolean;
67
+ showLandingPlaceholder: () => void;
68
+ updateFavoriteStar: (path: string) => void;
69
+ addListener?: (type: string, handler: () => void) => void;
70
+ },
71
+ ) {
72
+ const addListener = options.addListener || ((type: string, handler: () => void) => {
73
+ window.addEventListener(type, handler);
74
+ });
75
+
76
+ addListener('popstate', () => {
77
+ handlePopstateRepoEntry(ctx, {
78
+ disposed: options.isDisposed(),
79
+ currentRepoPath: ctx.snap().context.repoPath,
80
+ showLandingPlaceholder: options.showLandingPlaceholder,
81
+ updateFavoriteStar: options.updateFavoriteStar,
82
+ });
83
+ });
84
+ }
@@ -165,6 +165,20 @@ export function unloadRepo(ctx: CanvasContext, repoPath: string) {
165
165
  /**
166
166
  * Create repo zone tabs in the sidebar for switching between repos.
167
167
  */
168
+ export function clearMultiRepoWorkspace(ctx?: CanvasContext) {
169
+ for (const [, repo] of loadedRepos) {
170
+ if (repo.zoneLabel) repo.zoneLabel.remove();
171
+ }
172
+ loadedRepos.clear();
173
+ _activeRepoPath = null;
174
+
175
+ const container = document.getElementById('repoTabs');
176
+ if (container) {
177
+ container.innerHTML = '';
178
+ container.style.display = 'none';
179
+ }
180
+ }
181
+
168
182
  export function renderRepoTabs(ctx: CanvasContext) {
169
183
  const container = document.getElementById('repoTabs');
170
184
  if (!container) return;
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Interactive Onboarding Tutorial — Guide new users through GitMaps
3
+ *
4
+ * Features:
5
+ * - Step-by-step interactive tour
6
+ * - Highlights UI elements as it explains them
7
+ * - Keyboard shortcuts cheat sheet
8
+ * - Skip/resume anytime
9
+ * - Persists completion state
10
+ */
11
+
12
+ import type { CanvasContext } from './context';
13
+
14
+ export interface TutorialStep {
15
+ id: string;
16
+ title: string;
17
+ description: string;
18
+ highlightSelector: string;
19
+ position: 'top' | 'bottom' | 'left' | 'right';
20
+ action?: () => void;
21
+ }
22
+
23
+ const TUTORIAL_STEPS: TutorialStep[] = [
24
+ {
25
+ id: 'welcome',
26
+ title: 'Welcome to GitMaps! 🎉',
27
+ description: 'Explore codebases on an infinite canvas. Let\'s take a quick tour of the features.',
28
+ highlightSelector: '#app',
29
+ position: 'bottom',
30
+ },
31
+ {
32
+ id: 'repo-selector',
33
+ title: 'Repository Selector',
34
+ description: 'Select any loaded repository from the dropdown. Import new repos from GitHub with the button.',
35
+ highlightSelector: '#repoSelect',
36
+ position: 'bottom',
37
+ },
38
+ {
39
+ id: 'commit-timeline',
40
+ title: 'Commit Timeline',
41
+ description: 'Browse through commit history. Click any commit to see what changed. Use ← → arrow keys to navigate.',
42
+ highlightSelector: '#commitTimeline',
43
+ position: 'right',
44
+ },
45
+ {
46
+ id: 'canvas-area',
47
+ title: 'Infinite Canvas',
48
+ description: 'Your code lives here! Each file is a card. Pan with Space+Drag or middle-click. Scroll to zoom.',
49
+ highlightSelector: '#canvasViewport',
50
+ position: 'top',
51
+ },
52
+ {
53
+ id: 'file-cards',
54
+ title: 'File Cards',
55
+ description: 'Each card shows a file with code preview. Green/red markers show additions/deletions. Hover to see full preview.',
56
+ highlightSelector: '.file-card',
57
+ position: 'bottom',
58
+ },
59
+ {
60
+ id: 'minimap',
61
+ title: 'Minimap',
62
+ description: 'Never get lost! The minimap shows your entire canvas. Click to jump to any area.',
63
+ highlightSelector: '#minimap',
64
+ position: 'top',
65
+ },
66
+ {
67
+ id: 'arrange-toolbar',
68
+ title: 'Arrange Tools',
69
+ description: 'Organize cards with H (row), V (column), or G (grid). W fits all cards on screen.',
70
+ highlightSelector: '#arrangeToolbar',
71
+ position: 'bottom',
72
+ },
73
+ {
74
+ id: 'zoom-controls',
75
+ title: 'Zoom Controls',
76
+ description: 'Fine-tune zoom with the slider or +/- keys. Current zoom level shown in percentage.',
77
+ highlightSelector: '#zoomSlider',
78
+ position: 'top',
79
+ },
80
+ {
81
+ id: 'shortcuts',
82
+ title: 'Keyboard Shortcuts',
83
+ description: 'Press ? anytime to see all shortcuts. Power users love Ctrl+F (search), Ctrl+G (dependency graph), and Ctrl+O (find file).',
84
+ highlightSelector: '#hotkeyToggle',
85
+ position: 'bottom',
86
+ },
87
+ {
88
+ id: 'done',
89
+ title: 'You\'re Ready! 🚀',
90
+ description: 'Start exploring! Import a repo, arrange cards your way, and enjoy spatial code exploration.',
91
+ highlightSelector: '#app',
92
+ position: 'bottom',
93
+ action: () => {
94
+ localStorage.setItem('gitcanvas:onboardingComplete', 'true');
95
+ },
96
+ },
97
+ ];
98
+
99
+ let currentStep = 0;
100
+ let tutorialOverlay: HTMLElement | null = null;
101
+
102
+ /**
103
+ * Check if user has completed onboarding
104
+ */
105
+ export function hasCompletedOnboarding(): boolean {
106
+ return localStorage.getItem('gitcanvas:onboardingComplete') === 'true';
107
+ }
108
+
109
+ /**
110
+ * Reset onboarding progress
111
+ */
112
+ export function resetOnboarding(): void {
113
+ localStorage.removeItem('gitcanvas:onboardingComplete');
114
+ currentStep = 0;
115
+ }
116
+
117
+ /**
118
+ * Start the onboarding tutorial
119
+ */
120
+ export function startOnboarding(ctx: CanvasContext): void {
121
+ if (tutorialOverlay) return;
122
+
123
+ currentStep = 0;
124
+ showTutorialStep(ctx, currentStep);
125
+ }
126
+
127
+ /**
128
+ * Show a specific tutorial step
129
+ */
130
+ function showTutorialStep(ctx: CanvasContext, stepIndex: number): void {
131
+ if (stepIndex < 0 || stepIndex >= TUTORIAL_STEPS.length) {
132
+ hideTutorial();
133
+ return;
134
+ }
135
+
136
+ const step = TUTORIAL_STEPS[stepIndex];
137
+
138
+ // Create overlay if needed
139
+ if (!tutorialOverlay) {
140
+ tutorialOverlay = document.createElement('div');
141
+ tutorialOverlay.className = 'tutorial-overlay';
142
+ tutorialOverlay.innerHTML = `
143
+ <div class="tutorial-backdrop"></div>
144
+ <div class="tutorial-content">
145
+ <div class="tutorial-header">
146
+ <h3 class="tutorial-title"></h3>
147
+ <button class="tutorial-close" id="tutorialClose">×</button>
148
+ </div>
149
+ <div class="tutorial-description"></div>
150
+ <div class="tutorial-progress">
151
+ <span class="tutorial-step-count"></span>
152
+ </div>
153
+ <div class="tutorial-actions">
154
+ <button class="btn-ghost" id="tutorialSkip">Skip Tour</button>
155
+ <button class="btn-primary" id="tutorialNext">Next →</button>
156
+ </div>
157
+ </div>
158
+ `;
159
+
160
+ document.body.appendChild(tutorialOverlay);
161
+
162
+ // Add styles
163
+ const style = document.createElement('style');
164
+ style.textContent = `
165
+ .tutorial-overlay {
166
+ position: fixed;
167
+ inset: 0;
168
+ z-index: 99999;
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ animation: fadeIn 0.2s ease;
173
+ }
174
+ .tutorial-backdrop {
175
+ position: absolute;
176
+ inset: 0;
177
+ background: rgba(10, 10, 15, 0.85);
178
+ backdrop-filter: blur(4px);
179
+ }
180
+ .tutorial-content {
181
+ position: relative;
182
+ background: var(--bg-secondary);
183
+ border: 1px solid var(--border-primary);
184
+ border-radius: 12px;
185
+ padding: 24px;
186
+ max-width: 450px;
187
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
188
+ animation: slideUp 0.3s ease;
189
+ }
190
+ .tutorial-header {
191
+ display: flex;
192
+ align-items: center;
193
+ justify-content: space-between;
194
+ margin-bottom: 12px;
195
+ }
196
+ .tutorial-title {
197
+ margin: 0;
198
+ font-size: 18px;
199
+ font-weight: 600;
200
+ background: linear-gradient(135deg, #a78bfa, #60a5fa);
201
+ -webkit-background-clip: text;
202
+ -webkit-text-fill-color: transparent;
203
+ }
204
+ .tutorial-close {
205
+ background: none;
206
+ border: none;
207
+ color: var(--text-muted);
208
+ font-size: 24px;
209
+ cursor: pointer;
210
+ padding: 4px 8px;
211
+ border-radius: 4px;
212
+ transition: all 0.2s;
213
+ }
214
+ .tutorial-close:hover {
215
+ background: var(--bg-tertiary);
216
+ color: var(--text-primary);
217
+ }
218
+ .tutorial-description {
219
+ color: var(--text-primary);
220
+ font-size: 14px;
221
+ line-height: 1.6;
222
+ margin-bottom: 20px;
223
+ }
224
+ .tutorial-progress {
225
+ margin-bottom: 20px;
226
+ }
227
+ .tutorial-step-count {
228
+ font-size: 12px;
229
+ color: var(--text-muted);
230
+ }
231
+ .tutorial-actions {
232
+ display: flex;
233
+ gap: 8px;
234
+ justify-content: space-between;
235
+ }
236
+ .tutorial-actions button {
237
+ padding: 10px 20px;
238
+ border-radius: 8px;
239
+ border: none;
240
+ font-size: 13px;
241
+ font-weight: 600;
242
+ cursor: pointer;
243
+ transition: all 0.2s;
244
+ }
245
+ .btn-primary {
246
+ background: linear-gradient(135deg, #7c3aed, #3b82f6);
247
+ color: white;
248
+ }
249
+ .btn-primary:hover {
250
+ transform: translateY(-1px);
251
+ box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4);
252
+ }
253
+ @keyframes fadeIn {
254
+ from { opacity: 0; }
255
+ to { opacity: 1; }
256
+ }
257
+ @keyframes slideUp {
258
+ from { transform: translateY(20px); opacity: 0; }
259
+ to { transform: translateY(0); opacity: 1; }
260
+ }
261
+ `;
262
+ document.head.appendChild(style);
263
+
264
+ // Wire up buttons
265
+ tutorialOverlay.querySelector('#tutorialClose')?.addEventListener('click', hideTutorial);
266
+ tutorialOverlay.querySelector('#tutorialSkip')?.addEventListener('click', () => {
267
+ hideTutorial();
268
+ localStorage.setItem('gitcanvas:onboardingComplete', 'true');
269
+ });
270
+ tutorialOverlay.querySelector('#tutorialNext')?.addEventListener('click', () => {
271
+ const step = TUTORIAL_STEPS[currentStep];
272
+ if (step.action) step.action();
273
+ currentStep++;
274
+ showTutorialStep(ctx, currentStep);
275
+ });
276
+
277
+ // Keyboard navigation
278
+ document.addEventListener('keydown', ha
@@ -26,11 +26,22 @@ let _lastFpsTime = 0;
26
26
  let _currentFps = 0;
27
27
  let _fpsHistory: number[] = [];
28
28
  const FPS_HISTORY_LENGTH = 60; // 1 second of history at 60fps
29
+ let _lastFrameTime = 0; // ms per frame
29
30
 
30
31
  // DOM count tracking (expensive, sample every ~500ms)
31
32
  let _lastDomCount = 0;
32
33
  let _lastDomTime = 0;
33
34
 
35
+ // Render timing (external instrumentation can set these)
36
+ let _lastCullTimeMs = 0;
37
+ let _lastRenderTimeMs = 0;
38
+
39
+ /** Set cull/render timing from external code (viewport-culling, connections) */
40
+ export function reportRenderTiming(phase: 'cull' | 'render', ms: number) {
41
+ if (phase === 'cull') _lastCullTimeMs = ms;
42
+ else _lastRenderTimeMs = ms;
43
+ }
44
+
34
45
  // ── DOM Elements (cached) ──────────────────────────────────
35
46
  let _elFps: HTMLElement;
36
47
  let _elFpsBar: HTMLElement;
@@ -39,6 +50,10 @@ let _elDom: HTMLElement;
39
50
  let _elCards: HTMLElement;
40
51
  let _elZoom: HTMLElement;
41
52
  let _elMemory: HTMLElement;
53
+ let _elFrameTime: HTMLElement;
54
+ let _elConnections: HTMLElement;
55
+ let _elRenderBudget: HTMLElement;
56
+ let _elRenderBudgetBar: HTMLElement;
42
57
 
43
58
  /**
44
59
  * Creates the overlay DOM once.
@@ -91,6 +106,23 @@ function createOverlay(): HTMLElement {
91
106
  <div class="perf-label">Zoom</div>
92
107
  <div class="perf-value" id="perf-zoom">--</div>
93
108
  </div>
109
+ <div class="perf-stat">
110
+ <div class="perf-label">Frame</div>
111
+ <div class="perf-value" id="perf-frametime">--</div>
112
+ </div>
113
+ <div class="perf-stat">
114
+ <div class="perf-label">Lines</div>
115
+ <div class="perf-value" id="perf-connections">--</div>
116
+ </div>
117
+ </div>
118
+ <div class="perf-stat" style="margin-top:6px;">
119
+ <div class="perf-label">Render Budget</div>
120
+ <div style="display:flex;align-items:center;gap:6px">
121
+ <div class="perf-value" id="perf-render-budget" style="min-width:50px">--</div>
122
+ <div class="perf-bar-wrap" style="flex:1">
123
+ <div class="perf-bar" id="perf-render-budget-bar" style="width:0%;background:#22c55e;"></div>
124
+ </div>
125
+ </div>
94
126
  </div>
95
127
  <div class="perf-stat" style="margin-top:6px;">
96
128
  <div class="perf-label">Memory</div>
@@ -147,6 +179,10 @@ function createOverlay(): HTMLElement {
147
179
  _elCards = el.querySelector('#perf-cards')!;
148
180
  _elZoom = el.querySelector('#perf-zoom')!;
149
181
  _elMemory = el.querySelector('#perf-memory')!;
182
+ _elFrameTime = el.querySelector('#perf-frametime')!;
183
+ _elConnections = el.querySelector('#perf-connections')!;
184
+ _elRenderBudget = el.querySelector('#perf-render-budget')!;
185
+ _elRenderBudgetBar = el.querySelector('#perf-render-budget-bar')!;
150
186
 
151
187
  // Close button
152
188
  el.querySelector('#perf-close')!.addEventListener('click', () => togglePerfOverlay(_ctx!));
@@ -250,6 +286,17 @@ function drawFpsGraph() {
250
286
  function measureFrame(timestamp: number) {
251
287
  if (!_visible || !_ctx) return;
252
288
 
289
+ // Frame time (ms since last frame)
290
+ if (_lastFrameTime > 0) {
291
+ const frameMs = timestamp - _lastFrameTime;
292
+ const frameMsRounded = Math.round(frameMs * 10) / 10;
293
+ _elFrameTime.textContent = frameMsRounded + 'ms';
294
+ if (frameMs > 33) _elFrameTime.style.color = '#ef4444'; // < 30fps
295
+ else if (frameMs > 20) _elFrameTime.style.color = '#fbbf24'; // < 50fps
296
+ else _elFrameTime.style.color = '#e0e0f0';
297
+ }
298
+ _lastFrameTime = timestamp;
299
+
253
300
  _frameCount++;
254
301
 
255
302
  // Calculate FPS every 500ms
@@ -295,6 +342,16 @@ function measureFrame(timestamp: number) {
295
342
  _elCards.style.color = culled > 0 ? '#22c55e' : '#e0e0f0';
296
343
  }
297
344
 
345
+ // Connection line count
346
+ const svgLayer = _ctx.connectionLayer || document.querySelector('.connections-layer');
347
+ if (svgLayer) {
348
+ const lineCount = svgLayer.querySelectorAll('line, path').length;
349
+ _elConnections.textContent = lineCount.toLocaleString();
350
+ if (lineCount > 1000) _elConnections.style.color = '#ef4444';
351
+ else if (lineCount > 500) _elConnections.style.color = '#fbbf24';
352
+ else _elConnections.style.color = '#e0e0f0';
353
+ }
354
+
298
355
  // Zoom level
299
356
  if (_ctx.snap) {
300
357
  try {
@@ -304,6 +361,24 @@ function measureFrame(timestamp: number) {
304
361
  } catch (_) { }
305
362
  }
306
363
 
364
+ // Render budget: cull + render time vs 16.67ms target
365
+ const totalRenderMs = _lastCullTimeMs + _lastRenderTimeMs;
366
+ if (totalRenderMs > 0) {
367
+ const budgetPct = Math.min((totalRenderMs / 16.67) * 100, 100);
368
+ _elRenderBudget.textContent = totalRenderMs.toFixed(1) + 'ms';
369
+ _elRenderBudgetBar.style.width = budgetPct + '%';
370
+ if (totalRenderMs > 16.67) {
371
+ _elRenderBudget.style.color = '#ef4444';
372
+ _elRenderBudgetBar.style.background = '#ef4444';
373
+ } else if (totalRenderMs > 10) {
374
+ _elRenderBudget.style.color = '#fbbf24';
375
+ _elRenderBudgetBar.style.background = '#fbbf24';
376
+ } else {
377
+ _elRenderBudget.style.color = '#22c55e';
378
+ _elRenderBudgetBar.style.background = '#22c55e';
379
+ }
380
+ }
381
+
307
382
  // Memory (Chrome only)
308
383
  const perf = (performance as any);
309
384
  if (perf.memory) {
@@ -352,6 +427,9 @@ export function togglePerfOverlay(ctx: CanvasContext) {
352
427
  export function setupPerfOverlay(ctx: CanvasContext) {
353
428
  _ctx = ctx;
354
429
  window.addEventListener('keydown', (e: KeyboardEvent) => {
430
+ // Don't steal Shift+P from text inputs
431
+ const tag = (e.target as HTMLElement)?.tagName;
432
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) return;
355
433
  if (e.shiftKey && e.key === 'P') {
356
434
  e.preventDefault();
357
435
  togglePerfOverlay(ctx);