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