@objectifthunes/whiteboard 0.1.1 → 0.2.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/dist/index.cjs DELETED
@@ -1,939 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- ConfirmDialog: () => ConfirmDialog,
24
- FloatingPanel: () => FloatingPanel,
25
- Minimap: () => Minimap,
26
- PanelErrorBoundary: () => PanelErrorBoundary,
27
- WHITEBOARD_GRID: () => WHITEBOARD_GRID,
28
- WhiteboardShell: () => WhiteboardShell,
29
- ZoomBar: () => ZoomBar,
30
- belowPanel: () => belowPanel,
31
- cn: () => cn,
32
- computeWhiteboardFit: () => computeWhiteboardFit,
33
- computeWhiteboardRectFocus: () => computeWhiteboardRectFocus,
34
- snapToWhiteboardGrid: () => snapToWhiteboardGrid,
35
- usePanelRect: () => usePanelRect,
36
- useWhiteboardLayout: () => useWhiteboardLayout,
37
- useWhiteboardStore: () => useWhiteboardStore
38
- });
39
- module.exports = __toCommonJS(index_exports);
40
-
41
- // src/WhiteboardShell.tsx
42
- var import_react3 = require("react");
43
-
44
- // src/ZoomBar.tsx
45
- var import_react = require("react");
46
-
47
- // src/icons.tsx
48
- var import_jsx_runtime = require("react/jsx-runtime");
49
- var defaults = (size = 14) => ({
50
- xmlns: "http://www.w3.org/2000/svg",
51
- width: size,
52
- height: size,
53
- viewBox: "0 0 24 24",
54
- fill: "none",
55
- stroke: "currentColor",
56
- strokeWidth: 2,
57
- strokeLinecap: "round",
58
- strokeLinejoin: "round"
59
- });
60
- function Minus({ size, ...props }) {
61
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { ...defaults(size), ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 12h14" }) });
62
- }
63
- function Plus({ size, ...props }) {
64
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...defaults(size), ...props, children: [
65
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 12h14" }),
66
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 5v14" })
67
- ] });
68
- }
69
- function ScanSearch({ size, ...props }) {
70
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...defaults(size), ...props, children: [
71
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z", fill: "none", stroke: "none" }),
72
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 7V5a2 2 0 0 1 2-2h2" }),
73
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M17 3h2a2 2 0 0 1 2 2v2" }),
74
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 17v2a2 2 0 0 1-2 2h-2" }),
75
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M7 21H5a2 2 0 0 1-2-2v-2" }),
76
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "3" }),
77
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m16 16-1.9-1.9" })
78
- ] });
79
- }
80
- function RotateCcw({ size, ...props }) {
81
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...defaults(size), ...props, children: [
82
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
83
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 3v5h5" })
84
- ] });
85
- }
86
- function Grid3x3({ size, ...props }) {
87
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...defaults(size), ...props, children: [
88
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
89
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 9h18" }),
90
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 15h18" }),
91
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 3v18" }),
92
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M15 3v18" })
93
- ] });
94
- }
95
- function Maximize2({ size, ...props }) {
96
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...defaults(size), ...props, children: [
97
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "15 3 21 3 21 9" }),
98
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "9 21 3 21 3 15" }),
99
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "21", x2: "14", y1: "3", y2: "10" }),
100
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "3", x2: "10", y1: "21", y2: "14" })
101
- ] });
102
- }
103
- function Loader2({ size, ...props }) {
104
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { ...defaults(size), ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) });
105
- }
106
- function AlertTriangle({ size, ...props }) {
107
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...defaults(size), ...props, children: [
108
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }),
109
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 9v4" }),
110
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 17h.01" })
111
- ] });
112
- }
113
- function Check({ size, ...props }) {
114
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { ...defaults(size), ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M20 6 9 17l-5-5" }) });
115
- }
116
- function X({ size, ...props }) {
117
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...defaults(size), ...props, children: [
118
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18 6 6 18" }),
119
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m6 6 12 12" })
120
- ] });
121
- }
122
-
123
- // src/store.ts
124
- var import_zustand = require("zustand");
125
-
126
- // src/grid.ts
127
- var WHITEBOARD_GRID = 20;
128
- function snapToWhiteboardGrid(value) {
129
- return Math.round(value / WHITEBOARD_GRID) * WHITEBOARD_GRID;
130
- }
131
-
132
- // src/store.ts
133
- function computeWhiteboardFit(panels, viewportSize, padding = 60) {
134
- if (panels.size === 0) return null;
135
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
136
- for (const r of panels.values()) {
137
- minX = Math.min(minX, r.x);
138
- minY = Math.min(minY, r.y);
139
- maxX = Math.max(maxX, r.x + r.width);
140
- maxY = Math.max(maxY, r.y + r.height);
141
- }
142
- const contentW = maxX - minX + padding * 2;
143
- const contentH = maxY - minY + padding * 2;
144
- const vpW = viewportSize.width || window.innerWidth;
145
- const vpH = viewportSize.height || window.innerHeight;
146
- const fitScale = Math.min(vpW / contentW, vpH / contentH, 1.5);
147
- const centerX = (minX + maxX) / 2;
148
- const centerY = (minY + maxY) / 2;
149
- return {
150
- scale: Math.min(3, Math.max(0.2, fitScale)),
151
- offset: {
152
- x: vpW / 2 - centerX * fitScale,
153
- y: vpH / 2 - centerY * fitScale
154
- }
155
- };
156
- }
157
- function computeWhiteboardRectFocus(rect, viewportSize, padding = 40, maxScale = 1.5) {
158
- const vpW = viewportSize.width || window.innerWidth;
159
- const vpH = viewportSize.height || window.innerHeight;
160
- const safeWidth = Math.max(1, rect.width);
161
- const safeHeight = Math.max(1, rect.height);
162
- const fitScale = Math.min(
163
- (vpW - padding * 2) / safeWidth,
164
- (vpH - padding * 2) / safeHeight,
165
- maxScale
166
- );
167
- const nextScale = Math.min(3, Math.max(0.2, fitScale));
168
- return {
169
- scale: nextScale,
170
- offset: {
171
- x: vpW / 2 - (rect.x + safeWidth / 2) * nextScale,
172
- y: vpH / 2 - (rect.y + safeHeight / 2) * nextScale
173
- }
174
- };
175
- }
176
- var useWhiteboardStore = (0, import_zustand.create)((set, get) => ({
177
- offset: { x: 0, y: 0 },
178
- scale: 1,
179
- viewportSize: { width: 0, height: 0 },
180
- snapToGrid: false,
181
- snapGridSize: WHITEBOARD_GRID,
182
- panels: /* @__PURE__ */ new Map(),
183
- resetFns: /* @__PURE__ */ new Map(),
184
- registryVersion: 0,
185
- setOffset: (v) => set((s) => ({ offset: typeof v === "function" ? v(s.offset) : v })),
186
- setScale: (v) => set((s) => ({ scale: typeof v === "function" ? v(s.scale) : v })),
187
- setViewportSize: (v) => set({ viewportSize: v }),
188
- setSnapToGrid: (v) => set((s) => ({ snapToGrid: typeof v === "function" ? v(s.snapToGrid) : v })),
189
- register: (id, rect) => {
190
- get().panels.set(id, rect);
191
- set((s) => ({ registryVersion: s.registryVersion + 1 }));
192
- },
193
- unregister: (id) => {
194
- get().panels.delete(id);
195
- set((s) => ({ registryVersion: s.registryVersion + 1 }));
196
- },
197
- registerReset: (id, fn) => {
198
- get().resetFns.set(id, fn);
199
- },
200
- unregisterReset: (id) => {
201
- get().resetFns.delete(id);
202
- },
203
- fitToContent: () => {
204
- const { panels, viewportSize } = get();
205
- const fit = computeWhiteboardFit(panels, viewportSize);
206
- if (fit) set({ scale: fit.scale, offset: fit.offset });
207
- },
208
- focusPanel: (rect, options) => {
209
- const { viewportSize } = get();
210
- const fit = computeWhiteboardRectFocus(
211
- rect,
212
- viewportSize,
213
- options?.padding ?? 40,
214
- options?.maxScale ?? 1.5
215
- );
216
- set({ scale: fit.scale, offset: fit.offset });
217
- },
218
- resetWidgets: () => {
219
- for (const fn of get().resetFns.values()) fn();
220
- const attempt = (tries = 0) => {
221
- const { panels, viewportSize } = get();
222
- const fit = computeWhiteboardFit(panels, viewportSize);
223
- if (fit) {
224
- set({ scale: fit.scale, offset: fit.offset });
225
- return;
226
- }
227
- if (tries < 6) requestAnimationFrame(() => attempt(tries + 1));
228
- };
229
- requestAnimationFrame(() => requestAnimationFrame(() => attempt()));
230
- },
231
- resetSession: () => set({
232
- offset: { x: 0, y: 0 },
233
- scale: 1,
234
- viewportSize: { width: 0, height: 0 },
235
- snapToGrid: false,
236
- registryVersion: 0,
237
- panels: /* @__PURE__ */ new Map(),
238
- resetFns: /* @__PURE__ */ new Map()
239
- })
240
- }));
241
-
242
- // src/ZoomBar.tsx
243
- var import_jsx_runtime2 = require("react/jsx-runtime");
244
- function ZoomBar({ extraActions }) {
245
- const scale = useWhiteboardStore((s) => s.scale);
246
- const viewportSize = useWhiteboardStore((s) => s.viewportSize);
247
- const snapToGrid = useWhiteboardStore((s) => s.snapToGrid);
248
- const setScale = useWhiteboardStore((s) => s.setScale);
249
- const setOffset = useWhiteboardStore((s) => s.setOffset);
250
- const setSnapToGrid = useWhiteboardStore((s) => s.setSnapToGrid);
251
- const fitToContent = useWhiteboardStore((s) => s.fitToContent);
252
- const resetWidgets = useWhiteboardStore((s) => s.resetWidgets);
253
- (0, import_react.useEffect)(() => {
254
- if (!snapToGrid) return;
255
- window.dispatchEvent(new Event("whiteboard-snap-now"));
256
- }, [snapToGrid]);
257
- const zoomTo = (nextScale) => {
258
- const clamped = Math.min(3, Math.max(0.2, nextScale));
259
- const ratio = clamped / scale;
260
- const cx = (viewportSize.width || window.innerWidth) / 2;
261
- const cy = (viewportSize.height || window.innerHeight) / 2;
262
- setOffset((prev) => ({
263
- x: cx - (cx - prev.x) * ratio,
264
- y: cy - (cy - prev.y) * ratio
265
- }));
266
- setScale(clamped);
267
- };
268
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
269
- "div",
270
- {
271
- className: "zoom-bar",
272
- onPointerDown: (e) => e.stopPropagation(),
273
- onWheel: (e) => e.stopPropagation(),
274
- children: [
275
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "wb-btn wb-btn--secondary wb-btn--icon-only zoom-bar__icon", onClick: () => zoomTo(scale * 0.8), title: "Zoom out", "aria-label": "Zoom out", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Minus, { size: 14 }) }),
276
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "zoom-bar__value", children: [
277
- Math.round(scale * 100),
278
- "%"
279
- ] }),
280
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "wb-btn wb-btn--secondary wb-btn--icon-only zoom-bar__icon", onClick: () => zoomTo(scale * 1.2), title: "Zoom in", "aria-label": "Zoom in", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Plus, { size: 14 }) }),
281
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "wb-btn wb-btn--secondary wb-btn--icon-only zoom-bar__action", onClick: fitToContent, title: "Fit camera to all panels", "aria-label": "Fit camera to all panels", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ScanSearch, { size: 14 }) }),
282
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "wb-btn wb-btn--secondary wb-btn--icon-only zoom-bar__action", onClick: resetWidgets, title: "Reset panel positions", "aria-label": "Reset panel positions", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RotateCcw, { size: 14 }) }),
283
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
284
- "button",
285
- {
286
- type: "button",
287
- className: `wb-btn wb-btn--secondary wb-btn--icon-only zoom-bar__action${snapToGrid ? " is-active" : ""}`,
288
- onClick: () => setSnapToGrid((prev) => !prev),
289
- title: snapToGrid ? "Disable snap to grid" : "Enable snap to grid",
290
- "aria-label": snapToGrid ? "Disable snap to grid" : "Enable snap to grid",
291
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Grid3x3, { size: 14 })
292
- }
293
- ),
294
- extraActions
295
- ]
296
- }
297
- );
298
- }
299
-
300
- // src/Minimap.tsx
301
- var import_react2 = require("react");
302
- var import_jsx_runtime3 = require("react/jsx-runtime");
303
- var MAP_W = 200;
304
- var MAP_H = 150;
305
- var PADDING = 40;
306
- var MIN_WORLD_SIZE = 1;
307
- function Minimap({ loading = false }) {
308
- useWhiteboardStore((s) => s.registryVersion);
309
- const offset = useWhiteboardStore((s) => s.offset);
310
- const scale = useWhiteboardStore((s) => s.scale);
311
- const viewportSize = useWhiteboardStore((s) => s.viewportSize);
312
- const panels = useWhiteboardStore((s) => s.panels);
313
- const setOffset = useWhiteboardStore((s) => s.setOffset);
314
- const setScale = useWhiteboardStore((s) => s.setScale);
315
- const focusPanel = useWhiteboardStore((s) => s.focusPanel);
316
- const containerRef = (0, import_react2.useRef)(null);
317
- const dragging = (0, import_react2.useRef)(false);
318
- const lastPanelTapRef = (0, import_react2.useRef)(null);
319
- const rectEntries = Array.from(panels.entries());
320
- const visibleRectEntries = rectEntries.filter(([, rect]) => Number.isFinite(rect.width) && Number.isFinite(rect.height));
321
- const rects = visibleRectEntries.map(([, rect]) => rect);
322
- if (loading || rects.length === 0) {
323
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "minimap minimap--loading", style: { width: MAP_W, height: MAP_H }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "minimap__loading", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Loader2, { size: 14, className: "icon-spin" }) }) });
324
- }
325
- const viewportWidth = viewportSize.width || window.innerWidth;
326
- const viewportHeight = viewportSize.height || window.innerHeight;
327
- const vpW = viewportWidth / scale;
328
- const vpH = viewportHeight / scale;
329
- const vpX = -offset.x / scale;
330
- const vpY = -offset.y / scale;
331
- let minX = Infinity;
332
- let minY = Infinity;
333
- let maxX = -Infinity;
334
- let maxY = -Infinity;
335
- for (const r of rects) {
336
- minX = Math.min(minX, r.x);
337
- minY = Math.min(minY, r.y);
338
- maxX = Math.max(maxX, r.x + r.width);
339
- maxY = Math.max(maxY, r.y + r.height);
340
- }
341
- minX -= PADDING;
342
- minY -= PADDING;
343
- maxX += PADDING;
344
- maxY += PADDING;
345
- const worldW = Math.max(MIN_WORLD_SIZE, maxX - minX);
346
- const worldH = Math.max(MIN_WORLD_SIZE, maxY - minY);
347
- const mapScale = Math.min(MAP_W / worldW, MAP_H / worldH);
348
- const contentW = worldW * mapScale;
349
- const contentH = worldH * mapScale;
350
- const mapOffsetX = (MAP_W - contentW) / 2;
351
- const mapOffsetY = (MAP_H - contentH) / 2;
352
- const toMapX = (wx) => mapOffsetX + (wx - minX) * mapScale;
353
- const toMapY = (wy) => mapOffsetY + (wy - minY) * mapScale;
354
- const clientToWorld = (clientX, clientY) => {
355
- if (!containerRef.current) return;
356
- const mapRect = containerRef.current.getBoundingClientRect();
357
- const localX = Math.min(MAP_W, Math.max(0, clientX - mapRect.left));
358
- const localY = Math.min(MAP_H, Math.max(0, clientY - mapRect.top));
359
- const boundedX = Math.min(contentW + mapOffsetX, Math.max(mapOffsetX, localX));
360
- const boundedY = Math.min(contentH + mapOffsetY, Math.max(mapOffsetY, localY));
361
- const worldX = (boundedX - mapOffsetX) / mapScale + minX;
362
- const worldY = (boundedY - mapOffsetY) / mapScale + minY;
363
- return { worldX, worldY };
364
- };
365
- const centerToWorld = (worldX, worldY, targetScale) => {
366
- const clampedScale = Math.min(3, Math.max(0.2, targetScale));
367
- setScale(clampedScale);
368
- setOffset({
369
- x: viewportWidth / 2 - worldX * clampedScale,
370
- y: viewportHeight / 2 - worldY * clampedScale
371
- });
372
- };
373
- const panTo = (clientX, clientY) => {
374
- const world = clientToWorld(clientX, clientY);
375
- if (!world) return;
376
- centerToWorld(world.worldX, world.worldY, scale);
377
- };
378
- const onDown = (e) => {
379
- e.stopPropagation();
380
- e.preventDefault();
381
- dragging.current = true;
382
- e.currentTarget.setPointerCapture(e.pointerId);
383
- panTo(e.clientX, e.clientY);
384
- };
385
- const onMove = (e) => {
386
- if (!dragging.current) return;
387
- panTo(e.clientX, e.clientY);
388
- };
389
- const onUp = () => {
390
- dragging.current = false;
391
- };
392
- const onWheel = (e) => {
393
- e.preventDefault();
394
- e.stopPropagation();
395
- const world = clientToWorld(e.clientX, e.clientY);
396
- if (!world) return;
397
- const factor = e.deltaY > 0 ? 0.9 : 1.1;
398
- centerToWorld(world.worldX, world.worldY, scale * factor);
399
- };
400
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
401
- "div",
402
- {
403
- ref: containerRef,
404
- onPointerDown: onDown,
405
- onPointerMove: onMove,
406
- onPointerUp: onUp,
407
- onPointerCancel: onUp,
408
- onWheel,
409
- className: "minimap",
410
- style: {
411
- width: MAP_W,
412
- height: MAP_H
413
- },
414
- children: [
415
- visibleRectEntries.map(([id, r]) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
416
- "div",
417
- {
418
- className: "minimap__panel",
419
- onPointerDown: (event) => {
420
- event.stopPropagation();
421
- },
422
- onPointerUp: (event) => {
423
- event.stopPropagation();
424
- const now = Date.now();
425
- const last = lastPanelTapRef.current;
426
- if (last && last.id === id && now - last.time < 300) {
427
- event.preventDefault();
428
- focusPanel(r, { padding: r.focusPadding, maxScale: r.focusMaxScale });
429
- lastPanelTapRef.current = null;
430
- return;
431
- }
432
- lastPanelTapRef.current = { id, time: now };
433
- },
434
- onDoubleClick: (event) => {
435
- event.preventDefault();
436
- event.stopPropagation();
437
- focusPanel(r, { padding: r.focusPadding, maxScale: r.focusMaxScale });
438
- },
439
- style: {
440
- left: toMapX(r.x),
441
- top: toMapY(r.y),
442
- width: Math.max(1, r.width * mapScale),
443
- height: Math.max(1, r.height * mapScale)
444
- }
445
- },
446
- id
447
- )),
448
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
449
- "div",
450
- {
451
- className: "minimap__viewport",
452
- style: {
453
- left: toMapX(vpX),
454
- top: toMapY(vpY),
455
- width: vpW * mapScale,
456
- height: vpH * mapScale
457
- }
458
- }
459
- )
460
- ]
461
- }
462
- );
463
- }
464
-
465
- // src/WhiteboardShell.tsx
466
- var import_jsx_runtime4 = require("react/jsx-runtime");
467
- function WhiteboardShell({ children, showMinimap = true, minimapLoading = false, extraActions }) {
468
- const offset = useWhiteboardStore((s) => s.offset);
469
- const scale = useWhiteboardStore((s) => s.scale);
470
- const registryVersion = useWhiteboardStore((s) => s.registryVersion);
471
- const viewportSize = useWhiteboardStore((s) => s.viewportSize);
472
- const setOffset = useWhiteboardStore((s) => s.setOffset);
473
- const setScale = useWhiteboardStore((s) => s.setScale);
474
- const setViewportSize = useWhiteboardStore((s) => s.setViewportSize);
475
- const shellRef = (0, import_react3.useRef)(null);
476
- const panning = (0, import_react3.useRef)(false);
477
- const last = (0, import_react3.useRef)({ x: 0, y: 0 });
478
- const activePointerId = (0, import_react3.useRef)(null);
479
- const scaleRef = (0, import_react3.useRef)(scale);
480
- const hasFitted = (0, import_react3.useRef)(false);
481
- (0, import_react3.useEffect)(() => {
482
- return () => {
483
- useWhiteboardStore.getState().resetSession();
484
- };
485
- }, []);
486
- (0, import_react3.useEffect)(() => {
487
- scaleRef.current = scale;
488
- }, [scale]);
489
- (0, import_react3.useEffect)(() => {
490
- const shell = shellRef.current;
491
- if (!shell) return;
492
- const updateViewport = () => {
493
- const rect = shell.getBoundingClientRect();
494
- setViewportSize({
495
- width: Math.max(0, rect.width),
496
- height: Math.max(0, rect.height)
497
- });
498
- };
499
- updateViewport();
500
- if (typeof ResizeObserver === "undefined") return;
501
- const observer = new ResizeObserver(updateViewport);
502
- observer.observe(shell);
503
- return () => observer.disconnect();
504
- }, [setViewportSize]);
505
- (0, import_react3.useEffect)(() => {
506
- if (hasFitted.current) return;
507
- const { panels } = useWhiteboardStore.getState();
508
- if (panels.size === 0 || viewportSize.width <= 0 || viewportSize.height <= 0) return;
509
- hasFitted.current = true;
510
- requestAnimationFrame(() => {
511
- useWhiteboardStore.getState().fitToContent();
512
- });
513
- }, [registryVersion, viewportSize]);
514
- const onDown = (0, import_react3.useCallback)((e) => {
515
- if (e.target !== e.currentTarget) return;
516
- if (e.button === 0 || e.button === 1) {
517
- panning.current = true;
518
- activePointerId.current = e.pointerId;
519
- last.current = { x: e.clientX, y: e.clientY };
520
- e.currentTarget.setPointerCapture(e.pointerId);
521
- e.preventDefault();
522
- }
523
- }, []);
524
- const onMove = (0, import_react3.useCallback)((e) => {
525
- if (!panning.current || activePointerId.current !== e.pointerId) return;
526
- const dx = e.movementX ?? e.clientX - last.current.x;
527
- const dy = e.movementY ?? e.clientY - last.current.y;
528
- setOffset((p) => ({ x: p.x + dx, y: p.y + dy }));
529
- last.current = { x: e.clientX, y: e.clientY };
530
- }, [setOffset]);
531
- const stopPan = (0, import_react3.useCallback)((e) => {
532
- if (activePointerId.current !== null) {
533
- try {
534
- e.currentTarget.releasePointerCapture(activePointerId.current);
535
- } catch {
536
- }
537
- }
538
- panning.current = false;
539
- activePointerId.current = null;
540
- }, []);
541
- const onWheel = (0, import_react3.useCallback)((e) => {
542
- const s = scaleRef.current;
543
- const rect = e.currentTarget.getBoundingClientRect();
544
- const anchor = { x: e.clientX - rect.left, y: e.clientY - rect.top };
545
- const nextScale = Math.min(3, Math.max(0.2, s * (e.deltaY > 0 ? 0.9 : 1.1)));
546
- const ratio = nextScale / s;
547
- setOffset((prev) => ({
548
- x: anchor.x - (anchor.x - prev.x) * ratio,
549
- y: anchor.y - (anchor.y - prev.y) * ratio
550
- }));
551
- setScale(nextScale);
552
- }, [setOffset, setScale]);
553
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
554
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
555
- "div",
556
- {
557
- ref: shellRef,
558
- onPointerDown: onDown,
559
- onPointerMove: onMove,
560
- onPointerUp: stopPan,
561
- onPointerCancel: stopPan,
562
- onWheel,
563
- onContextMenu: (e) => e.preventDefault(),
564
- className: "whiteboard-shell",
565
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
566
- "div",
567
- {
568
- className: "whiteboard-canvas",
569
- style: { transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})` },
570
- children: [
571
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "whiteboard-grid", "aria-hidden": true }),
572
- children
573
- ]
574
- }
575
- )
576
- }
577
- ),
578
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ZoomBar, { extraActions }),
579
- showMinimap ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Minimap, { loading: minimapLoading }) : null
580
- ] });
581
- }
582
-
583
- // src/FloatingPanel.tsx
584
- var import_react4 = require("react");
585
- var import_jsx_runtime5 = require("react/jsx-runtime");
586
- var FloatingPanel = (0, import_react4.memo)(function FloatingPanel2({
587
- title,
588
- defaultPosition,
589
- width = 300,
590
- className,
591
- trackRect: trackRectRef,
592
- focusable,
593
- focusPadding = 40,
594
- focusMaxScale = 1.5,
595
- headerActions,
596
- children
597
- }) {
598
- const panelId = (0, import_react4.useId)();
599
- const [pos, setPos] = (0, import_react4.useState)(defaultPosition);
600
- const dragging = (0, import_react4.useRef)(false);
601
- const posRef = (0, import_react4.useRef)(pos);
602
- const elRef = (0, import_react4.useRef)(null);
603
- const lastRegisteredRectRef = (0, import_react4.useRef)(null);
604
- const scale = useWhiteboardStore((s) => s.scale);
605
- const snapToGrid = useWhiteboardStore((s) => s.snapToGrid);
606
- const snapGridSize = useWhiteboardStore((s) => s.snapGridSize);
607
- const register = useWhiteboardStore((s) => s.register);
608
- const unregister = useWhiteboardStore((s) => s.unregister);
609
- const registerReset = useWhiteboardStore((s) => s.registerReset);
610
- const unregisterReset = useWhiteboardStore((s) => s.unregisterReset);
611
- const focusPanel = useWhiteboardStore((s) => s.focusPanel);
612
- const scaleRef = (0, import_react4.useRef)(scale);
613
- const snapToGridRef = (0, import_react4.useRef)(false);
614
- const snapGridSizeRef = (0, import_react4.useRef)(20);
615
- const defaultPosRef = (0, import_react4.useRef)(defaultPosition);
616
- const cleanupRef = (0, import_react4.useRef)(null);
617
- const lastTapRef = (0, import_react4.useRef)(null);
618
- (0, import_react4.useEffect)(() => {
619
- scaleRef.current = scale;
620
- }, [scale]);
621
- (0, import_react4.useEffect)(() => {
622
- snapToGridRef.current = snapToGrid;
623
- snapGridSizeRef.current = snapGridSize;
624
- }, [snapToGrid, snapGridSize]);
625
- (0, import_react4.useEffect)(() => {
626
- const onSnapNow = () => {
627
- const snapSize = snapGridSizeRef.current;
628
- setPos((current) => {
629
- const next = {
630
- x: Math.round(current.x / snapSize) * snapSize,
631
- y: Math.round(current.y / snapSize) * snapSize
632
- };
633
- return next.x === current.x && next.y === current.y ? current : next;
634
- });
635
- };
636
- window.addEventListener("whiteboard-snap-now", onSnapNow);
637
- return () => window.removeEventListener("whiteboard-snap-now", onSnapNow);
638
- }, []);
639
- (0, import_react4.useEffect)(() => {
640
- posRef.current = pos;
641
- }, [pos]);
642
- const registerRect = (0, import_react4.useCallback)(() => {
643
- const el = elRef.current;
644
- if (!el) return;
645
- const nextRect = {
646
- x: posRef.current.x,
647
- y: posRef.current.y,
648
- width: el.offsetWidth,
649
- height: el.offsetHeight,
650
- focusPadding,
651
- focusMaxScale
652
- };
653
- const prev = lastRegisteredRectRef.current;
654
- if (prev && prev.x === nextRect.x && prev.y === nextRect.y && prev.width === nextRect.width && prev.height === nextRect.height && prev.focusPadding === nextRect.focusPadding && prev.focusMaxScale === nextRect.focusMaxScale) {
655
- return;
656
- }
657
- lastRegisteredRectRef.current = nextRect;
658
- register(panelId, nextRect);
659
- if (trackRectRef) {
660
- trackRectRef.current = nextRect;
661
- }
662
- }, [focusMaxScale, focusPadding, panelId, register, trackRectRef]);
663
- const getCurrentRect = (0, import_react4.useCallback)(() => {
664
- const el = elRef.current;
665
- if (!el) return null;
666
- return {
667
- x: posRef.current.x,
668
- y: posRef.current.y,
669
- width: el.offsetWidth,
670
- height: el.offsetHeight,
671
- focusPadding,
672
- focusMaxScale
673
- };
674
- }, [focusMaxScale, focusPadding]);
675
- const handleFocus = (0, import_react4.useCallback)(() => {
676
- const rect = getCurrentRect();
677
- if (!rect) return;
678
- focusPanel(rect, { padding: focusPadding, maxScale: focusMaxScale });
679
- }, [focusPanel, focusPadding, focusMaxScale, getCurrentRect]);
680
- (0, import_react4.useEffect)(() => {
681
- registerReset(panelId, () => setPos(defaultPosRef.current));
682
- return () => {
683
- cleanupRef.current?.();
684
- unregister(panelId);
685
- unregisterReset(panelId);
686
- lastRegisteredRectRef.current = null;
687
- };
688
- }, [panelId, registerReset, unregister, unregisterReset]);
689
- (0, import_react4.useLayoutEffect)(() => {
690
- registerRect();
691
- }, [pos.x, pos.y, width, registerRect]);
692
- (0, import_react4.useEffect)(() => {
693
- const el = elRef.current;
694
- if (!el) return;
695
- if (typeof ResizeObserver === "undefined") return;
696
- const obs = new ResizeObserver(() => {
697
- registerRect();
698
- });
699
- obs.observe(el);
700
- return () => obs.disconnect();
701
- }, [registerRect]);
702
- const onDown = (0, import_react4.useCallback)((e) => {
703
- if (e.button !== 0) return;
704
- dragging.current = true;
705
- const startX = e.clientX;
706
- const startY = e.clientY;
707
- const startPosX = posRef.current.x;
708
- const startPosY = posRef.current.y;
709
- const startScale = scaleRef.current;
710
- e.preventDefault();
711
- e.stopPropagation();
712
- e.target.setPointerCapture(e.pointerId);
713
- const move = (ev) => {
714
- if (!dragging.current) return;
715
- const rawX = startPosX + (ev.clientX - startX) / startScale;
716
- const rawY = startPosY + (ev.clientY - startY) / startScale;
717
- const snapSize = snapGridSizeRef.current;
718
- const nextX = snapToGridRef.current ? Math.round(rawX / snapSize) * snapSize : rawX;
719
- const nextY = snapToGridRef.current ? Math.round(rawY / snapSize) * snapSize : rawY;
720
- setPos({
721
- x: nextX,
722
- y: nextY
723
- });
724
- };
725
- const up = () => {
726
- dragging.current = false;
727
- window.removeEventListener("pointermove", move);
728
- window.removeEventListener("pointerup", up);
729
- cleanupRef.current = null;
730
- };
731
- cleanupRef.current?.();
732
- window.addEventListener("pointermove", move);
733
- window.addEventListener("pointerup", up);
734
- cleanupRef.current = up;
735
- }, []);
736
- const panelClassName = className ? `floating-panel ${className}` : "floating-panel";
737
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
738
- "div",
739
- {
740
- ref: elRef,
741
- className: panelClassName,
742
- style: { left: pos.x, top: pos.y, width },
743
- onPointerDown: (e) => e.stopPropagation(),
744
- onPointerUp: (e) => {
745
- if (dragging.current) return;
746
- const now = Date.now();
747
- const last = lastTapRef.current;
748
- if (last && now - last.time < 300) {
749
- const dx = e.clientX - last.x;
750
- const dy = e.clientY - last.y;
751
- if (dx * dx + dy * dy < 100) {
752
- e.stopPropagation();
753
- handleFocus();
754
- lastTapRef.current = null;
755
- return;
756
- }
757
- }
758
- lastTapRef.current = { time: now, x: e.clientX, y: e.clientY };
759
- },
760
- onWheel: (e) => e.stopPropagation(),
761
- onDoubleClick: (e) => {
762
- e.stopPropagation();
763
- handleFocus();
764
- },
765
- children: [
766
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { onPointerDown: onDown, className: "floating-panel__header", children: [
767
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { className: "floating-panel__title", children: title }),
768
- headerActions,
769
- focusable && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
770
- "button",
771
- {
772
- type: "button",
773
- className: "wb-btn wb-btn--secondary wb-btn--icon-only floating-panel__focus",
774
- onClick: handleFocus,
775
- onPointerDown: (e) => e.stopPropagation(),
776
- title: "Focus on this panel",
777
- "aria-label": "Focus on this panel",
778
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Maximize2, { size: 14 })
779
- }
780
- )
781
- ] }),
782
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "floating-panel__body", children })
783
- ]
784
- }
785
- );
786
- });
787
- function usePanelRect(initial) {
788
- const ref = (0, import_react4.useRef)({ ...initial, width: 0, height: 0 });
789
- return ref;
790
- }
791
- function belowPanel(rect, gap = WHITEBOARD_GRID) {
792
- return { x: rect.x, y: rect.y + rect.height + gap };
793
- }
794
-
795
- // src/ConfirmDialog.tsx
796
- var import_react5 = require("react");
797
- var import_react_dom = require("react-dom");
798
- var import_jsx_runtime6 = require("react/jsx-runtime");
799
- function ConfirmDialog({
800
- open,
801
- title,
802
- message,
803
- onConfirm,
804
- onCancel,
805
- confirmLabel = "Confirm",
806
- loading,
807
- error
808
- }) {
809
- (0, import_react5.useEffect)(() => {
810
- if (!open) return;
811
- const onKey = (e) => {
812
- if (e.key === "Escape") onCancel();
813
- };
814
- window.addEventListener("keydown", onKey);
815
- return () => window.removeEventListener("keydown", onKey);
816
- }, [open, onCancel]);
817
- if (!open || typeof document === "undefined") return null;
818
- return (0, import_react_dom.createPortal)(
819
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "confirm-modal-overlay", onMouseDown: onCancel, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
820
- "div",
821
- {
822
- className: "confirm-modal",
823
- role: "dialog",
824
- "aria-modal": "true",
825
- "aria-label": title,
826
- onMouseDown: (e) => e.stopPropagation(),
827
- children: [
828
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "confirm-modal__header", children: [
829
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "confirm-modal__title", children: [
830
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AlertTriangle, { size: 16 }),
831
- title
832
- ] }),
833
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "wb-btn wb-btn--secondary wb-btn--icon-only", onClick: onCancel, "aria-label": "Close dialog", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(X, { size: 14 }) })
834
- ] }),
835
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "confirm-modal__message", children: message }),
836
- error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "wb-alert wb-alert--error", children: error }),
837
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "wb-btn-row", children: [
838
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "wb-btn wb-btn--secondary", onClick: onCancel, children: "Cancel" }),
839
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "wb-btn wb-btn--danger", onClick: onConfirm, disabled: loading, children: loading ? "Deleting..." : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
840
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Check, { size: 14 }),
841
- confirmLabel
842
- ] }) })
843
- ] })
844
- ]
845
- }
846
- ) }),
847
- document.body
848
- );
849
- }
850
-
851
- // src/PanelErrorBoundary.tsx
852
- var import_react6 = require("react");
853
- var import_jsx_runtime7 = require("react/jsx-runtime");
854
- var PanelErrorBoundary = class extends import_react6.Component {
855
- constructor() {
856
- super(...arguments);
857
- this.state = { error: null };
858
- }
859
- static getDerivedStateFromError(error) {
860
- return { error };
861
- }
862
- render() {
863
- if (this.state.error) {
864
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "wb-stack wb-stack--sm", children: [
865
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "wb-alert wb-alert--error", children: this.props.fallbackMessage ?? "This panel crashed." }),
866
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
867
- "button",
868
- {
869
- type: "button",
870
- className: "wb-btn wb-btn--secondary",
871
- onClick: () => this.setState({ error: null }),
872
- children: "Retry"
873
- }
874
- )
875
- ] });
876
- }
877
- return this.props.children;
878
- }
879
- };
880
-
881
- // src/useWhiteboardLayout.ts
882
- var import_react7 = require("react");
883
- function useWhiteboardLayout({
884
- widths,
885
- startX = 20,
886
- y = 40,
887
- gap = 20
888
- }) {
889
- const panelWidth = (0, import_react7.useMemo)(() => {
890
- const normalized = {};
891
- for (const [key, value] of Object.entries(widths)) {
892
- normalized[key] = snapToWhiteboardGrid(value);
893
- }
894
- return normalized;
895
- }, [widths]);
896
- const layout = (0, import_react7.useMemo)(
897
- () => ({
898
- startX: snapToWhiteboardGrid(startX),
899
- y: snapToWhiteboardGrid(y),
900
- gap: snapToWhiteboardGrid(gap)
901
- }),
902
- [startX, y, gap]
903
- );
904
- const positions = (0, import_react7.useMemo)(() => {
905
- const next = {};
906
- let x = layout.startX;
907
- for (const [key, width] of Object.entries(panelWidth)) {
908
- ;
909
- next[key] = { x, y: layout.y };
910
- x += width + layout.gap;
911
- }
912
- return next;
913
- }, [layout.gap, layout.startX, layout.y, panelWidth]);
914
- return { layout, panelWidth, positions };
915
- }
916
-
917
- // src/cn.ts
918
- function cn(...args) {
919
- return args.filter(Boolean).join(" ");
920
- }
921
- // Annotate the CommonJS export names for ESM import in node:
922
- 0 && (module.exports = {
923
- ConfirmDialog,
924
- FloatingPanel,
925
- Minimap,
926
- PanelErrorBoundary,
927
- WHITEBOARD_GRID,
928
- WhiteboardShell,
929
- ZoomBar,
930
- belowPanel,
931
- cn,
932
- computeWhiteboardFit,
933
- computeWhiteboardRectFocus,
934
- snapToWhiteboardGrid,
935
- usePanelRect,
936
- useWhiteboardLayout,
937
- useWhiteboardStore
938
- });
939
- //# sourceMappingURL=index.cjs.map