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,216 +0,0 @@
1
- /**
2
- * Minimap — Small overview of the entire canvas
3
- *
4
- * Renders a proportional view of all card positions as colored
5
- * rectangles. Click/drag on the minimap to navigate.
6
- */
7
-
8
- import type { CanvasState } from './state';
9
- import type { CardManager } from './cards';
10
-
11
- export class Minimap {
12
- private el: HTMLElement;
13
- private mapCanvas: HTMLCanvasElement;
14
- private ctx2d: CanvasRenderingContext2D | null;
15
- private rafPending = false;
16
-
17
- /** Minimap dimensions */
18
- width = 180;
19
- height = 120;
20
-
21
- constructor(
22
- private state: CanvasState,
23
- private cards: CardManager,
24
- container: HTMLElement,
25
- ) {
26
- this.el = document.createElement('div');
27
- this.el.className = 'gd-minimap';
28
- this.el.style.cssText = `
29
- position: absolute;
30
- bottom: 12px;
31
- right: 12px;
32
- width: ${this.width}px;
33
- height: ${this.height}px;
34
- border-radius: 8px;
35
- overflow: hidden;
36
- backdrop-filter: blur(12px);
37
- background: rgba(0, 0, 0, 0.5);
38
- border: 1px solid rgba(255, 255, 255, 0.1);
39
- cursor: pointer;
40
- z-index: 999;
41
- `;
42
-
43
- this.mapCanvas = document.createElement('canvas');
44
- this.mapCanvas.width = this.width;
45
- this.mapCanvas.height = this.height;
46
- this.el.appendChild(this.mapCanvas);
47
- container.appendChild(this.el);
48
-
49
- this.ctx2d = this.mapCanvas.getContext('2d');
50
-
51
- // Click to navigate
52
- this.el.addEventListener('mousedown', (e) => this.handleClick(e));
53
-
54
- // Auto-rebuild on state change
55
- this.state.subscribe(() => this.scheduleRebuild());
56
- }
57
-
58
- /** Schedule a redraw */
59
- scheduleRebuild() {
60
- if (this.rafPending) return;
61
- this.rafPending = true;
62
- requestAnimationFrame(() => {
63
- this.rafPending = false;
64
- this.rebuild();
65
- });
66
- }
67
-
68
- /** Force immediate redraw */
69
- rebuild() {
70
- const ctx = this.ctx2d;
71
- if (!ctx) return;
72
- ctx.clearRect(0, 0, this.width, this.height);
73
-
74
- // Compute world bounding box of all cards
75
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
76
- for (const [, card] of this.cards.cards) {
77
- const x = parseFloat(card.style.left) || 0;
78
- const y = parseFloat(card.style.top) || 0;
79
- const w = card.offsetWidth || 400;
80
- const h = card.offsetHeight || 300;
81
- minX = Math.min(minX, x);
82
- minY = Math.min(minY, y);
83
- maxX = Math.max(maxX, x + w);
84
- maxY = Math.max(maxY, y + h);
85
- }
86
-
87
- // Include deferred cards in bounding box
88
- for (const [, data] of this.cards.deferred) {
89
- minX = Math.min(minX, data.x);
90
- minY = Math.min(minY, data.y);
91
- maxX = Math.max(maxX, data.x + data.width);
92
- maxY = Math.max(maxY, data.y + data.height);
93
- }
94
-
95
- if (minX === Infinity) return; // no cards
96
-
97
- const pad = 50;
98
- const worldW = maxX - minX + pad * 2;
99
- const worldH = maxY - minY + pad * 2;
100
- const scale = Math.min(this.width / worldW, this.height / worldH);
101
-
102
- const ox = (this.width - worldW * scale) / 2;
103
- const oy = (this.height - worldH * scale) / 2;
104
-
105
- // Draw card dots
106
- ctx.fillStyle = 'rgba(147, 130, 255, 0.6)';
107
- for (const [, card] of this.cards.cards) {
108
- const x = (parseFloat(card.style.left) || 0) - minX + pad;
109
- const y = (parseFloat(card.style.top) || 0) - minY + pad;
110
- const w = card.offsetWidth || 400;
111
- const h = card.offsetHeight || 300;
112
- ctx.fillRect(ox + x * scale, oy + y * scale, Math.max(2, w * scale), Math.max(2, h * scale));
113
- }
114
-
115
- // Draw deferred card dots (dimmer)
116
- ctx.fillStyle = 'rgba(147, 130, 255, 0.2)';
117
- for (const [, data] of this.cards.deferred) {
118
- const x = data.x - minX + pad;
119
- const y = data.y - minY + pad;
120
- ctx.fillRect(ox + x * scale, oy + y * scale, Math.max(2, data.width * scale), Math.max(2, data.height * scale));
121
- }
122
-
123
- // Draw viewport rect
124
- const vp = this.state.getVisibleWorldRect();
125
- if (vp) {
126
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
127
- ctx.lineWidth = 1.5;
128
- const rx = ox + (vp.left - minX + pad) * scale;
129
- const ry = oy + (vp.top - minY + pad) * scale;
130
- const rw = vp.width * scale;
131
- const rh = vp.height * scale;
132
- ctx.strokeRect(rx, ry, rw, rh);
133
- }
134
- }
135
-
136
- private handleClick(e: MouseEvent) {
137
- e.stopPropagation();
138
- e.preventDefault();
139
-
140
- // Compute the same bounding box used in rebuild()
141
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
142
- for (const [, card] of this.cards.cards) {
143
- const x = parseFloat(card.style.left) || 0;
144
- const y = parseFloat(card.style.top) || 0;
145
- const w = card.offsetWidth || 400;
146
- const h = card.offsetHeight || 300;
147
- minX = Math.min(minX, x);
148
- minY = Math.min(minY, y);
149
- maxX = Math.max(maxX, x + w);
150
- maxY = Math.max(maxY, y + h);
151
- }
152
- for (const [, data] of this.cards.deferred) {
153
- minX = Math.min(minX, data.x);
154
- minY = Math.min(minY, data.y);
155
- maxX = Math.max(maxX, data.x + data.width);
156
- maxY = Math.max(maxY, data.y + data.height);
157
- }
158
- if (minX === Infinity) return;
159
-
160
- const pad = 50;
161
- const worldW = maxX - minX + pad * 2;
162
- const worldH = maxY - minY + pad * 2;
163
- const scale = Math.min(this.width / worldW, this.height / worldH);
164
- const ox = (this.width - worldW * scale) / 2;
165
- const oy = (this.height - worldH * scale) / 2;
166
-
167
- // Convert minimap click → world coordinates
168
- const rect = this.el.getBoundingClientRect();
169
- const clickX = e.clientX - rect.left;
170
- const clickY = e.clientY - rect.top;
171
-
172
- const worldX = (clickX - ox) / scale + minX - pad;
173
- const worldY = (clickY - oy) / scale + minY - pad;
174
-
175
- // Center the viewport on this world position
176
- const vp = this.state.getVisibleWorldRect();
177
- if (vp) {
178
- const vpWorldW = vp.width;
179
- const vpWorldH = vp.height;
180
- const newOffsetX = -(worldX - vpWorldW / 2) * this.state.zoom;
181
- const newOffsetY = -(worldY - vpWorldH / 2) * this.state.zoom;
182
- this.state.set(this.state.zoom, newOffsetX, newOffsetY);
183
- }
184
-
185
- // Support drag on the minimap
186
- const onMove = (ev: MouseEvent) => {
187
- const mx = ev.clientX - rect.left;
188
- const my = ev.clientY - rect.top;
189
- const wx = (mx - ox) / scale + minX - pad;
190
- const wy = (my - oy) / scale + minY - pad;
191
- const v = this.state.getVisibleWorldRect();
192
- if (v) {
193
- this.state.set(
194
- this.state.zoom,
195
- -(wx - v.width / 2) * this.state.zoom,
196
- -(wy - v.height / 2) * this.state.zoom,
197
- );
198
- }
199
- };
200
- const onUp = () => {
201
- window.removeEventListener('mousemove', onMove);
202
- window.removeEventListener('mouseup', onUp);
203
- };
204
- window.addEventListener('mousemove', onMove);
205
- window.addEventListener('mouseup', onUp);
206
- }
207
-
208
- /** Show/hide the minimap */
209
- setVisible(visible: boolean) {
210
- this.el.style.display = visible ? '' : 'none';
211
- }
212
-
213
- destroy() {
214
- this.el.remove();
215
- }
216
- }
@@ -1,177 +0,0 @@
1
- /**
2
- * CanvasState — Reactive state container for the infinite canvas
3
- *
4
- * Tracks zoom level, pan offset, and provides world↔screen
5
- * coordinate conversion utilities.
6
- */
7
-
8
- export interface CanvasStateSnapshot {
9
- zoom: number;
10
- offsetX: number;
11
- offsetY: number;
12
- }
13
-
14
- export interface ViewportRect {
15
- left: number;
16
- top: number;
17
- right: number;
18
- bottom: number;
19
- width: number;
20
- height: number;
21
- }
22
-
23
- export class CanvasState {
24
- zoom = 1;
25
- offsetX = 0;
26
- offsetY = 0;
27
-
28
- private viewportEl: HTMLElement | null = null;
29
- private contentEl: HTMLElement | null = null;
30
- private listeners = new Set<() => void>();
31
-
32
- // ─── Zoom limits ─────────────────────────────────────
33
- readonly MIN_ZOOM = 0.05;
34
- readonly MAX_ZOOM = 5;
35
-
36
- constructor(viewport?: HTMLElement, content?: HTMLElement) {
37
- this.viewportEl = viewport ?? null;
38
- this.contentEl = content ?? null;
39
- }
40
-
41
- /** Bind to DOM elements after mount */
42
- bind(viewport: HTMLElement, content: HTMLElement) {
43
- this.viewportEl = viewport;
44
- this.contentEl = content;
45
- this.applyTransform();
46
- }
47
-
48
- /** Get a frozen snapshot of current state */
49
- snapshot(): CanvasStateSnapshot {
50
- return { zoom: this.zoom, offsetX: this.offsetX, offsetY: this.offsetY };
51
- }
52
-
53
- /** Subscribe to state changes */
54
- subscribe(fn: () => void): () => void {
55
- this.listeners.add(fn);
56
- return () => this.listeners.delete(fn);
57
- }
58
-
59
- private notify() {
60
- for (const fn of this.listeners) fn();
61
- }
62
-
63
- // ─── Transform ───────────────────────────────────────
64
-
65
- /** Apply current state to the content element's CSS transform */
66
- applyTransform() {
67
- if (!this.contentEl) return;
68
- // Round translate to whole pixels to prevent subpixel text blurring
69
- const tx = Math.round(this.offsetX);
70
- const ty = Math.round(this.offsetY);
71
- this.contentEl.style.transform =
72
- `translate(${tx}px, ${ty}px) scale(${this.zoom})`;
73
- }
74
-
75
- /** Set zoom + offset, clamping zoom to limits */
76
- set(zoom: number, offsetX: number, offsetY: number) {
77
- this.zoom = Math.max(this.MIN_ZOOM, Math.min(this.MAX_ZOOM, zoom));
78
- this.offsetX = offsetX;
79
- this.offsetY = offsetY;
80
- this.applyTransform();
81
- this.notify();
82
- }
83
-
84
- /** Pan by delta pixels */
85
- pan(dx: number, dy: number) {
86
- this.offsetX += dx;
87
- this.offsetY += dy;
88
- this.applyTransform();
89
- this.notify();
90
- }
91
-
92
- /** Center the viewport on a world coordinate */
93
- panTo(worldX: number, worldY: number) {
94
- if (!this.viewportEl) return;
95
- const vpW = this.viewportEl.clientWidth;
96
- const vpH = this.viewportEl.clientHeight;
97
- this.offsetX = vpW / 2 - worldX * this.zoom;
98
- this.offsetY = vpH / 2 - worldY * this.zoom;
99
- this.applyTransform();
100
- this.notify();
101
- }
102
-
103
- /** Zoom toward a screen point (e.g. cursor position) */
104
- zoomToward(screenX: number, screenY: number, factor: number) {
105
- const newZoom = Math.max(this.MIN_ZOOM, Math.min(this.MAX_ZOOM, this.zoom * factor));
106
- if (newZoom === this.zoom) return;
107
-
108
- const rect = this.viewportEl?.getBoundingClientRect();
109
- const mouseX = screenX - (rect?.left ?? 0);
110
- const mouseY = screenY - (rect?.top ?? 0);
111
-
112
- // Convert screen point to world coordinates at current zoom
113
- const worldX = (mouseX - this.offsetX) / this.zoom;
114
- const worldY = (mouseY - this.offsetY) / this.zoom;
115
-
116
- // Update zoom and recalculate offset to keep world point under cursor
117
- this.zoom = newZoom;
118
- this.offsetX = mouseX - worldX * newZoom;
119
- this.offsetY = mouseY - worldY * newZoom;
120
-
121
- this.applyTransform();
122
- this.notify();
123
- }
124
-
125
- // ─── Coordinate conversion ───────────────────────────
126
-
127
- /** Screen pixel → world coordinate */
128
- screenToWorld(screenX: number, screenY: number): { x: number; y: number } {
129
- const rect = this.viewportEl?.getBoundingClientRect();
130
- const localX = screenX - (rect?.left ?? 0);
131
- const localY = screenY - (rect?.top ?? 0);
132
- return {
133
- x: (localX - this.offsetX) / this.zoom,
134
- y: (localY - this.offsetY) / this.zoom,
135
- };
136
- }
137
-
138
- /** World coordinate → screen pixel */
139
- worldToScreen(worldX: number, worldY: number): { x: number; y: number } {
140
- const rect = this.viewportEl?.getBoundingClientRect();
141
- return {
142
- x: worldX * this.zoom + this.offsetX + (rect?.left ?? 0),
143
- y: worldY * this.zoom + this.offsetY + (rect?.top ?? 0),
144
- };
145
- }
146
-
147
- /** Get the visible world-space rectangle (with optional margin in screen px) */
148
- getVisibleWorldRect(margin = 0): ViewportRect | null {
149
- if (!this.viewportEl) return null;
150
- const vpW = this.viewportEl.clientWidth;
151
- const vpH = this.viewportEl.clientHeight;
152
-
153
- const left = (-this.offsetX - margin) / this.zoom;
154
- const top = (-this.offsetY - margin) / this.zoom;
155
- const right = (vpW - this.offsetX + margin) / this.zoom;
156
- const bottom = (vpH - this.offsetY + margin) / this.zoom;
157
-
158
- return { left, top, right, bottom, width: right - left, height: bottom - top };
159
- }
160
-
161
- /** Fit a world-space bounding box into the viewport */
162
- fitRect(worldLeft: number, worldTop: number, worldRight: number, worldBottom: number, padding = 60) {
163
- if (!this.viewportEl) return;
164
- const vpW = this.viewportEl.clientWidth;
165
- const vpH = this.viewportEl.clientHeight;
166
-
167
- const w = worldRight - worldLeft + padding * 2;
168
- const h = worldBottom - worldTop + padding * 2;
169
- const zoom = Math.min(vpW / w, vpH / h, this.MAX_ZOOM);
170
-
171
- this.set(
172
- zoom,
173
- (vpW - w * zoom) / 2 - (worldLeft - padding) * zoom,
174
- (vpH - h * zoom) / 2 - (worldTop - padding) * zoom,
175
- );
176
- }
177
- }
@@ -1,106 +0,0 @@
1
- /**
2
- * ViewportCuller — Hide/show cards based on viewport visibility
3
- *
4
- * Uses content-visibility: hidden for off-screen cards (keeps dimensions
5
- * for layout stability) and materializes deferred cards on demand.
6
- */
7
-
8
- import type { CanvasState } from './state';
9
- import type { CardManager } from './cards';
10
- import type { EventBus } from './events';
11
-
12
- export interface CullResult {
13
- shown: number;
14
- culled: number;
15
- materialized: number;
16
- total: number;
17
- }
18
-
19
- export class ViewportCuller {
20
- private rafPending = false;
21
- private enabled = true;
22
- /** Margin in screen pixels beyond the viewport for pre-rendering */
23
- margin = 500;
24
-
25
- constructor(
26
- private state: CanvasState,
27
- private cards: CardManager,
28
- private bus: EventBus,
29
- ) { }
30
-
31
- /** Enable/disable culling */
32
- setEnabled(enabled: boolean) {
33
- this.enabled = enabled;
34
- }
35
-
36
- /** Schedule a culling pass on the next animation frame */
37
- schedule() {
38
- if (this.rafPending || !this.enabled) return;
39
- this.rafPending = true;
40
- requestAnimationFrame(() => {
41
- this.rafPending = false;
42
- this.perform();
43
- });
44
- }
45
-
46
- /** Perform immediate culling pass */
47
- perform(): CullResult {
48
- const result: CullResult = { shown: 0, culled: 0, materialized: 0, total: 0 };
49
- if (!this.enabled) return result;
50
-
51
- const worldRect = this.state.getVisibleWorldRect(this.margin);
52
- if (!worldRect) return result;
53
-
54
- // 1. Cull/show existing DOM cards
55
- for (const [id, card] of this.cards.cards) {
56
- const visible = this.isCardInRect(card, worldRect);
57
- const wasCulled = card.dataset.culled === 'true';
58
-
59
- if (visible && wasCulled) {
60
- card.style.contentVisibility = '';
61
- card.style.visibility = '';
62
- card.dataset.culled = 'false';
63
- result.shown++;
64
- } else if (!visible && !wasCulled) {
65
- card.style.contentVisibility = 'hidden';
66
- card.style.visibility = 'hidden';
67
- card.dataset.culled = 'true';
68
- result.culled++;
69
- } else if (visible) {
70
- result.shown++;
71
- } else {
72
- result.culled++;
73
- }
74
- }
75
-
76
- // 2. Materialize deferred cards in viewport
77
- if (this.cards.deferred.size > 0) {
78
- result.materialized = this.cards.materializeInRect(worldRect);
79
- }
80
-
81
- result.total = this.cards.cards.size + this.cards.deferred.size;
82
-
83
- if (result.materialized > 0) {
84
- this.bus.emit('viewport:cull', result);
85
- }
86
-
87
- return result;
88
- }
89
-
90
- /** Force all cards visible (for operations that need to measure everything) */
91
- uncullAll() {
92
- for (const [, card] of this.cards.cards) {
93
- card.style.contentVisibility = '';
94
- card.style.visibility = '';
95
- card.dataset.culled = 'false';
96
- }
97
- }
98
-
99
- private isCardInRect(card: HTMLElement, rect: { left: number; top: number; right: number; bottom: number }): boolean {
100
- const x = parseFloat(card.style.left) || 0;
101
- const y = parseFloat(card.style.top) || 0;
102
- const w = card.offsetWidth || 400;
103
- const h = card.offsetHeight || 300;
104
- return x + w > rect.left && x < rect.right && y + h > rect.top && y < rect.bottom;
105
- }
106
- }
@@ -1,166 +0,0 @@
1
- /* galaxydraw.css — Base styles for the infinite canvas */
2
-
3
- /* ─── Viewport & Canvas ─────────────────────────────────── */
4
- .gd-viewport {
5
- position: relative;
6
- width: 100%;
7
- height: 100%;
8
- overflow: hidden;
9
- background: radial-gradient(circle at 50% 50%, #0c0e1a 0%, #060812 100%);
10
- user-select: none;
11
- -webkit-user-select: none;
12
- }
13
-
14
- .gd-viewport.gd-space-pan {
15
- cursor: grab;
16
- }
17
-
18
- .gd-canvas {
19
- position: absolute;
20
- top: 0;
21
- left: 0;
22
- transform-origin: 0 0;
23
- will-change: transform;
24
- }
25
-
26
- /* ─── Dot grid background ───────────────────────────────── */
27
- .gd-viewport::before {
28
- content: '';
29
- position: absolute;
30
- inset: 0;
31
- background-image: radial-gradient(circle, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
32
- background-size: 24px 24px;
33
- pointer-events: none;
34
- z-index: 0;
35
- }
36
-
37
- /* ─── Cards ─────────────────────────────────────────────── */
38
- .gd-card {
39
- position: absolute;
40
- border-radius: 12px;
41
- border: 1px solid rgba(255, 255, 255, 0.08);
42
- background: rgba(13, 15, 28, 0.92);
43
- backdrop-filter: blur(8px);
44
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.03);
45
- display: flex;
46
- flex-direction: column;
47
- overflow: hidden;
48
- transition: box-shadow 0.2s ease, border-color 0.2s ease;
49
- contain: layout style;
50
- }
51
-
52
- .gd-card--selected {
53
- border-color: rgba(147, 130, 255, 0.4);
54
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4), 0 0 0 2px rgba(147, 130, 255, 0.25);
55
- }
56
-
57
- .gd-card--dragging {
58
- opacity: 0.9;
59
- transition: none;
60
- }
61
-
62
- .gd-card--resizing {
63
- transition: none;
64
- }
65
-
66
- .gd-card--collapsed .gd-card-body {
67
- display: none;
68
- }
69
-
70
- /* ─── Card header ───────────────────────────────────────── */
71
- .gd-card-header {
72
- display: flex;
73
- align-items: center;
74
- gap: 8px;
75
- padding: 10px 14px;
76
- border-bottom: 1px solid rgba(255, 255, 255, 0.06);
77
- cursor: grab;
78
- min-height: 40px;
79
- flex-shrink: 0;
80
- }
81
-
82
- .gd-card-header:active {
83
- cursor: grabbing;
84
- }
85
-
86
- .gd-card-header .title {
87
- font-size: 13px;
88
- font-weight: 600;
89
- color: rgba(255, 255, 255, 0.9);
90
- white-space: nowrap;
91
- overflow: hidden;
92
- text-overflow: ellipsis;
93
- flex: 1;
94
- font-family: 'Inter', sans-serif;
95
- }
96
-
97
- /* ─── Card body ─────────────────────────────────────────── */
98
- .gd-card-body {
99
- flex: 1;
100
- overflow: auto;
101
- min-height: 0;
102
- }
103
-
104
- /* ─── Resize handle ─────────────────────────────────────── */
105
- .gd-resize-handle {
106
- position: absolute;
107
- bottom: 0;
108
- right: 0;
109
- width: 20px;
110
- height: 20px;
111
- cursor: nw-resize;
112
- z-index: 1;
113
- }
114
-
115
- .gd-resize-handle::after {
116
- content: '';
117
- position: absolute;
118
- bottom: 4px;
119
- right: 4px;
120
- width: 8px;
121
- height: 8px;
122
- border-right: 2px solid rgba(255, 255, 255, 0.15);
123
- border-bottom: 2px solid rgba(255, 255, 255, 0.15);
124
- border-radius: 0 0 2px 0;
125
- }
126
-
127
- .gd-card:hover .gd-resize-handle::after {
128
- border-color: rgba(255, 255, 255, 0.3);
129
- }
130
-
131
- /* ─── Minimap ───────────────────────────────────────────── */
132
- .gd-minimap {
133
- position: absolute;
134
- bottom: 12px;
135
- right: 12px;
136
- border-radius: 8px;
137
- overflow: hidden;
138
- backdrop-filter: blur(12px);
139
- background: rgba(0, 0, 0, 0.5);
140
- border: 1px solid rgba(255, 255, 255, 0.1);
141
- cursor: pointer;
142
- z-index: 999;
143
- transition: opacity 0.2s ease;
144
- }
145
-
146
- .gd-minimap:hover {
147
- opacity: 1;
148
- }
149
-
150
- /* ─── Scrollbar styling ─────────────────────────────────── */
151
- .gd-card-body::-webkit-scrollbar {
152
- width: 4px;
153
- }
154
-
155
- .gd-card-body::-webkit-scrollbar-track {
156
- background: transparent;
157
- }
158
-
159
- .gd-card-body::-webkit-scrollbar-thumb {
160
- background: rgba(255, 255, 255, 0.1);
161
- border-radius: 2px;
162
- }
163
-
164
- .gd-card-body::-webkit-scrollbar-thumb:hover {
165
- background: rgba(255, 255, 255, 0.2);
166
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * galaxydraw — Infinite canvas framework
3
- *
4
- * Core engine for spatial applications. Provides:
5
- * - Infinite pan/zoom canvas with GPU-accelerated transforms
6
- * - Virtualized card rendering (only DOM for visible cards)
7
- * - Drag, resize, z-order, collapse for cards
8
- * - Minimap with click-to-navigate
9
- * - Layout persistence (localStorage + optional server sync)
10
- * - Dual control modes (Simple: drag=pan / Advanced: space+drag=pan)
11
- * - Keyboard shortcuts system
12
- * - Plugin architecture for custom card types
13
- */
14
-
15
- // ─── Core ────────────────────────────────────────────────
16
- export { GalaxyDraw } from './core/engine';
17
- export type { GalaxyDrawOptions, ControlMode } from './core/engine';
18
-
19
- // ─── Canvas State ────────────────────────────────────────
20
- export { CanvasState } from './core/state';
21
- export type { CanvasStateSnapshot, ViewportRect } from './core/state';
22
-
23
- // ─── Cards ───────────────────────────────────────────────
24
- export { CardManager } from './core/cards';
25
- export type { CardOptions, CardData, CardPlugin } from './core/cards';
26
-
27
- // ─── Viewport & Virtualization ───────────────────────────
28
- export { ViewportCuller } from './core/viewport';
29
- export type { CullResult } from './core/viewport';
30
-
31
- // ─── Layout ──────────────────────────────────────────────
32
- export { LayoutManager } from './core/layout';
33
- export type { LayoutData, LayoutProvider } from './core/layout';
34
-
35
- // ─── Minimap ─────────────────────────────────────────────
36
- export { Minimap } from './core/minimap';
37
-
38
- // ─── Events ──────────────────────────────────────────────
39
- export { EventBus } from './core/events';
40
- export type { GalaxyDrawEvent, EventHandler } from './core/events';