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