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,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GalaxyDraw — Main engine class
|
|
3
|
+
*
|
|
4
|
+
* Creates an infinite canvas with pan/zoom/drag support.
|
|
5
|
+
* Supports two control modes:
|
|
6
|
+
* - Simple: click-drag = pan canvas (WARMAPS style)
|
|
7
|
+
* - Advanced: click-drag = select, Space+drag = pan (GitMaps style)
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const gd = new GalaxyDraw(containerEl, { mode: 'simple' });
|
|
11
|
+
* gd.cards.registerPlugin(myPlugin);
|
|
12
|
+
* gd.cards.create('widget', { id: 'w1', x: 100, y: 100 });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { CanvasState } from './state';
|
|
16
|
+
import { CardManager } from './cards';
|
|
17
|
+
import type { CardOptions, CardPlugin } from './cards';
|
|
18
|
+
import { ViewportCuller } from './viewport';
|
|
19
|
+
import { EventBus } from './events';
|
|
20
|
+
|
|
21
|
+
export type ControlMode = 'simple' | 'advanced';
|
|
22
|
+
|
|
23
|
+
export interface GalaxyDrawOptions {
|
|
24
|
+
/** Control scheme: 'simple' = drag to pan, 'advanced' = space+drag to pan */
|
|
25
|
+
mode?: ControlMode;
|
|
26
|
+
/** Card defaults */
|
|
27
|
+
cards?: CardOptions;
|
|
28
|
+
/** Viewport culling margin in px */
|
|
29
|
+
cullMargin?: number;
|
|
30
|
+
/** Enable minimap */
|
|
31
|
+
minimap?: boolean;
|
|
32
|
+
/** Custom CSS class added to the root */
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class GalaxyDraw {
|
|
37
|
+
readonly state: CanvasState;
|
|
38
|
+
readonly cards: CardManager;
|
|
39
|
+
readonly culler: ViewportCuller;
|
|
40
|
+
readonly bus: EventBus;
|
|
41
|
+
|
|
42
|
+
private mode: ControlMode;
|
|
43
|
+
private viewport: HTMLElement;
|
|
44
|
+
private canvas: HTMLElement;
|
|
45
|
+
private spaceHeld = false;
|
|
46
|
+
private isDragging = false;
|
|
47
|
+
private dragStartX = 0;
|
|
48
|
+
private dragStartY = 0;
|
|
49
|
+
private cleanupFns: (() => void)[] = [];
|
|
50
|
+
|
|
51
|
+
// Touch state
|
|
52
|
+
private touchStartX = 0;
|
|
53
|
+
private touchStartY = 0;
|
|
54
|
+
private lastPinchDist = 0;
|
|
55
|
+
|
|
56
|
+
constructor(container: HTMLElement, options?: GalaxyDrawOptions) {
|
|
57
|
+
this.mode = options?.mode ?? 'simple';
|
|
58
|
+
this.bus = new EventBus();
|
|
59
|
+
|
|
60
|
+
// ── Create DOM structure ──
|
|
61
|
+
this.viewport = document.createElement('div');
|
|
62
|
+
this.viewport.className = `gd-viewport ${options?.className ?? ''}`.trim();
|
|
63
|
+
this.viewport.style.cssText = 'position:relative;width:100%;height:100%;overflow:hidden;';
|
|
64
|
+
|
|
65
|
+
this.canvas = document.createElement('div');
|
|
66
|
+
this.canvas.className = 'gd-canvas';
|
|
67
|
+
this.canvas.style.cssText = 'position:absolute;top:0;left:0;transform-origin:0 0;will-change:transform;';
|
|
68
|
+
|
|
69
|
+
this.viewport.appendChild(this.canvas);
|
|
70
|
+
container.appendChild(this.viewport);
|
|
71
|
+
|
|
72
|
+
// ── Init subsystems ──
|
|
73
|
+
this.state = new CanvasState();
|
|
74
|
+
this.state.bind(this.viewport, this.canvas);
|
|
75
|
+
|
|
76
|
+
this.cards = new CardManager(this.state, this.bus, this.canvas, options?.cards);
|
|
77
|
+
this.culler = new ViewportCuller(this.state, this.cards, this.bus);
|
|
78
|
+
if (options?.cullMargin) this.culler.margin = options.cullMargin;
|
|
79
|
+
|
|
80
|
+
// ── Wire up interactions ──
|
|
81
|
+
this.setupWheel();
|
|
82
|
+
this.setupMouse();
|
|
83
|
+
this.setupTouch();
|
|
84
|
+
this.setupKeyboard();
|
|
85
|
+
|
|
86
|
+
// ── Cull on state change ──
|
|
87
|
+
const unsub = this.state.subscribe(() => this.culler.schedule());
|
|
88
|
+
this.cleanupFns.push(unsub);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Public API ──────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** Switch control mode at runtime */
|
|
94
|
+
setMode(mode: ControlMode) {
|
|
95
|
+
this.mode = mode;
|
|
96
|
+
this.bus.emit('mode:change', { mode });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getMode(): ControlMode {
|
|
100
|
+
return this.mode;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Register a card plugin */
|
|
104
|
+
registerPlugin(plugin: CardPlugin) {
|
|
105
|
+
this.cards.registerPlugin(plugin);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Fit all cards into view */
|
|
109
|
+
fitAll(padding = 60) {
|
|
110
|
+
this.culler.uncullAll();
|
|
111
|
+
if (this.cards.cards.size === 0) return;
|
|
112
|
+
|
|
113
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
114
|
+
for (const [, card] of this.cards.cards) {
|
|
115
|
+
const x = parseFloat(card.style.left) || 0;
|
|
116
|
+
const y = parseFloat(card.style.top) || 0;
|
|
117
|
+
const w = card.offsetWidth || 400;
|
|
118
|
+
const h = card.offsetHeight || 300;
|
|
119
|
+
minX = Math.min(minX, x);
|
|
120
|
+
minY = Math.min(minY, y);
|
|
121
|
+
maxX = Math.max(maxX, x + w);
|
|
122
|
+
maxY = Math.max(maxY, y + h);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.state.fitRect(minX, minY, maxX, maxY, padding);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Get the viewport DOM element (for appending overlays, minimap, etc.) */
|
|
129
|
+
getViewport(): HTMLElement {
|
|
130
|
+
return this.viewport;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Get the canvas DOM element */
|
|
134
|
+
getCanvas(): HTMLElement {
|
|
135
|
+
return this.canvas;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Destroy the engine, remove all listeners and DOM */
|
|
139
|
+
destroy() {
|
|
140
|
+
this.cleanupFns.forEach(fn => fn());
|
|
141
|
+
this.cleanupFns = [];
|
|
142
|
+
this.cards.clear();
|
|
143
|
+
this.bus.clear();
|
|
144
|
+
this.viewport.remove();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Wheel zoom ──────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
private setupWheel() {
|
|
150
|
+
this.viewport.addEventListener('wheel', (e) => {
|
|
151
|
+
const target = e.target as HTMLElement;
|
|
152
|
+
|
|
153
|
+
// Let card plugins handle their own scroll (maps, feeds, etc.)
|
|
154
|
+
if (this.cards.consumesWheel(target)) return;
|
|
155
|
+
|
|
156
|
+
// Let scrollable card bodies scroll naturally
|
|
157
|
+
const cardEl = target.closest('.gd-card') || target.closest('[data-card-type]');
|
|
158
|
+
if (cardEl) {
|
|
159
|
+
const scrollBody = (target.closest('.gd-card-body') || target.closest('.wm-container-body')) as HTMLElement | null;
|
|
160
|
+
if (scrollBody && scrollBody.scrollHeight > scrollBody.clientHeight) {
|
|
161
|
+
const atTop = scrollBody.scrollTop <= 0 && e.deltaY < 0;
|
|
162
|
+
const atBottom = scrollBody.scrollTop + scrollBody.clientHeight >= scrollBody.scrollHeight - 1 && e.deltaY > 0;
|
|
163
|
+
if (!atTop && !atBottom) return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
const factor = e.deltaY < 0 ? 1.08 : 1 / 1.08;
|
|
169
|
+
this.state.zoomToward(e.clientX, e.clientY, factor);
|
|
170
|
+
}, { passive: false });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Mouse interactions ──────────────────────────────
|
|
174
|
+
|
|
175
|
+
private setupMouse() {
|
|
176
|
+
this.viewport.addEventListener('mousedown', (e) => {
|
|
177
|
+
const target = e.target as HTMLElement;
|
|
178
|
+
|
|
179
|
+
// Let card plugins handle their own mouse (maps, interactive widgets)
|
|
180
|
+
if (this.cards.consumesMouse(target)) return;
|
|
181
|
+
|
|
182
|
+
// Card header drag is handled by CardManager
|
|
183
|
+
if (target.closest('.gd-card-header') || target.closest('.wm-container-header') || target.closest('.gd-resize-handle')) return;
|
|
184
|
+
|
|
185
|
+
// Click on card = bring to front + select
|
|
186
|
+
const card = (target.closest('.gd-card') || target.closest('[data-card-type]')) as HTMLElement | null;
|
|
187
|
+
if (card && e.button === 0) {
|
|
188
|
+
const id = card.dataset.cardId;
|
|
189
|
+
if (id) {
|
|
190
|
+
this.cards.bringToFront(card);
|
|
191
|
+
this.cards.select(id, e.shiftKey);
|
|
192
|
+
}
|
|
193
|
+
// In advanced mode, clicking a card body doesn't pan
|
|
194
|
+
if (this.mode === 'advanced') return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Pan logic ──
|
|
198
|
+
const shouldPan =
|
|
199
|
+
e.button === 1 || // middle click always pans
|
|
200
|
+
(this.mode === 'simple' && e.button === 0 && !card) || // simple: left click on empty canvas
|
|
201
|
+
(this.mode === 'advanced' && this.spaceHeld); // advanced: space held
|
|
202
|
+
|
|
203
|
+
if (shouldPan) {
|
|
204
|
+
this.isDragging = true;
|
|
205
|
+
this.dragStartX = e.clientX - this.state.offsetX;
|
|
206
|
+
this.dragStartY = e.clientY - this.state.offsetY;
|
|
207
|
+
this.viewport.style.cursor = 'grabbing';
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
window.addEventListener('mousemove', (e) => {
|
|
213
|
+
if (this.isDragging) {
|
|
214
|
+
this.state.set(
|
|
215
|
+
this.state.zoom,
|
|
216
|
+
e.clientX - this.dragStartX,
|
|
217
|
+
e.clientY - this.dragStartY,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
window.addEventListener('mouseup', () => {
|
|
223
|
+
if (this.isDragging) {
|
|
224
|
+
this.isDragging = false;
|
|
225
|
+
this.viewport.style.cursor = '';
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Touch interactions ──────────────────────────────
|
|
231
|
+
|
|
232
|
+
private setupTouch() {
|
|
233
|
+
const onTouchStart = (e: TouchEvent) => {
|
|
234
|
+
const target = e.touches[0]?.target as HTMLElement;
|
|
235
|
+
if (!target) return;
|
|
236
|
+
|
|
237
|
+
// Let card plugins handle their own touch
|
|
238
|
+
if (this.cards.consumesMouse(target)) return;
|
|
239
|
+
|
|
240
|
+
if (e.touches.length === 1) {
|
|
241
|
+
// Single finger = pan (in simple mode or space held)
|
|
242
|
+
const touch = e.touches[0];
|
|
243
|
+
const card = target.closest('.gd-card') || target.closest('[data-card-type]');
|
|
244
|
+
|
|
245
|
+
const shouldPan =
|
|
246
|
+
(this.mode === 'simple' && !card) ||
|
|
247
|
+
(this.mode === 'advanced' && this.spaceHeld);
|
|
248
|
+
|
|
249
|
+
if (shouldPan) {
|
|
250
|
+
this.isDragging = true;
|
|
251
|
+
this.touchStartX = touch.clientX - this.state.offsetX;
|
|
252
|
+
this.touchStartY = touch.clientY - this.state.offsetY;
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
}
|
|
255
|
+
} else if (e.touches.length === 2) {
|
|
256
|
+
// Two fingers = pinch zoom
|
|
257
|
+
this.isDragging = false;
|
|
258
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
259
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
260
|
+
this.lastPinchDist = Math.sqrt(dx * dx + dy * dy);
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const onTouchMove = (e: TouchEvent) => {
|
|
266
|
+
if (this.isDragging && e.touches.length === 1) {
|
|
267
|
+
const touch = e.touches[0];
|
|
268
|
+
this.state.set(
|
|
269
|
+
this.state.zoom,
|
|
270
|
+
touch.clientX - this.touchStartX,
|
|
271
|
+
touch.clientY - this.touchStartY,
|
|
272
|
+
);
|
|
273
|
+
e.preventDefault();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (e.touches.length === 2) {
|
|
277
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
278
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
279
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
280
|
+
|
|
281
|
+
if (this.lastPinchDist > 0) {
|
|
282
|
+
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
|
283
|
+
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
284
|
+
const factor = dist / this.lastPinchDist;
|
|
285
|
+
this.state.zoomToward(midX, midY, factor);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.lastPinchDist = dist;
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const onTouchEnd = () => {
|
|
294
|
+
this.isDragging = false;
|
|
295
|
+
this.lastPinchDist = 0;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
this.viewport.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
299
|
+
this.viewport.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
300
|
+
this.viewport.addEventListener('touchend', onTouchEnd);
|
|
301
|
+
this.cleanupFns.push(() => {
|
|
302
|
+
this.viewport.removeEventListener('touchstart', onTouchStart);
|
|
303
|
+
this.viewport.removeEventListener('touchmove', onTouchMove);
|
|
304
|
+
this.viewport.removeEventListener('touchend', onTouchEnd);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ─── Keyboard ────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
private setupKeyboard() {
|
|
311
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
312
|
+
if (e.code === 'Space' && !e.repeat) {
|
|
313
|
+
const tag = (e.target as HTMLElement).tagName;
|
|
314
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
315
|
+
e.preventDefault();
|
|
316
|
+
this.spaceHeld = true;
|
|
317
|
+
this.viewport.classList.add('gd-space-pan');
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const onKeyUp = (e: KeyboardEvent) => {
|
|
322
|
+
if (e.code === 'Space') {
|
|
323
|
+
this.spaceHeld = false;
|
|
324
|
+
this.viewport.classList.remove('gd-space-pan');
|
|
325
|
+
if (this.isDragging) {
|
|
326
|
+
this.isDragging = false;
|
|
327
|
+
this.viewport.style.cursor = '';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
window.addEventListener('keydown', onKeyDown);
|
|
333
|
+
window.addEventListener('keyup', onKeyUp);
|
|
334
|
+
this.cleanupFns.push(() => {
|
|
335
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
336
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBus — Type-safe pub/sub for canvas events
|
|
3
|
+
*
|
|
4
|
+
* Every action in the canvas (pan, zoom, card move, select, etc.)
|
|
5
|
+
* flows through the event bus. This enables plugins to react to
|
|
6
|
+
* state changes without tight coupling.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface GalaxyDrawEvent {
|
|
10
|
+
'canvas:pan': { offsetX: number; offsetY: number };
|
|
11
|
+
'canvas:zoom': { zoom: number; centerX: number; centerY: number };
|
|
12
|
+
'canvas:resize': { width: number; height: number };
|
|
13
|
+
|
|
14
|
+
'card:create': { id: string; x: number; y: number };
|
|
15
|
+
'card:move': { id: string; x: number; y: number };
|
|
16
|
+
'card:resize': { id: string; width: number; height: number };
|
|
17
|
+
'card:select': { ids: string[] };
|
|
18
|
+
'card:deselect': { ids: string[] };
|
|
19
|
+
'card:remove': { id: string };
|
|
20
|
+
'card:collapse': { id: string; collapsed: boolean };
|
|
21
|
+
'card:focus': { id: string };
|
|
22
|
+
|
|
23
|
+
'layout:save': { layouts: any[] };
|
|
24
|
+
'layout:restore': { layouts: any[] };
|
|
25
|
+
'layout:reset': {};
|
|
26
|
+
|
|
27
|
+
'viewport:cull': { shown: number; culled: number; materialized: number };
|
|
28
|
+
|
|
29
|
+
'mode:change': { mode: 'simple' | 'advanced' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type EventHandler<K extends keyof GalaxyDrawEvent> = (data: GalaxyDrawEvent[K]) => void;
|
|
33
|
+
|
|
34
|
+
export class EventBus {
|
|
35
|
+
private handlers = new Map<string, Set<Function>>();
|
|
36
|
+
|
|
37
|
+
on<K extends keyof GalaxyDrawEvent>(event: K, handler: EventHandler<K>): () => void {
|
|
38
|
+
if (!this.handlers.has(event)) {
|
|
39
|
+
this.handlers.set(event, new Set());
|
|
40
|
+
}
|
|
41
|
+
this.handlers.get(event)!.add(handler);
|
|
42
|
+
|
|
43
|
+
// Return unsubscribe function
|
|
44
|
+
return () => {
|
|
45
|
+
this.handlers.get(event)?.delete(handler);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
once<K extends keyof GalaxyDrawEvent>(event: K, handler: EventHandler<K>): () => void {
|
|
50
|
+
const wrapper = (data: GalaxyDrawEvent[K]) => {
|
|
51
|
+
unsub();
|
|
52
|
+
handler(data);
|
|
53
|
+
};
|
|
54
|
+
const unsub = this.on(event, wrapper as any);
|
|
55
|
+
return unsub;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
emit<K extends keyof GalaxyDrawEvent>(event: K, data: GalaxyDrawEvent[K]): void {
|
|
59
|
+
const handlers = this.handlers.get(event);
|
|
60
|
+
if (!handlers) return;
|
|
61
|
+
for (const handler of handlers) {
|
|
62
|
+
try {
|
|
63
|
+
handler(data);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`[galaxydraw] Event handler error for "${event}":`, err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
off<K extends keyof GalaxyDrawEvent>(event: K, handler?: EventHandler<K>): void {
|
|
71
|
+
if (handler) {
|
|
72
|
+
this.handlers.get(event)?.delete(handler);
|
|
73
|
+
} else {
|
|
74
|
+
this.handlers.delete(event);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
clear(): void {
|
|
79
|
+
this.handlers.clear();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LayoutManager — Persist and restore card positions/sizes
|
|
3
|
+
*
|
|
4
|
+
* Supports dual storage:
|
|
5
|
+
* - localStorage (always available)
|
|
6
|
+
* - Optional server-side provider (for multi-device sync)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CardManager, CardData } from './cards';
|
|
10
|
+
import type { EventBus } from './events';
|
|
11
|
+
|
|
12
|
+
export interface LayoutData {
|
|
13
|
+
id: string;
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
collapsed?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Override this to add server-side persistence */
|
|
22
|
+
export interface LayoutProvider {
|
|
23
|
+
save(key: string, layouts: LayoutData[]): Promise<void>;
|
|
24
|
+
load(key: string): Promise<LayoutData[]>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class LayoutManager {
|
|
28
|
+
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
29
|
+
private debounceMs = 300;
|
|
30
|
+
private provider: LayoutProvider | null = null;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private cards: CardManager,
|
|
34
|
+
private bus: EventBus,
|
|
35
|
+
private storagePrefix = 'galaxydraw',
|
|
36
|
+
) {
|
|
37
|
+
// Auto-save on card move/resize
|
|
38
|
+
this.bus.on('card:move', () => this.debounceSave());
|
|
39
|
+
this.bus.on('card:resize', () => this.debounceSave());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Set a custom persistence provider (e.g. server-side) */
|
|
43
|
+
setProvider(provider: LayoutProvider) {
|
|
44
|
+
this.provider = provider;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Save current card layouts */
|
|
48
|
+
async save(key: string) {
|
|
49
|
+
const layouts: LayoutData[] = [];
|
|
50
|
+
for (const [id, el] of this.cards.cards) {
|
|
51
|
+
layouts.push({
|
|
52
|
+
id,
|
|
53
|
+
x: parseFloat(el.style.left) || 0,
|
|
54
|
+
y: parseFloat(el.style.top) || 0,
|
|
55
|
+
width: el.offsetWidth,
|
|
56
|
+
height: el.offsetHeight,
|
|
57
|
+
collapsed: el.classList.contains('gd-card--collapsed'),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Save to localStorage
|
|
62
|
+
const lsKey = `${this.storagePrefix}:layout:${key}`;
|
|
63
|
+
try {
|
|
64
|
+
localStorage.setItem(lsKey, JSON.stringify(layouts));
|
|
65
|
+
} catch { }
|
|
66
|
+
|
|
67
|
+
// Save to provider
|
|
68
|
+
if (this.provider) {
|
|
69
|
+
try {
|
|
70
|
+
await this.provider.save(key, layouts);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.warn('[galaxydraw] Layout save to provider failed:', err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.bus.emit('layout:save', { layouts });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Load layouts from storage */
|
|
80
|
+
async load(key: string): Promise<LayoutData[]> {
|
|
81
|
+
// Try provider first
|
|
82
|
+
if (this.provider) {
|
|
83
|
+
try {
|
|
84
|
+
const remote = await this.provider.load(key);
|
|
85
|
+
if (remote.length > 0) return remote;
|
|
86
|
+
} catch { }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fall back to localStorage
|
|
90
|
+
const lsKey = `${this.storagePrefix}:layout:${key}`;
|
|
91
|
+
try {
|
|
92
|
+
const raw = localStorage.getItem(lsKey);
|
|
93
|
+
if (raw) return JSON.parse(raw);
|
|
94
|
+
} catch { }
|
|
95
|
+
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Apply loaded layouts to existing cards */
|
|
100
|
+
apply(layouts: LayoutData[]) {
|
|
101
|
+
const layoutMap = new Map(layouts.map(l => [l.id, l]));
|
|
102
|
+
|
|
103
|
+
for (const [id, el] of this.cards.cards) {
|
|
104
|
+
const layout = layoutMap.get(id);
|
|
105
|
+
if (!layout) continue;
|
|
106
|
+
|
|
107
|
+
el.style.left = `${layout.x}px`;
|
|
108
|
+
el.style.top = `${layout.y}px`;
|
|
109
|
+
el.style.width = `${layout.width}px`;
|
|
110
|
+
el.style.height = `${layout.height}px`;
|
|
111
|
+
if (layout.collapsed) {
|
|
112
|
+
el.classList.add('gd-card--collapsed');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.bus.emit('layout:restore', { layouts });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Clear saved layouts */
|
|
120
|
+
reset(key: string) {
|
|
121
|
+
const lsKey = `${this.storagePrefix}:layout:${key}`;
|
|
122
|
+
try { localStorage.removeItem(lsKey); } catch { }
|
|
123
|
+
this.bus.emit('layout:reset', {});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private _currentKey = '';
|
|
127
|
+
setCurrentKey(key: string) { this._currentKey = key; }
|
|
128
|
+
|
|
129
|
+
private debounceSave() {
|
|
130
|
+
if (!this._currentKey) return;
|
|
131
|
+
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
132
|
+
this.saveTimer = setTimeout(() => {
|
|
133
|
+
this.save(this._currentKey);
|
|
134
|
+
}, this.debounceMs);
|
|
135
|
+
}
|
|
136
|
+
}
|