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