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,213 @@
1
+ import { render } from 'melina/client';
2
+ import type { CanvasContext } from './context';
3
+ import { updateCanvasTransform } from './canvas';
4
+
5
+ const steps = [
6
+ {
7
+ title: "Welcome to GitMaps ✨",
8
+ text: "Let's take a quick tour to help you navigate your code visually.",
9
+ highlightId: null,
10
+ position: 'center'
11
+ },
12
+ {
13
+ title: "Pan & Zoom 🗺️",
14
+ text: "Drag the background or hold Space to pan around. Scroll your mouse wheel or use the slider below to zoom in and out.",
15
+ highlightId: 'zoomSlider',
16
+ position: 'bottom'
17
+ },
18
+ {
19
+ title: "Organize Cards 🗂️",
20
+ text: "You can drag any file card by its header to organize your workspace. The layout saves automatically.",
21
+ highlightId: null,
22
+ position: 'center'
23
+ },
24
+ {
25
+ title: "Draw Connections 🔗",
26
+ text: "Hold Shift, click a specific line of code, and drag to another file to create a lasting connection.",
27
+ highlightId: 'toggleConnections',
28
+ position: 'top-left'
29
+ },
30
+ {
31
+ title: "Layers & Focus 🥞",
32
+ text: "Group files into Layers to filter your view and focus on specific subsystems without clutter.",
33
+ highlightId: 'layersBarContainer',
34
+ position: 'top-right'
35
+ },
36
+ {
37
+ title: "You're all set! 🚀",
38
+ text: "Use the arrangement tools on the right to tidy up. Happy exploring!",
39
+ highlightId: 'arrangeGrid',
40
+ position: 'right'
41
+ }
42
+ ];
43
+
44
+ export function startOnboarding(ctx: CanvasContext) {
45
+ if (document.getElementById('onboardingOverlay')) return;
46
+
47
+ localStorage.setItem('gitcanvas:onboarded', 'true');
48
+
49
+ const overlay = document.createElement('div');
50
+ overlay.id = 'onboardingOverlay';
51
+ overlay.className = 'onboarding-overlay';
52
+ document.body.appendChild(overlay);
53
+
54
+ let currentStep = 0;
55
+
56
+ function renderStep() {
57
+ const step = steps[currentStep];
58
+
59
+ // Clear previous highlights
60
+ document.querySelectorAll('.onboarding-highlight').forEach(el => {
61
+ el.classList.remove('onboarding-highlight');
62
+ });
63
+
64
+ if (step.highlightId) {
65
+ const el = document.getElementById(step.highlightId);
66
+ if (el) el.classList.add('onboarding-highlight');
67
+ }
68
+
69
+ render(
70
+ <div className={`onboarding-modal pos-${step.position}`}>
71
+ <h3>{step.title}</h3>
72
+ <p>{step.text}</p>
73
+ <div className="onboarding-controls">
74
+ <div className="onboarding-dots">
75
+ {steps.map((s, i) => (
76
+ <span className={i === currentStep ? 'active' : ''} key={i}></span>
77
+ ))}
78
+ </div>
79
+ <div className="onboarding-buttons">
80
+ {currentStep < steps.length - 1 ? (
81
+ <>
82
+ <button className="btn-secondary" onClick={closeOnboarding}>Skip</button>
83
+ <button className="btn-primary" onClick={nextStep}>Next</button>
84
+ </>
85
+ ) : (
86
+ <button className="btn-primary" onClick={closeOnboarding}>Get Started</button>
87
+ )}
88
+ </div>
89
+ </div>
90
+ </div>,
91
+ overlay
92
+ );
93
+ }
94
+
95
+ function nextStep() {
96
+ if (currentStep < steps.length - 1) {
97
+ currentStep++;
98
+ renderStep();
99
+ }
100
+ }
101
+
102
+ function closeOnboarding() {
103
+ document.querySelectorAll('.onboarding-highlight').forEach(el => {
104
+ el.classList.remove('onboarding-highlight');
105
+ });
106
+ render(null, overlay);
107
+ overlay.remove();
108
+
109
+ // After onboarding is done, quickly focus on the canvas center
110
+ // so they don't get lost
111
+ const state = ctx.snap().context;
112
+ ctx.actor.send({ type: 'SET_ZOOM', zoom: 1 });
113
+ ctx.actor.send({ type: 'SET_OFFSET', x: 0, y: 0 });
114
+ updateCanvasTransform(ctx);
115
+ }
116
+
117
+ // Include basic styles for the onboarding inline
118
+ const styleId = 'onboardingStyles';
119
+ if (!document.getElementById(styleId)) {
120
+ const style = document.createElement('style');
121
+ style.id = styleId;
122
+ style.innerHTML = `
123
+ .onboarding-overlay {
124
+ position: fixed;
125
+ top: 0; left: 0; right: 0; bottom: 0;
126
+ background: rgba(0,0,0,0.6);
127
+ z-index: 99999;
128
+ display: flex;
129
+ pointer-events: all;
130
+ }
131
+ .onboarding-modal {
132
+ background: var(--bg-secondary);
133
+ border: 1px solid var(--border-color);
134
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
135
+ border-radius: 12px;
136
+ padding: 24px;
137
+ width: 360px;
138
+ position: absolute;
139
+ animation: slide-up 0.3s ease-out forwards;
140
+ color: var(--text-primary);
141
+ }
142
+ .onboarding-modal h3 {
143
+ margin: 0 0 12px 0;
144
+ font-size: 1.25rem;
145
+ color: var(--accent-primary);
146
+ }
147
+ .onboarding-modal p {
148
+ margin: 0 0 24px 0;
149
+ font-size: 0.95rem;
150
+ line-height: 1.5;
151
+ color: var(--text-secondary);
152
+ }
153
+ .onboarding-controls {
154
+ display: flex;
155
+ justify-content: space-between;
156
+ align-items: center;
157
+ }
158
+ .onboarding-dots span {
159
+ display: inline-block;
160
+ width: 8px; height: 8px;
161
+ background: var(--border-color);
162
+ border-radius: 50%;
163
+ margin-right: 6px;
164
+ transition: background 0.2s;
165
+ }
166
+ .onboarding-dots span.active {
167
+ background: var(--accent-primary);
168
+ }
169
+ .onboarding-buttons {
170
+ display: flex;
171
+ gap: 12px;
172
+ }
173
+ .onboarding-buttons .btn-secondary {
174
+ background: transparent;
175
+ border: none;
176
+ color: var(--text-muted);
177
+ cursor: pointer;
178
+ }
179
+ .onboarding-buttons .btn-secondary:hover {
180
+ color: var(--text-primary);
181
+ }
182
+ .onboarding-buttons .btn-primary {
183
+ background: var(--accent-primary);
184
+ color: white;
185
+ border: none;
186
+ padding: 8px 16px;
187
+ border-radius: 6px;
188
+ cursor: pointer;
189
+ font-weight: 500;
190
+ }
191
+ .onboarding-buttons .btn-primary:hover {
192
+ filter: brightness(1.1);
193
+ }
194
+ .pos-center { top: 50%; left: 50%; transform: translate(-50%, -50%); }
195
+ .pos-bottom { bottom: 80px; left: 50%; transform: translateX(-50%); }
196
+ .pos-top-left { top: 80px; left: 24px; }
197
+ .pos-top-right { top: 80px; right: 24px; }
198
+ .pos-right { top: 50%; right: 80px; transform: translateY(-50%); }
199
+
200
+ .onboarding-highlight {
201
+ position: relative;
202
+ z-index: 100000;
203
+ box-shadow: 0 0 0 4px var(--accent-primary), 0 0 20px rgba(99, 102, 241, 0.5) !important;
204
+ border-radius: 4px;
205
+ transition: all 0.3s;
206
+ background: var(--bg-secondary);
207
+ }
208
+ `;
209
+ document.head.appendChild(style);
210
+ }
211
+
212
+ renderStep();
213
+ }
@@ -0,0 +1,360 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Performance measurement overlay — toggled with Shift+P.
4
+ *
5
+ * Shows a floating HUD with real-time metrics:
6
+ * - FPS (frames per second) with color-coded indicator
7
+ * - DOM node count (total elements in document)
8
+ * - Visible vs culled card count
9
+ * - Current zoom level
10
+ * - Memory usage (if available)
11
+ *
12
+ * Zero overhead when hidden — the rAF loop only runs when visible.
13
+ */
14
+ import type { CanvasContext } from './context';
15
+ import { performViewportCulling } from './viewport-culling';
16
+
17
+ // ── State ──────────────────────────────────────────────────
18
+ let _overlay: HTMLElement | null = null;
19
+ let _visible = false;
20
+ let _rafId: number | null = null;
21
+ let _ctx: CanvasContext | null = null;
22
+
23
+ // FPS tracking
24
+ let _frameCount = 0;
25
+ let _lastFpsTime = 0;
26
+ let _currentFps = 0;
27
+ let _fpsHistory: number[] = [];
28
+ const FPS_HISTORY_LENGTH = 60; // 1 second of history at 60fps
29
+
30
+ // DOM count tracking (expensive, sample every ~500ms)
31
+ let _lastDomCount = 0;
32
+ let _lastDomTime = 0;
33
+
34
+ // ── DOM Elements (cached) ──────────────────────────────────
35
+ let _elFps: HTMLElement;
36
+ let _elFpsBar: HTMLElement;
37
+ let _elFpsGraph: HTMLCanvasElement;
38
+ let _elDom: HTMLElement;
39
+ let _elCards: HTMLElement;
40
+ let _elZoom: HTMLElement;
41
+ let _elMemory: HTMLElement;
42
+
43
+ /**
44
+ * Creates the overlay DOM once.
45
+ */
46
+ function createOverlay(): HTMLElement {
47
+ const el = document.createElement('div');
48
+ el.id = 'perf-overlay';
49
+ el.style.cssText = `
50
+ position: fixed;
51
+ top: 60px;
52
+ right: 16px;
53
+ z-index: 10000;
54
+ width: 220px;
55
+ background: rgba(6, 6, 18, 0.92);
56
+ backdrop-filter: blur(16px);
57
+ border: 1px solid rgba(128, 128, 200, 0.15);
58
+ border-radius: 12px;
59
+ padding: 14px 16px;
60
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
61
+ font-size: 11px;
62
+ color: #a0a0cc;
63
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 20px rgba(124, 58, 237, 0.08);
64
+ pointer-events: auto;
65
+ user-select: none;
66
+ display: none;
67
+ line-height: 1.4;
68
+ `;
69
+
70
+ el.innerHTML = `
71
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
72
+ <span style="font-weight:700;font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:#7c7cb0;">⚡ Performance</span>
73
+ <span id="perf-close" style="cursor:pointer;color:#5a5a7a;font-size:14px;line-height:1;" title="Close (Shift+P)">✕</span>
74
+ </div>
75
+ <canvas id="perf-fps-graph" width="376" height="60" style="width:188px;height:30px;border-radius:6px;background:rgba(0,0,0,0.3);margin-bottom:8px;display:block;"></canvas>
76
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;">
77
+ <div class="perf-stat">
78
+ <div class="perf-label">FPS</div>
79
+ <div class="perf-value" id="perf-fps">--</div>
80
+ <div class="perf-bar-wrap"><div class="perf-bar" id="perf-fps-bar" style="width:0%;background:#22c55e;"></div></div>
81
+ </div>
82
+ <div class="perf-stat">
83
+ <div class="perf-label">DOM Nodes</div>
84
+ <div class="perf-value" id="perf-dom">--</div>
85
+ </div>
86
+ <div class="perf-stat">
87
+ <div class="perf-label">Cards</div>
88
+ <div class="perf-value" id="perf-cards">--</div>
89
+ </div>
90
+ <div class="perf-stat">
91
+ <div class="perf-label">Zoom</div>
92
+ <div class="perf-value" id="perf-zoom">--</div>
93
+ </div>
94
+ </div>
95
+ <div class="perf-stat" style="margin-top:6px;">
96
+ <div class="perf-label">Memory</div>
97
+ <div class="perf-value" id="perf-memory">--</div>
98
+ </div>
99
+ `;
100
+
101
+ // Inject scoped styles
102
+ const style = document.createElement('style');
103
+ style.textContent = `
104
+ #perf-overlay .perf-stat {
105
+ background: rgba(255,255,255,0.03);
106
+ border-radius: 8px;
107
+ padding: 6px 10px;
108
+ border: 1px solid rgba(128,128,200,0.06);
109
+ }
110
+ #perf-overlay .perf-label {
111
+ font-size: 9px;
112
+ text-transform: uppercase;
113
+ letter-spacing: 0.08em;
114
+ color: #5a5a7a;
115
+ margin-bottom: 2px;
116
+ }
117
+ #perf-overlay .perf-value {
118
+ font-size: 14px;
119
+ font-weight: 700;
120
+ color: #e0e0f0;
121
+ }
122
+ #perf-overlay .perf-bar-wrap {
123
+ height: 3px;
124
+ background: rgba(255,255,255,0.05);
125
+ border-radius: 2px;
126
+ margin-top: 4px;
127
+ overflow: hidden;
128
+ }
129
+ #perf-overlay .perf-bar {
130
+ height: 100%;
131
+ border-radius: 2px;
132
+ transition: width 0.3s ease, background 0.3s ease;
133
+ }
134
+ #perf-overlay #perf-close:hover {
135
+ color: #ef4444;
136
+ }
137
+ `;
138
+ document.head.appendChild(style);
139
+
140
+ document.body.appendChild(el);
141
+
142
+ // Cache element refs
143
+ _elFps = el.querySelector('#perf-fps')!;
144
+ _elFpsBar = el.querySelector('#perf-fps-bar')!;
145
+ _elFpsGraph = el.querySelector('#perf-fps-graph')! as HTMLCanvasElement;
146
+ _elDom = el.querySelector('#perf-dom')!;
147
+ _elCards = el.querySelector('#perf-cards')!;
148
+ _elZoom = el.querySelector('#perf-zoom')!;
149
+ _elMemory = el.querySelector('#perf-memory')!;
150
+
151
+ // Close button
152
+ el.querySelector('#perf-close')!.addEventListener('click', () => togglePerfOverlay(_ctx!));
153
+
154
+ // Make draggable
155
+ let isDragging = false;
156
+ let dX = 0, dY = 0;
157
+ const header = el.querySelector('div')! as HTMLElement;
158
+ header.style.cursor = 'grab';
159
+ header.addEventListener('pointerdown', (e: PointerEvent) => {
160
+ isDragging = true;
161
+ dX = e.clientX - el.offsetLeft;
162
+ dY = e.clientY - el.offsetTop;
163
+ header.style.cursor = 'grabbing';
164
+ e.preventDefault();
165
+ });
166
+ window.addEventListener('pointermove', (e: PointerEvent) => {
167
+ if (!isDragging) return;
168
+ el.style.right = 'auto';
169
+ el.style.left = (e.clientX - dX) + 'px';
170
+ el.style.top = (e.clientY - dY) + 'px';
171
+ });
172
+ window.addEventListener('pointerup', () => {
173
+ isDragging = false;
174
+ header.style.cursor = 'grab';
175
+ });
176
+
177
+ return el;
178
+ }
179
+
180
+ /**
181
+ * Gets the FPS color based on performance level.
182
+ */
183
+ function fpsColor(fps: number): string {
184
+ if (fps >= 55) return '#22c55e'; // green — great
185
+ if (fps >= 40) return '#fbbf24'; // amber — okay
186
+ if (fps >= 25) return '#f97316'; // orange — struggling
187
+ return '#ef4444'; // red — bad
188
+ }
189
+
190
+ /**
191
+ * Draws the FPS sparkline graph.
192
+ */
193
+ function drawFpsGraph() {
194
+ const ctx = _elFpsGraph.getContext('2d')!;
195
+ const w = _elFpsGraph.width;
196
+ const h = _elFpsGraph.height;
197
+ ctx.clearRect(0, 0, w, h);
198
+
199
+ if (_fpsHistory.length < 2) return;
200
+
201
+ const max = 65;
202
+ const step = w / (FPS_HISTORY_LENGTH - 1);
203
+
204
+ // Area fill
205
+ ctx.beginPath();
206
+ ctx.moveTo(0, h);
207
+ _fpsHistory.forEach((fps, i) => {
208
+ const x = i * step;
209
+ const y = h - (Math.min(fps, max) / max) * h;
210
+ if (i === 0) ctx.lineTo(x, y);
211
+ else ctx.lineTo(x, y);
212
+ });
213
+ ctx.lineTo((_fpsHistory.length - 1) * step, h);
214
+ ctx.closePath();
215
+
216
+ const grad = ctx.createLinearGradient(0, 0, 0, h);
217
+ const color = fpsColor(_currentFps);
218
+ grad.addColorStop(0, color + '40');
219
+ grad.addColorStop(1, color + '05');
220
+ ctx.fillStyle = grad;
221
+ ctx.fill();
222
+
223
+ // Line
224
+ ctx.beginPath();
225
+ _fpsHistory.forEach((fps, i) => {
226
+ const x = i * step;
227
+ const y = h - (Math.min(fps, max) / max) * h;
228
+ if (i === 0) ctx.moveTo(x, y);
229
+ else ctx.lineTo(x, y);
230
+ });
231
+ ctx.strokeStyle = color;
232
+ ctx.lineWidth = 2;
233
+ ctx.stroke();
234
+
235
+ // 60fps target line
236
+ const targetY = h - (60 / max) * h;
237
+ ctx.strokeStyle = 'rgba(128, 128, 200, 0.15)';
238
+ ctx.lineWidth = 1;
239
+ ctx.setLineDash([4, 4]);
240
+ ctx.beginPath();
241
+ ctx.moveTo(0, targetY);
242
+ ctx.lineTo(w, targetY);
243
+ ctx.stroke();
244
+ ctx.setLineDash([]);
245
+ }
246
+
247
+ /**
248
+ * The main measurement loop — runs only when overlay is visible.
249
+ */
250
+ function measureFrame(timestamp: number) {
251
+ if (!_visible || !_ctx) return;
252
+
253
+ _frameCount++;
254
+
255
+ // Calculate FPS every 500ms
256
+ if (timestamp - _lastFpsTime >= 500) {
257
+ _currentFps = Math.round((_frameCount * 1000) / (timestamp - _lastFpsTime));
258
+ _frameCount = 0;
259
+ _lastFpsTime = timestamp;
260
+
261
+ // Update FPS display
262
+ const color = fpsColor(_currentFps);
263
+ _elFps.textContent = _currentFps.toString();
264
+ _elFps.style.color = color;
265
+ _elFpsBar.style.width = Math.min((_currentFps / 60) * 100, 100) + '%';
266
+ _elFpsBar.style.background = color;
267
+
268
+ // FPS history for graph
269
+ _fpsHistory.push(_currentFps);
270
+ if (_fpsHistory.length > FPS_HISTORY_LENGTH) _fpsHistory.shift();
271
+ drawFpsGraph();
272
+ }
273
+
274
+ // Sample DOM count every ~1s (expensive operation)
275
+ if (timestamp - _lastDomTime >= 1000) {
276
+ _lastDomCount = document.querySelectorAll('*').length;
277
+ _lastDomTime = timestamp;
278
+ _elDom.textContent = _lastDomCount.toLocaleString();
279
+
280
+ // Color code DOM count
281
+ if (_lastDomCount > 10000) _elDom.style.color = '#ef4444';
282
+ else if (_lastDomCount > 5000) _elDom.style.color = '#fbbf24';
283
+ else _elDom.style.color = '#e0e0f0';
284
+ }
285
+
286
+ // Cards visible/culled (cheap — read from existing state)
287
+ if (_ctx.fileCards) {
288
+ const total = _ctx.fileCards.size;
289
+ let culled = 0;
290
+ for (const [, card] of _ctx.fileCards) {
291
+ if (card.dataset.culled === 'true') culled++;
292
+ }
293
+ const visible = total - culled;
294
+ _elCards.textContent = `${visible}/${total}`;
295
+ _elCards.style.color = culled > 0 ? '#22c55e' : '#e0e0f0';
296
+ }
297
+
298
+ // Zoom level
299
+ if (_ctx.snap) {
300
+ try {
301
+ const state = _ctx.snap().context;
302
+ const zoomPct = Math.round(state.zoom * 100);
303
+ _elZoom.textContent = zoomPct + '%';
304
+ } catch (_) { }
305
+ }
306
+
307
+ // Memory (Chrome only)
308
+ const perf = (performance as any);
309
+ if (perf.memory) {
310
+ const usedMB = Math.round(perf.memory.usedJSHeapSize / 1048576);
311
+ const totalMB = Math.round(perf.memory.jsHeapSizeLimit / 1048576);
312
+ _elMemory.textContent = `${usedMB}MB / ${totalMB}MB`;
313
+ if (usedMB > totalMB * 0.8) _elMemory.style.color = '#ef4444';
314
+ else if (usedMB > totalMB * 0.5) _elMemory.style.color = '#fbbf24';
315
+ else _elMemory.style.color = '#e0e0f0';
316
+ } else {
317
+ _elMemory.textContent = 'N/A';
318
+ }
319
+
320
+ _rafId = requestAnimationFrame(measureFrame);
321
+ }
322
+
323
+ /**
324
+ * Toggle the performance overlay visibility.
325
+ */
326
+ export function togglePerfOverlay(ctx: CanvasContext) {
327
+ _ctx = ctx;
328
+ if (!_overlay) _overlay = createOverlay();
329
+
330
+ _visible = !_visible;
331
+
332
+ if (_visible) {
333
+ _overlay.style.display = 'block';
334
+ _frameCount = 0;
335
+ _lastFpsTime = performance.now();
336
+ _lastDomTime = 0;
337
+ _fpsHistory = [];
338
+ _rafId = requestAnimationFrame(measureFrame);
339
+ } else {
340
+ _overlay.style.display = 'none';
341
+ if (_rafId) {
342
+ cancelAnimationFrame(_rafId);
343
+ _rafId = null;
344
+ }
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Setup the Shift+P keyboard shortcut.
350
+ * Call this once during app initialization.
351
+ */
352
+ export function setupPerfOverlay(ctx: CanvasContext) {
353
+ _ctx = ctx;
354
+ window.addEventListener('keydown', (e: KeyboardEvent) => {
355
+ if (e.shiftKey && e.key === 'P') {
356
+ e.preventDefault();
357
+ togglePerfOverlay(ctx);
358
+ }
359
+ });
360
+ }