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.
- package/README.md +167 -0
- package/app/api/auth/favorites/route.ts +56 -0
- package/app/api/auth/github/callback/route.ts +103 -0
- package/app/api/auth/github/route.ts +32 -0
- package/app/api/auth/me/route.ts +52 -0
- package/app/api/auth/positions/route.ts +50 -0
- package/app/api/chat/route.ts +101 -0
- package/app/api/connections/route.ts +72 -0
- package/app/api/github/repos/route.ts +111 -0
- package/app/api/positions/route.ts +80 -0
- package/app/api/repo/branch-diff/route.ts +201 -0
- package/app/api/repo/branches/route.ts +53 -0
- package/app/api/repo/browse/route.ts +55 -0
- package/app/api/repo/clone/route.ts +78 -0
- package/app/api/repo/clone-stream/route.ts +131 -0
- package/app/api/repo/file-content/route.ts +28 -0
- package/app/api/repo/file-delete/route.ts +62 -0
- package/app/api/repo/file-history/route.ts +45 -0
- package/app/api/repo/file-rename/route.ts +83 -0
- package/app/api/repo/file-save/route.ts +45 -0
- package/app/api/repo/files/route.ts +169 -0
- package/app/api/repo/git-blame/route.ts +86 -0
- package/app/api/repo/git-commit/route.ts +40 -0
- package/app/api/repo/git-heatmap/route.ts +55 -0
- package/app/api/repo/imports/route.ts +154 -0
- package/app/api/repo/load/route.ts +56 -0
- package/app/api/repo/mode/route.ts +14 -0
- package/app/api/repo/search/route.ts +127 -0
- package/app/api/repo/tree/route.ts +104 -0
- package/app/api/repo/upload/route.ts +53 -0
- package/app/api/repo/validate-path.ts +53 -0
- package/app/canvas_users.db +0 -0
- package/app/canvas_users.db-shm +0 -0
- package/app/canvas_users.db-wal +0 -0
- package/app/globals.css +7899 -0
- package/app/layout.tsx +493 -0
- package/app/lib/auth.ts +193 -0
- package/app/lib/auto-save.ts +137 -0
- package/app/lib/branch-compare.ts +443 -0
- package/app/lib/breadcrumbs.ts +170 -0
- package/app/lib/canvas-export.ts +358 -0
- package/app/lib/canvas-text.ts +912 -0
- package/app/lib/canvas.ts +564 -0
- package/app/lib/card-arrangement.ts +188 -0
- package/app/lib/card-context-menu.tsx +453 -0
- package/app/lib/card-diff-markers.ts +270 -0
- package/app/lib/card-expand.ts +189 -0
- package/app/lib/card-groups.ts +246 -0
- package/app/lib/cards.tsx +914 -0
- package/app/lib/chat.tsx +308 -0
- package/app/lib/code-editor.ts +508 -0
- package/app/lib/command-palette.ts +262 -0
- package/app/lib/connections.tsx +1037 -0
- package/app/lib/context.ts +94 -0
- package/app/lib/cursor-sharing.ts +281 -0
- package/app/lib/dependency-graph.ts +438 -0
- package/app/lib/events.tsx +1747 -0
- package/app/lib/file-card-plugin.ts +134 -0
- package/app/lib/file-modal.tsx +849 -0
- package/app/lib/file-preview.ts +400 -0
- package/app/lib/file-tabs.ts +318 -0
- package/app/lib/galaxydraw-bridge.ts +477 -0
- package/app/lib/galaxydraw.test.ts +229 -0
- package/app/lib/global-search.ts +264 -0
- package/app/lib/goto-definition.ts +224 -0
- package/app/lib/heatmap.ts +178 -0
- package/app/lib/hidden-files.tsx +222 -0
- package/app/lib/layers.ts +0 -0
- package/app/lib/layers.tsx +365 -0
- package/app/lib/loading.tsx +45 -0
- package/app/lib/multi-repo.ts +286 -0
- package/app/lib/new-file-dialog.tsx +230 -0
- package/app/lib/onboarding.tsx +213 -0
- package/app/lib/perf-overlay.ts +360 -0
- package/app/lib/positions.ts +176 -0
- package/app/lib/pr-review.ts +374 -0
- package/app/lib/production-mode.ts +47 -0
- package/app/lib/repo.tsx +977 -0
- package/app/lib/settings-modal.tsx +374 -0
- package/app/lib/settings.ts +97 -0
- package/app/lib/shortcuts-panel.ts +141 -0
- package/app/lib/status-bar.ts +128 -0
- package/app/lib/symbol-outline.ts +212 -0
- package/app/lib/syntax.ts +177 -0
- package/app/lib/tab-diff.ts +238 -0
- package/app/lib/user.tsx +133 -0
- package/app/lib/utils.ts +78 -0
- package/app/lib/viewport-culling.ts +728 -0
- package/app/page.client.tsx +215 -0
- package/app/page.tsx +291 -0
- package/app/state/machine.js +196 -0
- package/app/styles/main.css +2168 -0
- package/banner.png +0 -0
- package/cli.ts +44 -0
- package/package.json +75 -0
- package/packages/galaxydraw/README.md +296 -0
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +100 -0
- package/packages/galaxydraw/demo/client.ts +154 -0
- package/packages/galaxydraw/demo/dist/client.js +8 -0
- package/packages/galaxydraw/demo/index.html +256 -0
- package/packages/galaxydraw/demo/server.ts +96 -0
- package/packages/galaxydraw/dist/index.js +984 -0
- package/packages/galaxydraw/dist/index.js.map +16 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +49 -0
- package/packages/galaxydraw/perf.test.ts +284 -0
- package/packages/galaxydraw/src/core/cards.ts +435 -0
- package/packages/galaxydraw/src/core/engine.ts +339 -0
- package/packages/galaxydraw/src/core/events.ts +81 -0
- package/packages/galaxydraw/src/core/layout.ts +136 -0
- package/packages/galaxydraw/src/core/minimap.ts +216 -0
- package/packages/galaxydraw/src/core/state.ts +177 -0
- package/packages/galaxydraw/src/core/viewport.ts +106 -0
- package/packages/galaxydraw/src/galaxydraw.css +166 -0
- package/packages/galaxydraw/src/index.ts +40 -0
- package/packages/galaxydraw/tsconfig.json +30 -0
- 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
|
+
}
|