mesurer 0.0.0 → 0.0.2

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