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,94 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Shared canvas context — passed to every sub-module.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the giant closure that was `mount()` in the monolith.
|
|
6
|
+
* Every module gets read/write access to the same mutable state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface CanvasContext {
|
|
10
|
+
/** XState actor */
|
|
11
|
+
actor: any;
|
|
12
|
+
/** Shortcut: actor.getSnapshot() */
|
|
13
|
+
snap: () => any;
|
|
14
|
+
|
|
15
|
+
// ─── DOM refs ─────────────────────────────
|
|
16
|
+
canvas: HTMLElement | null;
|
|
17
|
+
canvasViewport: HTMLElement | null;
|
|
18
|
+
svgOverlay: SVGSVGElement | null;
|
|
19
|
+
|
|
20
|
+
// ─── Shared maps ──────────────────────────
|
|
21
|
+
fileCards: Map<string, HTMLElement>;
|
|
22
|
+
positions: Map<string, any>;
|
|
23
|
+
|
|
24
|
+
// ─── Drag/pan state ───────────────────────
|
|
25
|
+
isDragging: boolean;
|
|
26
|
+
spaceHeld: boolean;
|
|
27
|
+
|
|
28
|
+
// ─── Hidden files ─────────────────────────
|
|
29
|
+
hiddenFiles: Set<string>;
|
|
30
|
+
|
|
31
|
+
// ─── Constants ────────────────────────────
|
|
32
|
+
CORNER_SIZE: number;
|
|
33
|
+
|
|
34
|
+
// ─── Scroll debounce timers ───────────────
|
|
35
|
+
scrollTimers: Record<string, any>;
|
|
36
|
+
|
|
37
|
+
// ─── Connection drag state ────────────────
|
|
38
|
+
connectionDragState: any;
|
|
39
|
+
|
|
40
|
+
// ─── Loading overlay ref ──────────────────
|
|
41
|
+
loadingOverlay: HTMLElement | null;
|
|
42
|
+
|
|
43
|
+
// ─── Text rendering mode ──────────────────
|
|
44
|
+
useCanvasText: boolean;
|
|
45
|
+
|
|
46
|
+
// ─── All-files mode state ─────────────────
|
|
47
|
+
allFilesActive: boolean;
|
|
48
|
+
changedFilePaths: Set<string>;
|
|
49
|
+
allFilesData: any[] | null;
|
|
50
|
+
commitFilesData: any[] | null;
|
|
51
|
+
|
|
52
|
+
// ─── Virtualized rendering ────────────────
|
|
53
|
+
// Cards deferred until they scroll into the viewport.
|
|
54
|
+
// Key: file path, Value: { file data, x, y, size, isChanged }
|
|
55
|
+
deferredCards: Map<string, { file: any; x: number; y: number; size: any; isChanged: boolean }>;
|
|
56
|
+
|
|
57
|
+
// ─── Control mode ─────────────────────────
|
|
58
|
+
// 'simple' = drag canvas to pan (WARMAPS style)
|
|
59
|
+
// 'advanced' = space+drag to pan, drag for rect select (GitMaps style)
|
|
60
|
+
controlMode: 'simple' | 'advanced';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Creates a fresh context (call once per mount). */
|
|
64
|
+
export function createCanvasContext(actor: any): CanvasContext {
|
|
65
|
+
return {
|
|
66
|
+
actor,
|
|
67
|
+
snap: () => actor.getSnapshot(),
|
|
68
|
+
|
|
69
|
+
canvas: null,
|
|
70
|
+
canvasViewport: null,
|
|
71
|
+
svgOverlay: null,
|
|
72
|
+
|
|
73
|
+
fileCards: new Map(),
|
|
74
|
+
positions: new Map(),
|
|
75
|
+
|
|
76
|
+
isDragging: false,
|
|
77
|
+
spaceHeld: false,
|
|
78
|
+
|
|
79
|
+
hiddenFiles: new Set(),
|
|
80
|
+
|
|
81
|
+
CORNER_SIZE: 40,
|
|
82
|
+
scrollTimers: {},
|
|
83
|
+
connectionDragState: null,
|
|
84
|
+
loadingOverlay: null,
|
|
85
|
+
useCanvasText: true,
|
|
86
|
+
|
|
87
|
+
allFilesActive: true,
|
|
88
|
+
changedFilePaths: new Set(),
|
|
89
|
+
allFilesData: null,
|
|
90
|
+
commitFilesData: null,
|
|
91
|
+
deferredCards: new Map(),
|
|
92
|
+
controlMode: (localStorage.getItem('gitcanvas:controlMode') as any) || 'advanced',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* cursor-sharing.ts — WebSocket-based live cursor presence.
|
|
4
|
+
* Shows other users' cursors on the canvas in real-time.
|
|
5
|
+
*
|
|
6
|
+
* Broadcasts local mouse position in canvas-space coordinates.
|
|
7
|
+
* Renders remote cursors as colored SVG pointers with name labels.
|
|
8
|
+
*/
|
|
9
|
+
import type { CanvasContext } from './context';
|
|
10
|
+
|
|
11
|
+
interface RemoteCursor {
|
|
12
|
+
peerId: string;
|
|
13
|
+
color: string;
|
|
14
|
+
name: string;
|
|
15
|
+
x: number; // canvas-space X
|
|
16
|
+
y: number; // canvas-space Y
|
|
17
|
+
viewportX: number; // viewport offset X
|
|
18
|
+
viewportY: number; // viewport offset Y
|
|
19
|
+
zoom: number;
|
|
20
|
+
lastSeen: number;
|
|
21
|
+
element?: HTMLElement;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let _ws: WebSocket | null = null;
|
|
25
|
+
let _peerId: string | null = null;
|
|
26
|
+
let _color: string | null = null;
|
|
27
|
+
let _ctx: CanvasContext | null = null;
|
|
28
|
+
let _remoteCursors: Map<string, RemoteCursor> = new Map();
|
|
29
|
+
let _container: HTMLElement | null = null;
|
|
30
|
+
let _staleInterval: any = null;
|
|
31
|
+
const THROTTLE_MS = 50; // broadcast max 20x/sec
|
|
32
|
+
const STALE_MS = 5000; // fade after 5s inactivity
|
|
33
|
+
const REMOVE_MS = 15000; // remove after 15s
|
|
34
|
+
|
|
35
|
+
let _lastBroadcast = 0;
|
|
36
|
+
|
|
37
|
+
// ─── Initialize ─────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export function initCursorSharing(ctx: CanvasContext) {
|
|
40
|
+
_ctx = ctx;
|
|
41
|
+
|
|
42
|
+
// Create cursor overlay container
|
|
43
|
+
_container = document.createElement('div');
|
|
44
|
+
_container.id = 'cursor-sharing-overlay';
|
|
45
|
+
_container.style.cssText = `
|
|
46
|
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
47
|
+
pointer-events: none; z-index: 9999; overflow: hidden;
|
|
48
|
+
`;
|
|
49
|
+
document.body.appendChild(_container);
|
|
50
|
+
|
|
51
|
+
connectWebSocket();
|
|
52
|
+
|
|
53
|
+
// Track local mouse movement
|
|
54
|
+
document.addEventListener('mousemove', onLocalMouseMove);
|
|
55
|
+
|
|
56
|
+
// Clean up stale cursors periodically
|
|
57
|
+
_staleInterval = setInterval(cleanStaleCursors, 2000);
|
|
58
|
+
|
|
59
|
+
// Send leave on unload
|
|
60
|
+
window.addEventListener('beforeunload', () => {
|
|
61
|
+
if (_ws?.readyState === WebSocket.OPEN) {
|
|
62
|
+
_ws.send(JSON.stringify({ type: 'leave' }));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Update remote cursor positions on viewport pan/zoom
|
|
67
|
+
window.addEventListener('gitcanvas:viewport-changed', () => {
|
|
68
|
+
updateRemoteCursorPositions();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function destroyCursorSharing() {
|
|
73
|
+
if (_ws) {
|
|
74
|
+
_ws.close();
|
|
75
|
+
_ws = null;
|
|
76
|
+
}
|
|
77
|
+
if (_container) {
|
|
78
|
+
_container.remove();
|
|
79
|
+
_container = null;
|
|
80
|
+
}
|
|
81
|
+
if (_staleInterval) {
|
|
82
|
+
clearInterval(_staleInterval);
|
|
83
|
+
_staleInterval = null;
|
|
84
|
+
}
|
|
85
|
+
document.removeEventListener('mousemove', onLocalMouseMove);
|
|
86
|
+
_remoteCursors.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── WebSocket Connection ───────────────────────────────
|
|
90
|
+
|
|
91
|
+
function connectWebSocket() {
|
|
92
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
93
|
+
const url = `${protocol}//${location.host}/ws/cursors`;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
_ws = new WebSocket(url);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.warn('[Cursors] WebSocket connection failed:', e);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_ws.onopen = () => {
|
|
103
|
+
console.log('[Cursors] Connected');
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
_ws.onmessage = (event) => {
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(event.data);
|
|
109
|
+
if (data.type === 'identity') {
|
|
110
|
+
_peerId = data.peerId;
|
|
111
|
+
_color = data.color;
|
|
112
|
+
} else if (data.type === 'cursor') {
|
|
113
|
+
handleRemoteCursor(data);
|
|
114
|
+
} else if (data.type === 'leave') {
|
|
115
|
+
removeRemoteCursor(data.peerId);
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore */ }
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
_ws.onclose = () => {
|
|
121
|
+
console.log('[Cursors] Disconnected, reconnecting in 3s...');
|
|
122
|
+
setTimeout(connectWebSocket, 3000);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
_ws.onerror = () => {
|
|
126
|
+
// onclose will fire after this
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Local Mouse Broadcasting ───────────────────────────
|
|
131
|
+
|
|
132
|
+
function onLocalMouseMove(e: MouseEvent) {
|
|
133
|
+
if (!_ws || _ws.readyState !== WebSocket.OPEN || !_ctx) return;
|
|
134
|
+
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
if (now - _lastBroadcast < THROTTLE_MS) return;
|
|
137
|
+
_lastBroadcast = now;
|
|
138
|
+
|
|
139
|
+
// Convert screen position to canvas-space coordinates
|
|
140
|
+
const state = _ctx.snap().context;
|
|
141
|
+
const canvasX = (e.clientX - (state.viewportX || 0)) / (state.zoom || 1);
|
|
142
|
+
const canvasY = (e.clientY - (state.viewportY || 0)) / (state.zoom || 1);
|
|
143
|
+
|
|
144
|
+
const userName = localStorage.getItem('gitcanvas:username') || _peerId || 'anonymous';
|
|
145
|
+
|
|
146
|
+
_ws.send(JSON.stringify({
|
|
147
|
+
type: 'cursor',
|
|
148
|
+
name: userName,
|
|
149
|
+
x: Math.round(canvasX),
|
|
150
|
+
y: Math.round(canvasY),
|
|
151
|
+
viewportX: state.viewportX || 0,
|
|
152
|
+
viewportY: state.viewportY || 0,
|
|
153
|
+
zoom: state.zoom || 1,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Remote Cursor Rendering ────────────────────────────
|
|
158
|
+
|
|
159
|
+
function handleRemoteCursor(data: any) {
|
|
160
|
+
if (data.peerId === _peerId) return; // Skip own cursor
|
|
161
|
+
|
|
162
|
+
let cursor = _remoteCursors.get(data.peerId);
|
|
163
|
+
if (!cursor) {
|
|
164
|
+
cursor = {
|
|
165
|
+
peerId: data.peerId,
|
|
166
|
+
color: data.color,
|
|
167
|
+
name: data.name,
|
|
168
|
+
x: data.x,
|
|
169
|
+
y: data.y,
|
|
170
|
+
viewportX: data.viewportX,
|
|
171
|
+
viewportY: data.viewportY,
|
|
172
|
+
zoom: data.zoom,
|
|
173
|
+
lastSeen: Date.now(),
|
|
174
|
+
};
|
|
175
|
+
cursor.element = createCursorElement(cursor);
|
|
176
|
+
_remoteCursors.set(data.peerId, cursor);
|
|
177
|
+
_container?.appendChild(cursor.element);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Update position
|
|
181
|
+
cursor.x = data.x;
|
|
182
|
+
cursor.y = data.y;
|
|
183
|
+
cursor.viewportX = data.viewportX;
|
|
184
|
+
cursor.viewportY = data.viewportY;
|
|
185
|
+
cursor.zoom = data.zoom;
|
|
186
|
+
cursor.name = data.name;
|
|
187
|
+
cursor.lastSeen = Date.now();
|
|
188
|
+
|
|
189
|
+
updateCursorPosition(cursor);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function createCursorElement(cursor: RemoteCursor): HTMLElement {
|
|
193
|
+
const el = document.createElement('div');
|
|
194
|
+
el.className = 'remote-cursor';
|
|
195
|
+
el.style.cssText = `
|
|
196
|
+
position: fixed;
|
|
197
|
+
pointer-events: none;
|
|
198
|
+
z-index: 9999;
|
|
199
|
+
transition: left 0.08s linear, top 0.08s linear, opacity 0.3s;
|
|
200
|
+
will-change: transform;
|
|
201
|
+
`;
|
|
202
|
+
el.innerHTML = `
|
|
203
|
+
<svg width="20" height="24" viewBox="0 0 20 24" fill="none" style="filter: drop-shadow(0 1px 3px rgba(0,0,0,0.4));">
|
|
204
|
+
<path d="M1 1L7 21L10 14L18 12L1 1Z" fill="${cursor.color}" stroke="white" stroke-width="1.5" stroke-linejoin="round"/>
|
|
205
|
+
</svg>
|
|
206
|
+
<span style="
|
|
207
|
+
position: absolute;
|
|
208
|
+
left: 18px;
|
|
209
|
+
top: 14px;
|
|
210
|
+
background: ${cursor.color};
|
|
211
|
+
color: white;
|
|
212
|
+
font-size: 11px;
|
|
213
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
214
|
+
font-weight: 500;
|
|
215
|
+
padding: 2px 6px;
|
|
216
|
+
border-radius: 4px;
|
|
217
|
+
white-space: nowrap;
|
|
218
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
219
|
+
letter-spacing: 0.3px;
|
|
220
|
+
">${cursor.name}</span>
|
|
221
|
+
`;
|
|
222
|
+
return el;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function updateCursorPosition(cursor: RemoteCursor) {
|
|
226
|
+
if (!cursor.element || !_ctx) return;
|
|
227
|
+
|
|
228
|
+
// Convert remote canvas-space coordinates to local screen position
|
|
229
|
+
const state = _ctx.snap().context;
|
|
230
|
+
const localZoom = state.zoom || 1;
|
|
231
|
+
const localViewportX = state.viewportX || 0;
|
|
232
|
+
const localViewportY = state.viewportY || 0;
|
|
233
|
+
|
|
234
|
+
const screenX = cursor.x * localZoom + localViewportX;
|
|
235
|
+
const screenY = cursor.y * localZoom + localViewportY;
|
|
236
|
+
|
|
237
|
+
cursor.element.style.left = `${screenX}px`;
|
|
238
|
+
cursor.element.style.top = `${screenY}px`;
|
|
239
|
+
cursor.element.style.opacity = '1';
|
|
240
|
+
|
|
241
|
+
// Update name label if changed
|
|
242
|
+
const nameLabel = cursor.element.querySelector('span');
|
|
243
|
+
if (nameLabel && nameLabel.textContent !== cursor.name) {
|
|
244
|
+
nameLabel.textContent = cursor.name;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function removeRemoteCursor(peerId: string) {
|
|
249
|
+
const cursor = _remoteCursors.get(peerId);
|
|
250
|
+
if (cursor?.element) {
|
|
251
|
+
cursor.element.style.opacity = '0';
|
|
252
|
+
setTimeout(() => cursor.element?.remove(), 300);
|
|
253
|
+
}
|
|
254
|
+
_remoteCursors.delete(peerId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function cleanStaleCursors() {
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
|
|
260
|
+
// Also update positions on every tick (viewport may have changed)
|
|
261
|
+
for (const [id, cursor] of _remoteCursors) {
|
|
262
|
+
const age = now - cursor.lastSeen;
|
|
263
|
+
if (age > REMOVE_MS) {
|
|
264
|
+
removeRemoteCursor(id);
|
|
265
|
+
} else if (age > STALE_MS) {
|
|
266
|
+
if (cursor.element) cursor.element.style.opacity = '0.3';
|
|
267
|
+
} else {
|
|
268
|
+
// Keep positions synced with local viewport changes
|
|
269
|
+
updateCursorPosition(cursor);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Viewport Change Handler ────────────────────────────
|
|
275
|
+
// Call this when local viewport changes (pan/zoom) to update remote cursor positions
|
|
276
|
+
|
|
277
|
+
export function updateRemoteCursorPositions() {
|
|
278
|
+
for (const cursor of _remoteCursors.values()) {
|
|
279
|
+
updateCursorPosition(cursor);
|
|
280
|
+
}
|
|
281
|
+
}
|