gitmaps 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +265 -122
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/[owner]/[repo]/page.client.tsx +5 -0
  5. package/app/[slug]/page.client.tsx +5 -0
  6. package/app/analytics.db +0 -0
  7. package/app/api/analytics/route.ts +64 -0
  8. package/app/api/auth/positions/route.ts +95 -33
  9. package/app/api/build-info/route.ts +19 -0
  10. package/app/api/chat/route.ts +13 -2
  11. package/app/api/manifest.json/route.ts +20 -0
  12. package/app/api/og-image/route.ts +14 -0
  13. package/app/api/pwa-icon/route.ts +14 -0
  14. package/app/api/repo/clone-stream/route.ts +20 -12
  15. package/app/api/repo/file-content/route.ts +73 -20
  16. package/app/api/repo/imports/route.ts +21 -3
  17. package/app/api/repo/list/route.ts +30 -0
  18. package/app/api/repo/load/route.test.ts +62 -0
  19. package/app/api/repo/load/route.ts +41 -1
  20. package/app/api/repo/pdf-thumb/route.ts +127 -0
  21. package/app/api/repo/resolve-slug/route.ts +51 -0
  22. package/app/api/repo/tree/route.ts +188 -104
  23. package/app/api/repo/upload/route.ts +6 -9
  24. package/app/api/sw.js/route.ts +70 -0
  25. package/app/api/version/route.ts +26 -0
  26. package/app/galaxy-canvas/page.client.tsx +2 -0
  27. package/app/galaxy-canvas/page.tsx +5 -0
  28. package/app/globals.css +5844 -4694
  29. package/app/icon.png +0 -0
  30. package/app/layout.tsx +1284 -467
  31. package/app/lib/auto-arrange.test.ts +158 -0
  32. package/app/lib/auto-arrange.ts +147 -0
  33. package/app/lib/canvas-export.ts +358 -358
  34. package/app/lib/canvas-text.ts +4 -72
  35. package/app/lib/canvas.ts +625 -564
  36. package/app/lib/card-arrangement.ts +21 -7
  37. package/app/lib/card-context-menu.tsx +2 -2
  38. package/app/lib/card-groups.ts +9 -2
  39. package/app/lib/cards.tsx +1361 -914
  40. package/app/lib/chat.tsx +65 -9
  41. package/app/lib/code-editor.ts +86 -2
  42. package/app/lib/connections.tsx +34 -43
  43. package/app/lib/context.test.ts +32 -0
  44. package/app/lib/context.ts +19 -3
  45. package/app/lib/cursor-sharing.ts +34 -0
  46. package/app/lib/events.tsx +76 -73
  47. package/app/lib/export-canvas.ts +287 -0
  48. package/app/lib/file-card-plugin.ts +148 -134
  49. package/app/lib/file-modal.tsx +49 -0
  50. package/app/lib/file-preview.ts +486 -400
  51. package/app/lib/github-import.test.ts +424 -0
  52. package/app/lib/global-search.ts +48 -27
  53. package/app/lib/initial-route-hydration.test.ts +283 -0
  54. package/app/lib/initial-route-hydration.ts +202 -0
  55. package/app/lib/landing-reset.test.ts +99 -0
  56. package/app/lib/landing-reset.ts +106 -0
  57. package/app/lib/landing-shell.test.ts +75 -0
  58. package/app/lib/large-repo-optimization.ts +37 -0
  59. package/app/lib/layers.tsx +17 -18
  60. package/app/lib/layout-snapshots.ts +320 -0
  61. package/app/lib/loading.test.ts +69 -0
  62. package/app/lib/loading.tsx +160 -45
  63. package/app/lib/mount-cleanup.test.ts +52 -0
  64. package/app/lib/mount-cleanup.ts +34 -0
  65. package/app/lib/mount-init.test.ts +123 -0
  66. package/app/lib/mount-init.ts +107 -0
  67. package/app/lib/mount-lifecycle.test.ts +39 -0
  68. package/app/lib/mount-lifecycle.ts +12 -0
  69. package/app/lib/mount-route-wiring.test.ts +87 -0
  70. package/app/lib/mount-route-wiring.ts +84 -0
  71. package/app/lib/multi-repo.ts +14 -0
  72. package/app/lib/onboarding-tutorial.ts +278 -0
  73. package/app/lib/perf-overlay.ts +78 -0
  74. package/app/lib/positions.ts +191 -122
  75. package/app/lib/recent-commits.test.ts +869 -0
  76. package/app/lib/recent-commits.ts +227 -0
  77. package/app/lib/repo-handoff.test.ts +23 -0
  78. package/app/lib/repo-handoff.ts +16 -0
  79. package/app/lib/repo-progressive.ts +119 -0
  80. package/app/lib/repo-select.test.ts +61 -0
  81. package/app/lib/repo-select.ts +74 -0
  82. package/app/lib/repo.tsx +1383 -977
  83. package/app/lib/role.ts +228 -0
  84. package/app/lib/route-catchall.test.ts +27 -0
  85. package/app/lib/route-repo-entry.test.ts +95 -0
  86. package/app/lib/route-repo-entry.ts +36 -0
  87. package/app/lib/router-contract.test.ts +22 -0
  88. package/app/lib/router-contract.ts +19 -0
  89. package/app/lib/shared-layout.test.ts +86 -0
  90. package/app/lib/shared-layout.ts +82 -0
  91. package/app/lib/shortcuts-panel.ts +2 -0
  92. package/app/lib/status-bar.test.ts +118 -0
  93. package/app/lib/status-bar.ts +365 -128
  94. package/app/lib/sync-controls.test.ts +43 -0
  95. package/app/lib/sync-controls.tsx +303 -0
  96. package/app/lib/test-dom.ts +145 -0
  97. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  98. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  99. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  100. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  101. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  102. package/app/lib/transclusion-smoke.test.ts +163 -0
  103. package/app/lib/tutorial.ts +301 -0
  104. package/app/lib/version.ts +93 -0
  105. package/app/lib/viewport-culling.ts +740 -728
  106. package/app/lib/virtual-files.ts +456 -0
  107. package/app/lib/webgl-text.ts +189 -0
  108. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -477
  109. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  110. package/app/og-image.png +0 -0
  111. package/app/page.client.tsx +70 -215
  112. package/app/page.tsx +27 -92
  113. package/app/state/machine.js +13 -0
  114. package/banner.png +0 -0
  115. package/package.json +17 -8
  116. package/server.ts +11 -1
  117. package/app/api/connections/route.ts +0 -72
  118. package/app/api/positions/route.ts +0 -80
  119. package/app/api/repo/browse/route.ts +0 -55
  120. package/app/lib/pr-review.ts +0 -374
  121. package/packages/galaxydraw/README.md +0 -296
  122. package/packages/galaxydraw/banner.png +0 -0
  123. package/packages/galaxydraw/demo/build-static.ts +0 -100
  124. package/packages/galaxydraw/demo/client.ts +0 -154
  125. package/packages/galaxydraw/demo/dist/client.js +0 -8
  126. package/packages/galaxydraw/demo/index.html +0 -256
  127. package/packages/galaxydraw/demo/server.ts +0 -96
  128. package/packages/galaxydraw/dist/index.js +0 -984
  129. package/packages/galaxydraw/dist/index.js.map +0 -16
  130. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  131. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  132. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  133. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  134. package/packages/galaxydraw/package.json +0 -49
  135. package/packages/galaxydraw/perf.test.ts +0 -284
  136. package/packages/galaxydraw/src/core/cards.ts +0 -435
  137. package/packages/galaxydraw/src/core/engine.ts +0 -339
  138. package/packages/galaxydraw/src/core/events.ts +0 -81
  139. package/packages/galaxydraw/src/core/layout.ts +0 -136
  140. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  141. package/packages/galaxydraw/src/core/state.ts +0 -177
  142. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  143. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  144. package/packages/galaxydraw/src/index.ts +0 -40
  145. package/packages/galaxydraw/tsconfig.json +0 -30
@@ -1,339 +0,0 @@
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
- }
@@ -1,81 +0,0 @@
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
- }
@@ -1,136 +0,0 @@
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
- }