mesurer 0.0.0 → 0.0.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 ADDED
@@ -0,0 +1,3698 @@
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
+ // index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Measurer: () => Measurer
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_styles = require("./styles.css");
27
+
28
+ // measurer.tsx
29
+ var import_react19 = require("react");
30
+ var import_react_dom = require("react-dom");
31
+
32
+ // components/toolbar.tsx
33
+ var import_react = require("react");
34
+
35
+ // utils.ts
36
+ var formatValue = (value) => Math.round(value);
37
+ var cn = (...inputs) => inputs.filter(Boolean).join(" ");
38
+ var createId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
39
+
40
+ // components/icons.tsx
41
+ var import_jsx_runtime = require("react/jsx-runtime");
42
+ var IconBase = ({ size = 24, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
43
+ "svg",
44
+ {
45
+ xmlns: "http://www.w3.org/2000/svg",
46
+ width: size,
47
+ height: size,
48
+ viewBox: "0 0 256 256",
49
+ fill: "currentColor",
50
+ ...props
51
+ }
52
+ );
53
+ var CursorIcon = ({ size = 20, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconBase, { size, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M166.59,134.1a1.91,1.91,0,0,1-.55-1.79,2,2,0,0,1,1.08-1.42l46.25-17.76.24-.1A14,14,0,0,0,212.38,87L52.29,34.7A13.95,13.95,0,0,0,34.7,52.29L87,212.38a13.82,13.82,0,0,0,12.6,9.6c.23,0,.46,0,.69,0A13.84,13.84,0,0,0,113,213.61a2.44,2.44,0,0,0,.1-.24l17.76-46.25a2,2,0,0,1,3.21-.53l51.31,51.31a14,14,0,0,0,19.8,0l12.69-12.69a14,14,0,0,0,0-19.8Zm42.82,62.63-12.68,12.68a2,2,0,0,1-2.83,0L142.59,158.1a14,14,0,0,0-22.74,4.32,2.44,2.44,0,0,0-.1.24L102,208.91a2,2,0,0,1-3.61-.26L46.11,48.57a1.87,1.87,0,0,1,.47-2A1.92,1.92,0,0,1,47.93,46a2.22,2.22,0,0,1,.64.1L208.65,98.38a2,2,0,0,1,.26,3.61l-46.25,17.76-.24.1a14,14,0,0,0-4.32,22.74h0l51.31,51.31A2,2,0,0,1,209.41,196.73Z" }) });
54
+ var RulerIcon = ({ size = 20, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconBase, { size, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M233.91,74.79,181.22,22.1a14,14,0,0,0-19.8,0L22.09,161.41a14,14,0,0,0,0,19.8L74.78,233.9a14,14,0,0,0,19.8,0L233.91,94.59A14,14,0,0,0,233.91,74.79ZM225.42,86.1,86.1,225.41h0a2,2,0,0,1-2.83,0L30.58,172.73a2,2,0,0,1,0-2.83L64,136.48l27.76,27.76a6,6,0,1,0,8.48-8.48L72.48,128,96,104.48l27.76,27.76a6,6,0,0,0,8.48-8.48L104.48,96,128,72.49l27.76,27.75a6,6,0,0,0,8.48-8.48L136.49,64,169.9,30.59a2,2,0,0,1,2.83,0l52.69,52.68A2,2,0,0,1,225.42,86.1Z" }) });
55
+ var CaretDownIcon = ({ size = 8, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconBase, { size, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z" }) });
56
+ var CheckIcon = ({ size = 12, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconBase, { size, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z" }) });
57
+ var MinusIcon = ({ size = 12, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconBase, { size, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M228,128a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,128Z" }) });
58
+
59
+ // components/toolbar.tsx
60
+ var import_jsx_runtime2 = require("react/jsx-runtime");
61
+ var TOOLBAR_TOOLTIP_DELAY_MS = 800;
62
+ var TOOLBAR_DRAG_SLOP = 6;
63
+ function ToolbarButton({
64
+ id,
65
+ active,
66
+ label,
67
+ shortcut,
68
+ onClick,
69
+ tooltipVisible,
70
+ tooltipSide,
71
+ onTooltipEnter,
72
+ onTooltipLeave,
73
+ children
74
+ }) {
75
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
76
+ "div",
77
+ {
78
+ className: "relative",
79
+ onMouseEnter: () => onTooltipEnter(id),
80
+ onMouseLeave: () => onTooltipLeave(id),
81
+ children: [
82
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
83
+ "button",
84
+ {
85
+ type: "button",
86
+ "aria-label": `${label} (${shortcut})`,
87
+ title: `${label} (${shortcut})`,
88
+ className: cn(
89
+ "flex size-8 items-center justify-center rounded-[8px] outline-none",
90
+ active ? "bg-[#0d99ff] text-white" : "bg-transparent text-black hover:bg-black/4"
91
+ ),
92
+ onClick,
93
+ children
94
+ }
95
+ ),
96
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
97
+ "span",
98
+ {
99
+ className: cn(
100
+ "pointer-events-none absolute left-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-black px-2 py-1 text-[11px] text-white transition-opacity duration-150 select-none",
101
+ tooltipSide === "top" ? "bottom-full mb-2" : "top-full mt-2",
102
+ tooltipVisible ? "opacity-100" : "opacity-0"
103
+ ),
104
+ children: [
105
+ label,
106
+ " ",
107
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("kbd", { className: "text-white/60", children: shortcut })
108
+ ]
109
+ }
110
+ )
111
+ ]
112
+ }
113
+ );
114
+ }
115
+ function useToolbarTooltip() {
116
+ const [visibleTooltipId, setVisibleTooltipId] = (0, import_react.useState)(null);
117
+ const timerRef = (0, import_react.useRef)(null);
118
+ const instantRef = (0, import_react.useRef)(false);
119
+ const clearTimer = (0, import_react.useCallback)(() => {
120
+ if (timerRef.current === null) return;
121
+ window.clearTimeout(timerRef.current);
122
+ timerRef.current = null;
123
+ }, []);
124
+ const onTooltipEnter = (0, import_react.useCallback)(
125
+ (id) => {
126
+ clearTimer();
127
+ if (instantRef.current) {
128
+ setVisibleTooltipId(id);
129
+ return;
130
+ }
131
+ timerRef.current = window.setTimeout(() => {
132
+ setVisibleTooltipId(id);
133
+ instantRef.current = true;
134
+ timerRef.current = null;
135
+ }, TOOLBAR_TOOLTIP_DELAY_MS);
136
+ },
137
+ [clearTimer]
138
+ );
139
+ const onTooltipLeave = (0, import_react.useCallback)(
140
+ (id) => {
141
+ clearTimer();
142
+ setVisibleTooltipId((prev) => prev === id ? null : prev);
143
+ },
144
+ [clearTimer]
145
+ );
146
+ const onToolbarLeave = (0, import_react.useCallback)(() => {
147
+ clearTimer();
148
+ setVisibleTooltipId(null);
149
+ instantRef.current = false;
150
+ }, [clearTimer]);
151
+ (0, import_react.useEffect)(() => {
152
+ return () => {
153
+ clearTimer();
154
+ };
155
+ }, [clearTimer]);
156
+ return { visibleTooltipId, onTooltipEnter, onTooltipLeave, onToolbarLeave };
157
+ }
158
+ function useToolbarDrag(initialPosition) {
159
+ const [position, setPosition] = (0, import_react.useState)(initialPosition);
160
+ const suppressClickRef = (0, import_react.useRef)(false);
161
+ const detachListenersRef = (0, import_react.useRef)(null);
162
+ const dragRef = (0, import_react.useRef)({
163
+ active: false,
164
+ didDrag: false,
165
+ pointerId: -1,
166
+ startX: 0,
167
+ startY: 0,
168
+ originX: 0,
169
+ originY: 0,
170
+ width: 0,
171
+ height: 0
172
+ });
173
+ const onPointerDown = (0, import_react.useCallback)(
174
+ (event) => {
175
+ if (event.button !== 0) return;
176
+ if (detachListenersRef.current) {
177
+ detachListenersRef.current();
178
+ detachListenersRef.current = null;
179
+ }
180
+ const state = dragRef.current;
181
+ state.active = false;
182
+ state.didDrag = false;
183
+ state.pointerId = event.pointerId;
184
+ state.startX = event.clientX;
185
+ state.startY = event.clientY;
186
+ state.originX = position.x;
187
+ state.originY = position.y;
188
+ const rect = event.currentTarget.getBoundingClientRect();
189
+ state.width = rect.width;
190
+ state.height = rect.height;
191
+ const handlePointerMove = (moveEvent) => {
192
+ const current = dragRef.current;
193
+ if (current.pointerId !== moveEvent.pointerId) return;
194
+ const dx = moveEvent.clientX - current.startX;
195
+ const dy = moveEvent.clientY - current.startY;
196
+ if (!current.active) {
197
+ current.active = Math.abs(dx) > TOOLBAR_DRAG_SLOP || Math.abs(dy) > TOOLBAR_DRAG_SLOP;
198
+ }
199
+ if (!current.active) return;
200
+ current.didDrag = true;
201
+ const maxX = Math.max(8, window.innerWidth - current.width - 8);
202
+ const maxY = Math.max(8, window.innerHeight - current.height - 8);
203
+ setPosition({
204
+ x: Math.min(maxX, Math.max(8, current.originX + dx)),
205
+ y: Math.min(maxY, Math.max(8, current.originY + dy))
206
+ });
207
+ };
208
+ const handlePointerEnd = (endEvent) => {
209
+ const current = dragRef.current;
210
+ if (current.pointerId !== endEvent.pointerId && current.pointerId !== -1)
211
+ return;
212
+ suppressClickRef.current = current.didDrag;
213
+ current.active = false;
214
+ current.didDrag = false;
215
+ current.pointerId = -1;
216
+ window.removeEventListener("pointermove", handlePointerMove);
217
+ window.removeEventListener("pointerup", handlePointerEnd);
218
+ window.removeEventListener("pointercancel", handlePointerEnd);
219
+ detachListenersRef.current = null;
220
+ };
221
+ window.addEventListener("pointermove", handlePointerMove);
222
+ window.addEventListener("pointerup", handlePointerEnd);
223
+ window.addEventListener("pointercancel", handlePointerEnd);
224
+ detachListenersRef.current = () => {
225
+ window.removeEventListener("pointermove", handlePointerMove);
226
+ window.removeEventListener("pointerup", handlePointerEnd);
227
+ window.removeEventListener("pointercancel", handlePointerEnd);
228
+ };
229
+ },
230
+ [position.x, position.y]
231
+ );
232
+ const onClickCapture = (0, import_react.useCallback)(
233
+ (event) => {
234
+ if (!suppressClickRef.current) return;
235
+ event.preventDefault();
236
+ event.stopPropagation();
237
+ suppressClickRef.current = false;
238
+ },
239
+ []
240
+ );
241
+ return { position, onPointerDown, onClickCapture };
242
+ }
243
+ function ToolbarComponent({
244
+ toolMode,
245
+ setEnabled,
246
+ setToolMode,
247
+ guideOrientation,
248
+ setGuideOrientation,
249
+ onInteract
250
+ }, ref) {
251
+ const { position, onPointerDown, onClickCapture } = useToolbarDrag({
252
+ x: 16,
253
+ y: 16
254
+ });
255
+ const { visibleTooltipId, onTooltipEnter, onTooltipLeave, onToolbarLeave } = useToolbarTooltip();
256
+ const [guideMenuOpen, setGuideMenuOpen] = (0, import_react.useState)(false);
257
+ const guideMenuRef = (0, import_react.useRef)(null);
258
+ const guideMenuPanelRef = (0, import_react.useRef)(null);
259
+ const [activeMenuIndex, setActiveMenuIndex] = (0, import_react.useState)(0);
260
+ const [menuAlign, setMenuAlign] = (0, import_react.useState)("right");
261
+ const viewportHeight = typeof window === "undefined" ? 0 : window.innerHeight || 0;
262
+ const nearTop = position.y < 56;
263
+ const nearBottom = viewportHeight > 0 && position.y > viewportHeight - 56;
264
+ const tooltipSide = nearTop && !nearBottom ? "bottom" : "top";
265
+ const menuSide = nearBottom ? "top" : "bottom";
266
+ const selectMode = (0, import_react.useCallback)(() => {
267
+ setEnabled(true);
268
+ setToolMode((prev) => prev === "select" ? "none" : "select");
269
+ onInteract();
270
+ }, [onInteract, setEnabled, setToolMode]);
271
+ const guidesMode = (0, import_react.useCallback)(() => {
272
+ setEnabled(true);
273
+ setToolMode((prev) => prev === "guides" ? "none" : "guides");
274
+ onInteract();
275
+ }, [onInteract, setEnabled, setToolMode]);
276
+ const selectGuideOrientation = (0, import_react.useCallback)(
277
+ (orientation) => {
278
+ setEnabled(true);
279
+ setToolMode("guides");
280
+ setGuideOrientation(orientation);
281
+ onInteract();
282
+ setGuideMenuOpen(false);
283
+ },
284
+ [onInteract, setEnabled, setGuideOrientation, setToolMode]
285
+ );
286
+ (0, import_react.useLayoutEffect)(() => {
287
+ if (!guideMenuOpen) return;
288
+ const frame = requestAnimationFrame(() => {
289
+ guideMenuRef.current?.querySelector("[role='menu']")?.focus();
290
+ const panel = guideMenuPanelRef.current;
291
+ if (!panel) return;
292
+ const rect = panel.getBoundingClientRect();
293
+ if (rect.left < 8) {
294
+ setMenuAlign("left");
295
+ return;
296
+ }
297
+ if (rect.right > window.innerWidth - 8) {
298
+ setMenuAlign("right");
299
+ }
300
+ });
301
+ const handlePointerDown = (event) => {
302
+ if (!guideMenuRef.current) return;
303
+ if (guideMenuRef.current.contains(event.target)) return;
304
+ setGuideMenuOpen(false);
305
+ };
306
+ const handleResize = () => {
307
+ const panel = guideMenuPanelRef.current;
308
+ if (!panel) return;
309
+ const rect = panel.getBoundingClientRect();
310
+ if (rect.left < 8) {
311
+ setMenuAlign("left");
312
+ return;
313
+ }
314
+ if (rect.right > window.innerWidth - 8) {
315
+ setMenuAlign("right");
316
+ }
317
+ };
318
+ window.addEventListener("pointerdown", handlePointerDown);
319
+ window.addEventListener("resize", handleResize);
320
+ return () => {
321
+ cancelAnimationFrame(frame);
322
+ window.removeEventListener("pointerdown", handlePointerDown);
323
+ window.removeEventListener("resize", handleResize);
324
+ };
325
+ }, [guideMenuOpen, guideOrientation]);
326
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
327
+ "div",
328
+ {
329
+ ref,
330
+ className: "pointer-events-auto absolute z-[90] flex items-center gap-1 rounded-[12px] bg-[#fff] p-1 outline outline-transparent shadow-[0px_0px_.5px_rgba(0,0,0,.18),0px_3px_8px_rgba(0,0,0,.1),0px_1px_3px_rgba(0,0,0,.1)]",
331
+ style: { left: position.x, top: position.y },
332
+ onPointerDown: (event) => {
333
+ onInteract();
334
+ onPointerDown(event);
335
+ },
336
+ onClickCapture,
337
+ onMouseLeave: onToolbarLeave,
338
+ children: [
339
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
340
+ ToolbarButton,
341
+ {
342
+ id: "select",
343
+ active: toolMode === "select",
344
+ label: "Select",
345
+ shortcut: "S",
346
+ onClick: selectMode,
347
+ tooltipVisible: visibleTooltipId === "select",
348
+ tooltipSide,
349
+ onTooltipEnter,
350
+ onTooltipLeave,
351
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CursorIcon, { size: 20 })
352
+ }
353
+ ),
354
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
355
+ ToolbarButton,
356
+ {
357
+ id: "guides",
358
+ active: toolMode === "guides",
359
+ label: "Guides",
360
+ shortcut: "G",
361
+ onClick: guidesMode,
362
+ tooltipVisible: visibleTooltipId === "guides",
363
+ tooltipSide,
364
+ onTooltipEnter,
365
+ onTooltipLeave,
366
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
367
+ RulerIcon,
368
+ {
369
+ size: 20,
370
+ className: cn(
371
+ guideOrientation === "vertical" ? "rotate-[135deg]" : "rotate-[45deg]"
372
+ )
373
+ }
374
+ )
375
+ }
376
+ ),
377
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
378
+ "div",
379
+ {
380
+ className: "group relative -ml-1 flex items-stretch",
381
+ ref: guideMenuRef,
382
+ onMouseEnter: () => onTooltipEnter("guide-menu"),
383
+ onMouseLeave: () => onTooltipLeave("guide-menu"),
384
+ children: [
385
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
386
+ "button",
387
+ {
388
+ type: "button",
389
+ "aria-label": "Guide orientation menu",
390
+ title: "Guide orientation",
391
+ className: cn(
392
+ "flex h-8 w-4 items-center justify-center rounded-[6px] outline-none hover:bg-black/10",
393
+ guideMenuOpen ? "bg-black/10 text-black" : "text-black"
394
+ ),
395
+ onClick: () => {
396
+ onInteract();
397
+ setGuideMenuOpen((prev) => {
398
+ if (!prev) {
399
+ setActiveMenuIndex(guideOrientation === "horizontal" ? 0 : 1);
400
+ }
401
+ return !prev;
402
+ });
403
+ },
404
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CaretDownIcon, { size: 8 })
405
+ }
406
+ ),
407
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
408
+ "span",
409
+ {
410
+ className: cn(
411
+ "pointer-events-none absolute left-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-black px-2 py-1 text-[11px] text-white transition-opacity duration-150 select-none",
412
+ tooltipSide === "top" ? "bottom-full mb-2" : "top-full mt-2",
413
+ visibleTooltipId === "guide-menu" && !guideMenuOpen ? "opacity-100" : "opacity-0"
414
+ ),
415
+ children: "Orientation Guide"
416
+ }
417
+ ),
418
+ guideMenuOpen ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
419
+ "div",
420
+ {
421
+ className: cn(
422
+ "absolute z-[70] w-44 rounded-lg border border-ink-200 bg-white p-1 shadow-[0px_10px_30px_rgba(0,0,0,0.08)] outline-none focus:outline-none",
423
+ "flex flex-col gap-px",
424
+ menuSide === "bottom" ? "top-full mt-2" : "bottom-full mb-2",
425
+ menuAlign === "left" ? "left-0" : "right-0"
426
+ ),
427
+ ref: guideMenuPanelRef,
428
+ role: "menu",
429
+ tabIndex: 0,
430
+ onKeyDown: (event) => {
431
+ const key = event.key.toLowerCase();
432
+ if (event.key === "ArrowDown") {
433
+ event.preventDefault();
434
+ setActiveMenuIndex((prev) => (prev + 1) % 2);
435
+ }
436
+ if (event.key === "ArrowUp") {
437
+ event.preventDefault();
438
+ setActiveMenuIndex((prev) => (prev + 1) % 2);
439
+ }
440
+ if (event.key === "Enter") {
441
+ event.preventDefault();
442
+ selectGuideOrientation(
443
+ activeMenuIndex === 0 ? "horizontal" : "vertical"
444
+ );
445
+ }
446
+ if (key === "h") {
447
+ event.preventDefault();
448
+ selectGuideOrientation("horizontal");
449
+ }
450
+ if (key === "v") {
451
+ event.preventDefault();
452
+ selectGuideOrientation("vertical");
453
+ }
454
+ if (event.key === "Escape") {
455
+ event.preventDefault();
456
+ setGuideMenuOpen(false);
457
+ }
458
+ },
459
+ children: [
460
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
461
+ "button",
462
+ {
463
+ type: "button",
464
+ className: cn(
465
+ "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[12px]",
466
+ activeMenuIndex === 0 || guideOrientation === "horizontal" ? "bg-[#0d99ff] text-white" : "text-ink-700 hover:bg-[#0d99ff] hover:text-white"
467
+ ),
468
+ onClick: () => selectGuideOrientation("horizontal"),
469
+ children: [
470
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
471
+ CheckIcon,
472
+ {
473
+ size: 12,
474
+ className: cn(
475
+ guideOrientation === "horizontal" ? "opacity-100" : "opacity-0"
476
+ )
477
+ }
478
+ ),
479
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MinusIcon, { size: 12 }),
480
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "flex-1", children: "Horizontal" }),
481
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-ink-500", children: "H" })
482
+ ]
483
+ }
484
+ ),
485
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
486
+ "button",
487
+ {
488
+ type: "button",
489
+ className: cn(
490
+ "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[12px]",
491
+ activeMenuIndex === 1 || guideOrientation === "vertical" ? "bg-[#0d99ff] text-white" : "text-ink-700 hover:bg-[#0d99ff] hover:text-white"
492
+ ),
493
+ onClick: () => selectGuideOrientation("vertical"),
494
+ children: [
495
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
496
+ CheckIcon,
497
+ {
498
+ size: 12,
499
+ className: cn(
500
+ guideOrientation === "vertical" ? "opacity-100" : "opacity-0"
501
+ )
502
+ }
503
+ ),
504
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MinusIcon, { size: 12, className: "rotate-90" }),
505
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "flex-1", children: "Vertical" }),
506
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-ink-500", children: "V" })
507
+ ]
508
+ }
509
+ )
510
+ ]
511
+ }
512
+ ) : null
513
+ ]
514
+ }
515
+ )
516
+ ]
517
+ }
518
+ );
519
+ }
520
+ var Toolbar = (0, import_react.memo)((0, import_react.forwardRef)(ToolbarComponent));
521
+
522
+ // hooks/use-drag-state.ts
523
+ var import_react2 = require("react");
524
+ var useDragState = () => {
525
+ const [start, setStart] = (0, import_react2.useState)(null);
526
+ const [end, setEnd] = (0, import_react2.useState)(null);
527
+ const [isDragging, setIsDragging] = (0, import_react2.useState)(false);
528
+ return { start, setStart, end, setEnd, isDragging, setIsDragging };
529
+ };
530
+
531
+ // hooks/use-guide-drag-hold.ts
532
+ var import_react3 = require("react");
533
+
534
+ // constants.ts
535
+ var MEASURE_LABEL_OFFSET = 2;
536
+ var GUIDE_SNAP_DISTANCE = 10;
537
+ var GUIDE_HITBOX_SIZE = 14;
538
+ var GUIDE_DRAG_HOLD_MS = 220;
539
+ var MEASURE_TRANSITION_MS = 140;
540
+ var MIN_MULTI_TARGET_SIZE = 8;
541
+ var MIN_MULTI_ELEMENT_COVERAGE = 0.45;
542
+ var ANCESTOR_PRUNE_SCALE = 1.6;
543
+ var ANCESTOR_KEEP_COVERAGE = 0.92;
544
+ var MIN_SINGLE_TARGET_SIZE = 8;
545
+ var MIN_SINGLE_ELEMENT_COVERAGE = 0.15;
546
+
547
+ // hooks/use-guide-drag-hold.ts
548
+ var useGuideDragHold = () => {
549
+ const guideDragHoldTimerRef = (0, import_react3.useRef)(null);
550
+ const guideDragHoldGuideIdRef = (0, import_react3.useRef)(null);
551
+ const clearGuideDragHold = (0, import_react3.useCallback)(() => {
552
+ if (guideDragHoldTimerRef.current !== null) {
553
+ window.clearTimeout(guideDragHoldTimerRef.current);
554
+ guideDragHoldTimerRef.current = null;
555
+ }
556
+ guideDragHoldGuideIdRef.current = null;
557
+ }, []);
558
+ const scheduleGuideDragHold = (0, import_react3.useCallback)(
559
+ (id, onHold) => {
560
+ clearGuideDragHold();
561
+ guideDragHoldGuideIdRef.current = id;
562
+ guideDragHoldTimerRef.current = window.setTimeout(() => {
563
+ if (guideDragHoldGuideIdRef.current === id) {
564
+ onHold(id);
565
+ }
566
+ }, GUIDE_DRAG_HOLD_MS);
567
+ },
568
+ [clearGuideDragHold]
569
+ );
570
+ return {
571
+ clearGuideDragHold,
572
+ scheduleGuideDragHold
573
+ };
574
+ };
575
+
576
+ // hooks/use-guide-state.ts
577
+ var import_react4 = require("react");
578
+ var useGuideState = (options = {}) => {
579
+ const [guides, setGuides] = (0, import_react4.useState)(options.initialGuides ?? []);
580
+ const [draggingGuideId, setDraggingGuideId] = (0, import_react4.useState)(null);
581
+ const [selectedGuideIds, setSelectedGuideIds] = (0, import_react4.useState)(
582
+ options.initialSelectedGuideIds ?? []
583
+ );
584
+ return {
585
+ guides,
586
+ setGuides,
587
+ draggingGuideId,
588
+ setDraggingGuideId,
589
+ selectedGuideIds,
590
+ setSelectedGuideIds
591
+ };
592
+ };
593
+
594
+ // hooks/use-hotkeys.ts
595
+ var import_react5 = require("react");
596
+ var useHotkeys = (options) => {
597
+ (0, import_react5.useEffect)(() => {
598
+ const handleKeyDown = (event) => {
599
+ if (event.key === "Escape") {
600
+ options.clearAll();
601
+ return;
602
+ }
603
+ if (event.key.toLowerCase() === "m") {
604
+ options.setEnabled((prev) => !prev);
605
+ }
606
+ const key = event.key.toLowerCase();
607
+ if (options.isOverlayActive()) {
608
+ if (key === "s") {
609
+ options.setToolMode((prev) => prev === "select" ? "none" : "select");
610
+ options.onInteract();
611
+ }
612
+ if (key === "g") {
613
+ options.setToolMode((prev) => prev === "guides" ? "none" : "guides");
614
+ options.onInteract();
615
+ }
616
+ if (key === "h") {
617
+ options.setGuideOrientation("horizontal");
618
+ options.onInteract();
619
+ }
620
+ if (key === "v") {
621
+ options.setGuideOrientation("vertical");
622
+ options.onInteract();
623
+ }
624
+ }
625
+ if (event.key === "Alt") {
626
+ options.setAltPressed(true);
627
+ }
628
+ if (event.key === "Backspace" || event.key === "Delete") {
629
+ const removed = options.removeSelectedGuides();
630
+ if (removed) {
631
+ event.preventDefault();
632
+ }
633
+ }
634
+ };
635
+ const handleKeyUp = (event) => {
636
+ if (event.key === "Alt") {
637
+ options.setAltPressed(false);
638
+ }
639
+ };
640
+ window.addEventListener("keydown", handleKeyDown);
641
+ window.addEventListener("keyup", handleKeyUp);
642
+ return () => {
643
+ window.removeEventListener("keydown", handleKeyDown);
644
+ window.removeEventListener("keyup", handleKeyUp);
645
+ };
646
+ }, [options]);
647
+ };
648
+
649
+ // hooks/use-live-element-tracking.ts
650
+ var import_react6 = require("react");
651
+
652
+ // geometry.ts
653
+ var getViewportSize = () => ({
654
+ width: typeof window === "undefined" ? 1 : window.innerWidth || 1,
655
+ height: typeof window === "undefined" ? 1 : window.innerHeight || 1
656
+ });
657
+ var normalizeRect = (rect, viewport = getViewportSize()) => ({
658
+ left: rect.left / viewport.width,
659
+ top: rect.top / viewport.height,
660
+ width: rect.width / viewport.width,
661
+ height: rect.height / viewport.height
662
+ });
663
+ var denormalizeRect = (rect, viewport = getViewportSize()) => ({
664
+ left: rect.left * viewport.width,
665
+ top: rect.top * viewport.height,
666
+ width: rect.width * viewport.width,
667
+ height: rect.height * viewport.height
668
+ });
669
+ var getRectFromPoints = (start, end) => {
670
+ const left = Math.min(start.x, end.x);
671
+ const top = Math.min(start.y, end.y);
672
+ const width = Math.abs(start.x - end.x);
673
+ const height = Math.abs(start.y - end.y);
674
+ return { left, top, width, height };
675
+ };
676
+ var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
677
+ var rectsOverlap = (rectA, rectB) => rectA.left < rectB.left + rectB.width && rectA.left + rectA.width > rectB.left && rectA.top < rectB.top + rectB.height && rectA.top + rectA.height > rectB.top;
678
+ var rectArea = (rect) => rect.width * rect.height;
679
+ var intersectionArea = (rectA, rectB) => {
680
+ const x1 = Math.max(rectA.left, rectB.left);
681
+ const y1 = Math.max(rectA.top, rectB.top);
682
+ const x2 = Math.min(rectA.left + rectA.width, rectB.left + rectB.width);
683
+ const y2 = Math.min(rectA.top + rectA.height, rectB.top + rectB.height);
684
+ const width = Math.max(0, x2 - x1);
685
+ const height = Math.max(0, y2 - y1);
686
+ return width * height;
687
+ };
688
+ var rectAlmostEqual = (a, b, epsilon = 0.25) => Math.abs(a.left - b.left) < epsilon && Math.abs(a.top - b.top) < epsilon && Math.abs(a.width - b.width) < epsilon && Math.abs(a.height - b.height) < epsilon;
689
+ var getDistanceToRect = (point, rect) => {
690
+ const right = rect.left + rect.width;
691
+ const bottom = rect.top + rect.height;
692
+ const dx = point.x < rect.left ? rect.left - point.x : point.x > right ? point.x - right : 0;
693
+ const dy = point.y < rect.top ? rect.top - point.y : point.y > bottom ? point.y - bottom : 0;
694
+ return Math.hypot(dx, dy);
695
+ };
696
+
697
+ // distances.ts
698
+ var getDistanceOverlay = (rectA, rectB, elementRefA, elementRefB) => {
699
+ const viewport = getViewportSize();
700
+ const normalizedRectA = normalizeRect(rectA, viewport);
701
+ const normalizedRectB = normalizeRect(rectB, viewport);
702
+ const rightA = rectA.left + rectA.width;
703
+ const bottomA = rectA.top + rectA.height;
704
+ const rightB = rectB.left + rectB.width;
705
+ const bottomB = rectB.top + rectB.height;
706
+ const centerAX = rectA.left + rectA.width / 2;
707
+ const centerAY = rectA.top + rectA.height / 2;
708
+ let horizontal = null;
709
+ let vertical = null;
710
+ const connectors = [];
711
+ const separatedX = rightA <= rectB.left || rightB <= rectA.left;
712
+ const separatedY = bottomA <= rectB.top || bottomB <= rectA.top;
713
+ if (separatedX) {
714
+ const aIsLeft = rightA <= rectB.left;
715
+ const x1 = aIsLeft ? rightA : rightB;
716
+ const x2 = aIsLeft ? rectB.left : rectA.left;
717
+ const y = centerAY;
718
+ horizontal = { x1, x2, y, value: Math.abs(x2 - x1) };
719
+ const edgeBX = aIsLeft ? rectB.left : rightB;
720
+ if (y < rectB.top) {
721
+ connectors.push({ x1: edgeBX, y1: y, x2: edgeBX, y2: rectB.top });
722
+ } else if (y > bottomB) {
723
+ connectors.push({ x1: edgeBX, y1: y, x2: edgeBX, y2: bottomB });
724
+ }
725
+ }
726
+ if (separatedY) {
727
+ const aIsTop = bottomA <= rectB.top;
728
+ const y1 = aIsTop ? bottomA : bottomB;
729
+ const y2 = aIsTop ? rectB.top : rectA.top;
730
+ const x = centerAX;
731
+ vertical = { y1, y2, x, value: Math.abs(y2 - y1) };
732
+ const edgeBY = aIsTop ? rectB.top : bottomB;
733
+ if (x < rectB.left) {
734
+ connectors.push({ x1: x, y1: edgeBY, x2: rectB.left, y2: edgeBY });
735
+ } else if (x > rightB) {
736
+ connectors.push({ x1: x, y1: edgeBY, x2: rightB, y2: edgeBY });
737
+ }
738
+ }
739
+ const normalizedConnectors = connectors.map((segment) => ({
740
+ x1: clamp(segment.x1, 0, window.innerWidth),
741
+ y1: clamp(segment.y1, 0, window.innerHeight),
742
+ x2: clamp(segment.x2, 0, window.innerWidth),
743
+ y2: clamp(segment.y2, 0, window.innerHeight)
744
+ })).filter(
745
+ (segment) => Math.abs(segment.x1 - segment.x2) > 0.5 || Math.abs(segment.y1 - segment.y2) > 0.5
746
+ );
747
+ return {
748
+ id: createId(),
749
+ rectA,
750
+ rectB,
751
+ normalizedRectA,
752
+ normalizedRectB,
753
+ elementRefA,
754
+ elementRefB,
755
+ horizontal,
756
+ vertical,
757
+ connectors: normalizedConnectors
758
+ };
759
+ };
760
+ var updateDistanceForResize = (distance, viewport = getViewportSize()) => {
761
+ const normalizedRectA = distance.normalizedRectA ?? normalizeRect(distance.rectA);
762
+ const normalizedRectB = distance.normalizedRectB ?? normalizeRect(distance.rectB);
763
+ let rectA = distance.rectA;
764
+ let rectB = distance.rectB;
765
+ if (distance.elementRefA && document.contains(distance.elementRefA)) {
766
+ rectA = distance.elementRefA.getBoundingClientRect();
767
+ } else {
768
+ rectA = denormalizeRect(normalizedRectA, viewport);
769
+ }
770
+ if (distance.elementRefB && document.contains(distance.elementRefB)) {
771
+ rectB = distance.elementRefB.getBoundingClientRect();
772
+ } else {
773
+ rectB = denormalizeRect(normalizedRectB, viewport);
774
+ }
775
+ const updated = getDistanceOverlay(
776
+ rectA,
777
+ rectB,
778
+ distance.elementRefA,
779
+ distance.elementRefB
780
+ );
781
+ return {
782
+ ...updated,
783
+ id: distance.id
784
+ };
785
+ };
786
+
787
+ // dom.ts
788
+ var getElementLabel = (element) => {
789
+ const tag = element.tagName.toLowerCase();
790
+ const id = element.id ? `#${element.id}` : "";
791
+ const className = element.className ? `.${element.className.toString().split(" ")[0]}` : "";
792
+ return `${tag}${id}${className}`;
793
+ };
794
+ var parseEdge = (value) => Number.parseFloat(value) || 0;
795
+ var getRectFromDom = (element) => {
796
+ const rect = element.getBoundingClientRect();
797
+ return {
798
+ left: rect.left,
799
+ top: rect.top,
800
+ width: rect.width,
801
+ height: rect.height
802
+ };
803
+ };
804
+ var rectCacheFrame = -1;
805
+ var rectCache = /* @__PURE__ */ new Map();
806
+ var getRectFromDomCached = (element) => {
807
+ const frame = getFrameToken();
808
+ if (frame !== rectCacheFrame) {
809
+ rectCacheFrame = frame;
810
+ rectCache.clear();
811
+ }
812
+ const cached = rectCache.get(element);
813
+ if (cached) return cached;
814
+ const rect = getRectFromDom(element);
815
+ rectCache.set(element, rect);
816
+ return rect;
817
+ };
818
+ var cachedElements = [];
819
+ var cachedFrame = -1;
820
+ var getFrameToken = () => {
821
+ if (typeof performance === "undefined") return 0;
822
+ return Math.floor(performance.now() / 16);
823
+ };
824
+ var getBodyElementsCached = () => {
825
+ const frame = getFrameToken();
826
+ if (frame === cachedFrame && cachedElements.length > 0) {
827
+ return cachedElements;
828
+ }
829
+ cachedFrame = frame;
830
+ cachedElements = Array.from(document.querySelectorAll("body *")).filter(
831
+ (element) => element instanceof HTMLElement
832
+ );
833
+ return cachedElements;
834
+ };
835
+ var getInspectMeasurement = (element) => {
836
+ const rect = element.getBoundingClientRect();
837
+ const style = window.getComputedStyle(element);
838
+ const padding = {
839
+ top: parseEdge(style.paddingTop),
840
+ right: parseEdge(style.paddingRight),
841
+ bottom: parseEdge(style.paddingBottom),
842
+ left: parseEdge(style.paddingLeft)
843
+ };
844
+ const margin = {
845
+ top: parseEdge(style.marginTop),
846
+ right: parseEdge(style.marginRight),
847
+ bottom: parseEdge(style.marginBottom),
848
+ left: parseEdge(style.marginLeft)
849
+ };
850
+ const paddingRect = {
851
+ left: rect.left + padding.left,
852
+ top: rect.top + padding.top,
853
+ width: Math.max(0, rect.width - padding.left - padding.right),
854
+ height: Math.max(0, rect.height - padding.top - padding.bottom)
855
+ };
856
+ const marginRect = {
857
+ left: rect.left - margin.left,
858
+ top: rect.top - margin.top,
859
+ width: rect.width + margin.left + margin.right,
860
+ height: rect.height + margin.top + margin.bottom
861
+ };
862
+ return {
863
+ id: createId(),
864
+ rect: {
865
+ left: rect.left,
866
+ top: rect.top,
867
+ width: rect.width,
868
+ height: rect.height
869
+ },
870
+ paddingRect,
871
+ marginRect,
872
+ padding,
873
+ margin,
874
+ label: getElementLabel(element),
875
+ elementRef: element
876
+ };
877
+ };
878
+ var updateMeasurementForResize = (measurement, viewport = getViewportSize()) => {
879
+ let rect = measurement.rect;
880
+ if (measurement.elementRef && document.contains(measurement.elementRef)) {
881
+ rect = getRectFromDom(measurement.elementRef);
882
+ } else if (measurement.normalizedRect) {
883
+ rect = denormalizeRect(measurement.normalizedRect, viewport);
884
+ }
885
+ return {
886
+ ...measurement,
887
+ rect,
888
+ normalizedRect: normalizeRect(rect, viewport),
889
+ originRect: void 0
890
+ };
891
+ };
892
+
893
+ // hooks/use-live-element-tracking.ts
894
+ var useLiveElementTracking = (params) => {
895
+ const frameRef = (0, import_react6.useRef)(null);
896
+ (0, import_react6.useEffect)(() => {
897
+ if (!params.enabled) {
898
+ if (frameRef.current) {
899
+ cancelAnimationFrame(frameRef.current);
900
+ }
901
+ frameRef.current = null;
902
+ return;
903
+ }
904
+ const tick = () => {
905
+ params.setMeasurements(
906
+ (prev) => prev.map((measurement) => {
907
+ if (!measurement.elementRef || !document.contains(measurement.elementRef)) {
908
+ return measurement;
909
+ }
910
+ const rect = getRectFromDom(measurement.elementRef);
911
+ if (rectAlmostEqual(rect, measurement.rect)) return measurement;
912
+ return {
913
+ ...measurement,
914
+ rect,
915
+ normalizedRect: normalizeRect(rect),
916
+ originRect: void 0
917
+ };
918
+ })
919
+ );
920
+ params.setActiveMeasurement((prev) => {
921
+ if (!prev?.elementRef || !document.contains(prev.elementRef))
922
+ return prev;
923
+ const rect = getRectFromDom(prev.elementRef);
924
+ if (rectAlmostEqual(rect, prev.rect)) return prev;
925
+ return {
926
+ ...prev,
927
+ rect,
928
+ normalizedRect: normalizeRect(rect),
929
+ originRect: void 0
930
+ };
931
+ });
932
+ params.setHeldDistances(
933
+ (prev) => prev.map((distance) => {
934
+ const canTrackA = distance.elementRefA && document.contains(distance.elementRefA);
935
+ const canTrackB = distance.elementRefB && document.contains(distance.elementRefB);
936
+ if (!canTrackA && !canTrackB) return distance;
937
+ const rectA = canTrackA ? getRectFromDom(distance.elementRefA) : distance.rectA;
938
+ const rectB = canTrackB ? getRectFromDom(distance.elementRefB) : distance.rectB;
939
+ if (rectAlmostEqual(rectA, distance.rectA) && rectAlmostEqual(rectB, distance.rectB)) {
940
+ return distance;
941
+ }
942
+ const updated = getDistanceOverlay(
943
+ rectA,
944
+ rectB,
945
+ distance.elementRefA,
946
+ distance.elementRefB
947
+ );
948
+ return {
949
+ ...updated,
950
+ id: distance.id
951
+ };
952
+ })
953
+ );
954
+ const selected = params.selectedElementRef.current;
955
+ if (selected && document.contains(selected)) {
956
+ params.setSelectedMeasurement((prev) => {
957
+ const next = getInspectMeasurement(selected);
958
+ if (prev && rectAlmostEqual(prev.rect, next.rect)) return prev;
959
+ return next;
960
+ });
961
+ }
962
+ params.setSelectedMeasurements(
963
+ (prev) => prev.map((measurement) => {
964
+ if (!measurement.elementRef || !document.contains(measurement.elementRef)) {
965
+ return measurement;
966
+ }
967
+ const next = getInspectMeasurement(measurement.elementRef);
968
+ if (rectAlmostEqual(next.rect, measurement.rect)) return measurement;
969
+ return {
970
+ ...next,
971
+ id: measurement.id
972
+ };
973
+ })
974
+ );
975
+ const hover = params.hoverElementRef.current;
976
+ if (hover && document.contains(hover)) {
977
+ const rect = getRectFromDom(hover);
978
+ params.setHoverRect(
979
+ (prev) => prev && rectAlmostEqual(prev, rect) ? prev : rect
980
+ );
981
+ }
982
+ frameRef.current = requestAnimationFrame(tick);
983
+ };
984
+ frameRef.current = requestAnimationFrame(tick);
985
+ return () => {
986
+ if (frameRef.current) {
987
+ cancelAnimationFrame(frameRef.current);
988
+ }
989
+ frameRef.current = null;
990
+ };
991
+ }, [params]);
992
+ };
993
+
994
+ // hooks/use-measure-toggles.ts
995
+ var import_react7 = require("react");
996
+ var useMeasureToggles = (options = {}) => {
997
+ const [enabled, setEnabled] = (0, import_react7.useState)(options.initialEnabled ?? true);
998
+ const [altPressed, setAltPressed] = (0, import_react7.useState)(false);
999
+ const [toolMode, setToolMode] = (0, import_react7.useState)(
1000
+ options.initialToolMode ?? "none"
1001
+ );
1002
+ const holdEnabled = false;
1003
+ const multiMeasureEnabled = false;
1004
+ const snapGuidesEnabled = true;
1005
+ const guidesEnabled = toolMode === "guides";
1006
+ const snapEnabled = true;
1007
+ return {
1008
+ enabled,
1009
+ setEnabled,
1010
+ holdEnabled,
1011
+ altPressed,
1012
+ setAltPressed,
1013
+ toolMode,
1014
+ setToolMode,
1015
+ guidesEnabled,
1016
+ multiMeasureEnabled,
1017
+ snapGuidesEnabled,
1018
+ snapEnabled
1019
+ };
1020
+ };
1021
+
1022
+ // hooks/use-measurement-state.ts
1023
+ var import_react8 = require("react");
1024
+ var useMeasurementState = (options = {}) => {
1025
+ const [activeMeasurement, setActiveMeasurement] = (0, import_react8.useState)(options.initialActiveMeasurement ?? null);
1026
+ const [measurements, setMeasurements] = (0, import_react8.useState)(
1027
+ options.initialMeasurements ?? []
1028
+ );
1029
+ const [selectedMeasurement, setSelectedMeasurement] = (0, import_react8.useState)(null);
1030
+ const [selectedMeasurements, setSelectedMeasurements] = (0, import_react8.useState)([]);
1031
+ const [hoverRect, setHoverRect] = (0, import_react8.useState)(null);
1032
+ const [heldDistances, setHeldDistances] = (0, import_react8.useState)(
1033
+ options.initialHeldDistances ?? []
1034
+ );
1035
+ return {
1036
+ activeMeasurement,
1037
+ setActiveMeasurement,
1038
+ measurements,
1039
+ setMeasurements,
1040
+ selectedMeasurement,
1041
+ setSelectedMeasurement,
1042
+ selectedMeasurements,
1043
+ setSelectedMeasurements,
1044
+ hoverRect,
1045
+ setHoverRect,
1046
+ heldDistances,
1047
+ setHeldDistances
1048
+ };
1049
+ };
1050
+
1051
+ // hooks/use-measurer-derived.ts
1052
+ var import_react9 = require("react");
1053
+
1054
+ // edge-visibility.ts
1055
+ var rangesOverlap = (a1, a2, b1, b2) => Math.min(a2, b2) - Math.max(a1, b1) > 0;
1056
+ var getEdgeVisibilityForRects = (rects, epsilon = 1) => {
1057
+ const visibility = rects.map(() => ({
1058
+ top: true,
1059
+ right: true,
1060
+ bottom: true,
1061
+ left: true
1062
+ }));
1063
+ for (let i = 0; i < rects.length; i += 1) {
1064
+ const a = rects[i];
1065
+ const aRight = a.left + a.width;
1066
+ const aBottom = a.top + a.height;
1067
+ for (let j = i + 1; j < rects.length; j += 1) {
1068
+ const b = rects[j];
1069
+ const bRight = b.left + b.width;
1070
+ const bBottom = b.top + b.height;
1071
+ const overlapX = rangesOverlap(a.left, aRight, b.left, bRight);
1072
+ const overlapY = rangesOverlap(a.top, aBottom, b.top, bBottom);
1073
+ if (Math.abs(aRight - b.left) <= epsilon && overlapY) {
1074
+ visibility[i].right = false;
1075
+ visibility[j].left = false;
1076
+ }
1077
+ if (Math.abs(a.left - bRight) <= epsilon && overlapY) {
1078
+ visibility[i].left = false;
1079
+ visibility[j].right = false;
1080
+ }
1081
+ if (Math.abs(aBottom - b.top) <= epsilon && overlapX) {
1082
+ visibility[i].bottom = false;
1083
+ visibility[j].top = false;
1084
+ }
1085
+ if (Math.abs(a.top - bBottom) <= epsilon && overlapX) {
1086
+ visibility[i].top = false;
1087
+ visibility[j].bottom = false;
1088
+ }
1089
+ }
1090
+ }
1091
+ return visibility;
1092
+ };
1093
+
1094
+ // guides.ts
1095
+ var getGuideRect = (guide) => {
1096
+ const viewport = getViewportSize();
1097
+ return guide.orientation === "vertical" ? { left: guide.position, top: 0, width: 1, height: viewport.height } : { left: 0, top: guide.position, width: viewport.width, height: 1 };
1098
+ };
1099
+ var getGuideDistance = (guide, point) => guide.orientation === "vertical" ? Math.abs(guide.position - point.x) : Math.abs(guide.position - point.y);
1100
+ var getSnapGuidePosition = (params) => {
1101
+ const { orientation, point, snapGuidesEnabled, overlayNode, guides } = params;
1102
+ if (!snapGuidesEnabled) {
1103
+ return orientation === "vertical" ? point.x : point.y;
1104
+ }
1105
+ const elements = getBodyElementsCached();
1106
+ let bestValue = orientation === "vertical" ? point.x : point.y;
1107
+ let bestDistance = GUIDE_SNAP_DISTANCE + 1;
1108
+ for (const element of elements) {
1109
+ if (!(element instanceof HTMLElement)) continue;
1110
+ if (overlayNode && overlayNode.contains(element)) continue;
1111
+ if (element === document.body || element === document.documentElement)
1112
+ continue;
1113
+ const rect = getRectFromDomCached(element);
1114
+ if (rect.width <= 2 || rect.height <= 2) continue;
1115
+ if (orientation === "vertical") {
1116
+ if (point.x < rect.left - GUIDE_SNAP_DISTANCE || point.x > rect.left + rect.width + GUIDE_SNAP_DISTANCE) {
1117
+ continue;
1118
+ }
1119
+ } else {
1120
+ if (point.y < rect.top - GUIDE_SNAP_DISTANCE || point.y > rect.top + rect.height + GUIDE_SNAP_DISTANCE) {
1121
+ continue;
1122
+ }
1123
+ }
1124
+ const candidates = orientation === "vertical" ? [rect.left, rect.left + rect.width, rect.left + rect.width / 2] : [rect.top, rect.top + rect.height, rect.top + rect.height / 2];
1125
+ candidates.forEach((candidate) => {
1126
+ const distance = orientation === "vertical" ? Math.abs(candidate - point.x) : Math.abs(candidate - point.y);
1127
+ if (distance > GUIDE_SNAP_DISTANCE) return;
1128
+ if (distance < bestDistance) {
1129
+ bestValue = candidate;
1130
+ bestDistance = distance;
1131
+ }
1132
+ });
1133
+ }
1134
+ guides.forEach((guide) => {
1135
+ if (guide.id === params.draggingGuideId) return;
1136
+ const candidate = guide.position;
1137
+ const distance = orientation === "vertical" ? Math.abs(candidate - point.x) : Math.abs(candidate - point.y);
1138
+ if (distance > GUIDE_SNAP_DISTANCE) return;
1139
+ if (distance < bestDistance) {
1140
+ bestValue = candidate;
1141
+ bestDistance = distance;
1142
+ }
1143
+ });
1144
+ return bestDistance <= GUIDE_SNAP_DISTANCE ? bestValue : orientation === "vertical" ? point.x : point.y;
1145
+ };
1146
+
1147
+ // option-measurements.ts
1148
+ var getHoveredGuide = (point, guides) => {
1149
+ if (!point || guides.length === 0) return null;
1150
+ let closest = null;
1151
+ let closestDistance = GUIDE_SNAP_DISTANCE + 1;
1152
+ for (const guide of guides) {
1153
+ const distance = getGuideDistance(guide, point);
1154
+ if (distance > GUIDE_SNAP_DISTANCE) continue;
1155
+ if (distance < closestDistance) {
1156
+ closest = guide;
1157
+ closestDistance = distance;
1158
+ }
1159
+ }
1160
+ return closest;
1161
+ };
1162
+ var getSelectedGuide = (guides, selectedGuideIds) => {
1163
+ if (selectedGuideIds.length === 0) return null;
1164
+ const lastId = selectedGuideIds[selectedGuideIds.length - 1];
1165
+ return guides.find((guide) => guide.id === lastId) ?? null;
1166
+ };
1167
+ var getOptionPairOverlay = (params) => {
1168
+ if (!params.altPressed || !params.primarySelectedMeasurement) return null;
1169
+ const selectedElement = params.selectedGuide ? null : params.primarySelectedMeasurement.elementRef ?? params.selectedElementRef ?? null;
1170
+ const selectedTarget = params.selectedGuide ? {
1171
+ rect: getGuideRect(params.selectedGuide),
1172
+ guideId: params.selectedGuide.id
1173
+ } : selectedElement ? {
1174
+ rect: params.primarySelectedMeasurement.rect,
1175
+ element: selectedElement
1176
+ } : null;
1177
+ const hoverTarget = params.hoverGuide ? { rect: getGuideRect(params.hoverGuide), guideId: params.hoverGuide.id } : params.hoverElement ? {
1178
+ rect: getRectFromDom(params.hoverElement),
1179
+ element: params.hoverElement
1180
+ } : null;
1181
+ if (!selectedTarget || !hoverTarget) return null;
1182
+ if (selectedTarget.element && hoverTarget.element) {
1183
+ if (selectedTarget.element === hoverTarget.element || selectedTarget.element.contains(hoverTarget.element) || hoverTarget.element.contains(selectedTarget.element)) {
1184
+ return null;
1185
+ }
1186
+ }
1187
+ if (selectedTarget.guideId && hoverTarget.guideId && selectedTarget.guideId === hoverTarget.guideId) {
1188
+ return null;
1189
+ }
1190
+ return getDistanceOverlay(
1191
+ selectedTarget.rect,
1192
+ hoverTarget.rect,
1193
+ selectedTarget.element ?? null,
1194
+ hoverTarget.element ?? null
1195
+ );
1196
+ };
1197
+ var getOptionContainerLines = (params) => {
1198
+ if (!params.altPressed || !params.primarySelectedMeasurement || params.optionPairOverlay)
1199
+ return null;
1200
+ if (params.selectedGuideIds.length > 0) return null;
1201
+ let containerElement = params.selectedElement?.parentElement ?? null;
1202
+ if (params.selectedElement && params.hoverElement && params.hoverElement !== params.selectedElement && params.hoverElement.contains(params.selectedElement)) {
1203
+ containerElement = params.hoverElement;
1204
+ }
1205
+ const containerRect = containerElement && containerElement !== document.body && containerElement !== document.documentElement ? getRectFromDom(containerElement) : {
1206
+ left: 0,
1207
+ top: 0,
1208
+ width: window.innerWidth,
1209
+ height: window.innerHeight
1210
+ };
1211
+ const rect = params.primarySelectedMeasurement.rect;
1212
+ const right = rect.left + rect.width;
1213
+ const bottom = rect.top + rect.height;
1214
+ const centerX = rect.left + rect.width / 2;
1215
+ const centerY = rect.top + rect.height / 2;
1216
+ const containerRight = containerRect.left + containerRect.width;
1217
+ const containerBottom = containerRect.top + containerRect.height;
1218
+ const lineX = Math.max(containerRect.left, Math.min(centerX, containerRight));
1219
+ const lineY = Math.max(containerRect.top, Math.min(centerY, containerBottom));
1220
+ return {
1221
+ top: {
1222
+ y1: containerRect.top,
1223
+ y2: rect.top,
1224
+ x: lineX,
1225
+ value: Math.max(0, rect.top - containerRect.top)
1226
+ },
1227
+ bottom: {
1228
+ y1: bottom,
1229
+ y2: containerBottom,
1230
+ x: lineX,
1231
+ value: Math.max(0, containerBottom - bottom)
1232
+ },
1233
+ left: {
1234
+ x1: containerRect.left,
1235
+ x2: rect.left,
1236
+ y: lineY,
1237
+ value: Math.max(0, rect.left - containerRect.left)
1238
+ },
1239
+ right: {
1240
+ x1: right,
1241
+ x2: containerRight,
1242
+ y: lineY,
1243
+ value: Math.max(0, containerRight - right)
1244
+ }
1245
+ };
1246
+ };
1247
+
1248
+ // selection-helpers.ts
1249
+ var getPrimarySelectedMeasurement = (selectedMeasurements, selectedMeasurement) => selectedMeasurements.length > 0 ? selectedMeasurements[selectedMeasurements.length - 1] : selectedMeasurement;
1250
+ var getSelectedMeasurementHit = (params) => {
1251
+ const candidates = params.selectedMeasurements.map((measurement) => {
1252
+ const element = measurement.elementRef;
1253
+ if (!element || !document.contains(element)) return null;
1254
+ const rect = getRectFromDom(element);
1255
+ return {
1256
+ measurement,
1257
+ element,
1258
+ rect,
1259
+ area: rect.width * rect.height
1260
+ };
1261
+ }).filter((item) => item !== null).sort((a, b) => a.area - b.area);
1262
+ if (candidates.length === 0) return null;
1263
+ const stack = document.elementsFromPoint(params.point.x, params.point.y);
1264
+ for (const element of stack) {
1265
+ if (!(element instanceof HTMLElement)) continue;
1266
+ if (params.overlayNode && params.overlayNode.contains(element)) continue;
1267
+ for (const candidate of candidates) {
1268
+ if (candidate.element === element || candidate.element.contains(element)) {
1269
+ return candidate.measurement;
1270
+ }
1271
+ }
1272
+ }
1273
+ for (const candidate of candidates) {
1274
+ const { rect } = candidate;
1275
+ const inRect = params.point.x >= rect.left && params.point.x <= rect.left + rect.width && params.point.y >= rect.top && params.point.y <= rect.top + rect.height;
1276
+ if (inRect) return candidate.measurement;
1277
+ }
1278
+ return null;
1279
+ };
1280
+
1281
+ // hooks/use-measurer-derived.ts
1282
+ var useMeasurerDerived = ({
1283
+ start,
1284
+ end,
1285
+ selectedMeasurements,
1286
+ selectedMeasurement,
1287
+ selectionOriginRect,
1288
+ guides,
1289
+ selectedGuideIds,
1290
+ hoverPointer,
1291
+ hoverRect,
1292
+ hoverElement,
1293
+ selectedElement,
1294
+ altPressed,
1295
+ guidesEnabled,
1296
+ guidePreview,
1297
+ displayedMeasurements,
1298
+ hoverHighlightEnabled,
1299
+ highlightColor,
1300
+ guideColor
1301
+ }) => {
1302
+ const activeRect = (0, import_react9.useMemo)(() => {
1303
+ if (!start || !end) return null;
1304
+ return {
1305
+ left: Math.min(start.x, end.x),
1306
+ top: Math.min(start.y, end.y),
1307
+ width: Math.abs(end.x - start.x),
1308
+ height: Math.abs(end.y - start.y)
1309
+ };
1310
+ }, [end, start]);
1311
+ const activeWidth = activeRect ? formatValue(activeRect.width) : 0;
1312
+ const activeHeight = activeRect ? formatValue(activeRect.height) : 0;
1313
+ const groupedSelection = (0, import_react9.useMemo)(() => {
1314
+ if (selectedMeasurements.length <= 1) return null;
1315
+ const selectionKey = selectedMeasurements.map((measurement) => measurement.id).join("|");
1316
+ const rects = selectedMeasurements.map((measurement) => measurement.rect);
1317
+ let left = Number.POSITIVE_INFINITY;
1318
+ let top = Number.POSITIVE_INFINITY;
1319
+ let right = Number.NEGATIVE_INFINITY;
1320
+ let bottom = Number.NEGATIVE_INFINITY;
1321
+ rects.forEach((rect) => {
1322
+ left = Math.min(left, rect.left);
1323
+ top = Math.min(top, rect.top);
1324
+ right = Math.max(right, rect.left + rect.width);
1325
+ bottom = Math.max(bottom, rect.top + rect.height);
1326
+ });
1327
+ const unionRect = {
1328
+ left,
1329
+ top,
1330
+ width: right - left,
1331
+ height: bottom - top
1332
+ };
1333
+ const base = selectedMeasurements[selectedMeasurements.length - 1];
1334
+ return {
1335
+ ...base,
1336
+ id: `group-${selectionKey}`,
1337
+ rect: unionRect,
1338
+ paddingRect: unionRect,
1339
+ marginRect: unionRect,
1340
+ originRect: selectionOriginRect ?? void 0
1341
+ };
1342
+ }, [selectedMeasurements, selectionOriginRect]);
1343
+ const displayedSelectedMeasurements = (0, import_react9.useMemo)(
1344
+ () => groupedSelection ? [groupedSelection] : selectedMeasurements.length > 0 ? selectedMeasurements : selectedMeasurement ? [selectedMeasurement] : [],
1345
+ [groupedSelection, selectedMeasurement, selectedMeasurements]
1346
+ );
1347
+ const selectedGuide = (0, import_react9.useMemo)(
1348
+ () => getSelectedGuide(guides, selectedGuideIds),
1349
+ [guides, selectedGuideIds]
1350
+ );
1351
+ const hoverGuide = (0, import_react9.useMemo)(
1352
+ () => getHoveredGuide(hoverPointer, guides),
1353
+ [guides, hoverPointer]
1354
+ );
1355
+ const primarySelectedMeasurement = (0, import_react9.useMemo)(
1356
+ () => getPrimarySelectedMeasurement(selectedMeasurements, selectedMeasurement),
1357
+ [selectedMeasurement, selectedMeasurements]
1358
+ );
1359
+ const effectivePrimarySelected = groupedSelection ?? primarySelectedMeasurement;
1360
+ const optionPairOverlay = (0, import_react9.useMemo)(() => {
1361
+ return getOptionPairOverlay({
1362
+ altPressed,
1363
+ primarySelectedMeasurement: effectivePrimarySelected,
1364
+ selectedGuide,
1365
+ hoverGuide,
1366
+ hoverElement,
1367
+ selectedElementRef: selectedElement
1368
+ });
1369
+ }, [
1370
+ altPressed,
1371
+ effectivePrimarySelected,
1372
+ hoverElement,
1373
+ hoverGuide,
1374
+ selectedElement,
1375
+ selectedGuide
1376
+ ]);
1377
+ const optionContainerLines = (0, import_react9.useMemo)(() => {
1378
+ return getOptionContainerLines({
1379
+ altPressed,
1380
+ primarySelectedMeasurement: effectivePrimarySelected,
1381
+ optionPairOverlay,
1382
+ selectedGuideIds,
1383
+ selectedElement,
1384
+ hoverElement
1385
+ });
1386
+ }, [
1387
+ altPressed,
1388
+ effectivePrimarySelected,
1389
+ hoverElement,
1390
+ optionPairOverlay,
1391
+ selectedElement,
1392
+ selectedGuideIds
1393
+ ]);
1394
+ const guideDistanceOverlay = (0, import_react9.useMemo)(() => {
1395
+ if (!altPressed || !guidesEnabled || !guidePreview) return null;
1396
+ const previewGuide = {
1397
+ id: "preview",
1398
+ orientation: guidePreview.orientation,
1399
+ position: guidePreview.position
1400
+ };
1401
+ const sameOrientation = guides.filter(
1402
+ (guide) => guide.orientation === guidePreview.orientation
1403
+ );
1404
+ if (sameOrientation.length === 0) return null;
1405
+ const nearest = sameOrientation.reduce(
1406
+ (closest, guide) => {
1407
+ const distance = Math.abs(guide.position - previewGuide.position);
1408
+ if (!closest) return { guide, distance };
1409
+ return distance < closest.distance ? { guide, distance } : closest;
1410
+ },
1411
+ null
1412
+ );
1413
+ if (!nearest) return null;
1414
+ return getDistanceOverlay(
1415
+ getGuideRect(previewGuide),
1416
+ getGuideRect(nearest.guide)
1417
+ );
1418
+ }, [altPressed, guidePreview, guides, guidesEnabled]);
1419
+ const {
1420
+ outlineColor,
1421
+ fillColor,
1422
+ guideColorActive,
1423
+ guideColorHover,
1424
+ guideColorDefault,
1425
+ guideColorPreview
1426
+ } = (0, import_react9.useMemo)(
1427
+ () => ({
1428
+ outlineColor: `color-mix(in oklch, ${highlightColor} 80%, transparent)`,
1429
+ fillColor: `color-mix(in oklch, ${highlightColor} 8%, transparent)`,
1430
+ guideColorActive: `color-mix(in oklch, ${guideColor} 100%, transparent)`,
1431
+ guideColorHover: `color-mix(in oklch, ${guideColor} 90%, transparent)`,
1432
+ guideColorDefault: `color-mix(in oklch, ${guideColor} 70%, transparent)`,
1433
+ guideColorPreview: `color-mix(in oklch, ${guideColor} 50%, transparent)`
1434
+ }),
1435
+ [guideColor, highlightColor]
1436
+ );
1437
+ const selectedRects = (0, import_react9.useMemo)(
1438
+ () => displayedSelectedMeasurements.map((measurement) => measurement.rect),
1439
+ [displayedSelectedMeasurements]
1440
+ );
1441
+ const hoverRectToShow = hoverHighlightEnabled && hoverRect && selectedMeasurements.length <= 1 ? hoverRect : null;
1442
+ const selectionEdgeVisibility = (0, import_react9.useMemo)(
1443
+ () => getEdgeVisibilityForRects(selectedRects),
1444
+ [selectedRects]
1445
+ );
1446
+ const selectedEdgeVisibility = selectionEdgeVisibility.slice(
1447
+ 0,
1448
+ selectedRects.length
1449
+ );
1450
+ const hoverEdgeVisibility = hoverRectToShow ? { top: true, right: true, bottom: true, left: true } : null;
1451
+ const measurementEdgeVisibility = (0, import_react9.useMemo)(
1452
+ () => getEdgeVisibilityForRects(displayedMeasurements.map((m) => m.rect)),
1453
+ [displayedMeasurements]
1454
+ );
1455
+ return {
1456
+ activeRect,
1457
+ activeWidth,
1458
+ activeHeight,
1459
+ displayedSelectedMeasurements,
1460
+ selectedGuide,
1461
+ hoverGuide,
1462
+ optionPairOverlay,
1463
+ optionContainerLines,
1464
+ guideDistanceOverlay,
1465
+ outlineColor,
1466
+ fillColor,
1467
+ guideColorActive,
1468
+ guideColorHover,
1469
+ guideColorDefault,
1470
+ guideColorPreview,
1471
+ hoverRectToShow,
1472
+ selectedEdgeVisibility,
1473
+ hoverEdgeVisibility,
1474
+ measurementEdgeVisibility
1475
+ };
1476
+ };
1477
+
1478
+ // hooks/use-measurer-history.ts
1479
+ var import_react10 = require("react");
1480
+ var HISTORY_LIMIT = 50;
1481
+ var useMeasurerHistory = ({
1482
+ toggles,
1483
+ measurements,
1484
+ guides,
1485
+ transient
1486
+ }) => {
1487
+ const {
1488
+ enabled,
1489
+ setEnabled,
1490
+ toolMode,
1491
+ setToolMode,
1492
+ guideOrientation,
1493
+ setGuideOrientation
1494
+ } = toggles;
1495
+ const {
1496
+ measurements: measurementEntries,
1497
+ setMeasurements,
1498
+ activeMeasurement,
1499
+ setActiveMeasurement,
1500
+ selectedMeasurements,
1501
+ setSelectedMeasurements,
1502
+ selectedMeasurement,
1503
+ setSelectedMeasurement,
1504
+ heldDistances,
1505
+ setHeldDistances
1506
+ } = measurements;
1507
+ const {
1508
+ guides: guideEntries,
1509
+ setGuides,
1510
+ selectedGuideIds,
1511
+ setSelectedGuideIds,
1512
+ draggingGuideId,
1513
+ setDraggingGuideId
1514
+ } = guides;
1515
+ const {
1516
+ setStart,
1517
+ setEnd,
1518
+ setIsDragging,
1519
+ setGuidePreview,
1520
+ setHoverRect,
1521
+ setHoverElement,
1522
+ setSelectedElement,
1523
+ clearSelectionRect
1524
+ } = transient;
1525
+ const historyRef = (0, import_react10.useRef)([]);
1526
+ const futureRef = (0, import_react10.useRef)([]);
1527
+ const historySignatureRef = (0, import_react10.useRef)(null);
1528
+ const captureSnapshot = (0, import_react10.useCallback)(() => {
1529
+ return {
1530
+ enabled,
1531
+ toolMode,
1532
+ guideOrientation,
1533
+ measurements: [...measurementEntries],
1534
+ activeMeasurement,
1535
+ selectedMeasurements: [...selectedMeasurements],
1536
+ selectedMeasurement,
1537
+ heldDistances: [...heldDistances],
1538
+ guides: [...guideEntries],
1539
+ selectedGuideIds: [...selectedGuideIds],
1540
+ draggingGuideId
1541
+ };
1542
+ }, [
1543
+ activeMeasurement,
1544
+ draggingGuideId,
1545
+ enabled,
1546
+ guideOrientation,
1547
+ guideEntries,
1548
+ heldDistances,
1549
+ measurementEntries,
1550
+ selectedGuideIds,
1551
+ selectedMeasurement,
1552
+ selectedMeasurements,
1553
+ toolMode
1554
+ ]);
1555
+ const getSnapshotSignature = (0, import_react10.useCallback)((snapshot) => {
1556
+ const serializeRect = (rect) => `${Math.round(rect.left)}:${Math.round(rect.top)}:${Math.round(rect.width)}:${Math.round(rect.height)}`;
1557
+ return [
1558
+ snapshot.enabled ? "1" : "0",
1559
+ snapshot.toolMode,
1560
+ snapshot.guideOrientation,
1561
+ snapshot.measurements.map((item) => `${item.id}@${serializeRect(item.rect)}`).join(","),
1562
+ snapshot.activeMeasurement ? `${snapshot.activeMeasurement.id}@${serializeRect(snapshot.activeMeasurement.rect)}` : "",
1563
+ snapshot.selectedMeasurements.map((item) => `${item.id}@${serializeRect(item.rect)}`).join(","),
1564
+ snapshot.selectedMeasurement ? `${snapshot.selectedMeasurement.id}@${serializeRect(snapshot.selectedMeasurement.rect)}` : "",
1565
+ snapshot.heldDistances.map((item) => item.id).join(","),
1566
+ snapshot.guides.map((item) => `${item.id}:${item.position}`).join(","),
1567
+ snapshot.selectedGuideIds.join(","),
1568
+ snapshot.draggingGuideId ?? ""
1569
+ ].join("|");
1570
+ }, []);
1571
+ const recordSnapshot = (0, import_react10.useCallback)(() => {
1572
+ const snapshot = captureSnapshot();
1573
+ const signature = getSnapshotSignature(snapshot);
1574
+ if (historySignatureRef.current === signature) return;
1575
+ historyRef.current.push(snapshot);
1576
+ futureRef.current = [];
1577
+ historySignatureRef.current = signature;
1578
+ if (historyRef.current.length > HISTORY_LIMIT) {
1579
+ historyRef.current.shift();
1580
+ }
1581
+ }, [captureSnapshot, getSnapshotSignature]);
1582
+ const restoreSnapshot = (0, import_react10.useCallback)(
1583
+ (snapshot) => {
1584
+ setEnabled(snapshot.enabled);
1585
+ setToolMode(snapshot.toolMode);
1586
+ setGuideOrientation(snapshot.guideOrientation);
1587
+ setMeasurements(snapshot.measurements);
1588
+ setActiveMeasurement(snapshot.activeMeasurement);
1589
+ setSelectedMeasurements(snapshot.selectedMeasurements);
1590
+ setSelectedMeasurement(snapshot.selectedMeasurement);
1591
+ setHeldDistances(snapshot.heldDistances);
1592
+ setGuides(snapshot.guides);
1593
+ setSelectedGuideIds(snapshot.selectedGuideIds);
1594
+ setDraggingGuideId(snapshot.draggingGuideId);
1595
+ setStart(null);
1596
+ setEnd(null);
1597
+ setIsDragging(false);
1598
+ setGuidePreview(null);
1599
+ setHoverRect(null);
1600
+ setHoverElement(null);
1601
+ clearSelectionRect();
1602
+ const nextSelectedElement = snapshot.selectedMeasurement?.elementRef ?? snapshot.selectedMeasurements[snapshot.selectedMeasurements.length - 1]?.elementRef ?? null;
1603
+ setSelectedElement(nextSelectedElement);
1604
+ },
1605
+ [
1606
+ clearSelectionRect,
1607
+ setActiveMeasurement,
1608
+ setDraggingGuideId,
1609
+ setEnabled,
1610
+ setEnd,
1611
+ setGuideOrientation,
1612
+ setGuidePreview,
1613
+ setGuides,
1614
+ setHeldDistances,
1615
+ setHoverRect,
1616
+ setIsDragging,
1617
+ setMeasurements,
1618
+ setSelectedGuideIds,
1619
+ setSelectedMeasurement,
1620
+ setSelectedMeasurements,
1621
+ setSelectedElement,
1622
+ setStart,
1623
+ setToolMode,
1624
+ setHoverElement
1625
+ ]
1626
+ );
1627
+ const undo = (0, import_react10.useCallback)(() => {
1628
+ const previousSnapshot = historyRef.current.pop();
1629
+ if (!previousSnapshot) return;
1630
+ const currentSnapshot = captureSnapshot();
1631
+ futureRef.current.push(currentSnapshot);
1632
+ if (futureRef.current.length > HISTORY_LIMIT) {
1633
+ futureRef.current.shift();
1634
+ }
1635
+ historySignatureRef.current = null;
1636
+ restoreSnapshot(previousSnapshot);
1637
+ }, [captureSnapshot, restoreSnapshot]);
1638
+ const redo = (0, import_react10.useCallback)(() => {
1639
+ const nextSnapshot = futureRef.current.pop();
1640
+ if (!nextSnapshot) return;
1641
+ historyRef.current.push(captureSnapshot());
1642
+ if (historyRef.current.length > HISTORY_LIMIT) {
1643
+ historyRef.current.shift();
1644
+ }
1645
+ historySignatureRef.current = null;
1646
+ restoreSnapshot(nextSnapshot);
1647
+ }, [captureSnapshot, restoreSnapshot]);
1648
+ const setToolModeWithHistory = (0, import_react10.useCallback)(
1649
+ (value) => {
1650
+ setToolMode((prev) => {
1651
+ const next = typeof value === "function" ? value(prev) : value;
1652
+ if (next === prev) return prev;
1653
+ recordSnapshot();
1654
+ return next;
1655
+ });
1656
+ },
1657
+ [recordSnapshot, setToolMode]
1658
+ );
1659
+ const setGuideOrientationWithHistory = (0, import_react10.useCallback)(
1660
+ (value) => {
1661
+ setGuideOrientation((prev) => {
1662
+ const next = typeof value === "function" ? value(prev) : value;
1663
+ if (next === prev) return prev;
1664
+ recordSnapshot();
1665
+ return next;
1666
+ });
1667
+ },
1668
+ [recordSnapshot, setGuideOrientation]
1669
+ );
1670
+ const setEnabledWithHistory = (0, import_react10.useCallback)(
1671
+ (value) => {
1672
+ setEnabled((prev) => {
1673
+ const next = typeof value === "function" ? value(prev) : value;
1674
+ if (next === prev) return prev;
1675
+ recordSnapshot();
1676
+ return next;
1677
+ });
1678
+ },
1679
+ [recordSnapshot, setEnabled]
1680
+ );
1681
+ const createActionCommit = (0, import_react10.useCallback)(() => {
1682
+ let committed = false;
1683
+ return () => {
1684
+ if (committed) return;
1685
+ recordSnapshot();
1686
+ committed = true;
1687
+ };
1688
+ }, [recordSnapshot]);
1689
+ (0, import_react10.useEffect)(() => {
1690
+ const handleKeyDown = (event) => {
1691
+ if (!(event.metaKey || event.ctrlKey)) return;
1692
+ if (event.key.toLowerCase() !== "z") return;
1693
+ if (event.shiftKey) {
1694
+ if (futureRef.current.length === 0) return;
1695
+ event.preventDefault();
1696
+ redo();
1697
+ return;
1698
+ }
1699
+ if (historyRef.current.length === 0) return;
1700
+ event.preventDefault();
1701
+ undo();
1702
+ };
1703
+ window.addEventListener("keydown", handleKeyDown);
1704
+ return () => {
1705
+ window.removeEventListener("keydown", handleKeyDown);
1706
+ };
1707
+ }, [redo, undo]);
1708
+ return {
1709
+ recordSnapshot,
1710
+ createActionCommit,
1711
+ setToolModeWithHistory,
1712
+ setGuideOrientationWithHistory,
1713
+ setEnabledWithHistory
1714
+ };
1715
+ };
1716
+
1717
+ // hooks/use-measurer-local-state.ts
1718
+ var import_react11 = require("react");
1719
+ var useMeasurerLocalState = ({
1720
+ selectedElementRef,
1721
+ hoverElementRef,
1722
+ selectionRectRef
1723
+ }) => {
1724
+ const [selectionOriginRect, setSelectionOriginRect] = (0, import_react11.useState)(
1725
+ null
1726
+ );
1727
+ const [hoverPointer, setHoverPointer] = (0, import_react11.useState)(null);
1728
+ const [hoverElement, setHoverElementState] = (0, import_react11.useState)(
1729
+ null
1730
+ );
1731
+ const [selectedElement, setSelectedElementState] = (0, import_react11.useState)(null);
1732
+ const setSelectedElement = (0, import_react11.useCallback)(
1733
+ (element) => {
1734
+ selectedElementRef.current = element;
1735
+ setSelectedElementState(element);
1736
+ },
1737
+ [selectedElementRef]
1738
+ );
1739
+ const setHoverElement = (0, import_react11.useCallback)(
1740
+ (element) => {
1741
+ hoverElementRef.current = element;
1742
+ setHoverElementState(element);
1743
+ },
1744
+ [hoverElementRef]
1745
+ );
1746
+ const clearSelectionRect = (0, import_react11.useCallback)(() => {
1747
+ selectionRectRef.current = null;
1748
+ setSelectionOriginRect(null);
1749
+ }, [selectionRectRef]);
1750
+ return {
1751
+ selectionOriginRect,
1752
+ setSelectionOriginRect,
1753
+ hoverPointer,
1754
+ setHoverPointer,
1755
+ hoverElement,
1756
+ setHoverElement,
1757
+ selectedElement,
1758
+ setSelectedElement,
1759
+ clearSelectionRect
1760
+ };
1761
+ };
1762
+
1763
+ // hooks/use-measurer-pointer.ts
1764
+ var import_react12 = require("react");
1765
+
1766
+ // targets.ts
1767
+ var pickMultiTargets = (selectionRect, items) => {
1768
+ const candidates = items.map(({ element, rect }) => {
1769
+ const overlapArea = intersectionArea(selectionRect, rect);
1770
+ const elementArea = Math.max(1, rectArea(rect));
1771
+ return {
1772
+ element,
1773
+ rect,
1774
+ overlapArea,
1775
+ elementCoverage: overlapArea / elementArea
1776
+ };
1777
+ }).filter(({ overlapArea, elementCoverage }) => {
1778
+ if (overlapArea <= 0) return false;
1779
+ return elementCoverage >= MIN_MULTI_ELEMENT_COVERAGE;
1780
+ });
1781
+ const pruned = candidates.filter((candidate, index) => {
1782
+ return !candidates.some((other, otherIndex) => {
1783
+ if (index === otherIndex) return false;
1784
+ const candidateArea = rectArea(candidate.rect);
1785
+ const otherArea = rectArea(other.rect);
1786
+ const isAncestorLike = candidateArea > otherArea * ANCESTOR_PRUNE_SCALE && candidate.rect.left <= other.rect.left && candidate.rect.top <= other.rect.top && candidate.rect.left + candidate.rect.width >= other.rect.left + other.rect.width && candidate.rect.top + candidate.rect.height >= other.rect.top + other.rect.height;
1787
+ return isAncestorLike && candidate.elementCoverage < ANCESTOR_KEEP_COVERAGE;
1788
+ });
1789
+ });
1790
+ return (pruned.length > 0 ? pruned : candidates).sort((a, b) => rectArea(a.rect) - rectArea(b.rect)).map(({ element }) => element);
1791
+ };
1792
+ var pickSingleTarget = (selectionRect, point, items) => {
1793
+ const selectionArea = Math.max(1, rectArea(selectionRect));
1794
+ const scored = items.map(({ element, rect }) => {
1795
+ const overlap = intersectionArea(selectionRect, rect);
1796
+ const elementArea = Math.max(1, rectArea(rect));
1797
+ const coverage = overlap / elementArea;
1798
+ const areaRatio = elementArea / selectionArea;
1799
+ const areaSimilarity = 1 - Math.min(1, Math.abs(Math.log(areaRatio)) / 2);
1800
+ const pointerInside = point.x >= rect.left && point.x <= rect.left + rect.width && point.y >= rect.top && point.y <= rect.top + rect.height;
1801
+ const largeContainerPenalty = elementArea > selectionArea * 4 && coverage < 0.9 ? 0.25 : 0;
1802
+ const score = coverage * 0.55 + areaSimilarity * 0.35 + (pointerInside ? 0.2 : 0) - largeContainerPenalty;
1803
+ return { element, rect, coverage, score };
1804
+ }).filter(({ rect, coverage }) => {
1805
+ if (rect.width < MIN_SINGLE_TARGET_SIZE || rect.height < MIN_SINGLE_TARGET_SIZE) {
1806
+ return false;
1807
+ }
1808
+ return coverage >= MIN_SINGLE_ELEMENT_COVERAGE;
1809
+ });
1810
+ if (scored.length === 0) return null;
1811
+ scored.sort((a, b) => b.score - a.score);
1812
+ return scored[0].element;
1813
+ };
1814
+ var pickPointTarget = (point, items) => {
1815
+ const scored = items.filter(({ rect }) => {
1816
+ return rect.width >= MIN_SINGLE_TARGET_SIZE && rect.height >= MIN_SINGLE_TARGET_SIZE;
1817
+ }).map(({ element, rect }) => {
1818
+ const area = Math.max(1, rectArea(rect));
1819
+ const inside = point.x >= rect.left && point.x <= rect.left + rect.width && point.y >= rect.top && point.y <= rect.top + rect.height;
1820
+ const distance = getDistanceToRect(point, rect);
1821
+ const proximity = 1 / (1 + distance);
1822
+ const areaWeight = 1 / (1 + Math.log(area));
1823
+ const score = (inside ? 2 : 0) + proximity * 0.9 + areaWeight * 0.35;
1824
+ return { element, score };
1825
+ });
1826
+ if (scored.length === 0) return null;
1827
+ scored.sort((a, b) => b.score - a.score);
1828
+ return scored[0].element;
1829
+ };
1830
+
1831
+ // selection.ts
1832
+ var getTargetElement = (point, overlayNode) => {
1833
+ if (overlayNode) {
1834
+ const previous = overlayNode.style.pointerEvents;
1835
+ overlayNode.style.pointerEvents = "none";
1836
+ const elements2 = document.elementsFromPoint(point.x, point.y);
1837
+ overlayNode.style.pointerEvents = previous;
1838
+ for (const element of elements2) {
1839
+ if (!(element instanceof HTMLElement)) continue;
1840
+ if (overlayNode.contains(element)) continue;
1841
+ if (element === document.body || element === document.documentElement)
1842
+ continue;
1843
+ const rect = element.getBoundingClientRect();
1844
+ if (rect.width <= 2 || rect.height <= 2) continue;
1845
+ return element;
1846
+ }
1847
+ return null;
1848
+ }
1849
+ const elements = document.elementsFromPoint(point.x, point.y);
1850
+ for (const element of elements) {
1851
+ if (!(element instanceof HTMLElement)) continue;
1852
+ if (element === document.body || element === document.documentElement)
1853
+ continue;
1854
+ const rect = element.getBoundingClientRect();
1855
+ if (rect.width <= 2 || rect.height <= 2) continue;
1856
+ return element;
1857
+ }
1858
+ return null;
1859
+ };
1860
+ var getSnappedClickTarget = (point, overlayNode, snapEnabled) => {
1861
+ if (!snapEnabled) return getTargetElement(point, overlayNode);
1862
+ const probeRect = {
1863
+ left: point.x - 20,
1864
+ top: point.y - 20,
1865
+ width: 40,
1866
+ height: 40
1867
+ };
1868
+ const entries = getSelectionEntries(probeRect, overlayNode);
1869
+ return pickPointTarget(point, entries) ?? pickSingleTarget(probeRect, point, entries) ?? getTargetElement(point, overlayNode);
1870
+ };
1871
+ var getSelectionEntries = (rect, overlayNode) => {
1872
+ const frame = getFrameToken();
1873
+ const key = `${Math.round(rect.left)}:${Math.round(rect.top)}:${Math.round(
1874
+ rect.width
1875
+ )}:${Math.round(rect.height)}`;
1876
+ if (frame === cachedSelectionFrame && cachedSelectionKey === key && cachedOverlayNode === overlayNode) {
1877
+ return cachedSelectionEntries;
1878
+ }
1879
+ const minLeft = rect.left - 1;
1880
+ const minTop = rect.top - 1;
1881
+ const maxRight = rect.left + rect.width + 1;
1882
+ const maxBottom = rect.top + rect.height + 1;
1883
+ const elements = getBodyElementsCached();
1884
+ const entries = elements.map((element) => ({ element, rect: getRectFromDomCached(element) })).filter(({ element, rect: elementRect }) => {
1885
+ if (overlayNode && overlayNode.contains(element)) return false;
1886
+ if (element === document.body || element === document.documentElement)
1887
+ return false;
1888
+ if (elementRect.width < MIN_MULTI_TARGET_SIZE || elementRect.height < MIN_MULTI_TARGET_SIZE) {
1889
+ return false;
1890
+ }
1891
+ if (elementRect.left > maxRight || elementRect.top > maxBottom)
1892
+ return false;
1893
+ if (elementRect.left + elementRect.width < minLeft) return false;
1894
+ if (elementRect.top + elementRect.height < minTop) return false;
1895
+ return rectsOverlap(rect, elementRect);
1896
+ });
1897
+ cachedSelectionFrame = frame;
1898
+ cachedSelectionKey = key;
1899
+ cachedOverlayNode = overlayNode;
1900
+ cachedSelectionEntries = entries;
1901
+ return entries;
1902
+ };
1903
+ var cachedSelectionFrame = -1;
1904
+ var cachedSelectionKey = "";
1905
+ var cachedSelectionEntries = [];
1906
+ var cachedOverlayNode = null;
1907
+ var getSelectionCacheKey = (rect) => `${Math.round(rect.left)}:${Math.round(rect.top)}:${Math.round(
1908
+ rect.width
1909
+ )}:${Math.round(rect.height)}`;
1910
+ var getSelectionEntriesCached = (rect, overlayNode, cache) => {
1911
+ const frame = getFrameToken();
1912
+ const key = getSelectionCacheKey(rect);
1913
+ if (cache.key === key && cache.overlayNode === overlayNode && cache.frame === frame) {
1914
+ return cache.entries;
1915
+ }
1916
+ const entries = getSelectionEntries(rect, overlayNode);
1917
+ cache.key = key;
1918
+ cache.overlayNode = overlayNode;
1919
+ cache.frame = frame;
1920
+ cache.entries = entries;
1921
+ return entries;
1922
+ };
1923
+ var getElementsInRectCached = (rect, overlayNode, cache) => {
1924
+ const entries = getSelectionEntriesCached(rect, overlayNode, cache);
1925
+ if (entries.length === 0) return [];
1926
+ return pickMultiTargets(rect, entries);
1927
+ };
1928
+
1929
+ // hooks/use-measurer-pointer.ts
1930
+ var useMeasurerPointer = ({
1931
+ toolbarRef,
1932
+ overlayRef,
1933
+ selectionRectRef,
1934
+ createActionCommit,
1935
+ clearGuideDragHold,
1936
+ scheduleGuideDragHold,
1937
+ enabled,
1938
+ toolMode,
1939
+ guidesEnabled,
1940
+ snapEnabled,
1941
+ snapGuidesEnabled,
1942
+ altPressed,
1943
+ guideOrientation,
1944
+ hoverHighlightEnabled,
1945
+ start,
1946
+ end,
1947
+ isDragging,
1948
+ selectedMeasurements,
1949
+ selectedMeasurement,
1950
+ selectedGuideIds,
1951
+ guides,
1952
+ draggingGuideId,
1953
+ optionPairOverlay,
1954
+ setAltPressed,
1955
+ setGuidePreview,
1956
+ setSelectedGuideIds,
1957
+ setGuides,
1958
+ setStart,
1959
+ setEnd,
1960
+ setIsDragging,
1961
+ setHeldDistances,
1962
+ setDraggingGuideId,
1963
+ setActiveMeasurement,
1964
+ setMeasurements,
1965
+ setSelectedMeasurements,
1966
+ setSelectedMeasurement,
1967
+ setSelectionOriginRect,
1968
+ setSelectedElement,
1969
+ setHoverRect,
1970
+ setHoverElement,
1971
+ setHoverPointer,
1972
+ clearSelectionRect
1973
+ }) => {
1974
+ const hoverFrameRef = (0, import_react12.useRef)(null);
1975
+ const hoverPointRef = (0, import_react12.useRef)(null);
1976
+ const selectionCacheRef = (0, import_react12.useRef)({
1977
+ key: "",
1978
+ entries: [],
1979
+ overlayNode: null,
1980
+ frame: -1
1981
+ });
1982
+ const shiftDragRef = (0, import_react12.useRef)(false);
1983
+ const shiftToggleElementRef = (0, import_react12.useRef)(null);
1984
+ const guidesEnabledRef = (0, import_react12.useRef)(guidesEnabled);
1985
+ const guidesRef = (0, import_react12.useRef)(guides);
1986
+ const snapGuidesEnabledRef = (0, import_react12.useRef)(snapGuidesEnabled);
1987
+ const draggingGuideIdRef = (0, import_react12.useRef)(draggingGuideId);
1988
+ const guideOrientationRef = (0, import_react12.useRef)(guideOrientation);
1989
+ (0, import_react12.useEffect)(() => {
1990
+ return () => {
1991
+ if (hoverFrameRef.current) {
1992
+ cancelAnimationFrame(hoverFrameRef.current);
1993
+ }
1994
+ };
1995
+ }, []);
1996
+ (0, import_react12.useEffect)(() => {
1997
+ guidesEnabledRef.current = guidesEnabled;
1998
+ }, [guidesEnabled]);
1999
+ (0, import_react12.useEffect)(() => {
2000
+ guidesRef.current = guides;
2001
+ }, [guides]);
2002
+ (0, import_react12.useEffect)(() => {
2003
+ snapGuidesEnabledRef.current = snapGuidesEnabled;
2004
+ }, [snapGuidesEnabled]);
2005
+ (0, import_react12.useEffect)(() => {
2006
+ guideOrientationRef.current = guideOrientation;
2007
+ }, [guideOrientation]);
2008
+ (0, import_react12.useEffect)(() => {
2009
+ draggingGuideIdRef.current = draggingGuideId;
2010
+ }, [draggingGuideId]);
2011
+ const updateHoverTarget = (0, import_react12.useCallback)(
2012
+ (point) => {
2013
+ const target = getTargetElement(point, overlayRef.current);
2014
+ if (target) {
2015
+ const rect = target.getBoundingClientRect();
2016
+ setHoverRect({
2017
+ left: rect.left,
2018
+ top: rect.top,
2019
+ width: rect.width,
2020
+ height: rect.height
2021
+ });
2022
+ setHoverElement(target);
2023
+ } else {
2024
+ setHoverRect(null);
2025
+ setHoverElement(null);
2026
+ }
2027
+ },
2028
+ [overlayRef, setHoverElement, setHoverRect]
2029
+ );
2030
+ const updateHoverElement = (0, import_react12.useCallback)(
2031
+ (point) => {
2032
+ const target = getTargetElement(point, overlayRef.current);
2033
+ setHoverElement(target);
2034
+ },
2035
+ [overlayRef, setHoverElement]
2036
+ );
2037
+ const handlePointerDown = (0, import_react12.useCallback)(
2038
+ (event) => {
2039
+ const commit = createActionCommit();
2040
+ const toolbarNode = toolbarRef.current;
2041
+ if (toolbarNode && toolbarNode.contains(event.target)) return;
2042
+ if (!enabled || event.button !== 0) return;
2043
+ if (toolMode === "none") return;
2044
+ clearSelectionRect();
2045
+ const point = { x: event.clientX, y: event.clientY };
2046
+ shiftDragRef.current = event.shiftKey;
2047
+ shiftToggleElementRef.current = event.shiftKey ? getSelectedMeasurementHit({
2048
+ point,
2049
+ selectedMeasurements,
2050
+ overlayNode: overlayRef.current
2051
+ })?.elementRef ?? null : null;
2052
+ selectionCacheRef.current.key = "";
2053
+ if (altPressed && optionPairOverlay) {
2054
+ commit();
2055
+ setHeldDistances((prev) => [
2056
+ ...prev,
2057
+ {
2058
+ ...optionPairOverlay,
2059
+ id: createId()
2060
+ }
2061
+ ]);
2062
+ return;
2063
+ }
2064
+ if (guidesEnabled) {
2065
+ commit();
2066
+ const position = getSnapGuidePosition({
2067
+ orientation: guideOrientation,
2068
+ point,
2069
+ snapGuidesEnabled,
2070
+ overlayNode: overlayRef.current,
2071
+ guides,
2072
+ draggingGuideId
2073
+ });
2074
+ const id = createId();
2075
+ setSelectedGuideIds([]);
2076
+ setGuides((prev) => [
2077
+ ...prev,
2078
+ { id, orientation: guideOrientation, position }
2079
+ ]);
2080
+ setSelectedGuideIds([id]);
2081
+ scheduleGuideDragHold(id, setDraggingGuideId);
2082
+ event.currentTarget.setPointerCapture(event.pointerId);
2083
+ return;
2084
+ }
2085
+ if (selectedGuideIds.length > 0) {
2086
+ commit();
2087
+ setSelectedGuideIds([]);
2088
+ }
2089
+ setStart(point);
2090
+ setEnd(point);
2091
+ setIsDragging(false);
2092
+ event.currentTarget.setPointerCapture(event.pointerId);
2093
+ },
2094
+ [
2095
+ altPressed,
2096
+ clearSelectionRect,
2097
+ createActionCommit,
2098
+ draggingGuideId,
2099
+ enabled,
2100
+ guideOrientation,
2101
+ guides,
2102
+ guidesEnabled,
2103
+ optionPairOverlay,
2104
+ overlayRef,
2105
+ scheduleGuideDragHold,
2106
+ selectedGuideIds.length,
2107
+ selectedMeasurements,
2108
+ setDraggingGuideId,
2109
+ setEnd,
2110
+ setGuides,
2111
+ setHeldDistances,
2112
+ setIsDragging,
2113
+ setSelectedGuideIds,
2114
+ setStart,
2115
+ snapGuidesEnabled,
2116
+ toolMode,
2117
+ toolbarRef
2118
+ ]
2119
+ );
2120
+ const handlePointerMove = (0, import_react12.useCallback)(
2121
+ (event) => {
2122
+ const toolbarNode = toolbarRef.current;
2123
+ if (toolbarNode && toolbarNode.contains(event.target)) return;
2124
+ if (!enabled) return;
2125
+ if (toolMode === "none") {
2126
+ if (hoverHighlightEnabled) {
2127
+ setHoverRect(null);
2128
+ setHoverElement(null);
2129
+ }
2130
+ setHoverPointer(null);
2131
+ setGuidePreview(null);
2132
+ return;
2133
+ }
2134
+ const point = { x: event.clientX, y: event.clientY };
2135
+ if (event.altKey !== altPressed) {
2136
+ setAltPressed(event.altKey);
2137
+ }
2138
+ hoverPointRef.current = point;
2139
+ if (!hoverFrameRef.current) {
2140
+ hoverFrameRef.current = requestAnimationFrame(() => {
2141
+ const latest = hoverPointRef.current;
2142
+ if (latest && !draggingGuideIdRef.current) {
2143
+ if (hoverHighlightEnabled) {
2144
+ updateHoverTarget(latest);
2145
+ } else {
2146
+ updateHoverElement(latest);
2147
+ }
2148
+ }
2149
+ if (latest && guidesRef.current.length > 0) {
2150
+ setHoverPointer(latest);
2151
+ } else {
2152
+ setHoverPointer(null);
2153
+ }
2154
+ if (guidesEnabledRef.current && latest && !draggingGuideIdRef.current) {
2155
+ const position = getSnapGuidePosition({
2156
+ orientation: guideOrientationRef.current,
2157
+ point: latest,
2158
+ snapGuidesEnabled: snapGuidesEnabledRef.current,
2159
+ overlayNode: overlayRef.current,
2160
+ guides: guidesRef.current,
2161
+ draggingGuideId: draggingGuideIdRef.current
2162
+ });
2163
+ setGuidePreview({
2164
+ orientation: guideOrientationRef.current,
2165
+ position
2166
+ });
2167
+ } else {
2168
+ setGuidePreview(null);
2169
+ }
2170
+ hoverFrameRef.current = null;
2171
+ });
2172
+ }
2173
+ if (draggingGuideId) {
2174
+ setGuides(
2175
+ (prev) => prev.map(
2176
+ (guide) => guide.id === draggingGuideId ? {
2177
+ ...guide,
2178
+ position: getSnapGuidePosition({
2179
+ orientation: guide.orientation,
2180
+ point,
2181
+ snapGuidesEnabled: snapGuidesEnabledRef.current,
2182
+ overlayNode: overlayRef.current,
2183
+ guides: guidesRef.current,
2184
+ draggingGuideId: draggingGuideIdRef.current
2185
+ })
2186
+ } : guide
2187
+ )
2188
+ );
2189
+ }
2190
+ if (!start) return;
2191
+ setEnd(point);
2192
+ if (!isDragging) {
2193
+ const dx = Math.abs(point.x - start.x);
2194
+ const dy = Math.abs(point.y - start.y);
2195
+ const threshold = shiftDragRef.current ? 12 : 4;
2196
+ if (dx > threshold || dy > threshold) {
2197
+ setIsDragging(true);
2198
+ }
2199
+ }
2200
+ },
2201
+ [
2202
+ altPressed,
2203
+ draggingGuideId,
2204
+ enabled,
2205
+ hoverHighlightEnabled,
2206
+ isDragging,
2207
+ overlayRef,
2208
+ setAltPressed,
2209
+ setEnd,
2210
+ setGuidePreview,
2211
+ setGuides,
2212
+ setHoverElement,
2213
+ setHoverPointer,
2214
+ setHoverRect,
2215
+ setIsDragging,
2216
+ start,
2217
+ toolMode,
2218
+ toolbarRef,
2219
+ updateHoverElement,
2220
+ updateHoverTarget
2221
+ ]
2222
+ );
2223
+ const handlePointerUp = (0, import_react12.useCallback)(
2224
+ (event) => {
2225
+ const commit = createActionCommit();
2226
+ const toolbarNode = toolbarRef.current;
2227
+ if (toolbarNode && toolbarNode.contains(event.target)) return;
2228
+ if (!enabled) return;
2229
+ clearGuideDragHold();
2230
+ if (toolMode === "none") {
2231
+ setStart(null);
2232
+ setEnd(null);
2233
+ setIsDragging(false);
2234
+ return;
2235
+ }
2236
+ const point = { x: event.clientX, y: event.clientY };
2237
+ const resetDragState = () => {
2238
+ setStart(null);
2239
+ setEnd(null);
2240
+ setIsDragging(false);
2241
+ shiftDragRef.current = false;
2242
+ shiftToggleElementRef.current = null;
2243
+ };
2244
+ const clearTransientMeasurements = () => {
2245
+ setActiveMeasurement(null);
2246
+ setMeasurements([]);
2247
+ };
2248
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
2249
+ event.currentTarget.releasePointerCapture(event.pointerId);
2250
+ }
2251
+ if (draggingGuideId) {
2252
+ setDraggingGuideId(null);
2253
+ }
2254
+ if (!start || !end) {
2255
+ resetDragState();
2256
+ return;
2257
+ }
2258
+ const dragDx = Math.abs(point.x - start.x);
2259
+ const dragDy = Math.abs(point.y - start.y);
2260
+ const shiftThreshold = 12;
2261
+ const isShiftClick = event.shiftKey && dragDx <= shiftThreshold && dragDy <= shiftThreshold;
2262
+ if (isDragging && !isShiftClick) {
2263
+ const selectionRect = getRectFromPoints(start, point);
2264
+ selectionRectRef.current = selectionRect;
2265
+ setSelectionOriginRect(selectionRect);
2266
+ const elements = getElementsInRectCached(
2267
+ selectionRect,
2268
+ overlayRef.current,
2269
+ selectionCacheRef.current
2270
+ );
2271
+ const hasSameSelection = elements.length === selectedMeasurements.length && elements.every(
2272
+ (element, index) => selectedMeasurements[index]?.elementRef === element
2273
+ );
2274
+ const lastElement = elements[elements.length - 1] ?? null;
2275
+ const lastChanged = (selectedMeasurement?.elementRef ?? null) !== lastElement;
2276
+ if (elements.length > 0) {
2277
+ if (!hasSameSelection) {
2278
+ commit();
2279
+ const nextMeasurements = elements.map((element) => ({
2280
+ ...getInspectMeasurement(element),
2281
+ originRect: selectionRect
2282
+ }));
2283
+ setSelectedMeasurements(nextMeasurements);
2284
+ setSelectedElement(lastElement);
2285
+ setSelectedMeasurement(
2286
+ nextMeasurements[nextMeasurements.length - 1]
2287
+ );
2288
+ } else if (lastChanged) {
2289
+ commit();
2290
+ setSelectedElement(lastElement);
2291
+ const lastMeasurement = selectedMeasurements.find(
2292
+ (measurement) => measurement.elementRef === lastElement
2293
+ );
2294
+ if (lastMeasurement) {
2295
+ setSelectedMeasurement(lastMeasurement);
2296
+ }
2297
+ }
2298
+ } else if (selectedMeasurements.length > 0 || selectedMeasurement) {
2299
+ commit();
2300
+ setSelectedElement(null);
2301
+ setSelectedMeasurement(null);
2302
+ setSelectedMeasurements([]);
2303
+ clearSelectionRect();
2304
+ }
2305
+ clearTransientMeasurements();
2306
+ resetDragState();
2307
+ return;
2308
+ }
2309
+ const selectedHit = shiftToggleElementRef.current ? selectedMeasurements.find(
2310
+ (measurement) => measurement.elementRef === shiftToggleElementRef.current
2311
+ ) ?? null : getSelectedMeasurementHit({
2312
+ point,
2313
+ selectedMeasurements,
2314
+ overlayNode: overlayRef.current
2315
+ });
2316
+ if (event.shiftKey && selectedHit) {
2317
+ commit();
2318
+ const nextSelected = selectedMeasurements.filter(
2319
+ (measurement) => measurement.elementRef !== selectedHit.elementRef
2320
+ );
2321
+ setSelectedMeasurements(nextSelected);
2322
+ clearSelectionRect();
2323
+ const nextPrimary = nextSelected.length > 0 ? nextSelected[nextSelected.length - 1] : null;
2324
+ setSelectedElement(nextPrimary?.elementRef ?? null);
2325
+ setSelectedMeasurement(nextPrimary);
2326
+ clearTransientMeasurements();
2327
+ resetDragState();
2328
+ return;
2329
+ }
2330
+ if (!hoverHighlightEnabled && !event.shiftKey && selectedHit) {
2331
+ commit();
2332
+ const nextSelected = selectedMeasurements.filter(
2333
+ (measurement) => measurement.elementRef !== selectedHit.elementRef
2334
+ );
2335
+ setSelectedMeasurements(nextSelected);
2336
+ clearSelectionRect();
2337
+ const nextPrimary = nextSelected.length > 0 ? nextSelected[nextSelected.length - 1] : null;
2338
+ setSelectedElement(nextPrimary?.elementRef ?? null);
2339
+ setSelectedMeasurement(nextPrimary);
2340
+ clearTransientMeasurements();
2341
+ resetDragState();
2342
+ return;
2343
+ }
2344
+ const target = event.shiftKey ? getTargetElement(point, overlayRef.current) ?? getSnappedClickTarget(point, overlayRef.current, snapEnabled) : getSnappedClickTarget(point, overlayRef.current, snapEnabled);
2345
+ if (target) {
2346
+ const inspectMeasurement = getInspectMeasurement(target);
2347
+ clearTransientMeasurements();
2348
+ if (event.shiftKey) {
2349
+ const alreadySelected = selectedMeasurements.some(
2350
+ (measurement) => measurement.elementRef === target
2351
+ );
2352
+ if (alreadySelected) {
2353
+ commit();
2354
+ const nextSelected = selectedMeasurements.filter(
2355
+ (measurement) => measurement.elementRef !== target
2356
+ );
2357
+ setSelectedMeasurements(nextSelected);
2358
+ clearSelectionRect();
2359
+ const nextPrimary = nextSelected.length > 0 ? nextSelected[nextSelected.length - 1] : null;
2360
+ setSelectedElement(nextPrimary?.elementRef ?? null);
2361
+ setSelectedMeasurement(nextPrimary);
2362
+ } else {
2363
+ commit();
2364
+ setSelectedMeasurements((prev) => [...prev, inspectMeasurement]);
2365
+ setSelectedElement(target);
2366
+ setSelectedMeasurement(inspectMeasurement);
2367
+ clearSelectionRect();
2368
+ }
2369
+ clearTransientMeasurements();
2370
+ resetDragState();
2371
+ return;
2372
+ }
2373
+ setSelectedElement(target);
2374
+ commit();
2375
+ setSelectedMeasurements([inspectMeasurement]);
2376
+ setSelectedMeasurement(inspectMeasurement);
2377
+ clearSelectionRect();
2378
+ } else {
2379
+ if (event.shiftKey) {
2380
+ clearTransientMeasurements();
2381
+ resetDragState();
2382
+ return;
2383
+ }
2384
+ commit();
2385
+ setSelectedElement(null);
2386
+ setSelectedMeasurement(null);
2387
+ setSelectedMeasurements([]);
2388
+ clearSelectionRect();
2389
+ }
2390
+ resetDragState();
2391
+ },
2392
+ [
2393
+ clearGuideDragHold,
2394
+ clearSelectionRect,
2395
+ createActionCommit,
2396
+ draggingGuideId,
2397
+ enabled,
2398
+ end,
2399
+ hoverHighlightEnabled,
2400
+ isDragging,
2401
+ overlayRef,
2402
+ selectedMeasurement,
2403
+ selectedMeasurements,
2404
+ selectionRectRef,
2405
+ setActiveMeasurement,
2406
+ setDraggingGuideId,
2407
+ setEnd,
2408
+ setIsDragging,
2409
+ setMeasurements,
2410
+ setSelectedElement,
2411
+ setSelectedMeasurement,
2412
+ setSelectedMeasurements,
2413
+ setSelectionOriginRect,
2414
+ setStart,
2415
+ snapEnabled,
2416
+ start,
2417
+ toolMode,
2418
+ toolbarRef
2419
+ ]
2420
+ );
2421
+ const handlePointerLeave = (0, import_react12.useCallback)(() => {
2422
+ clearGuideDragHold();
2423
+ setStart(null);
2424
+ setEnd(null);
2425
+ setIsDragging(false);
2426
+ setDraggingGuideId(null);
2427
+ setGuidePreview(null);
2428
+ }, [
2429
+ clearGuideDragHold,
2430
+ setDraggingGuideId,
2431
+ setEnd,
2432
+ setGuidePreview,
2433
+ setIsDragging,
2434
+ setStart
2435
+ ]);
2436
+ return {
2437
+ handlePointerDown,
2438
+ handlePointerMove,
2439
+ handlePointerUp,
2440
+ handlePointerLeave
2441
+ };
2442
+ };
2443
+
2444
+ // hooks/use-overlay-refs.ts
2445
+ var import_react13 = require("react");
2446
+ var useOverlayRefs = () => {
2447
+ const overlayRef = (0, import_react13.useRef)(null);
2448
+ const selectedElementRef = (0, import_react13.useRef)(null);
2449
+ const hoverElementRef = (0, import_react13.useRef)(null);
2450
+ return { overlayRef, selectedElementRef, hoverElementRef };
2451
+ };
2452
+
2453
+ // hooks/use-resize-sync.ts
2454
+ var import_react14 = require("react");
2455
+ var useResizeSync = (params) => {
2456
+ const resizeFrameRef = (0, import_react14.useRef)(null);
2457
+ const viewportRef = (0, import_react14.useRef)(getViewportSize());
2458
+ (0, import_react14.useEffect)(() => {
2459
+ const handleResize = () => {
2460
+ if (resizeFrameRef.current) {
2461
+ cancelAnimationFrame(resizeFrameRef.current);
2462
+ }
2463
+ resizeFrameRef.current = requestAnimationFrame(() => {
2464
+ const viewport = getViewportSize();
2465
+ const previousViewport = viewportRef.current;
2466
+ params.setMeasurements(
2467
+ (prev) => prev.map(
2468
+ (measurement) => updateMeasurementForResize(measurement, viewport)
2469
+ )
2470
+ );
2471
+ params.setActiveMeasurement(
2472
+ (prev) => prev ? updateMeasurementForResize(prev, viewport) : prev
2473
+ );
2474
+ params.setHeldDistances(
2475
+ (prev) => prev.map((distance) => updateDistanceForResize(distance, viewport))
2476
+ );
2477
+ if (params.selectedElementRef.current) {
2478
+ params.setSelectedMeasurement(
2479
+ getInspectMeasurement(params.selectedElementRef.current)
2480
+ );
2481
+ }
2482
+ if (previousViewport.width > 0 && previousViewport.height > 0) {
2483
+ const scaleX = viewport.width / previousViewport.width;
2484
+ const scaleY = viewport.height / previousViewport.height;
2485
+ params.setGuides(
2486
+ (prev) => prev.map(
2487
+ (guide) => guide.orientation === "vertical" ? { ...guide, position: guide.position * scaleX } : { ...guide, position: guide.position * scaleY }
2488
+ )
2489
+ );
2490
+ }
2491
+ viewportRef.current = viewport;
2492
+ });
2493
+ };
2494
+ window.addEventListener("resize", handleResize);
2495
+ return () => {
2496
+ if (resizeFrameRef.current) {
2497
+ cancelAnimationFrame(resizeFrameRef.current);
2498
+ }
2499
+ window.removeEventListener("resize", handleResize);
2500
+ };
2501
+ }, [params]);
2502
+ };
2503
+
2504
+ // render/measurer-overlay.tsx
2505
+ var import_react18 = require("react");
2506
+
2507
+ // components/measure-tag.tsx
2508
+ var import_jsx_runtime3 = require("react/jsx-runtime");
2509
+ function MeasureTag({
2510
+ children,
2511
+ className = "",
2512
+ style
2513
+ }) {
2514
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2515
+ "div",
2516
+ {
2517
+ className: cn(
2518
+ "pointer-events-none absolute rounded px-1 py-0.5 text-[10px] text-ink-50 tabular-nums select-none",
2519
+ className
2520
+ ),
2521
+ style,
2522
+ children
2523
+ }
2524
+ );
2525
+ }
2526
+
2527
+ // components/measurement-box.tsx
2528
+ var import_react15 = require("react");
2529
+ var import_jsx_runtime4 = require("react/jsx-runtime");
2530
+ var formatValue2 = (value) => Math.round(value);
2531
+ var MeasurementBox = (0, import_react15.memo)(function MeasurementBox2({
2532
+ measurement,
2533
+ transitionMs,
2534
+ labelOffset,
2535
+ edgeVisibility
2536
+ }) {
2537
+ const [rect, setRect] = (0, import_react15.useState)(
2538
+ measurement.originRect ?? measurement.rect
2539
+ );
2540
+ (0, import_react15.useEffect)(() => {
2541
+ if (!measurement.originRect) return;
2542
+ const frame = requestAnimationFrame(() => {
2543
+ setRect(measurement.rect);
2544
+ });
2545
+ return () => cancelAnimationFrame(frame);
2546
+ }, [measurement]);
2547
+ const edges = edgeVisibility ?? { top: true, right: true, bottom: true, left: true };
2548
+ const displayRect = measurement.originRect ? rect : measurement.rect;
2549
+ const outlineColor = "color-mix(in oklch, oklch(0.62 0.18 255) 80%, transparent)";
2550
+ const fillColor = "color-mix(in oklch, oklch(0.62 0.18 255) 8%, transparent)";
2551
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "pointer-events-none", children: [
2552
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2553
+ "div",
2554
+ {
2555
+ className: "absolute",
2556
+ style: {
2557
+ left: displayRect.left,
2558
+ top: displayRect.top,
2559
+ width: displayRect.width,
2560
+ height: displayRect.height,
2561
+ backgroundColor: fillColor,
2562
+ transition: measurement.originRect ? `left ${transitionMs}ms ease, top ${transitionMs}ms ease, width ${transitionMs}ms ease, height ${transitionMs}ms ease` : void 0
2563
+ },
2564
+ children: [
2565
+ edges.top ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2566
+ "div",
2567
+ {
2568
+ className: "absolute left-0 top-0 h-px w-full",
2569
+ style: { backgroundColor: outlineColor }
2570
+ }
2571
+ ) : null,
2572
+ edges.right ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2573
+ "div",
2574
+ {
2575
+ className: "absolute right-0 top-0 h-full w-px",
2576
+ style: { backgroundColor: outlineColor }
2577
+ }
2578
+ ) : null,
2579
+ edges.bottom ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2580
+ "div",
2581
+ {
2582
+ className: "absolute bottom-0 left-0 h-px w-full",
2583
+ style: { backgroundColor: outlineColor }
2584
+ }
2585
+ ) : null,
2586
+ edges.left ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2587
+ "div",
2588
+ {
2589
+ className: "absolute left-0 top-0 h-full w-px",
2590
+ style: { backgroundColor: outlineColor }
2591
+ }
2592
+ ) : null
2593
+ ]
2594
+ }
2595
+ ),
2596
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2597
+ MeasureTag,
2598
+ {
2599
+ className: "-translate-x-1/2 bg-ink-900/90",
2600
+ style: {
2601
+ left: displayRect.left + displayRect.width / 2,
2602
+ top: displayRect.top + displayRect.height + labelOffset,
2603
+ transition: measurement.originRect ? `left ${transitionMs}ms ease, top ${transitionMs}ms ease` : void 0
2604
+ },
2605
+ children: [
2606
+ formatValue2(displayRect.width),
2607
+ " x ",
2608
+ formatValue2(displayRect.height)
2609
+ ]
2610
+ }
2611
+ )
2612
+ ] });
2613
+ });
2614
+
2615
+ // components/selected-measurement-box.tsx
2616
+ var import_react16 = require("react");
2617
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2618
+ var formatValue3 = (value) => Math.round(value);
2619
+ var SelectedMeasurementBox = (0, import_react16.memo)(function SelectedMeasurementBox2({
2620
+ measurement,
2621
+ transitionMs,
2622
+ labelOffset,
2623
+ edgeVisibility
2624
+ }) {
2625
+ const [rect, setRect] = (0, import_react16.useState)(
2626
+ measurement.originRect ?? measurement.rect
2627
+ );
2628
+ (0, import_react16.useEffect)(() => {
2629
+ if (!measurement.originRect) return;
2630
+ const frame = requestAnimationFrame(() => {
2631
+ setRect(measurement.rect);
2632
+ });
2633
+ return () => cancelAnimationFrame(frame);
2634
+ }, [measurement]);
2635
+ const edges = edgeVisibility ?? { top: true, right: true, bottom: true, left: true };
2636
+ const displayRect = measurement.originRect ? rect : measurement.rect;
2637
+ const outlineColor = "color-mix(in oklch, oklch(0.62 0.18 255) 80%, transparent)";
2638
+ const fillColor = "color-mix(in oklch, oklch(0.62 0.18 255) 8%, transparent)";
2639
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "pointer-events-none", children: [
2640
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2641
+ "div",
2642
+ {
2643
+ className: "absolute",
2644
+ style: {
2645
+ left: displayRect.left,
2646
+ top: displayRect.top,
2647
+ width: displayRect.width,
2648
+ height: displayRect.height,
2649
+ backgroundColor: fillColor,
2650
+ transition: measurement.originRect ? `left ${transitionMs}ms ease, top ${transitionMs}ms ease, width ${transitionMs}ms ease, height ${transitionMs}ms ease` : void 0
2651
+ },
2652
+ children: [
2653
+ edges.top ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2654
+ "div",
2655
+ {
2656
+ className: "absolute left-0 top-0 h-px w-full",
2657
+ style: { backgroundColor: outlineColor }
2658
+ }
2659
+ ) : null,
2660
+ edges.right ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2661
+ "div",
2662
+ {
2663
+ className: "absolute right-0 top-0 h-full w-px",
2664
+ style: { backgroundColor: outlineColor }
2665
+ }
2666
+ ) : null,
2667
+ edges.bottom ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2668
+ "div",
2669
+ {
2670
+ className: "absolute bottom-0 left-0 h-px w-full",
2671
+ style: { backgroundColor: outlineColor }
2672
+ }
2673
+ ) : null,
2674
+ edges.left ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2675
+ "div",
2676
+ {
2677
+ className: "absolute left-0 top-0 h-full w-px",
2678
+ style: { backgroundColor: outlineColor }
2679
+ }
2680
+ ) : null
2681
+ ]
2682
+ }
2683
+ ),
2684
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2685
+ MeasureTag,
2686
+ {
2687
+ className: "-translate-x-1/2 bg-ink-900/90",
2688
+ style: {
2689
+ left: displayRect.left + displayRect.width / 2,
2690
+ top: displayRect.top + displayRect.height + labelOffset,
2691
+ transition: measurement.originRect ? `left ${transitionMs}ms ease, top ${transitionMs}ms ease` : void 0
2692
+ },
2693
+ children: [
2694
+ formatValue3(displayRect.width),
2695
+ " x ",
2696
+ formatValue3(displayRect.height)
2697
+ ]
2698
+ }
2699
+ )
2700
+ ] });
2701
+ });
2702
+
2703
+ // render/distance-overlay-item.tsx
2704
+ var import_react17 = require("react");
2705
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2706
+ var DistanceOverlayItem = (0, import_react17.memo)(function DistanceOverlayItem2({
2707
+ distance,
2708
+ labelOffset,
2709
+ onRemove
2710
+ }) {
2711
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2712
+ "div",
2713
+ {
2714
+ className: onRemove ? "pointer-events-auto" : "pointer-events-none",
2715
+ onClick: onRemove ? (event) => {
2716
+ event.stopPropagation();
2717
+ onRemove(distance.id);
2718
+ } : void 0,
2719
+ children: [
2720
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2721
+ "div",
2722
+ {
2723
+ className: "absolute rounded border border-[#2563eb]/70",
2724
+ style: {
2725
+ left: distance.rectA.left,
2726
+ top: distance.rectA.top,
2727
+ width: distance.rectA.width,
2728
+ height: distance.rectA.height
2729
+ }
2730
+ }
2731
+ ),
2732
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2733
+ "div",
2734
+ {
2735
+ className: "absolute rounded border border-[#2563eb]/70",
2736
+ style: {
2737
+ left: distance.rectB.left,
2738
+ top: distance.rectB.top,
2739
+ width: distance.rectB.width,
2740
+ height: distance.rectB.height
2741
+ }
2742
+ }
2743
+ ),
2744
+ distance.connectors.map(
2745
+ (connector, index) => Math.abs(connector.x1 - connector.x2) < 1 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2746
+ "div",
2747
+ {
2748
+ className: "absolute border-l border-dashed border-[#2563eb]/70",
2749
+ style: {
2750
+ left: connector.x1,
2751
+ top: Math.min(connector.y1, connector.y2),
2752
+ height: Math.abs(connector.y2 - connector.y1)
2753
+ }
2754
+ },
2755
+ `${distance.id}-connector-${index}`
2756
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2757
+ "div",
2758
+ {
2759
+ className: "absolute border-t border-dashed border-[#2563eb]/70",
2760
+ style: {
2761
+ left: Math.min(connector.x1, connector.x2),
2762
+ top: connector.y1,
2763
+ width: Math.abs(connector.x2 - connector.x1)
2764
+ }
2765
+ },
2766
+ `${distance.id}-connector-${index}`
2767
+ )
2768
+ ),
2769
+ distance.horizontal && distance.horizontal.value > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
2770
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2771
+ "div",
2772
+ {
2773
+ className: "absolute h-px bg-[#2563eb]",
2774
+ style: {
2775
+ left: Math.min(distance.horizontal.x1, distance.horizontal.x2),
2776
+ width: Math.abs(distance.horizontal.x2 - distance.horizontal.x1),
2777
+ top: distance.horizontal.y
2778
+ }
2779
+ }
2780
+ ),
2781
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2782
+ MeasureTag,
2783
+ {
2784
+ className: "-translate-x-1/2 bg-ink-900/90",
2785
+ style: {
2786
+ left: (distance.horizontal.x1 + distance.horizontal.x2) / 2,
2787
+ top: distance.horizontal.y + labelOffset
2788
+ },
2789
+ children: formatValue(distance.horizontal.value)
2790
+ }
2791
+ )
2792
+ ] }) : null,
2793
+ distance.vertical && distance.vertical.value > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
2794
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2795
+ "div",
2796
+ {
2797
+ className: "absolute w-px bg-[#2563eb]",
2798
+ style: {
2799
+ top: Math.min(distance.vertical.y1, distance.vertical.y2),
2800
+ height: Math.abs(distance.vertical.y2 - distance.vertical.y1),
2801
+ left: distance.vertical.x
2802
+ }
2803
+ }
2804
+ ),
2805
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2806
+ MeasureTag,
2807
+ {
2808
+ className: "-translate-y-1/2 bg-ink-900/90",
2809
+ style: {
2810
+ left: distance.vertical.x + labelOffset,
2811
+ top: (distance.vertical.y1 + distance.vertical.y2) / 2
2812
+ },
2813
+ children: formatValue(distance.vertical.value)
2814
+ }
2815
+ )
2816
+ ] }) : null
2817
+ ]
2818
+ }
2819
+ );
2820
+ });
2821
+
2822
+ // render/measurer-overlay.tsx
2823
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2824
+ var MeasurerOverlay = (0, import_react18.memo)(function MeasurerOverlay2({
2825
+ enabled,
2826
+ toolMode,
2827
+ guidesEnabled,
2828
+ altPressed,
2829
+ isDragging,
2830
+ displayedMeasurements,
2831
+ measurementEdgeVisibility,
2832
+ activeRect,
2833
+ activeWidth,
2834
+ activeHeight,
2835
+ fillColor,
2836
+ outlineColor,
2837
+ hoverRectToShow,
2838
+ hoverEdgeVisibility,
2839
+ guidePreview,
2840
+ guideColorPreview,
2841
+ displayedSelectedMeasurements,
2842
+ selectedEdgeVisibility,
2843
+ heldDistances,
2844
+ optionPairOverlay,
2845
+ guideDistanceOverlay,
2846
+ optionContainerLines,
2847
+ guides,
2848
+ hoverGuide,
2849
+ selectedGuideIds,
2850
+ guideColorActive,
2851
+ guideColorHover,
2852
+ guideColorDefault,
2853
+ onPointerDown,
2854
+ onPointerMove,
2855
+ onPointerUp,
2856
+ onPointerLeave,
2857
+ onRemoveHeldDistance,
2858
+ onGuidePointerDown,
2859
+ onGuidePointerUp,
2860
+ onGuidePointerCancel
2861
+ }) {
2862
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2863
+ "div",
2864
+ {
2865
+ className: `absolute inset-0 ${enabled && toolMode !== "none" ? `pointer-events-auto ${guidesEnabled ? "cursor-crosshair" : "cursor-default"} opacity-100` : "pointer-events-none opacity-0"}`,
2866
+ onPointerDown,
2867
+ onPointerMove,
2868
+ onPointerUp,
2869
+ onPointerLeave,
2870
+ children: [
2871
+ !guidesEnabled ? displayedMeasurements.map((measurement, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2872
+ MeasurementBox,
2873
+ {
2874
+ measurement,
2875
+ transitionMs: MEASURE_TRANSITION_MS,
2876
+ labelOffset: MEASURE_LABEL_OFFSET,
2877
+ edgeVisibility: measurementEdgeVisibility[index]
2878
+ },
2879
+ measurement.id
2880
+ )) : null,
2881
+ !guidesEnabled && activeRect && isDragging ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2882
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2883
+ "div",
2884
+ {
2885
+ className: "pointer-events-none absolute",
2886
+ style: {
2887
+ left: activeRect.left,
2888
+ top: activeRect.top,
2889
+ width: activeRect.width,
2890
+ height: activeRect.height,
2891
+ backgroundColor: fillColor
2892
+ },
2893
+ children: [
2894
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2895
+ "div",
2896
+ {
2897
+ className: "absolute left-0 top-0 h-px w-full",
2898
+ style: { backgroundColor: outlineColor }
2899
+ }
2900
+ ),
2901
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2902
+ "div",
2903
+ {
2904
+ className: "absolute right-0 top-0 h-full w-px",
2905
+ style: { backgroundColor: outlineColor }
2906
+ }
2907
+ ),
2908
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2909
+ "div",
2910
+ {
2911
+ className: "absolute bottom-0 left-0 h-px w-full",
2912
+ style: { backgroundColor: outlineColor }
2913
+ }
2914
+ ),
2915
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2916
+ "div",
2917
+ {
2918
+ className: "absolute left-0 top-0 h-full w-px",
2919
+ style: { backgroundColor: outlineColor }
2920
+ }
2921
+ )
2922
+ ]
2923
+ }
2924
+ ),
2925
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2926
+ MeasureTag,
2927
+ {
2928
+ className: "-translate-x-1/2 bg-ink-900/90",
2929
+ style: {
2930
+ left: activeRect.left + activeRect.width / 2,
2931
+ top: activeRect.top + activeRect.height + MEASURE_LABEL_OFFSET
2932
+ },
2933
+ children: [
2934
+ activeWidth,
2935
+ " x ",
2936
+ activeHeight
2937
+ ]
2938
+ }
2939
+ )
2940
+ ] }) : null,
2941
+ !guidesEnabled && hoverRectToShow ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2942
+ "div",
2943
+ {
2944
+ className: "pointer-events-none absolute",
2945
+ style: {
2946
+ left: hoverRectToShow.left,
2947
+ top: hoverRectToShow.top,
2948
+ width: hoverRectToShow.width,
2949
+ height: hoverRectToShow.height,
2950
+ backgroundColor: fillColor
2951
+ },
2952
+ children: [
2953
+ hoverEdgeVisibility?.top ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2954
+ "div",
2955
+ {
2956
+ className: "absolute left-0 top-0 h-px w-full",
2957
+ style: { backgroundColor: outlineColor }
2958
+ }
2959
+ ) : null,
2960
+ hoverEdgeVisibility?.right ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2961
+ "div",
2962
+ {
2963
+ className: "absolute right-0 top-0 h-full w-px",
2964
+ style: { backgroundColor: outlineColor }
2965
+ }
2966
+ ) : null,
2967
+ hoverEdgeVisibility?.bottom ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2968
+ "div",
2969
+ {
2970
+ className: "absolute bottom-0 left-0 h-px w-full",
2971
+ style: { backgroundColor: outlineColor }
2972
+ }
2973
+ ) : null,
2974
+ hoverEdgeVisibility?.left ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2975
+ "div",
2976
+ {
2977
+ className: "absolute left-0 top-0 h-full w-px",
2978
+ style: { backgroundColor: outlineColor }
2979
+ }
2980
+ ) : null
2981
+ ]
2982
+ }
2983
+ ) : null,
2984
+ guidesEnabled && guidePreview ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2985
+ "div",
2986
+ {
2987
+ className: "pointer-events-none absolute",
2988
+ style: guidePreview.orientation === "vertical" ? {
2989
+ left: guidePreview.position - GUIDE_HITBOX_SIZE / 2,
2990
+ top: 0,
2991
+ width: GUIDE_HITBOX_SIZE,
2992
+ height: "100%"
2993
+ } : {
2994
+ top: guidePreview.position - GUIDE_HITBOX_SIZE / 2,
2995
+ left: 0,
2996
+ height: GUIDE_HITBOX_SIZE,
2997
+ width: "100%"
2998
+ },
2999
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3000
+ "div",
3001
+ {
3002
+ className: "absolute",
3003
+ style: guidePreview.orientation === "vertical" ? {
3004
+ left: GUIDE_HITBOX_SIZE / 2 - 1,
3005
+ top: 0,
3006
+ width: 2,
3007
+ height: "100%",
3008
+ backgroundColor: guideColorPreview
3009
+ } : {
3010
+ top: GUIDE_HITBOX_SIZE / 2 - 1,
3011
+ left: 0,
3012
+ height: 2,
3013
+ width: "100%",
3014
+ backgroundColor: guideColorPreview
3015
+ }
3016
+ }
3017
+ )
3018
+ }
3019
+ ) : null,
3020
+ !guidesEnabled ? displayedSelectedMeasurements.map((measurement, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3021
+ SelectedMeasurementBox,
3022
+ {
3023
+ measurement,
3024
+ transitionMs: MEASURE_TRANSITION_MS,
3025
+ labelOffset: MEASURE_LABEL_OFFSET,
3026
+ edgeVisibility: selectedEdgeVisibility[index]
3027
+ },
3028
+ measurement.id
3029
+ )) : null,
3030
+ heldDistances.map((distance) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3031
+ DistanceOverlayItem,
3032
+ {
3033
+ distance,
3034
+ labelOffset: MEASURE_LABEL_OFFSET,
3035
+ onRemove: onRemoveHeldDistance
3036
+ },
3037
+ `held-${distance.id}`
3038
+ )),
3039
+ !guidesEnabled && altPressed && optionPairOverlay ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3040
+ DistanceOverlayItem,
3041
+ {
3042
+ distance: optionPairOverlay,
3043
+ labelOffset: MEASURE_LABEL_OFFSET
3044
+ },
3045
+ `option-${optionPairOverlay.id}`
3046
+ ) : null,
3047
+ guidesEnabled && altPressed && guideDistanceOverlay ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3048
+ DistanceOverlayItem,
3049
+ {
3050
+ distance: guideDistanceOverlay,
3051
+ labelOffset: MEASURE_LABEL_OFFSET
3052
+ },
3053
+ `guide-preview-${guideDistanceOverlay.id}`
3054
+ ) : null,
3055
+ !guidesEnabled && altPressed && optionContainerLines ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3056
+ optionContainerLines.top.value > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3057
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3058
+ "div",
3059
+ {
3060
+ className: "absolute w-px bg-[#2563eb]",
3061
+ style: {
3062
+ top: optionContainerLines.top.y1,
3063
+ height: optionContainerLines.top.y2 - optionContainerLines.top.y1,
3064
+ left: optionContainerLines.top.x
3065
+ }
3066
+ }
3067
+ ),
3068
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3069
+ MeasureTag,
3070
+ {
3071
+ className: "-translate-y-1/2 bg-ink-900/90",
3072
+ style: {
3073
+ left: optionContainerLines.top.x + MEASURE_LABEL_OFFSET,
3074
+ top: (optionContainerLines.top.y1 + optionContainerLines.top.y2) / 2
3075
+ },
3076
+ children: formatValue(optionContainerLines.top.value)
3077
+ }
3078
+ )
3079
+ ] }) : null,
3080
+ optionContainerLines.bottom.value > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3081
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3082
+ "div",
3083
+ {
3084
+ className: "absolute w-px bg-[#2563eb]",
3085
+ style: {
3086
+ top: optionContainerLines.bottom.y1,
3087
+ height: optionContainerLines.bottom.y2 - optionContainerLines.bottom.y1,
3088
+ left: optionContainerLines.bottom.x
3089
+ }
3090
+ }
3091
+ ),
3092
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3093
+ MeasureTag,
3094
+ {
3095
+ className: "-translate-y-1/2 bg-ink-900/90",
3096
+ style: {
3097
+ left: optionContainerLines.bottom.x + MEASURE_LABEL_OFFSET,
3098
+ top: (optionContainerLines.bottom.y1 + optionContainerLines.bottom.y2) / 2
3099
+ },
3100
+ children: formatValue(optionContainerLines.bottom.value)
3101
+ }
3102
+ )
3103
+ ] }) : null,
3104
+ optionContainerLines.left.value > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3105
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3106
+ "div",
3107
+ {
3108
+ className: "absolute h-px bg-[#2563eb]",
3109
+ style: {
3110
+ left: optionContainerLines.left.x1,
3111
+ width: optionContainerLines.left.x2 - optionContainerLines.left.x1,
3112
+ top: optionContainerLines.left.y
3113
+ }
3114
+ }
3115
+ ),
3116
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3117
+ MeasureTag,
3118
+ {
3119
+ className: "-translate-x-1/2 bg-ink-900/90",
3120
+ style: {
3121
+ left: (optionContainerLines.left.x1 + optionContainerLines.left.x2) / 2,
3122
+ top: optionContainerLines.left.y + MEASURE_LABEL_OFFSET
3123
+ },
3124
+ children: formatValue(optionContainerLines.left.value)
3125
+ }
3126
+ )
3127
+ ] }) : null,
3128
+ optionContainerLines.right.value > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3129
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3130
+ "div",
3131
+ {
3132
+ className: "absolute h-px bg-[#2563eb]",
3133
+ style: {
3134
+ left: optionContainerLines.right.x1,
3135
+ width: optionContainerLines.right.x2 - optionContainerLines.right.x1,
3136
+ top: optionContainerLines.right.y
3137
+ }
3138
+ }
3139
+ ),
3140
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3141
+ MeasureTag,
3142
+ {
3143
+ className: "-translate-x-1/2 bg-ink-900/90",
3144
+ style: {
3145
+ left: (optionContainerLines.right.x1 + optionContainerLines.right.x2) / 2,
3146
+ top: optionContainerLines.right.y + MEASURE_LABEL_OFFSET
3147
+ },
3148
+ children: formatValue(optionContainerLines.right.value)
3149
+ }
3150
+ )
3151
+ ] }) : null
3152
+ ] }) : null,
3153
+ guides.map((guide) => {
3154
+ const isSelected = selectedGuideIds.includes(guide.id);
3155
+ const isHovered = hoverGuide?.id === guide.id;
3156
+ const strokeWidth = isSelected || isHovered ? 2 : 1;
3157
+ const strokeColor = isSelected ? guideColorActive : isHovered ? guideColorHover : guideColorDefault;
3158
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3159
+ "div",
3160
+ {
3161
+ className: "absolute",
3162
+ style: guide.orientation === "vertical" ? {
3163
+ left: guide.position - GUIDE_HITBOX_SIZE / 2,
3164
+ top: 0,
3165
+ width: GUIDE_HITBOX_SIZE,
3166
+ height: "100%"
3167
+ } : {
3168
+ top: guide.position - GUIDE_HITBOX_SIZE / 2,
3169
+ left: 0,
3170
+ height: GUIDE_HITBOX_SIZE,
3171
+ width: "100%"
3172
+ },
3173
+ onPointerDown: (event) => onGuidePointerDown(guide, event),
3174
+ onPointerUp: (event) => onGuidePointerUp(guide, event),
3175
+ onPointerCancel: (event) => onGuidePointerCancel(guide, event),
3176
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3177
+ "div",
3178
+ {
3179
+ className: "absolute",
3180
+ style: guide.orientation === "vertical" ? {
3181
+ left: GUIDE_HITBOX_SIZE / 2 - 1,
3182
+ top: 0,
3183
+ width: strokeWidth,
3184
+ height: "100%",
3185
+ backgroundColor: strokeColor
3186
+ } : {
3187
+ top: GUIDE_HITBOX_SIZE / 2 - 1,
3188
+ left: 0,
3189
+ height: strokeWidth,
3190
+ width: "100%",
3191
+ backgroundColor: strokeColor
3192
+ }
3193
+ }
3194
+ )
3195
+ },
3196
+ guide.id
3197
+ );
3198
+ })
3199
+ ]
3200
+ }
3201
+ );
3202
+ });
3203
+
3204
+ // measurer.tsx
3205
+ var import_jsx_runtime8 = require("react/jsx-runtime");
3206
+ var subscribeHydration = () => () => {
3207
+ };
3208
+ var useHydrated = () => (0, import_react19.useSyncExternalStore)(
3209
+ subscribeHydration,
3210
+ () => true,
3211
+ () => false
3212
+ );
3213
+ var stripMeasurement = (measurement) => ({
3214
+ ...measurement,
3215
+ elementRef: void 0
3216
+ });
3217
+ var stripDistance = (distance) => ({
3218
+ ...distance,
3219
+ elementRefA: void 0,
3220
+ elementRefB: void 0
3221
+ });
3222
+ function MeasurerClient({
3223
+ highlightColor,
3224
+ guideColor,
3225
+ hoverHighlightEnabled,
3226
+ persistOnReload
3227
+ }) {
3228
+ const selectionRectRef = (0, import_react19.useRef)(null);
3229
+ const toolbarRef = (0, import_react19.useRef)(null);
3230
+ const persistedState = (0, import_react19.useMemo)(() => {
3231
+ if (!persistOnReload) return null;
3232
+ const stored = window.localStorage.getItem("mesurer-state");
3233
+ if (!stored) return null;
3234
+ try {
3235
+ const parsed = JSON.parse(stored);
3236
+ if (!parsed || parsed.version !== 1) return null;
3237
+ return parsed;
3238
+ } catch {
3239
+ return null;
3240
+ }
3241
+ }, [persistOnReload]);
3242
+ const { overlayRef, selectedElementRef, hoverElementRef } = useOverlayRefs();
3243
+ const {
3244
+ selectionOriginRect,
3245
+ setSelectionOriginRect,
3246
+ hoverPointer,
3247
+ setHoverPointer,
3248
+ hoverElement,
3249
+ setHoverElement,
3250
+ selectedElement,
3251
+ setSelectedElement,
3252
+ clearSelectionRect
3253
+ } = useMeasurerLocalState({
3254
+ selectedElementRef,
3255
+ hoverElementRef,
3256
+ selectionRectRef
3257
+ });
3258
+ const {
3259
+ enabled,
3260
+ setEnabled,
3261
+ holdEnabled,
3262
+ snapEnabled,
3263
+ altPressed,
3264
+ setAltPressed,
3265
+ toolMode,
3266
+ setToolMode,
3267
+ guidesEnabled,
3268
+ multiMeasureEnabled,
3269
+ snapGuidesEnabled
3270
+ } = useMeasureToggles({
3271
+ initialEnabled: persistedState?.enabled,
3272
+ initialToolMode: persistedState?.toolMode
3273
+ });
3274
+ const { start, setStart, end, setEnd, isDragging, setIsDragging } = useDragState();
3275
+ const {
3276
+ activeMeasurement,
3277
+ setActiveMeasurement,
3278
+ measurements,
3279
+ setMeasurements,
3280
+ selectedMeasurement,
3281
+ setSelectedMeasurement,
3282
+ selectedMeasurements,
3283
+ setSelectedMeasurements,
3284
+ hoverRect,
3285
+ setHoverRect,
3286
+ heldDistances,
3287
+ setHeldDistances
3288
+ } = useMeasurementState({
3289
+ initialActiveMeasurement: persistedState?.activeMeasurement ?? null,
3290
+ initialMeasurements: persistedState?.measurements ?? [],
3291
+ initialHeldDistances: persistedState?.heldDistances ?? []
3292
+ });
3293
+ const {
3294
+ guides,
3295
+ setGuides,
3296
+ draggingGuideId,
3297
+ setDraggingGuideId,
3298
+ selectedGuideIds,
3299
+ setSelectedGuideIds
3300
+ } = useGuideState({
3301
+ initialGuides: persistedState?.guides ?? [],
3302
+ initialSelectedGuideIds: persistedState?.selectedGuideIds ?? []
3303
+ });
3304
+ const [toolbarActive, setToolbarActive] = (0, import_react19.useState)(true);
3305
+ const { clearGuideDragHold, scheduleGuideDragHold } = useGuideDragHold();
3306
+ const [guidePreview, setGuidePreview] = (0, import_react19.useState)(null);
3307
+ const [guideOrientation, setGuideOrientation] = (0, import_react19.useState)(persistedState?.guideOrientation ?? "vertical");
3308
+ const persistPayload = (0, import_react19.useMemo)(() => {
3309
+ if (!persistOnReload) return null;
3310
+ return JSON.stringify({
3311
+ version: 1,
3312
+ enabled,
3313
+ toolMode,
3314
+ guideOrientation,
3315
+ guides,
3316
+ selectedGuideIds,
3317
+ measurements: measurements.map(stripMeasurement),
3318
+ activeMeasurement: activeMeasurement ? stripMeasurement(activeMeasurement) : null,
3319
+ heldDistances: heldDistances.map(stripDistance)
3320
+ });
3321
+ }, [
3322
+ activeMeasurement,
3323
+ enabled,
3324
+ guideOrientation,
3325
+ guides,
3326
+ heldDistances,
3327
+ measurements,
3328
+ persistOnReload,
3329
+ selectedGuideIds,
3330
+ toolMode
3331
+ ]);
3332
+ (0, import_react19.useEffect)(() => {
3333
+ if (!persistOnReload) return;
3334
+ if (typeof window === "undefined") return;
3335
+ if (!persistPayload) return;
3336
+ try {
3337
+ window.localStorage.setItem("mesurer-state", persistPayload);
3338
+ } catch {
3339
+ }
3340
+ const handlePageHide = () => {
3341
+ try {
3342
+ window.localStorage.setItem("mesurer-state", persistPayload);
3343
+ } catch {
3344
+ }
3345
+ };
3346
+ window.addEventListener("pagehide", handlePageHide);
3347
+ return () => {
3348
+ window.removeEventListener("pagehide", handlePageHide);
3349
+ };
3350
+ }, [persistOnReload, persistPayload]);
3351
+ const {
3352
+ recordSnapshot,
3353
+ createActionCommit,
3354
+ setToolModeWithHistory,
3355
+ setGuideOrientationWithHistory,
3356
+ setEnabledWithHistory
3357
+ } = useMeasurerHistory({
3358
+ toggles: {
3359
+ enabled,
3360
+ setEnabled,
3361
+ toolMode,
3362
+ setToolMode,
3363
+ guideOrientation,
3364
+ setGuideOrientation
3365
+ },
3366
+ measurements: {
3367
+ measurements,
3368
+ setMeasurements,
3369
+ activeMeasurement,
3370
+ setActiveMeasurement,
3371
+ selectedMeasurements,
3372
+ setSelectedMeasurements,
3373
+ selectedMeasurement,
3374
+ setSelectedMeasurement,
3375
+ heldDistances,
3376
+ setHeldDistances
3377
+ },
3378
+ guides: {
3379
+ guides,
3380
+ setGuides,
3381
+ selectedGuideIds,
3382
+ setSelectedGuideIds,
3383
+ draggingGuideId,
3384
+ setDraggingGuideId
3385
+ },
3386
+ transient: {
3387
+ setStart,
3388
+ setEnd,
3389
+ setIsDragging,
3390
+ setGuidePreview,
3391
+ setHoverRect,
3392
+ setHoverElement,
3393
+ setSelectedElement,
3394
+ clearSelectionRect
3395
+ }
3396
+ });
3397
+ const clearAll = (0, import_react19.useCallback)(() => {
3398
+ recordSnapshot();
3399
+ clearGuideDragHold();
3400
+ setStart(null);
3401
+ setEnd(null);
3402
+ setIsDragging(false);
3403
+ setActiveMeasurement(null);
3404
+ setMeasurements([]);
3405
+ setSelectedMeasurement(null);
3406
+ setSelectedMeasurements([]);
3407
+ clearSelectionRect();
3408
+ setSelectedElement(null);
3409
+ setHoverRect(null);
3410
+ setHoverElement(null);
3411
+ setGuides([]);
3412
+ setSelectedGuideIds([]);
3413
+ setHeldDistances([]);
3414
+ }, [
3415
+ clearGuideDragHold,
3416
+ clearSelectionRect,
3417
+ recordSnapshot,
3418
+ setActiveMeasurement,
3419
+ setEnd,
3420
+ setGuides,
3421
+ setHeldDistances,
3422
+ setHoverElement,
3423
+ setHoverRect,
3424
+ setIsDragging,
3425
+ setMeasurements,
3426
+ setSelectedElement,
3427
+ setSelectedGuideIds,
3428
+ setSelectedMeasurement,
3429
+ setSelectedMeasurements,
3430
+ setStart
3431
+ ]);
3432
+ const removeSelectedGuides = (0, import_react19.useCallback)(() => {
3433
+ if (selectedGuideIds.length === 0) return false;
3434
+ recordSnapshot();
3435
+ setGuides(
3436
+ (prev) => prev.filter((guide) => !selectedGuideIds.includes(guide.id))
3437
+ );
3438
+ setSelectedGuideIds([]);
3439
+ return true;
3440
+ }, [recordSnapshot, selectedGuideIds, setGuides, setSelectedGuideIds]);
3441
+ useHotkeys({
3442
+ clearAll,
3443
+ removeSelectedGuides,
3444
+ setEnabled: setEnabledWithHistory,
3445
+ setToolMode: setToolModeWithHistory,
3446
+ setAltPressed,
3447
+ isOverlayActive: () => enabled && (toolMode !== "none" || toolbarActive),
3448
+ setGuideOrientation: setGuideOrientationWithHistory,
3449
+ onInteract: () => setToolbarActive(true)
3450
+ });
3451
+ useResizeSync({
3452
+ setMeasurements,
3453
+ setActiveMeasurement,
3454
+ setHeldDistances,
3455
+ setSelectedMeasurement,
3456
+ setGuides,
3457
+ selectedElementRef
3458
+ });
3459
+ useLiveElementTracking({
3460
+ enabled,
3461
+ selectedElementRef,
3462
+ hoverElementRef,
3463
+ setSelectedMeasurement,
3464
+ setSelectedMeasurements,
3465
+ setHoverRect,
3466
+ setMeasurements,
3467
+ setActiveMeasurement,
3468
+ setHeldDistances
3469
+ });
3470
+ (0, import_react19.useEffect)(() => {
3471
+ if (!toolbarActive || toolMode !== "none") return;
3472
+ const handlePointerDown2 = (event) => {
3473
+ const toolbarNode = toolbarRef.current;
3474
+ if (toolbarNode && toolbarNode.contains(event.target)) return;
3475
+ setToolbarActive(false);
3476
+ };
3477
+ window.addEventListener("pointerdown", handlePointerDown2);
3478
+ return () => {
3479
+ window.removeEventListener("pointerdown", handlePointerDown2);
3480
+ };
3481
+ }, [toolbarActive, toolMode]);
3482
+ (0, import_react19.useEffect)(() => {
3483
+ if (hoverHighlightEnabled) return;
3484
+ setHoverRect(null);
3485
+ }, [hoverHighlightEnabled, setHoverRect]);
3486
+ const displayedMeasurements = holdEnabled ? measurements : multiMeasureEnabled && measurements.length > 0 ? measurements : activeMeasurement ? [activeMeasurement] : [];
3487
+ const {
3488
+ activeRect,
3489
+ activeWidth,
3490
+ activeHeight,
3491
+ displayedSelectedMeasurements,
3492
+ hoverGuide,
3493
+ optionPairOverlay,
3494
+ optionContainerLines,
3495
+ guideDistanceOverlay,
3496
+ outlineColor,
3497
+ fillColor,
3498
+ guideColorActive,
3499
+ guideColorHover,
3500
+ guideColorDefault,
3501
+ guideColorPreview,
3502
+ hoverRectToShow,
3503
+ selectedEdgeVisibility,
3504
+ hoverEdgeVisibility,
3505
+ measurementEdgeVisibility
3506
+ } = useMeasurerDerived({
3507
+ start,
3508
+ end,
3509
+ selectedMeasurements,
3510
+ selectedMeasurement,
3511
+ selectionOriginRect,
3512
+ guides,
3513
+ selectedGuideIds,
3514
+ hoverPointer,
3515
+ hoverRect,
3516
+ hoverElement,
3517
+ selectedElement,
3518
+ altPressed,
3519
+ guidesEnabled,
3520
+ guidePreview,
3521
+ displayedMeasurements,
3522
+ hoverHighlightEnabled,
3523
+ highlightColor,
3524
+ guideColor
3525
+ });
3526
+ const {
3527
+ handlePointerDown,
3528
+ handlePointerMove,
3529
+ handlePointerUp,
3530
+ handlePointerLeave
3531
+ } = useMeasurerPointer({
3532
+ toolbarRef,
3533
+ overlayRef,
3534
+ selectionRectRef,
3535
+ createActionCommit,
3536
+ clearGuideDragHold,
3537
+ scheduleGuideDragHold,
3538
+ enabled,
3539
+ toolMode,
3540
+ guidesEnabled,
3541
+ snapEnabled,
3542
+ snapGuidesEnabled,
3543
+ altPressed,
3544
+ guideOrientation,
3545
+ hoverHighlightEnabled,
3546
+ start,
3547
+ end,
3548
+ isDragging,
3549
+ selectedMeasurements,
3550
+ selectedMeasurement,
3551
+ selectedGuideIds,
3552
+ guides,
3553
+ draggingGuideId,
3554
+ optionPairOverlay,
3555
+ setAltPressed,
3556
+ setGuidePreview,
3557
+ setSelectedGuideIds,
3558
+ setGuides,
3559
+ setStart,
3560
+ setEnd,
3561
+ setIsDragging,
3562
+ setHeldDistances,
3563
+ setDraggingGuideId,
3564
+ setActiveMeasurement,
3565
+ setMeasurements,
3566
+ setSelectedMeasurements,
3567
+ setSelectedMeasurement,
3568
+ setSelectionOriginRect,
3569
+ setSelectedElement,
3570
+ setHoverRect,
3571
+ setHoverElement,
3572
+ setHoverPointer,
3573
+ clearSelectionRect
3574
+ });
3575
+ const removeHeldDistance = (0, import_react19.useCallback)(
3576
+ (id) => {
3577
+ recordSnapshot();
3578
+ setHeldDistances((prev) => prev.filter((distance) => distance.id !== id));
3579
+ },
3580
+ [recordSnapshot, setHeldDistances]
3581
+ );
3582
+ const handleGuidePointerDown = (0, import_react19.useCallback)(
3583
+ (guide, event) => {
3584
+ const commit = createActionCommit();
3585
+ if (!enabled) return;
3586
+ event.stopPropagation();
3587
+ if (event.shiftKey) {
3588
+ commit();
3589
+ setSelectedGuideIds(
3590
+ (prev) => prev.includes(guide.id) ? prev.filter((id) => id !== guide.id) : [...prev, guide.id]
3591
+ );
3592
+ return;
3593
+ }
3594
+ commit();
3595
+ setSelectedGuideIds([guide.id]);
3596
+ scheduleGuideDragHold(guide.id, setDraggingGuideId);
3597
+ event.currentTarget.setPointerCapture(event.pointerId);
3598
+ },
3599
+ [
3600
+ createActionCommit,
3601
+ enabled,
3602
+ scheduleGuideDragHold,
3603
+ setDraggingGuideId,
3604
+ setSelectedGuideIds
3605
+ ]
3606
+ );
3607
+ const handleGuidePointerUp = (0, import_react19.useCallback)(
3608
+ (guide, event) => {
3609
+ event.stopPropagation();
3610
+ clearGuideDragHold();
3611
+ setDraggingGuideId((prev) => prev === guide.id ? null : prev);
3612
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
3613
+ event.currentTarget.releasePointerCapture(event.pointerId);
3614
+ }
3615
+ },
3616
+ [clearGuideDragHold, setDraggingGuideId]
3617
+ );
3618
+ return (0, import_react_dom.createPortal)(
3619
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { ref: overlayRef, className: "pointer-events-none fixed inset-0 z-50", children: [
3620
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3621
+ MeasurerOverlay,
3622
+ {
3623
+ enabled,
3624
+ toolMode,
3625
+ guidesEnabled,
3626
+ altPressed,
3627
+ isDragging,
3628
+ displayedMeasurements,
3629
+ measurementEdgeVisibility,
3630
+ activeRect,
3631
+ activeWidth,
3632
+ activeHeight,
3633
+ fillColor,
3634
+ outlineColor,
3635
+ hoverRectToShow,
3636
+ hoverEdgeVisibility,
3637
+ guidePreview,
3638
+ guideColorPreview,
3639
+ displayedSelectedMeasurements,
3640
+ selectedEdgeVisibility,
3641
+ heldDistances,
3642
+ optionPairOverlay,
3643
+ guideDistanceOverlay,
3644
+ optionContainerLines,
3645
+ guides,
3646
+ hoverGuide,
3647
+ selectedGuideIds,
3648
+ guideColorActive,
3649
+ guideColorHover,
3650
+ guideColorDefault,
3651
+ onPointerDown: handlePointerDown,
3652
+ onPointerMove: handlePointerMove,
3653
+ onPointerUp: handlePointerUp,
3654
+ onPointerLeave: handlePointerLeave,
3655
+ onRemoveHeldDistance: removeHeldDistance,
3656
+ onGuidePointerDown: handleGuidePointerDown,
3657
+ onGuidePointerUp: handleGuidePointerUp,
3658
+ onGuidePointerCancel: handleGuidePointerUp
3659
+ }
3660
+ ),
3661
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3662
+ Toolbar,
3663
+ {
3664
+ ref: toolbarRef,
3665
+ toolMode,
3666
+ setEnabled: setEnabledWithHistory,
3667
+ setToolMode: setToolModeWithHistory,
3668
+ guideOrientation,
3669
+ setGuideOrientation: setGuideOrientationWithHistory,
3670
+ onInteract: () => setToolbarActive(true)
3671
+ }
3672
+ )
3673
+ ] }),
3674
+ document.body
3675
+ );
3676
+ }
3677
+ function Measurer({
3678
+ highlightColor = "oklch(0.62 0.18 255)",
3679
+ guideColor = "oklch(0.63 0.26 29.23)",
3680
+ hoverHighlightEnabled = true,
3681
+ persistOnReload = false
3682
+ }) {
3683
+ const hydrated = useHydrated();
3684
+ if (!hydrated) return null;
3685
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3686
+ MeasurerClient,
3687
+ {
3688
+ highlightColor,
3689
+ guideColor,
3690
+ hoverHighlightEnabled,
3691
+ persistOnReload
3692
+ }
3693
+ );
3694
+ }
3695
+ // Annotate the CommonJS export names for ESM import in node:
3696
+ 0 && (module.exports = {
3697
+ Measurer
3698
+ });