gitmaps 1.1.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.
- package/README.md +267 -118
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/og-image/route.ts +14 -0
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/version/route.ts +26 -0
- package/app/globals.css +5706 -4938
- package/app/layout.tsx +1279 -490
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas.ts +625 -564
- package/app/lib/cards.tsx +1361 -916
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +71 -93
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -148
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -427
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/positions.ts +190 -121
- package/app/lib/recent-commits.test.ts +869 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -987
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -735
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -269
- package/app/page.tsx +15 -16
- package/app/state/machine.js +13 -0
- package/package.json +16 -7
- package/server.ts +10 -0
- package/app/[owner]/[repo]/page.tsx +0 -6
- package/app/[slug]/page.tsx +0 -6
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- 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 +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="banner.png" alt="galaxydraw" width="100%" />
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
<p align="center">
|
|
6
|
-
<b>Infinite canvas framework for spatial applications. Zero dependencies, ~31KB.</b>
|
|
7
|
-
</p>
|
|
8
|
-
|
|
9
|
-
<p align="center">
|
|
10
|
-
<a href="https://7flash.github.io/gitmaps/"><img src="https://img.shields.io/badge/🌌_Live_Demo-galaxydraw-9382ff?style=flat-square" alt="Live Demo"></a>
|
|
11
|
-
<a href="https://www.npmjs.com/package/galaxydraw"><img src="https://img.shields.io/npm/v/galaxydraw.svg?style=flat-square" alt="npm version"></a>
|
|
12
|
-
<a href="https://www.npmjs.com/package/galaxydraw"><img src="https://img.shields.io/npm/dm/galaxydraw.svg?style=flat-square" alt="npm downloads"></a>
|
|
13
|
-
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg?style=flat-square" alt="License"></a>
|
|
14
|
-
</p>
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
**Before** — 760 lines of custom pan/zoom/drag/touch/minimap/resize code per project:
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
let state = { zoom: 1, offsetX: 0, offsetY: 0 };
|
|
22
|
-
viewport.addEventListener('wheel', (e) => {
|
|
23
|
-
e.preventDefault();
|
|
24
|
-
const zoomFactor = e.deltaY < 0 ? 1.08 : 1 / 1.08;
|
|
25
|
-
const newZoom = Math.max(0.15, Math.min(3, state.zoom * zoomFactor));
|
|
26
|
-
const rect = viewport.getBoundingClientRect();
|
|
27
|
-
const mouseX = e.clientX - rect.left;
|
|
28
|
-
const mouseY = e.clientY - rect.top;
|
|
29
|
-
const worldX = (mouseX - state.offsetX) / state.zoom;
|
|
30
|
-
const worldY = (mouseY - state.offsetY) / state.zoom;
|
|
31
|
-
state.zoom = newZoom;
|
|
32
|
-
state.offsetX = mouseX - worldX * newZoom;
|
|
33
|
-
state.offsetY = mouseY - worldY * newZoom;
|
|
34
|
-
content.style.transform = `translate(${state.offsetX}px,${state.offsetY}px) scale(${state.zoom})`;
|
|
35
|
-
});
|
|
36
|
-
// + 700 more lines for mouse pan, touch, drag, resize, minimap, z-order...
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**After** — galaxydraw handles all of it:
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
import { GalaxyDraw } from 'galaxydraw';
|
|
43
|
-
|
|
44
|
-
const gd = new GalaxyDraw(document.getElementById('app'), { mode: 'simple' });
|
|
45
|
-
// → Pan, zoom, touch, keyboard shortcuts — all working.
|
|
46
|
-
// → Cards, viewport culling, minimap — opt-in via plugins.
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Installation
|
|
50
|
-
|
|
51
|
-
```sh
|
|
52
|
-
bun add galaxydraw
|
|
53
|
-
# or: npm install galaxydraw
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
For local development across repos, use a `file:` dependency:
|
|
57
|
-
|
|
58
|
-
```json
|
|
59
|
-
"galaxydraw": "file:../galaxy-canvas/packages/galaxydraw"
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## ✨ What You Get
|
|
63
|
-
|
|
64
|
-
Every `new GalaxyDraw()` automatically:
|
|
65
|
-
|
|
66
|
-
- 🖱️ **Mouse pan/zoom** → wheel zoom toward cursor, click-drag pan
|
|
67
|
-
- 📱 **Touch support** → single-finger pan, pinch-to-zoom (native, no libraries)
|
|
68
|
-
- ⌨️ **Keyboard** → Space+drag pan (advanced mode), input element passthrough
|
|
69
|
-
- 🃏 **Card system** → drag, resize, z-order, selection via plugins
|
|
70
|
-
- 🔍 **Viewport culling** → only visible cards stay in DOM, deferred lazy-creation
|
|
71
|
-
- 🗺️ **Minimap** → optional overview with click-to-navigate
|
|
72
|
-
- 📐 **Layout persistence** → save/restore positions (localStorage or custom)
|
|
73
|
-
- 🎛️ **Dual control modes** → Simple (dashboard-style) or Advanced (design-tool-style)
|
|
74
|
-
- 🔌 **Plugin architecture** → custom card types with event passthrough
|
|
75
|
-
|
|
76
|
-
## ⚙️ Constructor Options
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
const gd = new GalaxyDraw(containerEl, {
|
|
80
|
-
mode: 'simple', // or 'advanced'
|
|
81
|
-
minimap: true, // render overview panel
|
|
82
|
-
cullMargin: 200, // px beyond viewport to keep cards alive
|
|
83
|
-
className: 'my-canvas', // custom CSS class on root
|
|
84
|
-
cards: {
|
|
85
|
-
defaultWidth: 400,
|
|
86
|
-
defaultHeight: 300,
|
|
87
|
-
minWidth: 200,
|
|
88
|
-
minHeight: 150,
|
|
89
|
-
gridSize: 20, // snap-to-grid resolution (0 = off)
|
|
90
|
-
cornerSize: 40, // resize handle hit area
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
| Option | Type | Default | Description |
|
|
96
|
-
|--------|------|---------|-------------|
|
|
97
|
-
| `mode` | `'simple' \| 'advanced'` | `'simple'` | Control scheme |
|
|
98
|
-
| `minimap` | `boolean` | `false` | Render overview panel |
|
|
99
|
-
| `cullMargin` | `number` | `200` | Viewport buffer in px |
|
|
100
|
-
| `className` | `string` | — | Custom root CSS class |
|
|
101
|
-
| `cards.defaultWidth` | `number` | `400` | Initial card width |
|
|
102
|
-
| `cards.defaultHeight` | `number` | `300` | Initial card height |
|
|
103
|
-
| `cards.minWidth` | `number` | `200` | Minimum resize width |
|
|
104
|
-
| `cards.minHeight` | `number` | `150` | Minimum resize height |
|
|
105
|
-
| `cards.gridSize` | `number` | `0` | Shift+drag snap grid (0 = off) |
|
|
106
|
-
| `cards.cornerSize` | `number` | `40` | Resize handle hit area |
|
|
107
|
-
|
|
108
|
-
## 🎛️ Control Modes
|
|
109
|
-
|
|
110
|
-
| Mode | Left-click on canvas | Left-click on card | Space+drag |
|
|
111
|
-
|------|---------------------|--------------------|------------|
|
|
112
|
-
| `simple` | Pan | — | Pan |
|
|
113
|
-
| `advanced` | — | Select | Pan |
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
gd.setMode('advanced');
|
|
117
|
-
console.log(gd.getMode()); // → 'advanced'
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## 🔌 Card Plugins
|
|
121
|
-
|
|
122
|
-
Cards are rendered by plugins. Each plugin handles one card type:
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
import { GalaxyDraw } from 'galaxydraw';
|
|
126
|
-
import type { CardPlugin, CardData } from 'galaxydraw';
|
|
127
|
-
|
|
128
|
-
const widgetPlugin: CardPlugin = {
|
|
129
|
-
type: 'widget',
|
|
130
|
-
|
|
131
|
-
render(data: CardData): HTMLElement {
|
|
132
|
-
const el = document.createElement('div');
|
|
133
|
-
el.innerHTML = `
|
|
134
|
-
<div class="gd-card-header">${data.meta?.title || 'Widget'}</div>
|
|
135
|
-
<div class="gd-card-body">Content here</div>
|
|
136
|
-
`;
|
|
137
|
-
return el;
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
// Optional: claim mouse/wheel events for interactive content
|
|
141
|
-
consumesWheel(target) {
|
|
142
|
-
return !!target.closest('.maplibregl-map');
|
|
143
|
-
},
|
|
144
|
-
consumesMouse(target) {
|
|
145
|
-
return !!target.closest('.maplibregl-map');
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
onResize(el, w, h) { /* handle resize */ },
|
|
149
|
-
onDestroy(el) { /* cleanup */ },
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const gd = new GalaxyDraw(containerEl, { mode: 'simple' });
|
|
153
|
-
gd.registerPlugin(widgetPlugin);
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### CardPlugin Interface
|
|
157
|
-
|
|
158
|
-
| Method | Required | Description |
|
|
159
|
-
|--------|----------|-------------|
|
|
160
|
-
| `type` | Yes | Unique string identifier |
|
|
161
|
-
| `render(data)` | Yes | Returns the card's DOM element |
|
|
162
|
-
| `consumesWheel(target)` | No | Return `true` to let the card handle wheel events (e.g., maps) |
|
|
163
|
-
| `consumesMouse(target)` | No | Return `true` to let the card handle mouse events |
|
|
164
|
-
| `onResize(el, w, h)` | No | Called after card resize |
|
|
165
|
-
| `onDestroy(el)` | No | Cleanup callback |
|
|
166
|
-
|
|
167
|
-
### Creating Cards
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
// Immediate creation (visible cards)
|
|
171
|
-
const el = gd.cards.create('widget', {
|
|
172
|
-
id: 'w1', x: 100, y: 100,
|
|
173
|
-
meta: { title: 'Map' },
|
|
174
|
-
});
|
|
175
|
-
// → HTMLElement appended to canvas at (100, 100)
|
|
176
|
-
|
|
177
|
-
// Deferred creation (off-screen cards, lazy-materialized on scroll)
|
|
178
|
-
gd.cards.defer('widget', {
|
|
179
|
-
id: 'w2', x: 3000, y: 3000, width: 400, height: 300,
|
|
180
|
-
meta: { title: 'Far Away' },
|
|
181
|
-
});
|
|
182
|
-
// → Stored in memory, created when user scrolls near (3000, 3000)
|
|
183
|
-
|
|
184
|
-
// Remove
|
|
185
|
-
gd.cards.remove('w1');
|
|
186
|
-
// → Calls onDestroy, removes from DOM
|
|
187
|
-
|
|
188
|
-
// Clear all
|
|
189
|
-
gd.cards.clear();
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
## 📡 Event Bus
|
|
193
|
-
|
|
194
|
-
Subscribe to card and engine events:
|
|
195
|
-
|
|
196
|
-
```ts
|
|
197
|
-
gd.bus.on('card:move', ({ id, x, y }) => {
|
|
198
|
-
console.log(`Card ${id} moved to (${x}, ${y})`);
|
|
199
|
-
// → "Card w1 moved to (250, 180)"
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
gd.bus.on('card:resize', ({ id, width, height }) => {
|
|
203
|
-
console.log(`Card ${id} resized: ${width}x${height}`);
|
|
204
|
-
// → "Card w1 resized: 500x400"
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
gd.bus.on('card:select', ({ ids }) => {
|
|
208
|
-
console.log(`Selected: ${ids.join(', ')}`);
|
|
209
|
-
// → "Selected: w1, w2"
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
gd.bus.on('card:collapse', ({ id, collapsed }) => {
|
|
213
|
-
console.log(`Card ${id} ${collapsed ? 'collapsed' : 'expanded'}`);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
gd.bus.on('mode:change', ({ mode }) => {
|
|
217
|
-
console.log(`Switched to ${mode} mode`);
|
|
218
|
-
});
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
| Event | Payload | When |
|
|
222
|
-
|-------|---------|------|
|
|
223
|
-
| `card:create` | `{ id, x, y }` | Card added to canvas |
|
|
224
|
-
| `card:move` | `{ id, x, y }` | Card drag ended |
|
|
225
|
-
| `card:resize` | `{ id, width, height }` | Card resize ended |
|
|
226
|
-
| `card:select` | `{ ids: string[] }` | Selection changed |
|
|
227
|
-
| `card:deselect` | `{ ids: string[] }` | Cards deselected |
|
|
228
|
-
| `card:collapse` | `{ id, collapsed }` | Collapse toggled |
|
|
229
|
-
| `card:remove` | `{ id }` | Card removed |
|
|
230
|
-
| `mode:change` | `{ mode }` | Control mode switched |
|
|
231
|
-
|
|
232
|
-
## 🧭 Canvas State
|
|
233
|
-
|
|
234
|
-
Direct access to pan/zoom state:
|
|
235
|
-
|
|
236
|
-
```ts
|
|
237
|
-
// Read current state
|
|
238
|
-
const { zoom, offsetX, offsetY } = gd.state.getSnapshot();
|
|
239
|
-
// → { zoom: 1.2, offsetX: -340, offsetY: -120 }
|
|
240
|
-
|
|
241
|
-
// Programmatic control
|
|
242
|
-
gd.state.set(1.5, -200, -100); // set zoom, offsetX, offsetY
|
|
243
|
-
gd.state.zoomToward(400, 300, 1.2); // zoom toward screen point
|
|
244
|
-
gd.state.pan(50, 0); // delta pan
|
|
245
|
-
|
|
246
|
-
// Subscribe to changes
|
|
247
|
-
const unsub = gd.state.subscribe(() => {
|
|
248
|
-
console.log('Zoom:', gd.state.zoom); // → "Zoom: 1.5"
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// Coordinate conversion
|
|
252
|
-
const worldPt = gd.state.screenToWorld(e.clientX, e.clientY);
|
|
253
|
-
// → { x: 842, y: 316 }
|
|
254
|
-
|
|
255
|
-
// Fit all content into view
|
|
256
|
-
gd.fitAll(60); // 60px padding
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
## 🏗️ Architecture
|
|
260
|
-
|
|
261
|
-
```
|
|
262
|
-
src/
|
|
263
|
-
├── index.ts # Package entry — re-exports everything
|
|
264
|
-
└── core/
|
|
265
|
-
├── engine.ts # GalaxyDraw class (337 lines)
|
|
266
|
-
├── state.ts # CanvasState — zoom/offset/transform
|
|
267
|
-
├── cards.ts # CardManager — create/defer/drag/resize/z-order
|
|
268
|
-
├── viewport.ts # ViewportCuller — show/hide by visibility
|
|
269
|
-
├── events.ts # EventBus — typed pub/sub
|
|
270
|
-
├── layout.ts # LayoutManager — save/restore positions
|
|
271
|
-
└── minimap.ts # Minimap — overview with click navigation
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
Total: ~1,200 lines of engine code. Zero dependencies.
|
|
275
|
-
|
|
276
|
-
## 🚀 Used By
|
|
277
|
-
|
|
278
|
-
- **[GitMaps](https://github.com/7flash/gitmaps)** — Repository visualization on an infinite canvas. Uses `advanced` mode with FileCardPlugin + DiffCardPlugin. Renders 6,800+ file cards with viewport culling (~35ms).
|
|
279
|
-
- **[WARMAPS](https://github.com/7flash/starwar)** — Real-time geopolitical intelligence dashboard. Uses `simple` mode with WarmapsContainerPlugin for MapLibre/WebSocket feed passthrough.
|
|
280
|
-
|
|
281
|
-
## 🧪 Testing
|
|
282
|
-
|
|
283
|
-
24 unit tests covering the core engine:
|
|
284
|
-
|
|
285
|
-
```sh
|
|
286
|
-
bun test
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
| Suite | Tests | Coverage |
|
|
290
|
-
|-------|-------|----------|
|
|
291
|
-
| CanvasState | 14 | zoom, pan, clamp, screenToWorld, subscribe, fitAll |
|
|
292
|
-
| EventBus | 10 | on/off, emit, multi-listener, wildcard, once |
|
|
293
|
-
|
|
294
|
-
## License
|
|
295
|
-
|
|
296
|
-
MIT
|
|
Binary file
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build a standalone static index.html demo for GitHub Pages
|
|
3
|
-
*/
|
|
4
|
-
const css = await Bun.file(import.meta.dir + '/../src/galaxydraw.css').text();
|
|
5
|
-
|
|
6
|
-
// Bundle the client code
|
|
7
|
-
const buildResult = await Bun.build({
|
|
8
|
-
entrypoints: [import.meta.dir + '/client.ts'],
|
|
9
|
-
target: 'browser',
|
|
10
|
-
format: 'esm',
|
|
11
|
-
minify: true,
|
|
12
|
-
});
|
|
13
|
-
const js = await buildResult.outputs[0].text();
|
|
14
|
-
|
|
15
|
-
const html = `<!DOCTYPE html>
|
|
16
|
-
<html lang="en">
|
|
17
|
-
<head>
|
|
18
|
-
<meta charset="UTF-8">
|
|
19
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
20
|
-
<title>galaxydraw — Interactive Demo</title>
|
|
21
|
-
<meta name="description" content="Interactive demo of galaxydraw, a zero-dependency infinite canvas framework for spatial applications.">
|
|
22
|
-
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
|
|
23
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
24
|
-
<style>
|
|
25
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
26
|
-
html, body { width: 100%; height: 100%; background: #060812; color: #e4e4e7; font-family: 'Inter', sans-serif; overflow: hidden; }
|
|
27
|
-
#app { width: 100vw; height: 100vh; }
|
|
28
|
-
|
|
29
|
-
.demo-toolbar {
|
|
30
|
-
position: fixed;
|
|
31
|
-
top: 16px;
|
|
32
|
-
left: 50%;
|
|
33
|
-
transform: translateX(-50%);
|
|
34
|
-
z-index: 1000;
|
|
35
|
-
display: flex;
|
|
36
|
-
gap: 8px;
|
|
37
|
-
padding: 8px 16px;
|
|
38
|
-
border-radius: 12px;
|
|
39
|
-
background: rgba(13, 15, 28, 0.85);
|
|
40
|
-
backdrop-filter: blur(16px);
|
|
41
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.demo-toolbar button {
|
|
45
|
-
padding: 6px 14px;
|
|
46
|
-
border-radius: 8px;
|
|
47
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
48
|
-
background: transparent;
|
|
49
|
-
color: rgba(255, 255, 255, 0.7);
|
|
50
|
-
cursor: pointer;
|
|
51
|
-
font-size: 12px;
|
|
52
|
-
font-family: 'Inter', sans-serif;
|
|
53
|
-
font-weight: 500;
|
|
54
|
-
transition: all 0.15s ease;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.demo-toolbar button:hover {
|
|
58
|
-
background: rgba(147, 130, 255, 0.15);
|
|
59
|
-
border-color: rgba(147, 130, 255, 0.3);
|
|
60
|
-
color: #fff;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.demo-toolbar button.active {
|
|
64
|
-
background: rgba(147, 130, 255, 0.2);
|
|
65
|
-
border-color: rgba(147, 130, 255, 0.4);
|
|
66
|
-
color: #fff;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.demo-toolbar .mode-label {
|
|
70
|
-
font-size: 11px;
|
|
71
|
-
color: rgba(255, 255, 255, 0.4);
|
|
72
|
-
display: flex;
|
|
73
|
-
align-items: center;
|
|
74
|
-
padding: 0 8px;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* Responsive toolbar */
|
|
78
|
-
@media (max-width: 640px) {
|
|
79
|
-
.demo-toolbar {
|
|
80
|
-
flex-wrap: wrap;
|
|
81
|
-
max-width: 90vw;
|
|
82
|
-
justify-content: center;
|
|
83
|
-
}
|
|
84
|
-
.demo-toolbar button {
|
|
85
|
-
font-size: 11px;
|
|
86
|
-
padding: 5px 10px;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
${css}
|
|
91
|
-
</style>
|
|
92
|
-
</head>
|
|
93
|
-
<body>
|
|
94
|
-
<div id="app"></div>
|
|
95
|
-
<script type="module">${js}</script>
|
|
96
|
-
</body>
|
|
97
|
-
</html>`;
|
|
98
|
-
|
|
99
|
-
await Bun.write(import.meta.dir + '/index.html', html);
|
|
100
|
-
console.log(`✓ demo/index.html created (${(html.length / 1024).toFixed(1)}KB)`);
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* galaxydraw demo — client entry point
|
|
3
|
-
*/
|
|
4
|
-
import { GalaxyDraw } from '../src/core/engine';
|
|
5
|
-
import type { CardPlugin } from '../src/core/cards';
|
|
6
|
-
|
|
7
|
-
// ─── Text card plugin ─────────────────────────────────────
|
|
8
|
-
const TextCardPlugin: CardPlugin = {
|
|
9
|
-
type: 'text',
|
|
10
|
-
render(data) {
|
|
11
|
-
const card = document.createElement('div');
|
|
12
|
-
const header = document.createElement('div');
|
|
13
|
-
header.className = 'gd-card-header';
|
|
14
|
-
header.innerHTML = `<span class="title">${data.meta?.title || data.id}</span>`;
|
|
15
|
-
|
|
16
|
-
const body = document.createElement('div');
|
|
17
|
-
body.className = 'gd-card-body';
|
|
18
|
-
body.style.cssText = 'padding:12px; font-size:13px; color:rgba(255,255,255,0.6); line-height:1.6;';
|
|
19
|
-
body.innerHTML = data.meta?.content || 'Drag by the header, resize from the corner.';
|
|
20
|
-
|
|
21
|
-
card.appendChild(header);
|
|
22
|
-
card.appendChild(body);
|
|
23
|
-
return card;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// ─── Note card plugin ─────────────────────────────────────
|
|
28
|
-
const NoteCardPlugin: CardPlugin = {
|
|
29
|
-
type: 'note',
|
|
30
|
-
render(data) {
|
|
31
|
-
const colors = ['#22c55e', '#eab308', '#ef4444', '#3b82f6', '#a855f7'];
|
|
32
|
-
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
33
|
-
const card = document.createElement('div');
|
|
34
|
-
|
|
35
|
-
const header = document.createElement('div');
|
|
36
|
-
header.className = 'gd-card-header';
|
|
37
|
-
header.style.borderLeft = `3px solid ${color}`;
|
|
38
|
-
header.innerHTML = `
|
|
39
|
-
<span class="title">${data.meta?.title || 'Note'}</span>
|
|
40
|
-
<span style="font-size:10px; color:${color}; text-transform:uppercase; letter-spacing:0.05em;">Note</span>
|
|
41
|
-
`;
|
|
42
|
-
|
|
43
|
-
const body = document.createElement('div');
|
|
44
|
-
body.className = 'gd-card-body';
|
|
45
|
-
body.style.padding = '16px';
|
|
46
|
-
body.innerHTML = `
|
|
47
|
-
<div contenteditable="true" style="font-size:13px; color:rgba(255,255,255,0.7); outline:none; min-height:60px; line-height:1.6;">
|
|
48
|
-
${data.meta?.text || 'Click to edit...'}
|
|
49
|
-
</div>
|
|
50
|
-
`;
|
|
51
|
-
|
|
52
|
-
card.appendChild(header);
|
|
53
|
-
card.appendChild(body);
|
|
54
|
-
return card;
|
|
55
|
-
},
|
|
56
|
-
consumesMouse(target: HTMLElement) {
|
|
57
|
-
return target.closest('[contenteditable]') !== null;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// ─── Init ─────────────────────────────────────────────────
|
|
62
|
-
const container = document.getElementById('app')!;
|
|
63
|
-
const gd = new GalaxyDraw(container, {
|
|
64
|
-
mode: 'simple',
|
|
65
|
-
cards: { defaultWidth: 320, defaultHeight: 240 },
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
gd.registerPlugin(TextCardPlugin);
|
|
69
|
-
gd.registerPlugin(NoteCardPlugin);
|
|
70
|
-
|
|
71
|
-
// ─── Create demo cards ────────────────────────────────────
|
|
72
|
-
const items = [
|
|
73
|
-
{
|
|
74
|
-
type: 'text', id: 'welcome', x: 100, y: 100, width: 380, height: 220, meta: {
|
|
75
|
-
title: 'galaxydraw',
|
|
76
|
-
content: '<div style="font-size:20px; font-weight:600; color:#fff; margin-bottom:8px;">Infinite Canvas Framework</div><div>The engine behind <strong>GitMaps</strong> and <strong>WARMAPS</strong>.</div><br/><div style="font-size:11px; color:rgba(255,255,255,0.35);">Pan: drag empty space | Zoom: scroll | Drag cards by headers</div>'
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
{ type: 'note', id: 'note1', x: 550, y: 80, width: 280, height: 200, meta: { title: 'Architecture', text: 'EventBus > CanvasState > CardManager > ViewportCuller' } },
|
|
80
|
-
{
|
|
81
|
-
type: 'text', id: 'features', x: 100, y: 380, width: 350, height: 280, meta: {
|
|
82
|
-
title: 'Features',
|
|
83
|
-
content: '<ul style="padding-left:16px;"><li>Virtualized rendering</li><li>Card plugins for custom content</li><li>Dual control modes (Simple / Advanced)</li><li>Viewport culling</li><li>Layout persistence</li><li>Minimap</li><li>Type-safe EventBus</li></ul>'
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
{ type: 'note', id: 'note2', x: 550, y: 340, width: 280, height: 180, meta: { title: 'Performance', text: 'React repo: 6833 files, only 9 DOM cards created. 6824 deferred. Over 300x speedup.' } },
|
|
87
|
-
{
|
|
88
|
-
type: 'text', id: 'modes', x: 900, y: 100, width: 300, height: 200, meta: {
|
|
89
|
-
title: 'Control Modes',
|
|
90
|
-
content: '<div><strong>Simple</strong> (WARMAPS): Drag = pan canvas<br/><strong>Advanced</strong> (GitMaps): Space+Drag = pan, Click = select</div>'
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
{ type: 'note', id: 'note3', x: 900, y: 360, width: 280, height: 160, meta: { title: 'In Production', text: 'Powering GitMaps (repo visualization) and WARMAPS (geopolitical intelligence dashboard). Touch support included.' } },
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
for (const c of items) {
|
|
97
|
-
gd.cards.create(c.type, c);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ─── Toolbar ──────────────────────────────────────────────
|
|
101
|
-
const toolbar = document.createElement('div');
|
|
102
|
-
toolbar.className = 'demo-toolbar';
|
|
103
|
-
|
|
104
|
-
const label = document.createElement('span');
|
|
105
|
-
label.className = 'mode-label';
|
|
106
|
-
label.textContent = 'Mode:';
|
|
107
|
-
toolbar.appendChild(label);
|
|
108
|
-
|
|
109
|
-
const btnSimple = document.createElement('button');
|
|
110
|
-
btnSimple.textContent = 'Simple (WARMAPS)';
|
|
111
|
-
btnSimple.className = 'active';
|
|
112
|
-
btnSimple.id = 'modeSimple';
|
|
113
|
-
toolbar.appendChild(btnSimple);
|
|
114
|
-
|
|
115
|
-
const btnAdvanced = document.createElement('button');
|
|
116
|
-
btnAdvanced.textContent = 'Advanced (GitMaps)';
|
|
117
|
-
btnAdvanced.id = 'modeAdvanced';
|
|
118
|
-
toolbar.appendChild(btnAdvanced);
|
|
119
|
-
|
|
120
|
-
const btnAdd = document.createElement('button');
|
|
121
|
-
btnAdd.textContent = '+ Card';
|
|
122
|
-
toolbar.appendChild(btnAdd);
|
|
123
|
-
|
|
124
|
-
const btnFit = document.createElement('button');
|
|
125
|
-
btnFit.textContent = 'Fit All';
|
|
126
|
-
toolbar.appendChild(btnFit);
|
|
127
|
-
|
|
128
|
-
document.body.appendChild(toolbar);
|
|
129
|
-
|
|
130
|
-
btnSimple.onclick = () => {
|
|
131
|
-
gd.setMode('simple');
|
|
132
|
-
btnSimple.classList.add('active');
|
|
133
|
-
btnAdvanced.classList.remove('active');
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
btnAdvanced.onclick = () => {
|
|
137
|
-
gd.setMode('advanced');
|
|
138
|
-
btnAdvanced.classList.add('active');
|
|
139
|
-
btnSimple.classList.remove('active');
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
btnAdd.onclick = () => {
|
|
143
|
-
const id = 'card-' + Date.now();
|
|
144
|
-
gd.cards.create('note', {
|
|
145
|
-
id, x: 200 + Math.random() * 600, y: 200 + Math.random() * 400,
|
|
146
|
-
width: 260, height: 180,
|
|
147
|
-
meta: { title: 'New Note', text: 'Click to edit...' }
|
|
148
|
-
});
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
btnFit.onclick = () => gd.fitAll();
|
|
152
|
-
|
|
153
|
-
// Global debug access
|
|
154
|
-
(window as any).gd = gd;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
class F{zoom=1;offsetX=0;offsetY=0;viewportEl=null;contentEl=null;listeners=new Set;MIN_ZOOM=0.05;MAX_ZOOM=5;constructor(E,K){this.viewportEl=E??null,this.contentEl=K??null}bind(E,K){this.viewportEl=E,this.contentEl=K,this.applyTransform()}snapshot(){return{zoom:this.zoom,offsetX:this.offsetX,offsetY:this.offsetY}}subscribe(E){return this.listeners.add(E),()=>this.listeners.delete(E)}notify(){for(let E of this.listeners)E()}applyTransform(){if(!this.contentEl)return;this.contentEl.style.transform=`translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.zoom})`}set(E,K,D){this.zoom=Math.max(this.MIN_ZOOM,Math.min(this.MAX_ZOOM,E)),this.offsetX=K,this.offsetY=D,this.applyTransform(),this.notify()}pan(E,K){this.offsetX+=E,this.offsetY+=K,this.applyTransform(),this.notify()}panTo(E,K){if(!this.viewportEl)return;let D=this.viewportEl.clientWidth,G=this.viewportEl.clientHeight;this.offsetX=D/2-E*this.zoom,this.offsetY=G/2-K*this.zoom,this.applyTransform(),this.notify()}zoomToward(E,K,D){let G=Math.max(this.MIN_ZOOM,Math.min(this.MAX_ZOOM,this.zoom*D));if(G===this.zoom)return;let k=this.viewportEl?.getBoundingClientRect(),$=E-(k?.left??0),j=K-(k?.top??0),J=($-this.offsetX)/this.zoom,q=(j-this.offsetY)/this.zoom;this.zoom=G,this.offsetX=$-J*G,this.offsetY=j-q*G,this.applyTransform(),this.notify()}screenToWorld(E,K){let D=this.viewportEl?.getBoundingClientRect(),G=E-(D?.left??0),k=K-(D?.top??0);return{x:(G-this.offsetX)/this.zoom,y:(k-this.offsetY)/this.zoom}}worldToScreen(E,K){let D=this.viewportEl?.getBoundingClientRect();return{x:E*this.zoom+this.offsetX+(D?.left??0),y:K*this.zoom+this.offsetY+(D?.top??0)}}getVisibleWorldRect(E=0){if(!this.viewportEl)return null;let K=this.viewportEl.clientWidth,D=this.viewportEl.clientHeight,G=(-this.offsetX-E)/this.zoom,k=(-this.offsetY-E)/this.zoom,$=(K-this.offsetX+E)/this.zoom,j=(D-this.offsetY+E)/this.zoom;return{left:G,top:k,right:$,bottom:j,width:$-G,height:j-k}}fitRect(E,K,D,G,k=60){if(!this.viewportEl)return;let $=this.viewportEl.clientWidth,j=this.viewportEl.clientHeight,J=D-E+k*2,q=G-K+k*2,Q=Math.min($/J,j/q,this.MAX_ZOOM);this.set(Q,($-J*Q)/2-(E-k)*Q,(j-q*Q)/2-(K-k)*Q)}}var A={defaultWidth:400,defaultHeight:300,minWidth:200,minHeight:150,gridSize:0,cornerSize:40};class L{state;bus;canvas;cards=new Map;deferred=new Map;selected=new Set;topZ=10;plugins=new Map;opts;constructor(E,K,D,G){this.state=E;this.bus=K;this.canvas=D;this.opts={...A,...G}}registerPlugin(E){this.plugins.set(E.type,E)}create(E,K){let D=this.plugins.get(E);if(!D)return console.warn(`[galaxydraw] No plugin registered for card type "${E}"`),null;let G={x:K.x??0,y:K.y??0,width:K.width??this.opts.defaultWidth,height:K.height??this.opts.defaultHeight,collapsed:K.collapsed??!1,meta:K.meta??{},...K},k=D.render(G);if(k.classList.add("gd-card"),k.dataset.cardId=G.id,k.dataset.cardType=E,k.style.left=`${G.x}px`,k.style.top=`${G.y}px`,k.style.width=`${G.width}px`,!G.collapsed)k.style.height=`${G.height}px`;return this.canvas.appendChild(k),this.cards.set(G.id,k),this.bringToFront(k),this.setupDrag(k),this.setupResize(k,E),this.bus.emit("card:create",{id:G.id,x:G.x,y:G.y}),k}remove(E){let K=this.cards.get(E);if(!K){this.deferred.delete(E);return}let D=K.dataset.cardType;if(D)this.plugins.get(D)?.onDestroy?.(K);K.remove(),this.cards.delete(E),this.selected.delete(E),this.bus.emit("card:remove",{id:E})}defer(E,K){this.deferred.set(K.id,{...K,plugin:E})}materializeInRect(E){let K=0,D=[];for(let[G,k]of this.deferred){let{x:$,y:j,width:J,height:q,plugin:Q}=k,V=J||this.opts.defaultWidth,_=q||this.opts.defaultHeight;if($+V>E.left&&$<E.right&&j+_>E.top&&j<E.bottom){if(Q)this.create(Q,k);D.push(G),K++}}for(let G of D)this.deferred.delete(G);return K}clear(){for(let[E,K]of this.cards){let D=K.dataset.cardType;if(D)this.plugins.get(D)?.onDestroy?.(K);K.remove()}this.cards.clear(),this.deferred.clear(),this.selected.clear()}bringToFront(E){this.topZ++,E.style.zIndex=String(this.topZ)}select(E,K=!1){if(!K)this.deselectAll();this.selected.add(E),this.cards.get(E)?.classList.add("gd-card--selected"),this.bus.emit("card:select",{ids:[...this.selected]})}deselect(E){this.selected.delete(E),this.cards.get(E)?.classList.remove("gd-card--selected"),this.bus.emit("card:deselect",{ids:[E]})}deselectAll(){for(let K of this.selected)this.cards.get(K)?.classList.remove("gd-card--selected");let E=[...this.selected];if(this.selected.clear(),E.length>0)this.bus.emit("card:deselect",{ids:E})}toggleCollapse(E){let K=this.cards.get(E);if(!K)return;let D=K.classList.toggle("gd-card--collapsed");this.bus.emit("card:collapse",{id:E,collapsed:D})}setupDrag(E){let K=E.querySelector(".gd-card-header"),D=K||E,G=!1,k=0,$=0,j=0,J=0;D.addEventListener("mousedown",(q)=>{if(q.button!==0)return;if(K&&q.target!==K&&!K.contains(q.target))return;q.preventDefault(),G=!0,this.bringToFront(E);let Q=this.state.screenToWorld(q.clientX,q.clientY);j=parseFloat(E.style.left)||0,J=parseFloat(E.style.top)||0,k=Q.x,$=Q.y,E.classList.add("gd-card--dragging");let V=(x)=>{if(!G)return;let O=this.state.screenToWorld(x.clientX,x.clientY),P=j+(O.x-k),z=J+(O.y-$);if(this.opts.gridSize>0&&x.shiftKey)P=Math.round(P/this.opts.gridSize)*this.opts.gridSize,z=Math.round(z/this.opts.gridSize)*this.opts.gridSize;E.style.left=`${P}px`,E.style.top=`${z}px`},_=()=>{G=!1,E.classList.remove("gd-card--dragging"),window.removeEventListener("mousemove",V),window.removeEventListener("mouseup",_);let x=parseFloat(E.style.left)||0,O=parseFloat(E.style.top)||0;this.bus.emit("card:move",{id:E.dataset.cardId,x,y:O})};window.addEventListener("mousemove",V),window.addEventListener("mouseup",_)})}setupResize(E,K){let D=document.createElement("div");D.className="gd-resize-handle",E.appendChild(D);let G=!1,k=0,$=0,j=0,J=0;D.addEventListener("mousedown",(q)=>{q.preventDefault(),q.stopPropagation(),G=!0,k=E.offsetWidth,$=E.offsetHeight,j=q.clientX,J=q.clientY,E.classList.add("gd-card--resizing");let Q=(_)=>{if(!G)return;let x=(_.clientX-j)/this.state.zoom,O=(_.clientY-J)/this.state.zoom,P=Math.max(this.opts.minWidth,k+x),z=Math.max(this.opts.minHeight,$+O);E.style.width=`${P}px`,E.style.height=`${z}px`,this.plugins.get(K)?.onResize?.(E,P,z)},V=()=>{G=!1,E.classList.remove("gd-card--resizing"),window.removeEventListener("mousemove",Q),window.removeEventListener("mouseup",V),this.bus.emit("card:resize",{id:E.dataset.cardId,width:E.offsetWidth,height:E.offsetHeight})};window.addEventListener("mousemove",Q),window.addEventListener("mouseup",V)})}consumesWheel(E){let K=E.closest(".gd-card")||E.closest("[data-card-type]");if(!K)return!1;let D=K.dataset.cardType;if(!D)return!1;return this.plugins.get(D)?.consumesWheel?.(E)??!1}consumesMouse(E){let K=E.closest(".gd-card")||E.closest("[data-card-type]");if(!K)return!1;let D=K.dataset.cardType;if(!D)return!1;return this.plugins.get(D)?.consumesMouse?.(E)??!1}}class M{state;cards;bus;rafPending=!1;enabled=!0;margin=500;constructor(E,K,D){this.state=E;this.cards=K;this.bus=D}setEnabled(E){this.enabled=E}schedule(){if(this.rafPending||!this.enabled)return;this.rafPending=!0,requestAnimationFrame(()=>{this.rafPending=!1,this.perform()})}perform(){let E={shown:0,culled:0,materialized:0,total:0};if(!this.enabled)return E;let K=this.state.getVisibleWorldRect(this.margin);if(!K)return E;for(let[D,G]of this.cards.cards){let k=this.isCardInRect(G,K),$=G.dataset.culled==="true";if(k&&$)G.style.contentVisibility="",G.style.visibility="",G.dataset.culled="false",E.shown++;else if(!k&&!$)G.style.contentVisibility="hidden",G.style.visibility="hidden",G.dataset.culled="true",E.culled++;else if(k)E.shown++;else E.culled++}if(this.cards.deferred.size>0)E.materialized=this.cards.materializeInRect(K);if(E.total=this.cards.cards.size+this.cards.deferred.size,E.materialized>0)this.bus.emit("viewport:cull",E);return E}uncullAll(){for(let[,E]of this.cards.cards)E.style.contentVisibility="",E.style.visibility="",E.dataset.culled="false"}isCardInRect(E,K){let D=parseFloat(E.style.left)||0,G=parseFloat(E.style.top)||0,k=E.offsetWidth||400,$=E.offsetHeight||300;return D+k>K.left&&D<K.right&&G+$>K.top&&G<K.bottom}}class U{handlers=new Map;on(E,K){if(!this.handlers.has(E))this.handlers.set(E,new Set);return this.handlers.get(E).add(K),()=>{this.handlers.get(E)?.delete(K)}}once(E,K){let D=(k)=>{G(),K(k)},G=this.on(E,D);return G}emit(E,K){let D=this.handlers.get(E);if(!D)return;for(let G of D)try{G(K)}catch(k){console.error(`[galaxydraw] Event handler error for "${E}":`,k)}}off(E,K){if(K)this.handlers.get(E)?.delete(K);else this.handlers.delete(E)}clear(){this.handlers.clear()}}class h{state;cards;culler;bus;mode;viewport;canvas;spaceHeld=!1;isDragging=!1;dragStartX=0;dragStartY=0;cleanupFns=[];touchStartX=0;touchStartY=0;lastPinchDist=0;constructor(E,K){if(this.mode=K?.mode??"simple",this.bus=new U,this.viewport=document.createElement("div"),this.viewport.className=`gd-viewport ${K?.className??""}`.trim(),this.viewport.style.cssText="position:relative;width:100%;height:100%;overflow:hidden;",this.canvas=document.createElement("div"),this.canvas.className="gd-canvas",this.canvas.style.cssText="position:absolute;top:0;left:0;transform-origin:0 0;will-change:transform;",this.viewport.appendChild(this.canvas),E.appendChild(this.viewport),this.state=new F,this.state.bind(this.viewport,this.canvas),this.cards=new L(this.state,this.bus,this.canvas,K?.cards),this.culler=new M(this.state,this.cards,this.bus),K?.cullMargin)this.culler.margin=K.cullMargin;this.setupWheel(),this.setupMouse(),this.setupTouch(),this.setupKeyboard();let D=this.state.subscribe(()=>this.culler.schedule());this.cleanupFns.push(D)}setMode(E){this.mode=E,this.bus.emit("mode:change",{mode:E})}getMode(){return this.mode}registerPlugin(E){this.cards.registerPlugin(E)}fitAll(E=60){if(this.culler.uncullAll(),this.cards.cards.size===0)return;let K=1/0,D=1/0,G=-1/0,k=-1/0;for(let[,$]of this.cards.cards){let j=parseFloat($.style.left)||0,J=parseFloat($.style.top)||0,q=$.offsetWidth||400,Q=$.offsetHeight||300;K=Math.min(K,j),D=Math.min(D,J),G=Math.max(G,j+q),k=Math.max(k,J+Q)}this.state.fitRect(K,D,G,k,E)}getViewport(){return this.viewport}getCanvas(){return this.canvas}destroy(){this.cleanupFns.forEach((E)=>E()),this.cleanupFns=[],this.cards.clear(),this.bus.clear(),this.viewport.remove()}setupWheel(){this.viewport.addEventListener("wheel",(E)=>{let K=E.target;if(this.cards.consumesWheel(K))return;if(K.closest(".gd-card")||K.closest("[data-card-type]")){let k=K.closest(".gd-card-body")||K.closest(".wm-container-body");if(k&&k.scrollHeight>k.clientHeight){let $=k.scrollTop<=0&&E.deltaY<0,j=k.scrollTop+k.clientHeight>=k.scrollHeight-1&&E.deltaY>0;if(!$&&!j)return}}E.preventDefault();let G=E.deltaY<0?1.08:0.9259259259259258;this.state.zoomToward(E.clientX,E.clientY,G)},{passive:!1})}setupMouse(){this.viewport.addEventListener("mousedown",(E)=>{let K=E.target;if(this.cards.consumesMouse(K))return;if(K.closest(".gd-card-header")||K.closest(".wm-container-header")||K.closest(".gd-resize-handle"))return;let D=K.closest(".gd-card")||K.closest("[data-card-type]");if(D&&E.button===0){let k=D.dataset.cardId;if(k)this.cards.bringToFront(D),this.cards.select(k,E.shiftKey);if(this.mode==="advanced")return}if(E.button===1||this.mode==="simple"&&E.button===0&&!D||this.mode==="advanced"&&this.spaceHeld)this.isDragging=!0,this.dragStartX=E.clientX-this.state.offsetX,this.dragStartY=E.clientY-this.state.offsetY,this.viewport.style.cursor="grabbing",E.preventDefault()}),window.addEventListener("mousemove",(E)=>{if(this.isDragging)this.state.set(this.state.zoom,E.clientX-this.dragStartX,E.clientY-this.dragStartY)}),window.addEventListener("mouseup",()=>{if(this.isDragging)this.isDragging=!1,this.viewport.style.cursor=""})}setupTouch(){let E=(G)=>{let k=G.touches[0]?.target;if(!k)return;if(this.cards.consumesMouse(k))return;if(G.touches.length===1){let $=G.touches[0],j=k.closest(".gd-card")||k.closest("[data-card-type]");if(this.mode==="simple"&&!j||this.mode==="advanced"&&this.spaceHeld)this.isDragging=!0,this.touchStartX=$.clientX-this.state.offsetX,this.touchStartY=$.clientY-this.state.offsetY,G.preventDefault()}else if(G.touches.length===2){this.isDragging=!1;let $=G.touches[0].clientX-G.touches[1].clientX,j=G.touches[0].clientY-G.touches[1].clientY;this.lastPinchDist=Math.sqrt($*$+j*j),G.preventDefault()}},K=(G)=>{if(this.isDragging&&G.touches.length===1){let k=G.touches[0];this.state.set(this.state.zoom,k.clientX-this.touchStartX,k.clientY-this.touchStartY),G.preventDefault()}if(G.touches.length===2){let k=G.touches[0].clientX-G.touches[1].clientX,$=G.touches[0].clientY-G.touches[1].clientY,j=Math.sqrt(k*k+$*$);if(this.lastPinchDist>0){let J=(G.touches[0].clientX+G.touches[1].clientX)/2,q=(G.touches[0].clientY+G.touches[1].clientY)/2,Q=j/this.lastPinchDist;this.state.zoomToward(J,q,Q)}this.lastPinchDist=j,G.preventDefault()}},D=()=>{this.isDragging=!1,this.lastPinchDist=0};this.viewport.addEventListener("touchstart",E,{passive:!1}),this.viewport.addEventListener("touchmove",K,{passive:!1}),this.viewport.addEventListener("touchend",D),this.cleanupFns.push(()=>{this.viewport.removeEventListener("touchstart",E),this.viewport.removeEventListener("touchmove",K),this.viewport.removeEventListener("touchend",D)})}setupKeyboard(){let E=(D)=>{if(D.code==="Space"&&!D.repeat){let G=D.target.tagName;if(G==="INPUT"||G==="TEXTAREA")return;D.preventDefault(),this.spaceHeld=!0,this.viewport.classList.add("gd-space-pan")}},K=(D)=>{if(D.code==="Space"){if(this.spaceHeld=!1,this.viewport.classList.remove("gd-space-pan"),this.isDragging)this.isDragging=!1,this.viewport.style.cursor=""}};window.addEventListener("keydown",E),window.addEventListener("keyup",K),this.cleanupFns.push(()=>{window.removeEventListener("keydown",E),window.removeEventListener("keyup",K)})}}var W={type:"text",render(E){let K=document.createElement("div"),D=document.createElement("div");D.className="gd-card-header",D.innerHTML=`<span class="title">${E.meta?.title||E.id}</span>`;let G=document.createElement("div");return G.className="gd-card-body",G.style.cssText="padding:12px; font-size:13px; color:rgba(255,255,255,0.6); line-height:1.6;",G.innerHTML=E.meta?.content||"Drag by the header, resize from the corner.",K.appendChild(D),K.appendChild(G),K}},S={type:"note",render(E){let K=["#22c55e","#eab308","#ef4444","#3b82f6","#a855f7"],D=K[Math.floor(Math.random()*K.length)],G=document.createElement("div"),k=document.createElement("div");k.className="gd-card-header",k.style.borderLeft=`3px solid ${D}`,k.innerHTML=`
|
|
2
|
-
<span class="title">${E.meta?.title||"Note"}</span>
|
|
3
|
-
<span style="font-size:10px; color:${D}; text-transform:uppercase; letter-spacing:0.05em;">Note</span>
|
|
4
|
-
`;let $=document.createElement("div");return $.className="gd-card-body",$.style.padding="16px",$.innerHTML=`
|
|
5
|
-
<div contenteditable="true" style="font-size:13px; color:rgba(255,255,255,0.7); outline:none; min-height:60px; line-height:1.6;">
|
|
6
|
-
${E.meta?.text||"Click to edit..."}
|
|
7
|
-
</div>
|
|
8
|
-
`,G.appendChild(k),G.appendChild($),G},consumesMouse(E){return E.closest("[contenteditable]")!==null}},T=document.getElementById("app"),I=new h(T,{mode:"simple",cards:{defaultWidth:320,defaultHeight:240}});I.registerPlugin(W);I.registerPlugin(S);var R=[{type:"text",id:"welcome",x:100,y:100,width:380,height:220,meta:{title:"galaxydraw",content:'<div style="font-size:20px; font-weight:600; color:#fff; margin-bottom:8px;">Infinite Canvas Framework</div><div>The engine behind <strong>GitMaps</strong> and <strong>WARMAPS</strong>.</div><br/><div style="font-size:11px; color:rgba(255,255,255,0.35);">Pan: drag empty space | Zoom: scroll | Drag cards by headers</div>'}},{type:"note",id:"note1",x:550,y:80,width:280,height:200,meta:{title:"Architecture",text:"EventBus > CanvasState > CardManager > ViewportCuller"}},{type:"text",id:"features",x:100,y:380,width:350,height:280,meta:{title:"Features",content:'<ul style="padding-left:16px;"><li>Virtualized rendering</li><li>Card plugins for custom content</li><li>Dual control modes (Simple / Advanced)</li><li>Viewport culling</li><li>Layout persistence</li><li>Minimap</li><li>Type-safe EventBus</li></ul>'}},{type:"note",id:"note2",x:550,y:340,width:280,height:180,meta:{title:"Performance",text:"React repo: 6833 files, only 9 DOM cards created. 6824 deferred. Over 300x speedup."}},{type:"text",id:"modes",x:900,y:100,width:300,height:200,meta:{title:"Control Modes",content:"<div><strong>Simple</strong> (WARMAPS): Drag = pan canvas<br/><strong>Advanced</strong> (GitMaps): Space+Drag = pan, Click = select</div>"}},{type:"note",id:"note3",x:900,y:360,width:280,height:160,meta:{title:"In Production",text:"Powering GitMaps (repo visualization) and WARMAPS (geopolitical intelligence dashboard). Touch support included."}}];for(let E of R)I.cards.create(E.type,E);var N=document.createElement("div");N.className="demo-toolbar";var C=document.createElement("span");C.className="mode-label";C.textContent="Mode:";N.appendChild(C);var H=document.createElement("button");H.textContent="Simple (WARMAPS)";H.className="active";H.id="modeSimple";N.appendChild(H);var Z=document.createElement("button");Z.textContent="Advanced (GitMaps)";Z.id="modeAdvanced";N.appendChild(Z);var B=document.createElement("button");B.textContent="+ Card";N.appendChild(B);var s=document.createElement("button");s.textContent="Fit All";N.appendChild(s);document.body.appendChild(N);H.onclick=()=>{I.setMode("simple"),H.classList.add("active"),Z.classList.remove("active")};Z.onclick=()=>{I.setMode("advanced"),Z.classList.add("active"),H.classList.remove("active")};B.onclick=()=>{let E="card-"+Date.now();I.cards.create("note",{id:E,x:200+Math.random()*600,y:200+Math.random()*400,width:260,height:180,meta:{title:"New Note",text:"Click to edit..."}})};s.onclick=()=>I.fitAll();window.gd=I;
|