gap-inspector 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,935 +1,2942 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
- import { createPortal } from "react-dom";
4
- import { inferAxis, inspectPoint, measureGap, resolveSelector } from "./measurement";
5
- import { gapInspectorStyles } from "./styles";
6
- export function GapInspector({ initiallyOpen = false, onMeasure }) {
7
- const rootRef = useRef(null);
8
- const [open, setOpen] = useState(initiallyOpen);
9
- const [drag, setDrag] = useState(null);
10
- const dragRef = useRef(null);
11
- const [measurement, setMeasurement] = useState(null);
12
- const [pointInspection, setPointInspection] = useState(null);
13
- const [copyState, setCopyState] = useState("idle");
14
- const [preview, setPreview] = useState(null);
15
- const [passThrough, setPassThrough] = useState(false);
16
- const panelRef = useRef(null);
17
- const launcherRef = useRef(null);
18
- const gripRef = useRef(null);
19
- const suppressLauncherClickRef = useRef(false);
20
- const [panelPosition, setPanelPosition] = useState(null);
21
- const [hover, setHover] = useState(null);
22
- useEffect(() => {
23
- setHover(null);
24
- }, [measurement]);
25
- const panelHeightAnimRef = useRef(null);
26
- const lastPanelHeightRef = useRef(null);
27
- const closingSizeRef = useRef(null);
28
- // The panel's height is auto, so CSS transitions can't animate content swaps
29
- // (empty ↔ report ↔ point inspection). FLIP it instead: animate from the last
30
- // rendered height (or the mid-flight height if interrupted) to the new one.
31
- useLayoutEffect(() => {
32
- const panel = panelRef.current;
33
- if (!panel) {
34
- panelHeightAnimRef.current = null;
35
- lastPanelHeightRef.current = null;
36
- return;
37
- }
38
- const previousAnim = panelHeightAnimRef.current;
39
- const inFlight = previousAnim && previousAnim.playState === "running"
40
- ? panel.getBoundingClientRect().height
41
- : null;
42
- previousAnim?.cancel();
43
- const natural = panel.offsetHeight;
44
- const from = inFlight ?? lastPanelHeightRef.current;
45
- lastPanelHeightRef.current = natural;
46
- if (from === null ||
47
- Math.abs(from - natural) < 1 ||
48
- window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
49
- return;
50
- }
51
- panelHeightAnimRef.current = panel.animate([{ height: `${from}px` }, { height: `${natural}px` }], { duration: 260, easing: "cubic-bezier(0.32, 0.72, 0, 1)" });
52
- }, [open, measurement, pointInspection]);
53
- // Closing morph: the pill appears at the panel's anchor corner, so animate it
54
- // from the panel's last size down to its own — one element shrinking into the
55
- // pill, not two distinct elements swapping.
56
- useLayoutEffect(() => {
57
- const launcher = launcherRef.current;
58
- const fromSize = closingSizeRef.current;
59
- closingSizeRef.current = null;
60
- if (open ||
61
- !launcher ||
62
- !fromSize ||
63
- window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
64
- return;
65
- }
66
- launcher.animate([
67
- { width: `${fromSize.width}px`, height: `${fromSize.height}px`, borderRadius: "14px" },
68
- { width: `${launcher.offsetWidth}px`, height: `${launcher.offsetHeight}px`, borderRadius: "10px" }
69
- ], { duration: 240, easing: "cubic-bezier(0.32, 0.72, 0, 1)" });
70
- }, [open]);
71
- const activeAxis = drag ? inferAxis(drag.start, drag.end) : "horizontal";
72
- // Computed in pointer handlers (not during render): measureGap reads layout,
73
- // and the boundary-scan fallback is skipped so dragging stays cheap on big pages.
74
- function buildPreview(nextDrag) {
75
- if (dragDistance(nextDrag) < 8) {
76
- return null;
77
- }
78
- const previewReport = measureGap({
79
- axis: inferAxis(nextDrag.start, nextDrag.end),
80
- start: nextDrag.start,
81
- end: nextDrag.end,
82
- ignoreElements: [rootRef.current],
83
- boundaryScan: false
84
- });
85
- if (!previewReport) {
86
- return null;
87
- }
88
- const snapshot = buildOverlaySnapshot(previewReport);
89
- return snapshot ? { measurement: previewReport, snapshot } : null;
1
+ import { useRef, useState, useEffect, useLayoutEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/index.tsx
6
+
7
+ // src/measurement.ts
8
+ var AXIS_PROPS = {
9
+ horizontal: {
10
+ start: "left",
11
+ end: "right",
12
+ size: "width",
13
+ beforeMargin: "marginLeft",
14
+ afterMargin: "marginRight",
15
+ beforePadding: "paddingLeft",
16
+ afterPadding: "paddingRight",
17
+ beforeBorder: "borderLeftWidth",
18
+ afterBorder: "borderRightWidth",
19
+ gap: "columnGap",
20
+ alternateGap: "gap",
21
+ perpStart: "top",
22
+ perpEnd: "bottom"
23
+ },
24
+ vertical: {
25
+ start: "top",
26
+ end: "bottom",
27
+ size: "height",
28
+ beforeMargin: "marginTop",
29
+ afterMargin: "marginBottom",
30
+ beforePadding: "paddingTop",
31
+ afterPadding: "paddingBottom",
32
+ beforeBorder: "borderTopWidth",
33
+ afterBorder: "borderBottomWidth",
34
+ gap: "rowGap",
35
+ alternateGap: "gap",
36
+ perpStart: "left",
37
+ perpEnd: "right"
38
+ }
39
+ };
40
+ function inferAxis(start, end) {
41
+ return Math.abs(end.x - start.x) >= Math.abs(end.y - start.y) ? "horizontal" : "vertical";
42
+ }
43
+ function measureGap(options) {
44
+ const doc = options.document ?? document;
45
+ const axis = options.axis ?? inferAxis(options.start, options.end);
46
+ const props = AXIS_PROPS[axis];
47
+ const ignoreElements = new Set(
48
+ (options.ignoreElements ?? []).filter((element) => Boolean(element))
49
+ );
50
+ const line = normalizeLine(axis, options.start, options.end);
51
+ const endpoints = findEndpointElementsFromPointer(doc, axis, options.start, options.end, ignoreElements) ?? (options.boundaryScan === false ? null : findBoundaryElements(doc, axis, line, ignoreElements));
52
+ if (!endpoints) {
53
+ return null;
54
+ }
55
+ if (endpoints.kind === "internal") {
56
+ return measureInternalGap(axis, endpoints);
57
+ }
58
+ const fromElement = endpoints.from;
59
+ const toElement = endpoints.to;
60
+ const fromRect = toGapRect(endpoints.from.getBoundingClientRect());
61
+ const toRect = toGapRect(endpoints.to.getBoundingClientRect());
62
+ const totalPx = Math.max(
63
+ 0,
64
+ toRect[props.start] - fromRect[props.end]
65
+ );
66
+ const commonAncestor = findCommonAncestor(fromElement, toElement);
67
+ const fromBranch = commonAncestor ? childBranchUnderAncestor(fromElement, commonAncestor) : null;
68
+ const toBranch = commonAncestor ? childBranchUnderAncestor(toElement, commonAncestor) : null;
69
+ const contributions = [];
70
+ const warnings = [];
71
+ for (const element of [fromElement, toElement]) {
72
+ if (element.tagName.toLowerCase() === "iframe") {
73
+ warnings.push(
74
+ `\`${selectorForElement(element)}\` is an iframe; its contents render in a separate document and cannot be inspected or attributed.`
75
+ );
90
76
  }
91
- useEffect(() => {
92
- if (!open) {
93
- return;
94
- }
95
- function handleKeyDown(event) {
96
- if (event.key === "Alt") {
97
- setPassThrough(true);
98
- }
99
- }
100
- function handleKeyUp(event) {
101
- if (event.key === "Alt") {
102
- setPassThrough(false);
103
- }
104
- }
105
- function handleBlur() {
106
- setPassThrough(false);
107
- }
108
- window.addEventListener("keydown", handleKeyDown);
109
- window.addEventListener("keyup", handleKeyUp);
110
- window.addEventListener("blur", handleBlur);
111
- return () => {
112
- window.removeEventListener("keydown", handleKeyDown);
113
- window.removeEventListener("keyup", handleKeyUp);
114
- window.removeEventListener("blur", handleBlur);
115
- setPassThrough(false);
116
- };
117
- }, [open]);
118
- function beginMeasure(event) {
119
- if (event.button !== 0) {
120
- return;
121
- }
122
- event.currentTarget.setPointerCapture(event.pointerId);
123
- const point = pointFromEvent(event);
124
- const nextDrag = { start: point, end: point };
125
- dragRef.current = nextDrag;
126
- setDrag(nextDrag);
127
- setPreview(null);
128
- setCopyState("idle");
129
- }
130
- function updateMeasure(event) {
131
- if (!dragRef.current) {
132
- return;
133
- }
134
- const nextDrag = { ...dragRef.current, end: pointFromEvent(event) };
135
- dragRef.current = nextDrag;
136
- setDrag(nextDrag);
137
- setPreview(buildPreview(nextDrag));
138
- }
139
- function finishMeasure(event) {
140
- if (!dragRef.current) {
141
- return;
142
- }
143
- const end = pointFromEvent(event);
144
- const nextDrag = { ...dragRef.current, end };
145
- dragRef.current = null;
146
- const distance = dragDistance(nextDrag);
147
- if (distance < 6) {
148
- if (measurement || pointInspection) {
149
- setMeasurement(null);
150
- setPointInspection(null);
151
- }
152
- else {
153
- const inspection = inspectPoint({
154
- point: end,
155
- ignoreElements: [rootRef.current]
156
- });
157
- setPointInspection(inspection ? toDocumentInspection(inspection) : inspection);
158
- }
159
- setDrag(null);
160
- setPreview(null);
161
- return;
162
- }
163
- const axis = inferAxis(nextDrag.start, nextDrag.end);
164
- const report = measureGap({
165
- axis,
166
- start: nextDrag.start,
167
- end,
168
- ignoreElements: [rootRef.current]
169
- });
170
- if (report) {
171
- setMeasurement(report);
172
- setPointInspection(null);
173
- onMeasure?.(report);
174
- }
175
- setDrag(null);
176
- setPreview(null);
177
- }
178
- function cancelMeasure() {
179
- dragRef.current = null;
180
- setDrag(null);
181
- setPreview(null);
182
- }
183
- // The pill and the panel share one position (their top-left corner), so the
184
- // inspector stays where the user put it when collapsing or expanding.
185
- function beginInspectorDrag(event, target) {
186
- if (event.button !== 0 || !target) {
187
- return;
188
- }
189
- if (event.target instanceof Element && event.target.closest(".gi-close")) {
190
- return;
191
- }
192
- const rect = target.getBoundingClientRect();
193
- gripRef.current = {
194
- dx: event.clientX - rect.left,
195
- dy: event.clientY - rect.top,
196
- startX: event.clientX,
197
- startY: event.clientY,
198
- target,
199
- moved: false
200
- };
201
- event.currentTarget.setPointerCapture(event.pointerId);
77
+ }
78
+ appendGeometryWarnings(warnings, [fromElement, toElement, fromBranch, toBranch, commonAncestor]);
79
+ if (commonAncestor && fromBranch && fromBranch !== endpoints.from) {
80
+ contributions.push(
81
+ ...describeInternalSpace(axis, fromElement, fromBranch, "after", warnings)
82
+ );
83
+ }
84
+ if (commonAncestor && fromBranch && toBranch && fromBranch !== toBranch) {
85
+ contributions.push(
86
+ ...describeBetweenBranches(axis, commonAncestor, fromBranch, toBranch, warnings)
87
+ );
88
+ } else {
89
+ contributions.push(
90
+ makeUnknownContribution(axis, fromElement, totalPx, "Unable to isolate sibling branches for this gap.")
91
+ );
92
+ }
93
+ if (commonAncestor && toBranch && toBranch !== endpoints.to) {
94
+ contributions.push(
95
+ ...describeInternalSpace(axis, toElement, toBranch, "before", warnings)
96
+ );
97
+ }
98
+ const visibleContributions = limitContributionsToMeasuredGap(
99
+ collapseContributions(contributions).filter((contribution) => contribution.valuePx > 0.49),
100
+ totalPx,
101
+ warnings
102
+ );
103
+ const attributedPx = roundPx(
104
+ visibleContributions.reduce((sum, contribution) => sum + contribution.valuePx, 0)
105
+ );
106
+ const unattributedPx = roundPx(Math.max(0, totalPx - attributedPx));
107
+ if (unattributedPx > 0.49) {
108
+ warnings.push(
109
+ `${formatPx(unattributedPx)} is rendered space that could not be tied to a direct margin, padding, border, or gap declaration. Check flex/grid distribution, widths, min-width, transforms, or empty wrappers.`
110
+ );
111
+ }
112
+ const measurement = {
113
+ axis,
114
+ totalPx: roundPx(totalPx),
115
+ attributedPx,
116
+ unattributedPx,
117
+ from: elementInfo(fromElement),
118
+ to: elementInfo(toElement),
119
+ commonAncestor: commonAncestor ? elementInfo(commonAncestor) : void 0,
120
+ contributions: visibleContributions,
121
+ warnings
122
+ };
123
+ const equation = buildEquation(measurement);
124
+ return {
125
+ ...measurement,
126
+ equation,
127
+ markdown: buildMarkdown({ ...measurement, equation})
128
+ };
129
+ }
130
+ function inspectPoint(options) {
131
+ const doc = options.document ?? document;
132
+ const axis = options.axis ?? "horizontal";
133
+ const ignoreElements = new Set(
134
+ (options.ignoreElements ?? []).filter((element) => Boolean(element))
135
+ );
136
+ const marginInspection = inspectMarginAtPoint(doc, axis, options.point, ignoreElements);
137
+ if (marginInspection) {
138
+ return marginInspection;
139
+ }
140
+ const target = deepestElementAtPoint(doc, options.point.x, options.point.y, ignoreElements);
141
+ if (!target || isStructuralPageElement(target)) {
142
+ return inspectGapAtPoint(doc, axis, options.point, ignoreElements);
143
+ }
144
+ if (isContainerLikeHit(target, options.point)) {
145
+ const gapInspection = inspectGapAtPoint(doc, axis, options.point, ignoreElements);
146
+ if (gapInspection) {
147
+ return gapInspection;
202
148
  }
203
- function updateInspectorDrag(event) {
204
- const grip = gripRef.current;
205
- if (!grip) {
206
- return;
207
- }
208
- if (!grip.moved && Math.hypot(event.clientX - grip.startX, event.clientY - grip.startY) < 4) {
209
- return;
210
- }
211
- grip.moved = true;
212
- setPanelPosition(clampToViewport(event.clientX - grip.dx, event.clientY - grip.dy, grip.target));
149
+ }
150
+ return inspectElementAtPoint(axis, options.point, target);
151
+ }
152
+ function measureInternalGap(axis, endpoints) {
153
+ const props = AXIS_PROPS[axis];
154
+ const containerRect = toGapRect(endpoints.container.getBoundingClientRect());
155
+ const childRect = toGapRect(endpoints.child.getBoundingClientRect());
156
+ const totalPx = endpoints.side === "before" ? Math.max(0, childRect[props.start] - containerRect[props.start]) : Math.max(0, containerRect[props.end] - childRect[props.end]);
157
+ const warnings = [];
158
+ appendGeometryWarnings(warnings, [endpoints.container, endpoints.child]);
159
+ const visibleContributions = limitContributionsToMeasuredGap(
160
+ collapseContributions(
161
+ describeContainedGap(axis, endpoints.container, endpoints.child, endpoints.side, warnings)
162
+ ).filter((contribution) => contribution.valuePx > 0.49),
163
+ totalPx,
164
+ warnings
165
+ );
166
+ const attributedPx = roundPx(
167
+ visibleContributions.reduce((sum, contribution) => sum + contribution.valuePx, 0)
168
+ );
169
+ const unattributedPx = roundPx(Math.max(0, totalPx - attributedPx));
170
+ if (unattributedPx > 0.49) {
171
+ warnings.push(
172
+ `${formatPx(unattributedPx)} is internal rendered space that could not be tied to a direct margin, padding, border, or gap declaration. Check nested wrappers, explicit widths, or positioned children.`
173
+ );
174
+ }
175
+ const measurement = {
176
+ axis,
177
+ totalPx: roundPx(totalPx),
178
+ attributedPx,
179
+ unattributedPx,
180
+ from: elementInfo(endpoints.container),
181
+ to: elementInfo(endpoints.child),
182
+ commonAncestor: elementInfo(endpoints.container),
183
+ internalSide: endpoints.side,
184
+ contributions: visibleContributions,
185
+ warnings
186
+ };
187
+ const equation = buildEquation(measurement);
188
+ return {
189
+ ...measurement,
190
+ equation,
191
+ markdown: buildMarkdown({ ...measurement, equation})
192
+ };
193
+ }
194
+ function normalizeLine(axis, start, end) {
195
+ const props = AXIS_PROPS[axis];
196
+ const startAlong = axis === "horizontal" ? start.x : start.y;
197
+ const endAlong = axis === "horizontal" ? end.x : end.y;
198
+ const perp = axis === "horizontal" ? (start.y + end.y) / 2 : (start.x + end.x) / 2;
199
+ return {
200
+ min: Math.min(startAlong, endAlong),
201
+ max: Math.max(startAlong, endAlong),
202
+ mid: (startAlong + endAlong) / 2,
203
+ perp,
204
+ props
205
+ };
206
+ }
207
+ function findBoundaryElements(doc, axis, line, ignoreElements) {
208
+ const elements = uniqueElementCandidates(
209
+ queryAllDeep(doc.body).filter((element) => shouldInspectElement(element, ignoreElements)).map((element) => normalizeScannedTarget(element)).filter((element) => !isStructuralPageElement(element))
210
+ ).map((element) => ({ element, rect: toGapRect(element.getBoundingClientRect()) })).filter(({ rect }) => rect.width > 0 && rect.height > 0).filter(({ rect }) => line.perp >= rect[line.props.perpStart] - 2 && line.perp <= rect[line.props.perpEnd] + 2);
211
+ const fromCandidates = elements.filter(({ rect }) => rect[line.props.end] <= line.max + 8).sort((a, b) => edgeCandidateScore(a.element, a.rect, line, "from") - edgeCandidateScore(b.element, b.rect, line, "from"));
212
+ const toCandidates = elements.filter(({ rect }) => rect[line.props.start] >= line.min - 8).sort((a, b) => edgeCandidateScore(a.element, a.rect, line, "to") - edgeCandidateScore(b.element, b.rect, line, "to"));
213
+ const bestPair = chooseBestEndpointPair(fromCandidates, toCandidates, line);
214
+ if (bestPair) {
215
+ return bestPair;
216
+ }
217
+ const startElement = elementFromPointIgnoring(doc, axis, line.min, line.perp, ignoreElements);
218
+ const endElement = elementFromPointIgnoring(doc, axis, line.max, line.perp, ignoreElements);
219
+ if (!startElement || !endElement || startElement === endElement) {
220
+ return null;
221
+ }
222
+ const startRect = toGapRect(startElement.getBoundingClientRect());
223
+ const endRect = toGapRect(endElement.getBoundingClientRect());
224
+ return startRect[line.props.end] <= endRect[line.props.start] ? { kind: "normal", from: startElement, to: endElement } : { kind: "normal", from: endElement, to: startElement };
225
+ }
226
+ function findEndpointElementsFromPointer(doc, axis, start, end, ignoreElements) {
227
+ const startElement = deepestElementAtPoint(doc, start.x, start.y, ignoreElements);
228
+ const endElement = deepestElementAtPoint(doc, end.x, end.y, ignoreElements);
229
+ if (!startElement || !endElement || startElement === endElement) {
230
+ return null;
231
+ }
232
+ if (startElement.contains(endElement) || endElement.contains(startElement)) {
233
+ return containedEndpointPair(axis, startElement, endElement, start, end);
234
+ }
235
+ const props = AXIS_PROPS[axis];
236
+ const startRect = toGapRect(startElement.getBoundingClientRect());
237
+ const endRect = toGapRect(endElement.getBoundingClientRect());
238
+ if (startRect[props.end] <= endRect[props.start]) {
239
+ if (endRect[props.start] - startRect[props.end] < 0.5) {
240
+ return null;
213
241
  }
214
- function endInspectorDrag() {
215
- suppressLauncherClickRef.current = Boolean(gripRef.current?.moved);
216
- gripRef.current = null;
242
+ return { kind: "normal", from: startElement, to: endElement };
243
+ }
244
+ if (endRect[props.end] <= startRect[props.start]) {
245
+ if (startRect[props.start] - endRect[props.end] < 0.5) {
246
+ return null;
217
247
  }
218
- function handleLauncherClick() {
219
- if (suppressLauncherClickRef.current) {
220
- suppressLauncherClickRef.current = false;
221
- return;
222
- }
223
- setOpen(true);
224
- }
225
- // Re-clamp whenever the visible element changes (pill <-> panel swap, window
226
- // resize): the panel is much larger than the pill, so a position that suits
227
- // one can put the other off screen.
228
- useEffect(() => {
229
- if (!panelPosition) {
230
- return;
231
- }
232
- const target = open ? panelRef.current : launcherRef.current;
233
- if (!target) {
234
- return;
235
- }
236
- const clampNow = () => {
237
- setPanelPosition((current) => current ? clampToViewport(current.x, current.y, target) : current);
238
- };
239
- clampNow();
240
- window.addEventListener("resize", clampNow);
241
- return () => {
242
- window.removeEventListener("resize", clampNow);
248
+ return { kind: "normal", from: endElement, to: startElement };
249
+ }
250
+ return null;
251
+ }
252
+ function containedEndpointPair(axis, startElement, endElement, start, end) {
253
+ const startContainsEnd = startElement.contains(endElement);
254
+ const container = startContainsEnd ? startElement : endElement;
255
+ const child = startContainsEnd ? endElement : startElement;
256
+ const containerPoint = startContainsEnd ? start : end;
257
+ const childPoint = startContainsEnd ? end : start;
258
+ const childRect = toGapRect(child.getBoundingClientRect());
259
+ const props = AXIS_PROPS[axis];
260
+ const pointAlong = axis === "horizontal" ? containerPoint.x : containerPoint.y;
261
+ const childPointAlong = axis === "horizontal" ? childPoint.x : childPoint.y;
262
+ const side = pointAlong <= childRect[props.start] ? "before" : pointAlong >= childRect[props.end] ? "after" : pointAlong < childPointAlong ? "before" : "after";
263
+ const containerRect = toGapRect(container.getBoundingClientRect());
264
+ const totalPx = side === "before" ? childRect[props.start] - containerRect[props.start] : containerRect[props.end] - childRect[props.end];
265
+ if (totalPx < 0.5) {
266
+ return null;
267
+ }
268
+ return {
269
+ kind: "internal",
270
+ container,
271
+ child,
272
+ side
273
+ };
274
+ }
275
+ function deepestElementAtPoint(doc, x, y, ignoreElements) {
276
+ const elements = elementsFromPointDeep(doc, x, y, ignoreElements);
277
+ for (const element of elements) {
278
+ if (!(element instanceof HTMLElement)) {
279
+ continue;
280
+ }
281
+ if (!shouldInspectElement(element, ignoreElements)) {
282
+ continue;
283
+ }
284
+ if (isStructuralPageElement(element)) {
285
+ continue;
286
+ }
287
+ return element;
288
+ }
289
+ return null;
290
+ }
291
+ function elementsFromPointDeep(doc, x, y, ignoreElements) {
292
+ const layers = [];
293
+ let scope = doc;
294
+ for (let depth = 0; depth < 12; depth += 1) {
295
+ const elements = scope.elementsFromPoint(x, y).filter((element) => element.getRootNode() === scope);
296
+ if (!elements.length) {
297
+ break;
298
+ }
299
+ layers.push(elements);
300
+ const top = elements.find(
301
+ (element) => element instanceof HTMLElement && !ignoreElements.has(element) && !containsAnyIgnored(element, ignoreElements)
302
+ );
303
+ const shadowRoot = top instanceof HTMLElement ? top.shadowRoot : null;
304
+ if (!shadowRoot) {
305
+ break;
306
+ }
307
+ scope = shadowRoot;
308
+ }
309
+ return layers.reverse().flat();
310
+ }
311
+ function queryAllDeep(scope) {
312
+ const results = [];
313
+ const visit = (node) => {
314
+ for (const element of Array.from(node.querySelectorAll("*"))) {
315
+ results.push(element);
316
+ if (element.shadowRoot) {
317
+ visit(element.shadowRoot);
318
+ }
319
+ }
320
+ };
321
+ visit(scope);
322
+ return results;
323
+ }
324
+ function parentThroughShadow(element) {
325
+ if (element.parentElement) {
326
+ return element.parentElement;
327
+ }
328
+ const root = element.getRootNode();
329
+ return root instanceof ShadowRoot ? root.host : null;
330
+ }
331
+ function normalizeScannedTarget(element) {
332
+ let current = element;
333
+ while (current.parentElement && !isStructuralPageElement(current.parentElement) && shouldClimbPointTarget(current, current.parentElement)) {
334
+ current = current.parentElement;
335
+ }
336
+ return current;
337
+ }
338
+ function uniqueElementCandidates(elements) {
339
+ return Array.from(new Set(elements));
340
+ }
341
+ function shouldClimbPointTarget(element, parent) {
342
+ const tagName = element.tagName.toLowerCase();
343
+ const rect = element.getBoundingClientRect();
344
+ const parentRect = parent.getBoundingClientRect();
345
+ const style = getComputedStyle(element);
346
+ if (["span", "strong", "em", "small", "label", "b", "i"].includes(tagName)) {
347
+ return true;
348
+ }
349
+ if (style.display === "inline") {
350
+ return true;
351
+ }
352
+ if (rect.height < 22 && parentRect.height > rect.height * 1.5) {
353
+ return true;
354
+ }
355
+ if (rect.width < 32 && parentRect.width > rect.width * 1.5) {
356
+ return true;
357
+ }
358
+ return false;
359
+ }
360
+ var STRUCTURAL_ROOT_IDS = /* @__PURE__ */ new Set(["root", "app", "__next", "__nuxt"]);
361
+ function isStructuralPageElement(element) {
362
+ const tagName = element.tagName.toLowerCase();
363
+ return tagName === "html" || tagName === "body" || STRUCTURAL_ROOT_IDS.has(element.id);
364
+ }
365
+ function edgeCandidateScore(element, rect, line, side) {
366
+ const edge = side === "from" ? rect[line.props.end] : rect[line.props.start];
367
+ const target = side === "from" ? line.min : line.max;
368
+ const endpointDistance = Math.abs(edge - target);
369
+ const elementSizePenalty = Math.sqrt(rectArea(rect)) * 0.015;
370
+ const depthReward = elementDepth(element) * 0.35;
371
+ return endpointDistance + elementSizePenalty - depthReward;
372
+ }
373
+ function chooseBestEndpointPair(fromCandidates, toCandidates, line) {
374
+ const drawnGap = line.max - line.min;
375
+ let best = null;
376
+ for (const fromCandidate of fromCandidates.slice(0, 40)) {
377
+ for (const toCandidate of toCandidates.slice(0, 40)) {
378
+ if (fromCandidate.element === toCandidate.element) {
379
+ continue;
380
+ }
381
+ if (fromCandidate.element.contains(toCandidate.element) || toCandidate.element.contains(fromCandidate.element)) {
382
+ continue;
383
+ }
384
+ const gap = toCandidate.rect[line.props.start] - fromCandidate.rect[line.props.end];
385
+ if (gap < 0.5) {
386
+ continue;
387
+ }
388
+ const score = edgeCandidateScore(fromCandidate.element, fromCandidate.rect, line, "from") + edgeCandidateScore(toCandidate.element, toCandidate.rect, line, "to") + Math.abs(gap - drawnGap) * 0.4 + sharedAncestorDistance(fromCandidate.element, toCandidate.element) * 0.2;
389
+ if (!best || score < best.score) {
390
+ best = {
391
+ from: fromCandidate.element,
392
+ to: toCandidate.element,
393
+ score
243
394
  };
244
- }, [panelPosition !== null, open]);
245
- async function copyMeasurement() {
246
- const markdown = measurement?.markdown ?? pointInspection?.markdown;
247
- if (!markdown) {
248
- return;
249
- }
250
- try {
251
- await navigator.clipboard.writeText(markdown);
252
- setCopyState("copied");
253
- }
254
- catch {
255
- setCopyState("failed");
256
- }
395
+ }
257
396
  }
258
- if (!open) {
259
- return (_jsxs("div", { className: "gi-root", ref: rootRef, children: [_jsx("style", { children: gapInspectorStyles }), _jsxs("button", { className: "gi-button", type: "button", ref: launcherRef, style: panelPosition
260
- ? { left: panelPosition.x, top: panelPosition.y, right: "auto", bottom: "auto" }
261
- : undefined, onPointerDown: (event) => beginInspectorDrag(event, launcherRef.current), onPointerMove: updateInspectorDrag, onPointerUp: endInspectorDrag, onPointerCancel: endInspectorDrag, onClick: handleLauncherClick, children: [_jsx("span", { className: "gi-button-mark", "aria-hidden": "true" }), "Gap Inspector"] })] }));
262
- }
263
- return (_jsxs("div", { className: "gi-root", ref: rootRef, children: [_jsx("style", { children: gapInspectorStyles }), _jsx("div", { className: "gi-canvas", "data-passthrough": passThrough ? "true" : undefined, onPointerDown: beginMeasure, onPointerMove: updateMeasure, onPointerUp: finishMeasure, onPointerCancel: cancelMeasure }), typeof document === "undefined"
264
- ? null
265
- : createPortal(_jsx(MeasurementOverlay, { drag: drag, axis: activeAxis, measurement: measurement, pointInspection: pointInspection, preview: preview, hover: hover }), document.body), _jsxs("section", { className: "gi-panel", "aria-label": "Gap Inspector", ref: panelRef, style: panelPosition
266
- ? { left: panelPosition.x, top: panelPosition.y, right: "auto", bottom: "auto" }
267
- : undefined, children: [_jsxs("div", { className: "gi-body", children: [_jsxs("div", { className: "gi-header", onPointerDown: (event) => beginInspectorDrag(event, panelRef.current), onPointerMove: updateInspectorDrag, onPointerUp: endInspectorDrag, onPointerCancel: endInspectorDrag, children: [_jsx("div", { className: "gi-title", children: "Gap Inspector" }), _jsx("button", { className: "gi-close", type: "button", "aria-label": "Close Gap Inspector", onClick: () => {
268
- const panel = panelRef.current;
269
- if (panel) {
270
- closingSizeRef.current = { width: panel.offsetWidth, height: panel.offsetHeight };
271
- }
272
- setOpen(false);
273
- }, children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: _jsx("path", { d: "M3 3l6 6M9 3l-6 6", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" }) }) })] }), _jsx("div", { className: "gi-content", children: measurement ? (_jsx(Report, { measurement: measurement, hover: hover, onHover: setHover })) : pointInspection ? (_jsx(PointReport, { inspection: pointInspection })) : (_jsxs("div", { className: "gi-empty", children: [_jsx("p", { children: "Draw a line between two rendered edges to measure the gap and see which CSS declarations produce it. Click once to inspect a point." }), _jsxs("div", { className: "gi-hint", children: [_jsx("span", { className: "gi-kbd", children: "\u2325" }), _jsx("span", { children: "Hold Alt to interact with the page underneath." })] })] })) })] }), measurement || pointInspection ? (_jsxs("div", { className: "gi-footer", children: [_jsx("button", { className: "gi-ghost", type: "button", onClick: () => {
274
- setMeasurement(null);
275
- setPointInspection(null);
276
- }, children: "Clear" }), _jsx("button", { className: "gi-primary", "data-state": copyState, type: "button", onClick: copyMeasurement, children: copyState === "copied" ? "Copied" : copyState === "failed" ? "Failed" : "Copy report" })] })) : null] })] }));
277
- }
278
- // A miniature of the measured gap: contributions are in geometric order along
279
- // the axis, so each segment sits where that spacing actually renders.
280
- function SpacingBar({ measurement, onHover }) {
281
- if (measurement.totalPx < 0.5) {
397
+ }
398
+ return best ? { kind: "normal", from: best.from, to: best.to } : null;
399
+ }
400
+ function describeInternalSpace(axis, element, branch, direction, warnings) {
401
+ const levels = [];
402
+ const props = AXIS_PROPS[axis];
403
+ let current = element;
404
+ while (current && current !== branch) {
405
+ const parent = parentThroughShadow(current);
406
+ if (!parent) {
407
+ break;
408
+ }
409
+ const currentRect = toGapRect(current.getBoundingClientRect());
410
+ const parentRect = toGapRect(parent.getBoundingClientRect());
411
+ const style = getComputedStyle(parent);
412
+ const childStyle = getComputedStyle(current);
413
+ const measured = direction === "after" ? parentRect[props.end] - currentRect[props.end] : currentRect[props.start] - parentRect[props.start];
414
+ warnNegativeMargin(warnings, childStyle, direction === "after" ? props.afterMargin : props.beforeMargin, current);
415
+ if (measured > 0.49) {
416
+ const marginProperty = direction === "after" ? props.afterMargin : props.beforeMargin;
417
+ const paddingProperty = direction === "after" ? props.afterPadding : props.beforePadding;
418
+ const borderProperty = direction === "after" ? props.afterBorder : props.beforeBorder;
419
+ const margin = positivePx(childStyle[marginProperty]);
420
+ const padding = positivePx(style[paddingProperty]);
421
+ const border = positivePx(style[borderProperty]);
422
+ const scrollbar = scrollbarGutterPx(axis, direction, parent, style);
423
+ const residual = measured - margin - padding - border - scrollbar;
424
+ const level = [];
425
+ pushContribution(level, "margin", marginProperty, margin, childStyle[marginProperty], current);
426
+ pushContribution(
427
+ level,
428
+ "layout",
429
+ direction === "after" ? "inner inline-end space" : "inner inline-start space",
430
+ residual,
431
+ void 0,
432
+ parent,
433
+ "Rendered space inside this wrapper between the measured element and the wrapper edge."
434
+ );
435
+ pushContribution(level, "padding", paddingProperty, padding, style[paddingProperty], parent);
436
+ pushContribution(
437
+ level,
438
+ "scrollbar",
439
+ "scrollbar gutter",
440
+ Math.min(scrollbar, measured),
441
+ style.scrollbarGutter,
442
+ parent,
443
+ "Native scrollbar gutter inside this scroll container."
444
+ );
445
+ pushContribution(level, "border", borderProperty, border, style[borderProperty], parent);
446
+ if (direction === "before") {
447
+ level.reverse();
448
+ }
449
+ levels.push(level);
450
+ }
451
+ current = parent;
452
+ }
453
+ if (direction === "before") {
454
+ levels.reverse();
455
+ }
456
+ return levels.flat();
457
+ }
458
+ function describeBetweenBranches(axis, ancestor, fromBranch, toBranch, warnings) {
459
+ const props = AXIS_PROPS[axis];
460
+ const ancestorStyle = getComputedStyle(ancestor);
461
+ const fromStyle = getComputedStyle(fromBranch);
462
+ const toStyle = getComputedStyle(toBranch);
463
+ const fromRect = toGapRect(fromBranch.getBoundingClientRect());
464
+ const toRect = toGapRect(toBranch.getBoundingClientRect());
465
+ const measured = Math.max(0, toRect[props.start] - fromRect[props.end]);
466
+ const contributions = [];
467
+ const afterMargin = positivePx(fromStyle[props.afterMargin]);
468
+ const beforeMargin = positivePx(toStyle[props.beforeMargin]);
469
+ const gap = layoutGapPx(ancestorStyle, props.gap, props.alternateGap);
470
+ warnNegativeMargin(warnings, fromStyle, props.afterMargin, fromBranch);
471
+ warnNegativeMargin(warnings, toStyle, props.beforeMargin, toBranch);
472
+ const marginsCollapse = axis === "vertical" && !isGapLayout(ancestorStyle.display) && !ancestorStyle.display.includes("table") && isBlockLevel(fromStyle) && isBlockLevel(toStyle);
473
+ if (marginsCollapse && (afterMargin > 0.49 || beforeMargin > 0.49)) {
474
+ const fromWins = afterMargin >= beforeMargin;
475
+ const winner = fromWins ? { property: props.afterMargin, value: afterMargin, cssValue: fromStyle[props.afterMargin], element: fromBranch } : { property: props.beforeMargin, value: beforeMargin, cssValue: toStyle[props.beforeMargin], element: toBranch };
476
+ const loser = fromWins ? { property: props.beforeMargin, value: beforeMargin, cssValue: toStyle[props.beforeMargin] } : { property: props.afterMargin, value: afterMargin, cssValue: fromStyle[props.afterMargin] };
477
+ pushContribution(
478
+ contributions,
479
+ "margin",
480
+ winner.property,
481
+ winner.value,
482
+ winner.cssValue,
483
+ winner.element,
484
+ loser.value > 0.49 ? `Adjacent vertical margins collapse: ${cssPropertyName(loser.property)} (${loser.cssValue}) on the sibling collapsed into this larger margin.` : void 0
485
+ );
486
+ } else {
487
+ pushContribution(contributions, "margin", props.afterMargin, afterMargin, fromStyle[props.afterMargin], fromBranch);
488
+ }
489
+ if (isGapLayout(ancestorStyle.display)) {
490
+ pushContribution(
491
+ contributions,
492
+ "gap",
493
+ props.gap,
494
+ Math.min(gap, measured),
495
+ ancestorStyle[props.gap] || ancestorStyle[props.alternateGap],
496
+ ancestor,
497
+ `${ancestorStyle.display} parent`
498
+ );
499
+ }
500
+ const attributedMargins = marginsCollapse ? Math.max(afterMargin, beforeMargin) : afterMargin + beforeMargin;
501
+ const residual = measured - attributedMargins - (isGapLayout(ancestorStyle.display) ? gap : 0);
502
+ pushContribution(
503
+ contributions,
504
+ "layout",
505
+ layoutDistributionProperty(ancestorStyle),
506
+ residual,
507
+ void 0,
508
+ ancestor,
509
+ "Rendered space between the sibling layout branches."
510
+ );
511
+ if (!marginsCollapse) {
512
+ pushContribution(contributions, "margin", props.beforeMargin, beforeMargin, toStyle[props.beforeMargin], toBranch);
513
+ }
514
+ return contributions;
515
+ }
516
+ function describeContainedGap(axis, container, child, side, warnings) {
517
+ const props = AXIS_PROPS[axis];
518
+ const containerRect = toGapRect(container.getBoundingClientRect());
519
+ const childRect = toGapRect(child.getBoundingClientRect());
520
+ const containerStyle = getComputedStyle(container);
521
+ const childStyle = getComputedStyle(child);
522
+ const measured = side === "before" ? childRect[props.start] - containerRect[props.start] : containerRect[props.end] - childRect[props.end];
523
+ const contributions = [];
524
+ const paddingProperty = side === "before" ? props.beforePadding : props.afterPadding;
525
+ const borderProperty = side === "before" ? props.beforeBorder : props.afterBorder;
526
+ const marginProperty = side === "before" ? props.beforeMargin : props.afterMargin;
527
+ const padding = positivePx(containerStyle[paddingProperty]);
528
+ const border = positivePx(containerStyle[borderProperty]);
529
+ const margin = positivePx(childStyle[marginProperty]);
530
+ warnNegativeMargin(warnings, childStyle, marginProperty, child);
531
+ const residual = measured - padding - border - margin;
532
+ const residualContribution = [
533
+ "layout",
534
+ side === "before" ? "internal start space" : "internal end space",
535
+ residual,
536
+ void 0,
537
+ container,
538
+ "Rendered internal space between the container edge and the selected child."
539
+ ];
540
+ if (side === "before") {
541
+ pushContribution(contributions, "border", borderProperty, border, containerStyle[borderProperty], container);
542
+ pushContribution(contributions, "padding", paddingProperty, padding, containerStyle[paddingProperty], container);
543
+ pushContribution(contributions, ...residualContribution);
544
+ pushContribution(contributions, "margin", marginProperty, margin, childStyle[marginProperty], child);
545
+ } else {
546
+ pushContribution(contributions, "margin", marginProperty, margin, childStyle[marginProperty], child);
547
+ pushContribution(contributions, ...residualContribution);
548
+ pushContribution(contributions, "padding", paddingProperty, padding, containerStyle[paddingProperty], container);
549
+ pushContribution(contributions, "border", borderProperty, border, containerStyle[borderProperty], container);
550
+ }
551
+ return contributions;
552
+ }
553
+ function makeUnknownContribution(axis, element, valuePx, note) {
554
+ return {
555
+ kind: "unknown",
556
+ property: `${axis} gap`,
557
+ valuePx: roundPx(valuePx),
558
+ selector: selectorForElement(element),
559
+ element: elementInfo(element),
560
+ note
561
+ };
562
+ }
563
+ function pushContribution(contributions, kind, property, valuePx, cssValue, element, note) {
564
+ if (valuePx <= 0.49) {
565
+ return;
566
+ }
567
+ contributions.push({
568
+ kind,
569
+ property: cssPropertyName(property),
570
+ valuePx: roundPx(valuePx),
571
+ cssValue,
572
+ selector: selectorForElement(element),
573
+ element: elementInfo(element),
574
+ note
575
+ });
576
+ }
577
+ function collapseContributions(contributions) {
578
+ const byKey = /* @__PURE__ */ new Map();
579
+ for (const contribution of contributions) {
580
+ const key = [
581
+ contribution.kind,
582
+ contribution.property,
583
+ contribution.selector,
584
+ contribution.note ?? ""
585
+ ].join("::");
586
+ const existing = byKey.get(key);
587
+ if (existing) {
588
+ existing.valuePx = roundPx(existing.valuePx + contribution.valuePx);
589
+ } else {
590
+ byKey.set(key, { ...contribution });
591
+ }
592
+ }
593
+ return Array.from(byKey.values());
594
+ }
595
+ function limitContributionsToMeasuredGap(contributions, totalPx, warnings) {
596
+ let remaining = roundPx(totalPx);
597
+ const limited = [];
598
+ let capped = false;
599
+ for (const contribution of contributions) {
600
+ if (remaining <= 0.49) {
601
+ capped = true;
602
+ continue;
603
+ }
604
+ if (contribution.valuePx > remaining) {
605
+ capped = true;
606
+ limited.push({
607
+ ...contribution,
608
+ valuePx: remaining,
609
+ note: [
610
+ contribution.note,
611
+ `Raw computed value was ${formatPx(contribution.valuePx)}; capped to the remaining measured gap.`
612
+ ].filter(Boolean).join(" ")
613
+ });
614
+ remaining = 0;
615
+ continue;
616
+ }
617
+ limited.push(contribution);
618
+ remaining = roundPx(remaining - contribution.valuePx);
619
+ }
620
+ if (capped) {
621
+ warnings.push(
622
+ "Candidate contributors exceeded the rendered gap, usually because nested wrappers overlap in the measurement path. Values were capped so the equation stays tied to measured geometry."
623
+ );
624
+ }
625
+ return limited;
626
+ }
627
+ function buildEquation(measurement) {
628
+ const parts = measurement.contributions.map((contribution) => formatPx(contribution.valuePx));
629
+ if (measurement.unattributedPx > 0.49) {
630
+ parts.push(`${formatPx(measurement.unattributedPx)} unattributed`);
631
+ }
632
+ return `${parts.length ? parts.join(" + ") : "0px"} = ${formatPx(measurement.totalPx)}`;
633
+ }
634
+ function buildMarkdown(measurement) {
635
+ const lines = [
636
+ `Measured ${measurement.axis} gap: ${formatPx(measurement.totalPx)}`,
637
+ "",
638
+ `From: \`${measurement.from.selector}\``,
639
+ `To: \`${measurement.to.selector}\``
640
+ ];
641
+ if (measurement.commonAncestor) {
642
+ lines.push(`Common ancestor: \`${measurement.commonAncestor.selector}\``);
643
+ }
644
+ lines.push("", "Contributions:");
645
+ if (measurement.contributions.length === 0) {
646
+ lines.push("- No direct box-model contributors found.");
647
+ } else {
648
+ for (const contribution of measurement.contributions) {
649
+ const cssValue = contribution.cssValue ? ` (${contribution.cssValue})` : "";
650
+ const note = contribution.note ? ` - ${contribution.note}` : "";
651
+ lines.push(
652
+ `- ${formatPx(contribution.valuePx)} from \`${contribution.selector}\` ${contribution.property}${cssValue}${note}`
653
+ );
654
+ }
655
+ }
656
+ lines.push("", `Equation: ${measurement.equation}`);
657
+ if (measurement.warnings.length) {
658
+ lines.push("", "Warnings:");
659
+ for (const warning of measurement.warnings) {
660
+ lines.push(`- ${warning}`);
661
+ }
662
+ }
663
+ return lines.join("\n");
664
+ }
665
+ function shouldInspectElement(element, ignoreElements) {
666
+ if (ignoreElements.has(element) || containsAnyIgnored(element, ignoreElements)) {
667
+ return false;
668
+ }
669
+ const tagName = element.tagName.toLowerCase();
670
+ if (["script", "style", "template", "noscript", "meta", "link"].includes(tagName)) {
671
+ return false;
672
+ }
673
+ const style = getComputedStyle(element);
674
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
675
+ }
676
+ function containsAnyIgnored(element, ignoreElements) {
677
+ for (const ignored of ignoreElements) {
678
+ if (ignored.contains(element) || element.contains(ignored)) {
679
+ return true;
680
+ }
681
+ }
682
+ return false;
683
+ }
684
+ function elementFromPointIgnoring(doc, axis, along, perp, ignoreElements) {
685
+ const x = axis === "horizontal" ? along : perp;
686
+ const y = axis === "horizontal" ? perp : along;
687
+ return elementsFromPointDeep(doc, x, y, ignoreElements).find(
688
+ (element) => element instanceof HTMLElement && !ignoreElements.has(element) && !containsAnyIgnored(element, ignoreElements)
689
+ ) ?? null;
690
+ }
691
+ function findCommonAncestor(a, b) {
692
+ const ancestors = /* @__PURE__ */ new Set();
693
+ let current = a;
694
+ while (current) {
695
+ ancestors.add(current);
696
+ current = parentThroughShadow(current);
697
+ }
698
+ current = b;
699
+ while (current) {
700
+ if (ancestors.has(current)) {
701
+ return current;
702
+ }
703
+ current = parentThroughShadow(current);
704
+ }
705
+ return null;
706
+ }
707
+ function childBranchUnderAncestor(element, ancestor) {
708
+ let current = element;
709
+ let parent = parentThroughShadow(current);
710
+ while (parent && parent !== ancestor) {
711
+ current = parent;
712
+ parent = parentThroughShadow(current);
713
+ }
714
+ return parent === ancestor ? current : null;
715
+ }
716
+ function layoutGapPx(style, primary, fallback) {
717
+ return positivePx(style[primary]) || positivePx(style[fallback]);
718
+ }
719
+ function isGapLayout(display) {
720
+ return display.includes("flex") || display.includes("grid");
721
+ }
722
+ function isBlockLevel(style) {
723
+ return !style.display.startsWith("inline") && style.cssFloat === "none" && style.position !== "absolute" && style.position !== "fixed";
724
+ }
725
+ function warnNegativeMargin(warnings, style, property, element) {
726
+ const cssValue = style[property];
727
+ const parsed = Number.parseFloat(cssValue);
728
+ if (Number.isFinite(parsed) && parsed < -0.49) {
729
+ const warning = `${cssPropertyName(property)}: ${cssValue} on \`${selectorForElement(element)}\` pulls the boxes closer together; negative margins are not listed as contributors.`;
730
+ if (!warnings.includes(warning)) {
731
+ warnings.push(warning);
732
+ }
733
+ }
734
+ }
735
+ function appendGeometryWarnings(warnings, elements) {
736
+ const seen = /* @__PURE__ */ new Set();
737
+ for (const element of elements) {
738
+ if (!element || seen.has(element)) {
739
+ continue;
740
+ }
741
+ seen.add(element);
742
+ const style = getComputedStyle(element);
743
+ const zoom = style.getPropertyValue("zoom");
744
+ const transformed = style.transform !== "none" || isActiveTransformValue(style.getPropertyValue("translate")) || isActiveTransformValue(style.getPropertyValue("rotate")) || isActiveTransformValue(style.getPropertyValue("scale")) || zoom !== "" && zoom !== "1" && zoom !== "normal";
745
+ if (transformed) {
746
+ warnings.push(
747
+ `\`${selectorForElement(element)}\` is transformed (transform/translate/rotate/scale/zoom); rendered pixels may not match its computed CSS values.`
748
+ );
749
+ }
750
+ }
751
+ }
752
+ function isActiveTransformValue(value) {
753
+ return value !== "" && value !== "none";
754
+ }
755
+ function scrollbarGutterPx(axis, direction, element, style) {
756
+ if (!(element instanceof HTMLElement)) {
757
+ return 0;
758
+ }
759
+ const directionStyle = style.direction || "ltr";
760
+ if (axis === "horizontal") {
761
+ const overflowY = style.overflowY;
762
+ const canScrollY = overflowY === "auto" || overflowY === "scroll";
763
+ const hasScrollableContent2 = element.scrollHeight > element.clientHeight;
764
+ if (!canScrollY || !hasScrollableContent2 && overflowY !== "scroll") {
765
+ return 0;
766
+ }
767
+ const borderLeft = positivePx(style.borderLeftWidth);
768
+ const borderRight = positivePx(style.borderRightWidth);
769
+ const gutter2 = Math.max(0, element.offsetWidth - element.clientWidth - borderLeft - borderRight);
770
+ const gutterIsOnMeasuredSide = direction === "after" && directionStyle !== "rtl" || direction === "before" && directionStyle === "rtl";
771
+ return gutterIsOnMeasuredSide ? gutter2 : 0;
772
+ }
773
+ const overflowX = style.overflowX;
774
+ const canScrollX = overflowX === "auto" || overflowX === "scroll";
775
+ const hasScrollableContent = element.scrollWidth > element.clientWidth;
776
+ if (!canScrollX || !hasScrollableContent && overflowX !== "scroll") {
777
+ return 0;
778
+ }
779
+ const borderTop = positivePx(style.borderTopWidth);
780
+ const borderBottom = positivePx(style.borderBottomWidth);
781
+ const gutter = Math.max(0, element.offsetHeight - element.clientHeight - borderTop - borderBottom);
782
+ return direction === "after" ? gutter : 0;
783
+ }
784
+ function layoutDistributionProperty(style) {
785
+ if (style.display.includes("flex")) {
786
+ return `flex ${style.justifyContent || "layout space"}`;
787
+ }
788
+ if (style.display.includes("grid")) {
789
+ return "grid track/layout space";
790
+ }
791
+ if (style.position !== "static") {
792
+ return `${style.position} positioning`;
793
+ }
794
+ return "layout space";
795
+ }
796
+ function selectorForElement(element) {
797
+ const root = element.getRootNode();
798
+ if (root instanceof ShadowRoot) {
799
+ return `${selectorForElement(root.host)} >>> ${selectorWithinRoot(element, root)}`;
800
+ }
801
+ return selectorWithinRoot(element, element.ownerDocument ?? document);
802
+ }
803
+ function selectorWithinRoot(element, root) {
804
+ if (element.id) {
805
+ const idSelector = `#${cssEscape(element.id)}`;
806
+ if (isUniqueInRoot(idSelector, element, root)) {
807
+ return idSelector;
808
+ }
809
+ }
810
+ const testId = element.getAttribute("data-testid") ?? element.getAttribute("data-test");
811
+ if (testId) {
812
+ const attribute = element.hasAttribute("data-testid") ? "data-testid" : "data-test";
813
+ const testSelector = `[${attribute}="${testId.replace(/"/g, '\\"')}"]`;
814
+ if (isUniqueInRoot(testSelector, element, root)) {
815
+ return testSelector;
816
+ }
817
+ }
818
+ const readable = readablePathSelector(element);
819
+ if (readable && isUniqueInRoot(readable, element, root)) {
820
+ return readable;
821
+ }
822
+ return uniquePathSelector(element);
823
+ }
824
+ function readablePathSelector(element) {
825
+ const parts = [];
826
+ let current = element;
827
+ while (current && parts.length < 4 && current.tagName.toLowerCase() !== "html") {
828
+ const tag = current.tagName.toLowerCase();
829
+ const classes = Array.from(current.classList).filter((className) => !className.includes(":")).slice(0, 2).map((className) => `.${cssEscape(className)}`).join("");
830
+ const nth = nthOfType(current);
831
+ parts.unshift(`${tag}${classes}${nth ? `:nth-of-type(${nth})` : ""}`);
832
+ current = current.parentElement;
833
+ }
834
+ return parts.join(" > ");
835
+ }
836
+ function isUniqueInRoot(selector, element, root) {
837
+ try {
838
+ const matches = root.querySelectorAll(selector);
839
+ return matches.length === 1 && matches[0] === element;
840
+ } catch {
841
+ return false;
842
+ }
843
+ }
844
+ function uniquePathSelector(element) {
845
+ const parts = [];
846
+ let current = element;
847
+ while (current) {
848
+ const tag = current.tagName.toLowerCase();
849
+ const parent = current.parentNode;
850
+ if (parent && (parent instanceof Element || parent instanceof ShadowRoot)) {
851
+ const index = Array.prototype.indexOf.call(parent.children, current) + 1;
852
+ parts.unshift(`${tag}:nth-child(${index})`);
853
+ current = parent instanceof Element ? parent : null;
854
+ } else {
855
+ parts.unshift(tag);
856
+ current = null;
857
+ }
858
+ }
859
+ return parts.join(" > ");
860
+ }
861
+ function resolveSelector(selector, doc = document) {
862
+ const segments = selector.split(" >>> ");
863
+ let scope = doc;
864
+ let element = null;
865
+ for (let index = 0; index < segments.length; index += 1) {
866
+ try {
867
+ element = scope.querySelector(segments[index]);
868
+ } catch {
869
+ return null;
870
+ }
871
+ if (!element) {
872
+ return null;
873
+ }
874
+ if (index < segments.length - 1) {
875
+ if (!element.shadowRoot) {
282
876
  return null;
877
+ }
878
+ scope = element.shadowRoot;
283
879
  }
284
- return (_jsxs("div", { className: "gi-bar", role: "img", "aria-label": measurement.equation, children: [measurement.contributions.map((contribution, index) => (_jsx("span", { className: `gi-bar-segment gi-kind-${contribution.kind}`, style: { flexGrow: contribution.valuePx }, onPointerEnter: (event) => onHover(contributionHover(contribution, index, event.currentTarget)), onPointerLeave: () => onHover(null) }, `${contribution.selector}-${contribution.property}-${index}`))), measurement.unattributedPx > 0.49 ? (_jsx("span", { className: "gi-bar-segment gi-bar-unattributed", style: { flexGrow: measurement.unattributedPx }, onPointerEnter: (event) => onHover(unattributedHover(measurement, event.currentTarget)), onPointerLeave: () => onHover(null) })) : null] }));
880
+ }
881
+ return element;
285
882
  }
286
- function contributionHover(contribution, index, target) {
287
- const element = liveElement(contribution.element);
288
- let meta;
289
- if (element) {
290
- const rect = element.getBoundingClientRect();
291
- meta = `${element.tagName.toLowerCase()} \u00b7 ${getComputedStyle(element).display} \u00b7 ${roundPx(rect.width)} \u00d7 ${roundPx(rect.height)}px`;
883
+ function nthOfType(element) {
884
+ const parent = element.parentElement;
885
+ if (!parent) {
886
+ return null;
887
+ }
888
+ const sameTagSiblings = Array.from(parent.children).filter((child) => child.tagName === element.tagName);
889
+ if (sameTagSiblings.length <= 1) {
890
+ return null;
891
+ }
892
+ return sameTagSiblings.indexOf(element) + 1;
893
+ }
894
+ function elementInfo(element) {
895
+ return {
896
+ selector: selectorForElement(element),
897
+ tagName: element.tagName.toLowerCase(),
898
+ className: element.getAttribute("class") ?? "",
899
+ rect: toGapRect(element.getBoundingClientRect()),
900
+ element
901
+ };
902
+ }
903
+ function inspectElementAtPoint(axis, point, element) {
904
+ const rect = toGapRect(element.getBoundingClientRect());
905
+ const style = getComputedStyle(element);
906
+ const borderRect = rect;
907
+ const paddingRect = shrinkRect(borderRect, {
908
+ top: positivePx(style.borderTopWidth),
909
+ right: positivePx(style.borderRightWidth),
910
+ bottom: positivePx(style.borderBottomWidth),
911
+ left: positivePx(style.borderLeftWidth)
912
+ });
913
+ const contentRect = shrinkRect(paddingRect, {
914
+ top: positivePx(style.paddingTop),
915
+ right: positivePx(style.paddingRight),
916
+ bottom: positivePx(style.paddingBottom),
917
+ left: positivePx(style.paddingLeft)
918
+ });
919
+ let region = "content";
920
+ let property = "content box";
921
+ let cssValue;
922
+ let totalPx;
923
+ if (!pointInRect(point, paddingRect)) {
924
+ region = "border";
925
+ const side = nearestSide(point, borderRect);
926
+ property = cssPropertyName(`border${capitalize(side)}Width`);
927
+ cssValue = style[`border${capitalize(side)}Width`];
928
+ totalPx = positivePx(cssValue);
929
+ } else if (!pointInRect(point, contentRect)) {
930
+ region = "padding";
931
+ const side = nearestSide(point, paddingRect);
932
+ property = cssPropertyName(`padding${capitalize(side)}`);
933
+ cssValue = style[`padding${capitalize(side)}`];
934
+ totalPx = positivePx(cssValue);
935
+ }
936
+ const inspection = {
937
+ kind: "point",
938
+ axis,
939
+ point,
940
+ region,
941
+ totalPx,
942
+ property,
943
+ cssValue,
944
+ element: elementInfo(element),
945
+ rect,
946
+ note: `Clicked inside ${region} region.`,
947
+ markdown: ""
948
+ };
949
+ return {
950
+ ...inspection,
951
+ markdown: buildPointMarkdown(inspection)
952
+ };
953
+ }
954
+ function inspectMarginAtPoint(doc, axis, point, ignoreElements) {
955
+ const candidates = queryAllDeep(doc.body).filter((element) => shouldInspectElement(element, ignoreElements)).map((element) => {
956
+ const style = getComputedStyle(element);
957
+ const rect = toGapRect(element.getBoundingClientRect());
958
+ const marginRect = expandRect(rect, {
959
+ top: positivePx(style.marginTop),
960
+ right: positivePx(style.marginRight),
961
+ bottom: positivePx(style.marginBottom),
962
+ left: positivePx(style.marginLeft)
963
+ });
964
+ return { element, style, rect, marginRect };
965
+ }).filter(({ rect, marginRect }) => pointInRect(point, marginRect) && !pointInRect(point, rect)).sort((a, b) => rectArea(a.marginRect) - rectArea(b.marginRect));
966
+ const candidate = candidates[0];
967
+ if (!candidate) {
968
+ return null;
969
+ }
970
+ const side = nearestMarginSide(point, candidate.rect);
971
+ const property = cssPropertyName(`margin${capitalize(side)}`);
972
+ const cssValue = candidate.style[`margin${capitalize(side)}`];
973
+ const inspection = {
974
+ kind: "point",
975
+ axis,
976
+ point,
977
+ region: "margin",
978
+ totalPx: positivePx(cssValue),
979
+ property,
980
+ cssValue,
981
+ element: elementInfo(candidate.element),
982
+ rect: candidate.marginRect,
983
+ note: "Clicked inside computed margin area.",
984
+ markdown: ""
985
+ };
986
+ return {
987
+ ...inspection,
988
+ markdown: buildPointMarkdown(inspection)
989
+ };
990
+ }
991
+ function inspectGapAtPoint(doc, axis, point, ignoreElements) {
992
+ const props = AXIS_PROPS[axis];
993
+ const along = axis === "horizontal" ? point.x : point.y;
994
+ const perp = axis === "horizontal" ? point.y : point.x;
995
+ const candidates = uniqueElementCandidates(
996
+ queryAllDeep(doc.body).filter((element) => shouldInspectElement(element, ignoreElements)).map((element) => normalizeScannedTarget(element)).filter((element) => !isStructuralPageElement(element))
997
+ ).map((element) => ({ element, rect: toGapRect(element.getBoundingClientRect()) })).filter(({ rect: rect2 }) => perp >= rect2[props.perpStart] - 1 && perp <= rect2[props.perpEnd] + 1).filter(({ rect: rect2 }) => !pointInRect(point, rect2));
998
+ const before = candidates.filter(({ rect: rect2 }) => rect2[props.end] <= along).sort((a, b) => b.rect[props.end] - a.rect[props.end] || rectArea(a.rect) - rectArea(b.rect))[0];
999
+ const after = candidates.filter(({ rect: rect2 }) => rect2[props.start] >= along).sort((a, b) => a.rect[props.start] - b.rect[props.start] || rectArea(a.rect) - rectArea(b.rect))[0];
1000
+ if (!before || !after || before.element === after.element) {
1001
+ return null;
1002
+ }
1003
+ const totalPx = roundPx(after.rect[props.start] - before.rect[props.end]);
1004
+ if (totalPx < 0.5) {
1005
+ return null;
1006
+ }
1007
+ const rect = axis === "horizontal" ? {
1008
+ left: before.rect.right,
1009
+ right: after.rect.left,
1010
+ top: perp - 6,
1011
+ bottom: perp + 6,
1012
+ width: totalPx,
1013
+ height: 12
1014
+ } : {
1015
+ left: perp - 6,
1016
+ right: perp + 6,
1017
+ top: before.rect.bottom,
1018
+ bottom: after.rect.top,
1019
+ width: 12,
1020
+ height: totalPx
1021
+ };
1022
+ const inspection = {
1023
+ kind: "point",
1024
+ axis,
1025
+ point,
1026
+ region: "gap",
1027
+ totalPx,
1028
+ property: `${axis} rendered gap`,
1029
+ from: elementInfo(before.element),
1030
+ to: elementInfo(after.element),
1031
+ rect,
1032
+ note: "Clicked in empty rendered space between two visible edges.",
1033
+ markdown: ""
1034
+ };
1035
+ return {
1036
+ ...inspection,
1037
+ markdown: buildPointMarkdown(inspection)
1038
+ };
1039
+ }
1040
+ function buildPointMarkdown(inspection) {
1041
+ const lines = [
1042
+ `Point inspection: ${inspection.region}`,
1043
+ `Point: ${Math.round(inspection.point.x)}, ${Math.round(inspection.point.y)}`
1044
+ ];
1045
+ if (inspection.totalPx !== void 0) {
1046
+ lines.push(`Value: ${formatPx(inspection.totalPx)}`);
1047
+ }
1048
+ if (inspection.property) {
1049
+ lines.push(`Property: ${inspection.property}${inspection.cssValue ? ` (${inspection.cssValue})` : ""}`);
1050
+ }
1051
+ if (inspection.element) {
1052
+ lines.push(`Element: \`${inspection.element.selector}\``);
1053
+ }
1054
+ if (inspection.from && inspection.to) {
1055
+ lines.push(`From: \`${inspection.from.selector}\``);
1056
+ lines.push(`To: \`${inspection.to.selector}\``);
1057
+ }
1058
+ if (inspection.note) {
1059
+ lines.push(`Note: ${inspection.note}`);
1060
+ }
1061
+ return lines.join("\n");
1062
+ }
1063
+ function isContainerLikeHit(element, point) {
1064
+ const rect = toGapRect(element.getBoundingClientRect());
1065
+ const tagName = element.tagName.toLowerCase();
1066
+ const area = rect.width * rect.height;
1067
+ if (["button", "a", "input", "textarea", "select", "h1", "h2", "h3", "p", "span"].includes(tagName)) {
1068
+ return false;
1069
+ }
1070
+ return area > 8e3 || !pointInRect(point, rect);
1071
+ }
1072
+ function pointInRect(point, rect) {
1073
+ return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom;
1074
+ }
1075
+ function shrinkRect(rect, inset) {
1076
+ const left = rect.left + inset.left;
1077
+ const right = rect.right - inset.right;
1078
+ const top = rect.top + inset.top;
1079
+ const bottom = rect.bottom - inset.bottom;
1080
+ return {
1081
+ left,
1082
+ right,
1083
+ top,
1084
+ bottom,
1085
+ width: Math.max(0, right - left),
1086
+ height: Math.max(0, bottom - top)
1087
+ };
1088
+ }
1089
+ function expandRect(rect, outset) {
1090
+ const left = rect.left - outset.left;
1091
+ const right = rect.right + outset.right;
1092
+ const top = rect.top - outset.top;
1093
+ const bottom = rect.bottom + outset.bottom;
1094
+ return {
1095
+ left,
1096
+ right,
1097
+ top,
1098
+ bottom,
1099
+ width: Math.max(0, right - left),
1100
+ height: Math.max(0, bottom - top)
1101
+ };
1102
+ }
1103
+ function nearestSide(point, rect) {
1104
+ const distances = [
1105
+ { side: "Top", distance: Math.abs(point.y - rect.top) },
1106
+ { side: "Right", distance: Math.abs(point.x - rect.right) },
1107
+ { side: "Bottom", distance: Math.abs(point.y - rect.bottom) },
1108
+ { side: "Left", distance: Math.abs(point.x - rect.left) }
1109
+ ];
1110
+ return distances.sort((a, b) => a.distance - b.distance)[0].side;
1111
+ }
1112
+ function nearestMarginSide(point, rect) {
1113
+ if (point.x < rect.left) {
1114
+ return "Left";
1115
+ }
1116
+ if (point.x > rect.right) {
1117
+ return "Right";
1118
+ }
1119
+ if (point.y < rect.top) {
1120
+ return "Top";
1121
+ }
1122
+ return "Bottom";
1123
+ }
1124
+ function capitalize(value) {
1125
+ return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
1126
+ }
1127
+ function toGapRect(rect) {
1128
+ return {
1129
+ top: roundPx(rect.top),
1130
+ right: roundPx(rect.right),
1131
+ bottom: roundPx(rect.bottom),
1132
+ left: roundPx(rect.left),
1133
+ width: roundPx(rect.width),
1134
+ height: roundPx(rect.height)
1135
+ };
1136
+ }
1137
+ function rectArea(rect) {
1138
+ return rect.width * rect.height;
1139
+ }
1140
+ function elementDepth(element) {
1141
+ let depth = 0;
1142
+ let current = element;
1143
+ while (current) {
1144
+ depth += 1;
1145
+ current = parentThroughShadow(current);
1146
+ }
1147
+ return depth;
1148
+ }
1149
+ function sharedAncestorDistance(a, b) {
1150
+ const commonAncestor = findCommonAncestor(a, b);
1151
+ if (!commonAncestor) {
1152
+ return 100;
1153
+ }
1154
+ return distanceToAncestor(a, commonAncestor) + distanceToAncestor(b, commonAncestor);
1155
+ }
1156
+ function distanceToAncestor(element, ancestor) {
1157
+ let distance = 0;
1158
+ let current = element;
1159
+ while (current && current !== ancestor) {
1160
+ distance += 1;
1161
+ current = parentThroughShadow(current);
1162
+ }
1163
+ return current === ancestor ? distance : 100;
1164
+ }
1165
+ function positivePx(value) {
1166
+ if (typeof value === "number") {
1167
+ return Number.isFinite(value) && value > 0 ? value : 0;
1168
+ }
1169
+ if (!value || value === "auto" || value === "normal") {
1170
+ return 0;
1171
+ }
1172
+ const parsed = Number.parseFloat(value);
1173
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
1174
+ }
1175
+ function roundPx(value) {
1176
+ const nearest = Math.round(value);
1177
+ if (Math.abs(value - nearest) < 0.15) {
1178
+ return nearest;
1179
+ }
1180
+ return Math.round(value * 10) / 10;
1181
+ }
1182
+ function formatPx(value) {
1183
+ const rounded = roundPx(value);
1184
+ return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}px`;
1185
+ }
1186
+ function cssPropertyName(property) {
1187
+ return property.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
1188
+ }
1189
+ function cssEscape(value) {
1190
+ if (typeof CSS !== "undefined" && CSS.escape) {
1191
+ return CSS.escape(value);
1192
+ }
1193
+ return value.replace(/[^a-zA-Z0-9_-]/g, (character) => `\\${character}`);
1194
+ }
1195
+
1196
+ // src/styles.ts
1197
+ var gapInspectorStyles = `
1198
+ .gi-root,
1199
+ .gi-svg {
1200
+ --gi-chrome: #131313;
1201
+ --gi-surface: #1a1a1a;
1202
+ --gi-well: #0d0d0d;
1203
+ --gi-text: #f7f7f7;
1204
+ --gi-text-secondary: #c2c6cc;
1205
+ --gi-text-muted: #8a9099;
1206
+ --gi-hairline: rgba(255, 255, 255, 0.09);
1207
+ --gi-hairline-soft: rgba(255, 255, 255, 0.05);
1208
+ --gi-accent: #12a0f0;
1209
+ --gi-danger: #f04438;
1210
+ --gi-amber: #f5a623;
1211
+ --gi-margin: #f5a623;
1212
+ --gi-padding: #4ade80;
1213
+ --gi-border-kind: #f87171;
1214
+ --gi-scrollbar: #2dd4bf;
1215
+ --gi-gap-kind: #a78bfa;
1216
+ --gi-layout: #9ba1a8;
1217
+ --gi-content: #12a0f0;
1218
+ --gi-unknown: #ec4899;
1219
+ --gi-shadow:
1220
+ inset 0 1px 0 rgba(255, 255, 255, 0.06),
1221
+ 0 1px 2px rgba(0, 0, 0, 0.6),
1222
+ 0 32px 70px -16px rgba(0, 0, 0, 0.75);
1223
+ --gi-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", sans-serif;
1224
+ --gi-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
1225
+ }
1226
+
1227
+ .gi-root {
1228
+ color: var(--gi-text);
1229
+ font-family: var(--gi-font);
1230
+ -webkit-font-smoothing: antialiased;
1231
+ -moz-osx-font-smoothing: grayscale;
1232
+ font-synthesis: none;
1233
+ position: fixed;
1234
+ inset: 0;
1235
+ z-index: 2147483647;
1236
+ pointer-events: none;
1237
+ }
1238
+
1239
+ .gi-button {
1240
+ align-items: center;
1241
+ background: var(--gi-chrome);
1242
+ border: 1px solid var(--gi-hairline);
1243
+ border-radius: 10px;
1244
+ bottom: 18px;
1245
+ box-shadow:
1246
+ inset 0 1px 0 rgba(255, 255, 255, 0.06),
1247
+ 0 1px 2px rgba(0, 0, 0, 0.6),
1248
+ 0 12px 32px -8px rgba(0, 0, 0, 0.6);
1249
+ color: var(--gi-text);
1250
+ cursor: pointer;
1251
+ display: inline-flex;
1252
+ font: inherit;
1253
+ font-size: 12.5px;
1254
+ font-weight: 500;
1255
+ gap: 8px;
1256
+ height: 34px;
1257
+ overflow: clip;
1258
+ padding: 0 14px;
1259
+ pointer-events: auto;
1260
+ position: fixed;
1261
+ right: 18px;
1262
+ touch-action: none;
1263
+ transition: border-color 120ms ease, background-color 120ms ease;
1264
+ white-space: nowrap;
1265
+ }
1266
+
1267
+ .gi-button:hover {
1268
+ background: #181818;
1269
+ border-color: rgba(255, 255, 255, 0.16);
1270
+ }
1271
+
1272
+ .gi-button-mark {
1273
+ background: var(--gi-accent);
1274
+ border-radius: 999px;
1275
+ box-shadow: 0 0 8px rgba(18, 160, 240, 0.8);
1276
+ display: inline-block;
1277
+ height: 6px;
1278
+ width: 6px;
1279
+ }
1280
+
1281
+ .gi-panel {
1282
+ background: var(--gi-chrome);
1283
+ border-radius: 14px;
1284
+ bottom: 18px;
1285
+ box-shadow: var(--gi-shadow);
1286
+ display: flex;
1287
+ flex-direction: column;
1288
+ max-height: min(560px, calc(100vh - 36px));
1289
+ overflow: clip;
1290
+ padding: 4px;
1291
+ pointer-events: auto;
1292
+ position: fixed;
1293
+ right: 18px;
1294
+ width: min(400px, calc(100vw - 36px));
1295
+ }
1296
+
1297
+ @media (prefers-reduced-motion: no-preference) {
1298
+ .gi-panel {
1299
+ animation: gi-pop-in 170ms cubic-bezier(0.32, 0.72, 0, 1);
1300
+ }
1301
+ }
1302
+
1303
+ @keyframes gi-pop-in {
1304
+ from {
1305
+ opacity: 0;
1306
+ transform: scale(0.96) translateY(6px);
1307
+ }
1308
+ }
1309
+
1310
+ .gi-body {
1311
+ background: var(--gi-surface);
1312
+ border: 1px solid var(--gi-hairline);
1313
+ border-radius: 10px;
1314
+ display: flex;
1315
+ flex-direction: column;
1316
+ min-height: 0;
1317
+ overflow: clip;
1318
+ }
1319
+
1320
+ .gi-header {
1321
+ align-items: center;
1322
+ border-bottom: 1px solid var(--gi-hairline-soft);
1323
+ cursor: grab;
1324
+ display: flex;
1325
+ flex-shrink: 0;
1326
+ justify-content: space-between;
1327
+ padding: 11px 11px 11px 14px;
1328
+ touch-action: none;
1329
+ user-select: none;
1330
+ }
1331
+
1332
+ .gi-header:active {
1333
+ cursor: grabbing;
1334
+ }
1335
+
1336
+ .gi-header .gi-close {
1337
+ cursor: pointer;
1338
+ }
1339
+
1340
+ .gi-title {
1341
+ font-size: 13px;
1342
+ font-weight: 600;
1343
+ letter-spacing: -0.01em;
1344
+ line-height: 16px;
1345
+ }
1346
+
1347
+ .gi-close {
1348
+ align-items: center;
1349
+ appearance: none;
1350
+ background: transparent;
1351
+ border: 1px solid transparent;
1352
+ border-radius: 7px;
1353
+ color: var(--gi-text-muted);
1354
+ cursor: pointer;
1355
+ display: inline-flex;
1356
+ height: 24px;
1357
+ justify-content: center;
1358
+ padding: 0;
1359
+ transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
1360
+ width: 24px;
1361
+ }
1362
+
1363
+ .gi-close:hover {
1364
+ background: rgba(255, 255, 255, 0.06);
1365
+ border-color: var(--gi-hairline);
1366
+ color: var(--gi-text-secondary);
1367
+ }
1368
+
1369
+ .gi-content {
1370
+ display: flex;
1371
+ flex-direction: column;
1372
+ gap: 12px;
1373
+ min-height: 0;
1374
+ overflow-y: auto;
1375
+ padding: 14px;
1376
+ }
1377
+
1378
+ .gi-content::-webkit-scrollbar {
1379
+ width: 8px;
1380
+ }
1381
+
1382
+ .gi-content::-webkit-scrollbar-thumb {
1383
+ background: rgba(255, 255, 255, 0.12);
1384
+ background-clip: padding-box;
1385
+ border: 2px solid transparent;
1386
+ border-radius: 999px;
1387
+ }
1388
+
1389
+ .gi-empty {
1390
+ color: var(--gi-text-muted);
1391
+ display: flex;
1392
+ flex-direction: column;
1393
+ font-size: 12.5px;
1394
+ gap: 12px;
1395
+ line-height: 1.55;
1396
+ }
1397
+
1398
+ .gi-empty p {
1399
+ margin: 0;
1400
+ }
1401
+
1402
+ .gi-hint {
1403
+ align-items: center;
1404
+ color: var(--gi-text-muted);
1405
+ display: flex;
1406
+ font-size: 12px;
1407
+ gap: 8px;
1408
+ }
1409
+
1410
+ .gi-kbd {
1411
+ align-items: center;
1412
+ background: var(--gi-well);
1413
+ border: 1px solid var(--gi-hairline);
1414
+ border-radius: 5px;
1415
+ color: var(--gi-text-secondary);
1416
+ display: inline-flex;
1417
+ flex-shrink: 0;
1418
+ font-size: 11px;
1419
+ height: 18px;
1420
+ justify-content: center;
1421
+ line-height: 1;
1422
+ min-width: 18px;
1423
+ padding: 0 4px;
1424
+ }
1425
+
1426
+ .gi-footer {
1427
+ align-items: center;
1428
+ display: flex;
1429
+ flex-shrink: 0;
1430
+ gap: 8px;
1431
+ justify-content: flex-end;
1432
+ padding: 8px 6px 4px;
1433
+ }
1434
+
1435
+ .gi-ghost,
1436
+ .gi-primary {
1437
+ appearance: none;
1438
+ border: 0;
1439
+ border-radius: 8px;
1440
+ cursor: pointer;
1441
+ font: inherit;
1442
+ font-size: 12.5px;
1443
+ line-height: 16px;
1444
+ padding: 7px 12px;
1445
+ transition: background-color 120ms ease, filter 120ms ease;
1446
+ }
1447
+
1448
+ .gi-ghost {
1449
+ background: transparent;
1450
+ color: var(--gi-text-secondary);
1451
+ font-weight: 500;
1452
+ }
1453
+
1454
+ .gi-ghost:hover {
1455
+ background: rgba(255, 255, 255, 0.06);
1456
+ }
1457
+
1458
+ .gi-primary {
1459
+ background: var(--gi-accent);
1460
+ box-shadow:
1461
+ inset 0 1px 0 rgba(255, 255, 255, 0.24),
1462
+ 0 1px 2px rgba(0, 0, 0, 0.45);
1463
+ color: #ffffff;
1464
+ font-weight: 600;
1465
+ }
1466
+
1467
+ .gi-primary:hover {
1468
+ filter: brightness(1.08);
1469
+ }
1470
+
1471
+ .gi-primary[data-state="failed"] {
1472
+ background: var(--gi-danger);
1473
+ }
1474
+
1475
+ .gi-report-title {
1476
+ align-items: baseline;
1477
+ display: flex;
1478
+ gap: 7px;
1479
+ }
1480
+
1481
+ .gi-metric {
1482
+ color: var(--gi-kind-color, var(--gi-text));
1483
+ font-family: var(--gi-mono);
1484
+ font-size: 18px;
1485
+ font-variant-numeric: tabular-nums;
1486
+ font-weight: 600;
1487
+ letter-spacing: -0.02em;
1488
+ line-height: 22px;
1489
+ }
1490
+
1491
+ .gi-metric-axis {
1492
+ color: var(--gi-text-muted);
1493
+ font-size: 12px;
1494
+ font-weight: 500;
1495
+ }
1496
+
1497
+ .gi-bar {
1498
+ display: flex;
1499
+ gap: 2px;
1500
+ height: 14px;
1501
+ }
1502
+
1503
+ .gi-bar-segment {
1504
+ background: var(--gi-kind-color, var(--gi-layout));
1505
+ background-clip: padding-box;
1506
+ border-block: 4px solid transparent;
1507
+ border-radius: 6px;
1508
+ box-sizing: border-box;
1509
+ flex-basis: 0;
1510
+ min-width: 3px;
1511
+ }
1512
+
1513
+ .gi-bar-segment:hover {
1514
+ filter: brightness(1.25);
1515
+ }
1516
+
1517
+ .gi-bar-unattributed {
1518
+ background: repeating-linear-gradient(
1519
+ 135deg,
1520
+ rgba(255, 255, 255, 0.28) 0 2px,
1521
+ rgba(255, 255, 255, 0.08) 2px 5px
1522
+ );
1523
+ }
1524
+
1525
+ .gi-equation {
1526
+ background: var(--gi-well);
1527
+ border: 1px solid var(--gi-hairline);
1528
+ border-radius: 9px;
1529
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
1530
+ color: var(--gi-text);
1531
+ font-family: var(--gi-mono);
1532
+ font-size: 11.5px;
1533
+ font-variant-numeric: tabular-nums;
1534
+ line-height: 1.6;
1535
+ overflow-wrap: anywhere;
1536
+ padding: 9px 11px;
1537
+ }
1538
+
1539
+ .gi-caps {
1540
+ color: var(--gi-text-muted);
1541
+ font-size: 10px;
1542
+ font-weight: 600;
1543
+ letter-spacing: 0.08em;
1544
+ text-transform: uppercase;
1545
+ }
1546
+
1547
+ .gi-contribs {
1548
+ display: flex;
1549
+ flex-direction: column;
1550
+ }
1551
+
1552
+ .gi-contribs .gi-caps {
1553
+ margin-bottom: 2px;
1554
+ }
1555
+
1556
+ .gi-contrib {
1557
+ display: flex;
1558
+ flex-direction: column;
1559
+ gap: 4px;
1560
+ padding: 9px 1px;
1561
+ }
1562
+
1563
+ .gi-contrib + .gi-contrib {
1564
+ border-top: 1px solid var(--gi-hairline-soft);
1565
+ }
1566
+
1567
+ .gi-contrib:hover {
1568
+ background: rgba(255, 255, 255, 0.03);
1569
+ }
1570
+
1571
+ .gi-contrib-row {
1572
+ align-items: center;
1573
+ display: flex;
1574
+ gap: 8px;
1575
+ }
1576
+
1577
+ .gi-kind-label {
1578
+ background: color-mix(in srgb, var(--gi-kind-color) 15%, transparent);
1579
+ border-radius: 5px;
1580
+ color: var(--gi-kind-color);
1581
+ flex-shrink: 0;
1582
+ font-size: 10px;
1583
+ font-weight: 600;
1584
+ letter-spacing: 0.04em;
1585
+ line-height: 1;
1586
+ padding: 4px 6px;
1587
+ text-transform: uppercase;
1588
+ }
1589
+
1590
+ .gi-contrib-prop {
1591
+ color: var(--gi-text);
1592
+ flex: 1;
1593
+ font-size: 12px;
1594
+ font-weight: 500;
1595
+ min-width: 0;
1596
+ overflow-wrap: anywhere;
1597
+ }
1598
+
1599
+ .gi-contrib-css {
1600
+ color: var(--gi-text-muted);
1601
+ font-family: var(--gi-mono);
1602
+ font-size: 11px;
1603
+ font-weight: 400;
1604
+ }
1605
+
1606
+ .gi-contrib-value {
1607
+ color: var(--gi-text);
1608
+ flex-shrink: 0;
1609
+ font-family: var(--gi-mono);
1610
+ font-size: 12px;
1611
+ font-variant-numeric: tabular-nums;
1612
+ font-weight: 600;
1613
+ margin-left: auto;
1614
+ }
1615
+
1616
+ .gi-contrib-note {
1617
+ color: var(--gi-text-muted);
1618
+ font-size: 11.5px;
1619
+ line-height: 1.5;
1620
+ }
1621
+
1622
+ .gi-warning {
1623
+ background: color-mix(in srgb, var(--gi-amber) 9%, transparent);
1624
+ border: 1px solid color-mix(in srgb, var(--gi-amber) 28%, transparent);
1625
+ border-radius: 9px;
1626
+ color: #f2c069;
1627
+ font-size: 11.5px;
1628
+ line-height: 1.55;
1629
+ padding: 8px 11px;
1630
+ }
1631
+
1632
+ .gi-hovercard {
1633
+ background: var(--gi-surface);
1634
+ border: 1px solid var(--gi-hairline);
1635
+ border-radius: 10px;
1636
+ box-shadow:
1637
+ inset 0 1px 0 rgba(255, 255, 255, 0.06),
1638
+ 0 8px 24px rgba(0, 0, 0, 0.55);
1639
+ display: flex;
1640
+ flex-direction: column;
1641
+ gap: 6px;
1642
+ padding: 10px 11px;
1643
+ pointer-events: none;
1644
+ position: fixed;
1645
+ z-index: 10;
1646
+ }
1647
+
1648
+ .gi-hovercard-row {
1649
+ align-items: center;
1650
+ display: flex;
1651
+ gap: 8px;
1652
+ }
1653
+
1654
+ .gi-hovercard-prop {
1655
+ color: var(--gi-text);
1656
+ flex: 1;
1657
+ font-size: 12px;
1658
+ font-weight: 500;
1659
+ min-width: 0;
1660
+ overflow-wrap: anywhere;
1661
+ }
1662
+
1663
+ .gi-hovercard-value {
1664
+ color: var(--gi-text);
1665
+ flex-shrink: 0;
1666
+ font-family: var(--gi-mono);
1667
+ font-size: 12px;
1668
+ font-variant-numeric: tabular-nums;
1669
+ font-weight: 600;
1670
+ }
1671
+
1672
+ .gi-hovercard-selector {
1673
+ color: #dfe2e6;
1674
+ font-family: var(--gi-mono);
1675
+ font-size: 10.5px;
1676
+ line-height: 1.5;
1677
+ overflow-wrap: anywhere;
1678
+ }
1679
+
1680
+ .gi-hovercard-meta {
1681
+ color: var(--gi-text-muted);
1682
+ font-family: var(--gi-mono);
1683
+ font-size: 10.5px;
1684
+ }
1685
+
1686
+ .gi-hovercard-note {
1687
+ color: var(--gi-text-muted);
1688
+ font-size: 11px;
1689
+ line-height: 1.5;
1690
+ }
1691
+
1692
+ .gi-canvas {
1693
+ cursor: crosshair;
1694
+ inset: 0;
1695
+ pointer-events: auto;
1696
+ position: fixed;
1697
+ touch-action: none;
1698
+ }
1699
+
1700
+ .gi-canvas[data-passthrough="true"] {
1701
+ pointer-events: none;
1702
+ }
1703
+
1704
+ .gi-svg {
1705
+ left: 0;
1706
+ overflow: visible;
1707
+ pointer-events: none;
1708
+ position: absolute;
1709
+ top: 0;
1710
+ z-index: 2147483646;
1711
+ }
1712
+
1713
+ .gi-line {
1714
+ stroke: var(--gi-accent);
1715
+ stroke-dasharray: 5 5;
1716
+ stroke-linecap: round;
1717
+ stroke-width: 1.5;
1718
+ }
1719
+
1720
+ .gi-gap-band {
1721
+ fill: rgba(18, 160, 240, 0.16);
1722
+ stroke: rgba(18, 160, 240, 0.8);
1723
+ stroke-width: 1;
1724
+ }
1725
+
1726
+ .gi-preview-gap {
1727
+ fill: rgba(18, 160, 240, 0.1);
1728
+ stroke-dasharray: 4 4;
1729
+ }
1730
+
1731
+ .gi-element-box {
1732
+ fill: rgba(18, 160, 240, 0.06);
1733
+ stroke: rgba(18, 160, 240, 0.85);
1734
+ stroke-width: 1;
1735
+ }
1736
+
1737
+ .gi-preview-box {
1738
+ fill: rgba(18, 160, 240, 0.03);
1739
+ stroke-dasharray: 7 4;
1740
+ }
1741
+
1742
+ .gi-contributor-box {
1743
+ fill: color-mix(in srgb, var(--gi-kind-color) 38%, transparent);
1744
+ stroke: color-mix(in srgb, var(--gi-kind-color) 85%, transparent);
1745
+ stroke-width: 1;
1746
+ }
1747
+
1748
+ .gi-preview-contributor {
1749
+ opacity: 0.62;
1750
+ }
1751
+
1752
+ .gi-series-box {
1753
+ fill: color-mix(in srgb, var(--gi-kind-color) 5%, transparent);
1754
+ opacity: 0.34;
1755
+ stroke: var(--gi-kind-color);
1756
+ stroke-dasharray: 2 6;
1757
+ stroke-width: 1;
1758
+ }
1759
+
1760
+ .gi-preview-series {
1761
+ opacity: 0.2;
1762
+ }
1763
+
1764
+ .gi-edge-marker {
1765
+ fill: var(--gi-accent);
1766
+ stroke: none;
1767
+ }
1768
+
1769
+ .gi-preview-edge {
1770
+ fill: rgba(18, 160, 240, 0.85);
1771
+ }
1772
+
1773
+ .gi-from-edge {
1774
+ opacity: 0.92;
1775
+ }
1776
+
1777
+ .gi-to-edge {
1778
+ opacity: 0.92;
1779
+ }
1780
+
1781
+ .gi-kind-margin {
1782
+ --gi-kind-color: var(--gi-margin);
1783
+ }
1784
+
1785
+ .gi-kind-padding {
1786
+ --gi-kind-color: var(--gi-padding);
1787
+ }
1788
+
1789
+ .gi-kind-border {
1790
+ --gi-kind-color: var(--gi-border-kind);
1791
+ }
1792
+
1793
+ .gi-kind-scrollbar {
1794
+ --gi-kind-color: var(--gi-scrollbar);
1795
+ }
1796
+
1797
+ .gi-kind-gap {
1798
+ --gi-kind-color: var(--gi-gap-kind);
1799
+ }
1800
+
1801
+ .gi-kind-layout {
1802
+ --gi-kind-color: var(--gi-layout);
1803
+ }
1804
+
1805
+ .gi-kind-content {
1806
+ --gi-kind-color: var(--gi-content);
1807
+ }
1808
+
1809
+ .gi-kind-unknown {
1810
+ --gi-kind-color: var(--gi-unknown);
1811
+ }
1812
+
1813
+ .gi-hover-box {
1814
+ fill: color-mix(in srgb, var(--gi-kind-color) 35%, transparent);
1815
+ stroke: var(--gi-kind-color);
1816
+ stroke-width: 1.5;
1817
+ }
1818
+
1819
+ .gi-point-region {
1820
+ fill: color-mix(in srgb, var(--gi-kind-color) 20%, transparent);
1821
+ stroke: var(--gi-kind-color);
1822
+ stroke-dasharray: 5 3;
1823
+ stroke-width: 1.5;
1824
+ }
1825
+
1826
+ .gi-point-dot {
1827
+ fill: var(--gi-kind-color);
1828
+ stroke: #131313;
1829
+ stroke-width: 2;
1830
+ }
1831
+ `;
1832
+ function GapInspector({
1833
+ initiallyOpen = false,
1834
+ onMeasure
1835
+ }) {
1836
+ const rootRef = useRef(null);
1837
+ const [open, setOpen] = useState(initiallyOpen);
1838
+ const [drag, setDrag] = useState(null);
1839
+ const dragRef = useRef(null);
1840
+ const [measurement, setMeasurement] = useState(null);
1841
+ const [pointInspection, setPointInspection] = useState(null);
1842
+ const [copyState, setCopyState] = useState("idle");
1843
+ const [preview, setPreview] = useState(null);
1844
+ const [passThrough, setPassThrough] = useState(false);
1845
+ const panelRef = useRef(null);
1846
+ const launcherRef = useRef(null);
1847
+ const gripRef = useRef(null);
1848
+ const suppressLauncherClickRef = useRef(false);
1849
+ const [panelPosition, setPanelPosition] = useState(null);
1850
+ const [hover, setHover] = useState(null);
1851
+ useEffect(() => {
1852
+ setHover(null);
1853
+ }, [measurement]);
1854
+ const panelHeightAnimRef = useRef(null);
1855
+ const lastPanelHeightRef = useRef(null);
1856
+ const closingSizeRef = useRef(null);
1857
+ useLayoutEffect(() => {
1858
+ const panel = panelRef.current;
1859
+ if (!panel) {
1860
+ panelHeightAnimRef.current = null;
1861
+ lastPanelHeightRef.current = null;
1862
+ return;
292
1863
  }
293
- const anchor = target.getBoundingClientRect();
294
- return {
295
- kind: contribution.kind,
296
- index,
297
- property: contribution.property,
298
- valuePx: contribution.valuePx,
299
- cssValue: contribution.cssValue,
300
- selector: contribution.selector,
301
- meta,
302
- note: contribution.note,
303
- element: contribution.element,
304
- anchor: { left: anchor.left, top: anchor.top, bottom: anchor.bottom }
1864
+ const previousAnim = panelHeightAnimRef.current;
1865
+ const inFlight = previousAnim && previousAnim.playState === "running" ? panel.getBoundingClientRect().height : null;
1866
+ previousAnim?.cancel();
1867
+ const natural = panel.offsetHeight;
1868
+ const from = inFlight ?? lastPanelHeightRef.current;
1869
+ lastPanelHeightRef.current = natural;
1870
+ if (from === null || Math.abs(from - natural) < 1 || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
1871
+ return;
1872
+ }
1873
+ panelHeightAnimRef.current = panel.animate(
1874
+ [{ height: `${from}px` }, { height: `${natural}px` }],
1875
+ { duration: 260, easing: "cubic-bezier(0.32, 0.72, 0, 1)" }
1876
+ );
1877
+ }, [open, measurement, pointInspection]);
1878
+ useLayoutEffect(() => {
1879
+ const launcher = launcherRef.current;
1880
+ const fromSize = closingSizeRef.current;
1881
+ closingSizeRef.current = null;
1882
+ if (open || !launcher || !fromSize || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
1883
+ return;
1884
+ }
1885
+ launcher.animate(
1886
+ [
1887
+ { width: `${fromSize.width}px`, height: `${fromSize.height}px`, borderRadius: "14px" },
1888
+ { width: `${launcher.offsetWidth}px`, height: `${launcher.offsetHeight}px`, borderRadius: "10px" }
1889
+ ],
1890
+ { duration: 240, easing: "cubic-bezier(0.32, 0.72, 0, 1)" }
1891
+ );
1892
+ }, [open]);
1893
+ const activeAxis = drag ? inferAxis(drag.start, drag.end) : "horizontal";
1894
+ function buildPreview(nextDrag) {
1895
+ if (dragDistance(nextDrag) < 8) {
1896
+ return null;
1897
+ }
1898
+ const previewReport = measureGap({
1899
+ axis: inferAxis(nextDrag.start, nextDrag.end),
1900
+ start: nextDrag.start,
1901
+ end: nextDrag.end,
1902
+ ignoreElements: [rootRef.current],
1903
+ boundaryScan: false
1904
+ });
1905
+ if (!previewReport) {
1906
+ return null;
1907
+ }
1908
+ const snapshot = buildOverlaySnapshot(previewReport);
1909
+ return snapshot ? { measurement: previewReport, snapshot } : null;
1910
+ }
1911
+ useEffect(() => {
1912
+ if (!open) {
1913
+ return;
1914
+ }
1915
+ function handleKeyDown(event) {
1916
+ if (event.key === "Alt") {
1917
+ setPassThrough(true);
1918
+ }
1919
+ }
1920
+ function handleKeyUp(event) {
1921
+ if (event.key === "Alt") {
1922
+ setPassThrough(false);
1923
+ }
1924
+ }
1925
+ function handleBlur() {
1926
+ setPassThrough(false);
1927
+ }
1928
+ window.addEventListener("keydown", handleKeyDown);
1929
+ window.addEventListener("keyup", handleKeyUp);
1930
+ window.addEventListener("blur", handleBlur);
1931
+ return () => {
1932
+ window.removeEventListener("keydown", handleKeyDown);
1933
+ window.removeEventListener("keyup", handleKeyUp);
1934
+ window.removeEventListener("blur", handleBlur);
1935
+ setPassThrough(false);
305
1936
  };
1937
+ }, [open]);
1938
+ function beginMeasure(event) {
1939
+ if (event.button !== 0) {
1940
+ return;
1941
+ }
1942
+ event.currentTarget.setPointerCapture(event.pointerId);
1943
+ const point = pointFromEvent(event);
1944
+ const nextDrag = { start: point, end: point };
1945
+ dragRef.current = nextDrag;
1946
+ setDrag(nextDrag);
1947
+ setPreview(null);
1948
+ setCopyState("idle");
1949
+ }
1950
+ function updateMeasure(event) {
1951
+ if (!dragRef.current) {
1952
+ return;
1953
+ }
1954
+ const nextDrag = { ...dragRef.current, end: pointFromEvent(event) };
1955
+ dragRef.current = nextDrag;
1956
+ setDrag(nextDrag);
1957
+ setPreview(buildPreview(nextDrag));
1958
+ }
1959
+ function finishMeasure(event) {
1960
+ if (!dragRef.current) {
1961
+ return;
1962
+ }
1963
+ const end = pointFromEvent(event);
1964
+ const nextDrag = { ...dragRef.current, end };
1965
+ dragRef.current = null;
1966
+ const distance = dragDistance(nextDrag);
1967
+ if (distance < 6) {
1968
+ if (measurement || pointInspection) {
1969
+ setMeasurement(null);
1970
+ setPointInspection(null);
1971
+ } else {
1972
+ const inspection = inspectPoint({
1973
+ point: end,
1974
+ ignoreElements: [rootRef.current]
1975
+ });
1976
+ setPointInspection(inspection ? toDocumentInspection(inspection) : inspection);
1977
+ }
1978
+ setDrag(null);
1979
+ setPreview(null);
1980
+ return;
1981
+ }
1982
+ const axis = inferAxis(nextDrag.start, nextDrag.end);
1983
+ const report = measureGap({
1984
+ axis,
1985
+ start: nextDrag.start,
1986
+ end,
1987
+ ignoreElements: [rootRef.current]
1988
+ });
1989
+ if (report) {
1990
+ setMeasurement(report);
1991
+ setPointInspection(null);
1992
+ onMeasure?.(report);
1993
+ }
1994
+ setDrag(null);
1995
+ setPreview(null);
1996
+ }
1997
+ function cancelMeasure() {
1998
+ dragRef.current = null;
1999
+ setDrag(null);
2000
+ setPreview(null);
2001
+ }
2002
+ function beginInspectorDrag(event, target) {
2003
+ if (event.button !== 0 || !target) {
2004
+ return;
2005
+ }
2006
+ if (event.target instanceof Element && event.target.closest(".gi-close")) {
2007
+ return;
2008
+ }
2009
+ const rect = target.getBoundingClientRect();
2010
+ gripRef.current = {
2011
+ dx: event.clientX - rect.left,
2012
+ dy: event.clientY - rect.top,
2013
+ startX: event.clientX,
2014
+ startY: event.clientY,
2015
+ target,
2016
+ moved: false
2017
+ };
2018
+ event.currentTarget.setPointerCapture(event.pointerId);
2019
+ }
2020
+ function updateInspectorDrag(event) {
2021
+ const grip = gripRef.current;
2022
+ if (!grip) {
2023
+ return;
2024
+ }
2025
+ if (!grip.moved && Math.hypot(event.clientX - grip.startX, event.clientY - grip.startY) < 4) {
2026
+ return;
2027
+ }
2028
+ grip.moved = true;
2029
+ setPanelPosition(clampToViewport(event.clientX - grip.dx, event.clientY - grip.dy, grip.target));
2030
+ }
2031
+ function endInspectorDrag() {
2032
+ suppressLauncherClickRef.current = Boolean(gripRef.current?.moved);
2033
+ gripRef.current = null;
2034
+ }
2035
+ function handleLauncherClick() {
2036
+ if (suppressLauncherClickRef.current) {
2037
+ suppressLauncherClickRef.current = false;
2038
+ return;
2039
+ }
2040
+ setOpen(true);
2041
+ }
2042
+ useEffect(() => {
2043
+ if (!panelPosition) {
2044
+ return;
2045
+ }
2046
+ const target = open ? panelRef.current : launcherRef.current;
2047
+ if (!target) {
2048
+ return;
2049
+ }
2050
+ const clampNow = () => {
2051
+ setPanelPosition(
2052
+ (current) => current ? clampToViewport(current.x, current.y, target) : current
2053
+ );
2054
+ };
2055
+ clampNow();
2056
+ window.addEventListener("resize", clampNow);
2057
+ return () => {
2058
+ window.removeEventListener("resize", clampNow);
2059
+ };
2060
+ }, [panelPosition !== null, open]);
2061
+ async function copyMeasurement() {
2062
+ const markdown = measurement?.markdown ?? pointInspection?.markdown;
2063
+ if (!markdown) {
2064
+ return;
2065
+ }
2066
+ try {
2067
+ await navigator.clipboard.writeText(markdown);
2068
+ setCopyState("copied");
2069
+ } catch {
2070
+ setCopyState("failed");
2071
+ }
2072
+ }
2073
+ if (!open) {
2074
+ return /* @__PURE__ */ jsxs("div", { className: "gi-root", ref: rootRef, children: [
2075
+ /* @__PURE__ */ jsx("style", { children: gapInspectorStyles }),
2076
+ /* @__PURE__ */ jsxs(
2077
+ "button",
2078
+ {
2079
+ className: "gi-button",
2080
+ type: "button",
2081
+ ref: launcherRef,
2082
+ style: panelPosition ? { left: panelPosition.x, top: panelPosition.y, right: "auto", bottom: "auto" } : void 0,
2083
+ onPointerDown: (event) => beginInspectorDrag(event, launcherRef.current),
2084
+ onPointerMove: updateInspectorDrag,
2085
+ onPointerUp: endInspectorDrag,
2086
+ onPointerCancel: endInspectorDrag,
2087
+ onClick: handleLauncherClick,
2088
+ children: [
2089
+ /* @__PURE__ */ jsx("span", { className: "gi-button-mark", "aria-hidden": "true" }),
2090
+ "Gap Inspector"
2091
+ ]
2092
+ }
2093
+ )
2094
+ ] });
2095
+ }
2096
+ return /* @__PURE__ */ jsxs("div", { className: "gi-root", ref: rootRef, children: [
2097
+ /* @__PURE__ */ jsx("style", { children: gapInspectorStyles }),
2098
+ /* @__PURE__ */ jsx(
2099
+ "div",
2100
+ {
2101
+ className: "gi-canvas",
2102
+ "data-passthrough": passThrough ? "true" : void 0,
2103
+ onPointerDown: beginMeasure,
2104
+ onPointerMove: updateMeasure,
2105
+ onPointerUp: finishMeasure,
2106
+ onPointerCancel: cancelMeasure
2107
+ }
2108
+ ),
2109
+ typeof document === "undefined" ? null : createPortal(
2110
+ /* @__PURE__ */ jsx(
2111
+ MeasurementOverlay,
2112
+ {
2113
+ drag,
2114
+ axis: activeAxis,
2115
+ measurement,
2116
+ pointInspection,
2117
+ preview,
2118
+ hover
2119
+ }
2120
+ ),
2121
+ document.body
2122
+ ),
2123
+ /* @__PURE__ */ jsxs(
2124
+ "section",
2125
+ {
2126
+ className: "gi-panel",
2127
+ "aria-label": "Gap Inspector",
2128
+ ref: panelRef,
2129
+ style: panelPosition ? { left: panelPosition.x, top: panelPosition.y, right: "auto", bottom: "auto" } : void 0,
2130
+ children: [
2131
+ /* @__PURE__ */ jsxs("div", { className: "gi-body", children: [
2132
+ /* @__PURE__ */ jsxs(
2133
+ "div",
2134
+ {
2135
+ className: "gi-header",
2136
+ onPointerDown: (event) => beginInspectorDrag(event, panelRef.current),
2137
+ onPointerMove: updateInspectorDrag,
2138
+ onPointerUp: endInspectorDrag,
2139
+ onPointerCancel: endInspectorDrag,
2140
+ children: [
2141
+ /* @__PURE__ */ jsx("div", { className: "gi-title", children: "Gap Inspector" }),
2142
+ /* @__PURE__ */ jsx(
2143
+ "button",
2144
+ {
2145
+ className: "gi-close",
2146
+ type: "button",
2147
+ "aria-label": "Close Gap Inspector",
2148
+ onClick: () => {
2149
+ const panel = panelRef.current;
2150
+ if (panel) {
2151
+ closingSizeRef.current = { width: panel.offsetWidth, height: panel.offsetHeight };
2152
+ }
2153
+ setOpen(false);
2154
+ },
2155
+ children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M3 3l6 6M9 3l-6 6", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" }) })
2156
+ }
2157
+ )
2158
+ ]
2159
+ }
2160
+ ),
2161
+ /* @__PURE__ */ jsx("div", { className: "gi-content", children: measurement ? /* @__PURE__ */ jsx(Report, { measurement, hover, onHover: setHover }) : pointInspection ? /* @__PURE__ */ jsx(PointReport, { inspection: pointInspection }) : /* @__PURE__ */ jsxs("div", { className: "gi-empty", children: [
2162
+ /* @__PURE__ */ jsx("p", { children: "Draw a line between two rendered edges to measure the gap and see which CSS declarations produce it. Click once to inspect a point." }),
2163
+ /* @__PURE__ */ jsxs("div", { className: "gi-hint", children: [
2164
+ /* @__PURE__ */ jsx("span", { className: "gi-kbd", children: "\u2325" }),
2165
+ /* @__PURE__ */ jsx("span", { children: "Hold Alt to interact with the page underneath." })
2166
+ ] })
2167
+ ] }) })
2168
+ ] }),
2169
+ measurement || pointInspection ? /* @__PURE__ */ jsxs("div", { className: "gi-footer", children: [
2170
+ /* @__PURE__ */ jsx(
2171
+ "button",
2172
+ {
2173
+ className: "gi-ghost",
2174
+ type: "button",
2175
+ onClick: () => {
2176
+ setMeasurement(null);
2177
+ setPointInspection(null);
2178
+ },
2179
+ children: "Clear"
2180
+ }
2181
+ ),
2182
+ /* @__PURE__ */ jsx(
2183
+ "button",
2184
+ {
2185
+ className: "gi-primary",
2186
+ "data-state": copyState,
2187
+ type: "button",
2188
+ onClick: copyMeasurement,
2189
+ children: copyState === "copied" ? "Copied" : copyState === "failed" ? "Failed" : "Copy report"
2190
+ }
2191
+ )
2192
+ ] }) : null
2193
+ ]
2194
+ }
2195
+ )
2196
+ ] });
2197
+ }
2198
+ function SpacingBar({
2199
+ measurement,
2200
+ onHover
2201
+ }) {
2202
+ if (measurement.totalPx < 0.5) {
2203
+ return null;
2204
+ }
2205
+ return /* @__PURE__ */ jsxs("div", { className: "gi-bar", role: "img", "aria-label": measurement.equation, children: [
2206
+ measurement.contributions.map((contribution, index) => /* @__PURE__ */ jsx(
2207
+ "span",
2208
+ {
2209
+ className: `gi-bar-segment gi-kind-${contribution.kind}`,
2210
+ style: { flexGrow: contribution.valuePx },
2211
+ onPointerEnter: (event) => onHover(contributionHover(contribution, index, event.currentTarget)),
2212
+ onPointerLeave: () => onHover(null)
2213
+ },
2214
+ `${contribution.selector}-${contribution.property}-${index}`
2215
+ )),
2216
+ measurement.unattributedPx > 0.49 ? /* @__PURE__ */ jsx(
2217
+ "span",
2218
+ {
2219
+ className: "gi-bar-segment gi-bar-unattributed",
2220
+ style: { flexGrow: measurement.unattributedPx },
2221
+ onPointerEnter: (event) => onHover(unattributedHover(measurement, event.currentTarget)),
2222
+ onPointerLeave: () => onHover(null)
2223
+ }
2224
+ ) : null
2225
+ ] });
2226
+ }
2227
+ function contributionHover(contribution, index, target) {
2228
+ const element = liveElement(contribution.element);
2229
+ let meta;
2230
+ if (element) {
2231
+ const rect = element.getBoundingClientRect();
2232
+ meta = `${element.tagName.toLowerCase()} \xB7 ${getComputedStyle(element).display} \xB7 ${roundPx2(rect.width)} \xD7 ${roundPx2(rect.height)}px`;
2233
+ }
2234
+ const anchor = target.getBoundingClientRect();
2235
+ return {
2236
+ kind: contribution.kind,
2237
+ index,
2238
+ property: contribution.property,
2239
+ valuePx: contribution.valuePx,
2240
+ cssValue: contribution.cssValue,
2241
+ selector: contribution.selector,
2242
+ meta,
2243
+ note: contribution.note,
2244
+ element: contribution.element,
2245
+ anchor: { left: anchor.left, top: anchor.top, bottom: anchor.bottom }
2246
+ };
306
2247
  }
307
2248
  function unattributedHover(measurement, target) {
308
- const anchor = target.getBoundingClientRect();
309
- return {
310
- kind: "unknown",
311
- index: measurement.contributions.length,
312
- property: "unattributed space",
313
- valuePx: measurement.unattributedPx,
314
- note: "Rendered space not tied to a direct margin, padding, border, or gap declaration.",
315
- anchor: { left: anchor.left, top: anchor.top, bottom: anchor.bottom }
316
- };
2249
+ const anchor = target.getBoundingClientRect();
2250
+ return {
2251
+ kind: "unknown",
2252
+ index: measurement.contributions.length,
2253
+ property: "unattributed space",
2254
+ valuePx: measurement.unattributedPx,
2255
+ note: "Rendered space not tied to a direct margin, padding, border, or gap declaration.",
2256
+ anchor: { left: anchor.left, top: anchor.top, bottom: anchor.bottom }
2257
+ };
317
2258
  }
318
2259
  function HoverCard({ info }) {
319
- const width = 280;
320
- const left = Math.min(Math.max(info.anchor.left, 8), Math.max(8, window.innerWidth - width - 8));
321
- const placeBelow = info.anchor.top < 240;
322
- const style = placeBelow
323
- ? { left, top: info.anchor.bottom + 8, width }
324
- : { left, bottom: window.innerHeight - info.anchor.top + 8, width };
325
- return (_jsxs("div", { className: `gi-hovercard gi-kind-${info.kind}`, style: style, children: [_jsxs("div", { className: "gi-hovercard-row", children: [_jsx("span", { className: "gi-kind-label", children: info.kind }), _jsxs("span", { className: "gi-hovercard-prop", children: [info.property, info.cssValue ? _jsxs("span", { className: "gi-contrib-css", children: [" \u00B7 ", info.cssValue] }) : null] }), _jsx("span", { className: "gi-hovercard-value", children: formatPx(info.valuePx) })] }), info.selector ? _jsx("div", { className: "gi-hovercard-selector", children: info.selector }) : null, info.meta ? _jsx("div", { className: "gi-hovercard-meta", children: info.meta }) : null, info.note ? _jsx("div", { className: "gi-hovercard-note", children: info.note }) : null] }));
2260
+ const width = 280;
2261
+ const left = Math.min(Math.max(info.anchor.left, 8), Math.max(8, window.innerWidth - width - 8));
2262
+ const placeBelow = info.anchor.top < 240;
2263
+ const style = placeBelow ? { left, top: info.anchor.bottom + 8, width } : { left, bottom: window.innerHeight - info.anchor.top + 8, width };
2264
+ return /* @__PURE__ */ jsxs("div", { className: `gi-hovercard gi-kind-${info.kind}`, style, children: [
2265
+ /* @__PURE__ */ jsxs("div", { className: "gi-hovercard-row", children: [
2266
+ /* @__PURE__ */ jsx("span", { className: "gi-kind-label", children: info.kind }),
2267
+ /* @__PURE__ */ jsxs("span", { className: "gi-hovercard-prop", children: [
2268
+ info.property,
2269
+ info.cssValue ? /* @__PURE__ */ jsxs("span", { className: "gi-contrib-css", children: [
2270
+ " \xB7 ",
2271
+ info.cssValue
2272
+ ] }) : null
2273
+ ] }),
2274
+ /* @__PURE__ */ jsx("span", { className: "gi-hovercard-value", children: formatPx2(info.valuePx) })
2275
+ ] }),
2276
+ info.selector ? /* @__PURE__ */ jsx("div", { className: "gi-hovercard-selector", children: info.selector }) : null,
2277
+ info.meta ? /* @__PURE__ */ jsx("div", { className: "gi-hovercard-meta", children: info.meta }) : null,
2278
+ info.note ? /* @__PURE__ */ jsx("div", { className: "gi-hovercard-note", children: info.note }) : null
2279
+ ] });
326
2280
  }
327
- function Report({ measurement, hover, onHover }) {
328
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "gi-report-title", children: [_jsx("span", { className: "gi-metric", children: formatPx(measurement.totalPx) }), " ", _jsx("span", { className: "gi-metric-axis", children: measurement.axis })] }), _jsx(SpacingBar, { measurement: measurement, onHover: onHover }), measurement.contributions.length ? (_jsxs("div", { className: "gi-contribs", children: [_jsx("div", { className: "gi-caps", children: "Contributors" }), measurement.contributions.map((contribution, index) => (_jsxs("div", { className: `gi-contrib gi-kind-${contribution.kind}`, "data-kind": contribution.kind, onPointerEnter: (event) => onHover(contributionHover(contribution, index, event.currentTarget)), onPointerLeave: () => onHover(null), children: [_jsxs("div", { className: "gi-contrib-row", children: [_jsx("span", { className: "gi-kind-label", children: contribution.kind }), _jsxs("span", { className: "gi-contrib-prop", children: [contribution.property, contribution.cssValue ? (_jsxs("span", { className: "gi-contrib-css", children: [" \u00B7 ", contribution.cssValue] })) : null] }), _jsx("span", { className: "gi-contrib-value", children: formatPx(contribution.valuePx) })] }), contribution.note ? _jsx("div", { className: "gi-contrib-note", children: contribution.note }) : null] }, `${contribution.selector}-${contribution.property}-${index}`)))] })) : null, measurement.warnings.map((warning) => (_jsx("div", { className: "gi-warning", children: warning }, warning))), hover ? _jsx(HoverCard, { info: hover }) : null] }));
2281
+ function Report({
2282
+ measurement,
2283
+ hover,
2284
+ onHover
2285
+ }) {
2286
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2287
+ /* @__PURE__ */ jsxs("div", { className: "gi-report-title", children: [
2288
+ /* @__PURE__ */ jsx("span", { className: "gi-metric", children: formatPx2(measurement.totalPx) }),
2289
+ " ",
2290
+ /* @__PURE__ */ jsx("span", { className: "gi-metric-axis", children: measurement.axis })
2291
+ ] }),
2292
+ /* @__PURE__ */ jsx(SpacingBar, { measurement, onHover }),
2293
+ measurement.contributions.length ? /* @__PURE__ */ jsxs("div", { className: "gi-contribs", children: [
2294
+ /* @__PURE__ */ jsx("div", { className: "gi-caps", children: "Contributors" }),
2295
+ measurement.contributions.map((contribution, index) => /* @__PURE__ */ jsxs(
2296
+ "div",
2297
+ {
2298
+ className: `gi-contrib gi-kind-${contribution.kind}`,
2299
+ "data-kind": contribution.kind,
2300
+ onPointerEnter: (event) => onHover(contributionHover(contribution, index, event.currentTarget)),
2301
+ onPointerLeave: () => onHover(null),
2302
+ children: [
2303
+ /* @__PURE__ */ jsxs("div", { className: "gi-contrib-row", children: [
2304
+ /* @__PURE__ */ jsx("span", { className: "gi-kind-label", children: contribution.kind }),
2305
+ /* @__PURE__ */ jsxs("span", { className: "gi-contrib-prop", children: [
2306
+ contribution.property,
2307
+ contribution.cssValue ? /* @__PURE__ */ jsxs("span", { className: "gi-contrib-css", children: [
2308
+ " \xB7 ",
2309
+ contribution.cssValue
2310
+ ] }) : null
2311
+ ] }),
2312
+ /* @__PURE__ */ jsx("span", { className: "gi-contrib-value", children: formatPx2(contribution.valuePx) })
2313
+ ] }),
2314
+ contribution.note ? /* @__PURE__ */ jsx("div", { className: "gi-contrib-note", children: contribution.note }) : null
2315
+ ]
2316
+ },
2317
+ `${contribution.selector}-${contribution.property}-${index}`
2318
+ ))
2319
+ ] }) : null,
2320
+ measurement.warnings.map((warning) => /* @__PURE__ */ jsx("div", { className: "gi-warning", children: warning }, warning)),
2321
+ hover ? /* @__PURE__ */ jsx(HoverCard, { info: hover }) : null
2322
+ ] });
329
2323
  }
330
2324
  function PointReport({ inspection }) {
331
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: `gi-report-title gi-kind-${inspection.region}`, children: inspection.totalPx !== undefined ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "gi-metric", children: formatPx(inspection.totalPx) }), " ", _jsx("span", { className: "gi-metric-axis", children: inspection.region })] })) : (_jsx("span", { className: "gi-metric", children: inspection.region })) }), _jsxs("div", { className: "gi-equation", children: [inspection.property ?? inspection.region, inspection.cssValue ? ` (${inspection.cssValue})` : ""] }), inspection.note ? _jsx("div", { className: "gi-contrib-note", children: inspection.note }) : null] }));
2325
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2326
+ /* @__PURE__ */ jsx("div", { className: `gi-report-title gi-kind-${inspection.region}`, children: inspection.totalPx !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
2327
+ /* @__PURE__ */ jsx("span", { className: "gi-metric", children: formatPx2(inspection.totalPx) }),
2328
+ " ",
2329
+ /* @__PURE__ */ jsx("span", { className: "gi-metric-axis", children: inspection.region })
2330
+ ] }) : /* @__PURE__ */ jsx("span", { className: "gi-metric", children: inspection.region }) }),
2331
+ /* @__PURE__ */ jsxs("div", { className: "gi-equation", children: [
2332
+ inspection.property ?? inspection.region,
2333
+ inspection.cssValue ? ` (${inspection.cssValue})` : ""
2334
+ ] }),
2335
+ inspection.note ? /* @__PURE__ */ jsx("div", { className: "gi-contrib-note", children: inspection.note }) : null
2336
+ ] });
332
2337
  }
333
- function MeasurementOverlay({ drag, axis, measurement, pointInspection, preview, hover }) {
334
- const snapshot = useLiveOverlaySnapshot(measurement);
335
- const pointSnapshot = useLivePointSnapshot(pointInspection);
336
- return (_jsxs("svg", { className: "gi-svg", width: "100%", height: "100%", "aria-hidden": "true", children: [measurement && snapshot && !drag ? (_jsx(MeasurementRects, { measurement: measurement, snapshot: snapshot, variant: "committed", hover: hover })) : null, pointInspection && pointSnapshot && !drag ? (_jsx(PointInspectionRects, { inspection: pointInspection, snapshot: pointSnapshot })) : null, drag && preview ? (_jsx(MeasurementRects, { measurement: preview.measurement, snapshot: preview.snapshot, variant: "preview", line: lineFromDrag(preview.measurement.axis, drag) })) : null, drag ? _jsx(DragLine, { drag: drag, axis: axis }) : null] }));
2338
+ function MeasurementOverlay({
2339
+ drag,
2340
+ axis,
2341
+ measurement,
2342
+ pointInspection,
2343
+ preview,
2344
+ hover
2345
+ }) {
2346
+ const snapshot = useLiveOverlaySnapshot(measurement);
2347
+ const pointSnapshot = useLivePointSnapshot(pointInspection);
2348
+ return /* @__PURE__ */ jsxs("svg", { className: "gi-svg", width: "100%", height: "100%", "aria-hidden": "true", children: [
2349
+ measurement && snapshot && !drag ? /* @__PURE__ */ jsx(MeasurementRects, { measurement, snapshot, variant: "committed", hover }) : null,
2350
+ pointInspection && pointSnapshot && !drag ? /* @__PURE__ */ jsx(PointInspectionRects, { inspection: pointInspection, snapshot: pointSnapshot }) : null,
2351
+ drag && preview ? /* @__PURE__ */ jsx(
2352
+ MeasurementRects,
2353
+ {
2354
+ measurement: preview.measurement,
2355
+ snapshot: preview.snapshot,
2356
+ variant: "preview",
2357
+ line: lineFromDrag(preview.measurement.axis, drag)
2358
+ }
2359
+ ) : null,
2360
+ drag ? /* @__PURE__ */ jsx(DragLine, { drag, axis }) : null
2361
+ ] });
337
2362
  }
338
2363
  function DragLine({ drag, axis }) {
339
- const start = toDocumentPoint(drag.start);
340
- const end = toDocumentPoint(drag.end);
341
- const x1 = axis === "horizontal" ? start.x : (start.x + end.x) / 2;
342
- const x2 = axis === "horizontal" ? end.x : (start.x + end.x) / 2;
343
- const y1 = axis === "horizontal" ? (start.y + end.y) / 2 : start.y;
344
- const y2 = axis === "horizontal" ? (start.y + end.y) / 2 : end.y;
345
- return _jsx("line", { className: "gi-line", x1: x1, y1: y1, x2: x2, y2: y2 });
346
- }
347
- function MeasurementRects({ measurement, snapshot, variant, line, hover }) {
348
- const from = snapshot.from;
349
- const to = snapshot.to;
350
- const geometry = bandGeometry(measurement, from, to, line);
351
- const band = geometry
352
- ? rectFromBand(measurement.axis, geometry.start, geometry.end, geometry.perp, GAP_BAND_THICKNESS)
353
- : null;
354
- const { strips, remainder } = geometry
355
- ? contributionStrips(measurement, geometry)
356
- : { strips: [], remainder: null };
357
- const hoverStrip = hover
358
- ? hover.index === measurement.contributions.length
359
- ? remainder
360
- : strips[hover.index] ?? null
361
- : null;
362
- const hoverRect = hover && hoverStrip
363
- ? hoverHighlightRect(measurement.axis, hoverStrip, hover)
364
- : null;
365
- const fromEdge = measurement.internalSide
366
- ? internalEdgeMarkerRect(measurement.axis, from, measurement.internalSide, "container")
367
- : edgeMarkerRect(measurement.axis, from, "from");
368
- const toEdge = measurement.internalSide
369
- ? internalEdgeMarkerRect(measurement.axis, to, measurement.internalSide, "child")
370
- : edgeMarkerRect(measurement.axis, to, "to");
371
- const prefix = variant === "preview" ? "gi-preview" : "gi-committed";
372
- return (_jsxs(_Fragment, { children: [snapshot.series.map((series, index) => (_jsx("rect", { className: `gi-series-box ${prefix}-series gi-kind-${series.kind}`, ...svgRect(series.rect) }, `${series.kind}-${index}`))), _jsx("rect", { className: `gi-element-box ${prefix}-box gi-from-box`, ...svgRect(from) }), _jsx("rect", { className: `gi-element-box ${prefix}-box gi-to-box`, ...svgRect(to) }), band ? _jsx("rect", { className: `gi-gap-band ${prefix}-gap`, ...svgRect(band) }) : null, strips.map((strip, index) => strip ? (_jsx("rect", { className: `gi-contributor-box ${prefix}-contributor gi-kind-${measurement.contributions[index].kind}`, ...svgRect(strip) }, `${measurement.contributions[index].kind}-${index}`)) : null), hover && hoverRect ? (_jsx("rect", { className: `gi-hover-box gi-kind-${hover.kind}`, ...svgRect(hoverRect) })) : null, _jsx("rect", { className: `gi-edge-marker ${prefix}-edge gi-from-edge`, ...svgRect(fromEdge) }), _jsx("rect", { className: `gi-edge-marker ${prefix}-edge gi-to-edge`, ...svgRect(toEdge) })] }));
373
- }
374
- const GAP_BAND_THICKNESS = 12;
375
- // The span of the measured gap along the axis plus the perpendicular center of
376
- // the drawn line (or the from/to overlap when re-rendered without a line).
2364
+ const start = toDocumentPoint(drag.start);
2365
+ const end = toDocumentPoint(drag.end);
2366
+ const x1 = axis === "horizontal" ? start.x : (start.x + end.x) / 2;
2367
+ const x2 = axis === "horizontal" ? end.x : (start.x + end.x) / 2;
2368
+ const y1 = axis === "horizontal" ? (start.y + end.y) / 2 : start.y;
2369
+ const y2 = axis === "horizontal" ? (start.y + end.y) / 2 : end.y;
2370
+ return /* @__PURE__ */ jsx("line", { className: "gi-line", x1, y1, x2, y2 });
2371
+ }
2372
+ function MeasurementRects({
2373
+ measurement,
2374
+ snapshot,
2375
+ variant,
2376
+ line,
2377
+ hover
2378
+ }) {
2379
+ const from = snapshot.from;
2380
+ const to = snapshot.to;
2381
+ const geometry = bandGeometry(measurement, from, to, line);
2382
+ const band = geometry ? rectFromBand(measurement.axis, geometry.start, geometry.end, geometry.perp, GAP_BAND_THICKNESS) : null;
2383
+ const { strips, remainder } = geometry ? contributionStrips(measurement, geometry) : { strips: [], remainder: null };
2384
+ const hoverStrip = hover ? hover.index === measurement.contributions.length ? remainder : strips[hover.index] ?? null : null;
2385
+ const hoverRect = hover && hoverStrip ? hoverHighlightRect(measurement.axis, hoverStrip, hover) : null;
2386
+ const fromEdge = measurement.internalSide ? internalEdgeMarkerRect(measurement.axis, from, measurement.internalSide, "container") : edgeMarkerRect(measurement.axis, from, "from");
2387
+ const toEdge = measurement.internalSide ? internalEdgeMarkerRect(measurement.axis, to, measurement.internalSide, "child") : edgeMarkerRect(measurement.axis, to, "to");
2388
+ const prefix = variant === "preview" ? "gi-preview" : "gi-committed";
2389
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2390
+ snapshot.series.map((series, index) => /* @__PURE__ */ jsx(
2391
+ "rect",
2392
+ {
2393
+ className: `gi-series-box ${prefix}-series gi-kind-${series.kind}`,
2394
+ ...svgRect(series.rect)
2395
+ },
2396
+ `${series.kind}-${index}`
2397
+ )),
2398
+ /* @__PURE__ */ jsx("rect", { className: `gi-element-box ${prefix}-box gi-from-box`, ...svgRect(from) }),
2399
+ /* @__PURE__ */ jsx("rect", { className: `gi-element-box ${prefix}-box gi-to-box`, ...svgRect(to) }),
2400
+ band ? /* @__PURE__ */ jsx("rect", { className: `gi-gap-band ${prefix}-gap`, ...svgRect(band) }) : null,
2401
+ strips.map(
2402
+ (strip, index) => strip ? /* @__PURE__ */ jsx(
2403
+ "rect",
2404
+ {
2405
+ className: `gi-contributor-box ${prefix}-contributor gi-kind-${measurement.contributions[index].kind}`,
2406
+ ...svgRect(strip)
2407
+ },
2408
+ `${measurement.contributions[index].kind}-${index}`
2409
+ ) : null
2410
+ ),
2411
+ hover && hoverRect ? /* @__PURE__ */ jsx("rect", { className: `gi-hover-box gi-kind-${hover.kind}`, ...svgRect(hoverRect) }) : null,
2412
+ /* @__PURE__ */ jsx("rect", { className: `gi-edge-marker ${prefix}-edge gi-from-edge`, ...svgRect(fromEdge) }),
2413
+ /* @__PURE__ */ jsx("rect", { className: `gi-edge-marker ${prefix}-edge gi-to-edge`, ...svgRect(toEdge) })
2414
+ ] });
2415
+ }
2416
+ var GAP_BAND_THICKNESS = 12;
377
2417
  function bandGeometry(measurement, from, to, line) {
378
- const horizontal = measurement.axis === "horizontal";
379
- const props = horizontal
380
- ? { start: "left", end: "right", perpStart: "top", perpEnd: "bottom" }
381
- : { start: "top", end: "bottom", perpStart: "left", perpEnd: "right" };
382
- let start;
383
- let end;
384
- let fallbackPerp;
385
- if (measurement.internalSide) {
386
- start = measurement.internalSide === "before" ? from[props.start] : to[props.end];
387
- end = measurement.internalSide === "before" ? to[props.start] : from[props.end];
388
- fallbackPerp = (to[props.perpStart] + to[props.perpEnd]) / 2;
389
- }
390
- else {
391
- start = from[props.end];
392
- end = to[props.start];
393
- fallbackPerp =
394
- (Math.max(Math.min(from[props.perpStart], to[props.perpStart]), 0)
395
- + Math.max(from[props.perpEnd], to[props.perpEnd])) / 2;
396
- }
397
- const overlapStart = Math.max(from[props.perpStart], to[props.perpStart]);
398
- const overlapEnd = Math.min(from[props.perpEnd], to[props.perpEnd]);
399
- const perp = line?.perp ?? (overlapEnd > overlapStart ? (overlapStart + overlapEnd) / 2 : fallbackPerp);
400
- return end > start ? { start, end, perp } : null;
2418
+ const horizontal = measurement.axis === "horizontal";
2419
+ const props = horizontal ? { start: "left", end: "right", perpStart: "top", perpEnd: "bottom" } : { start: "top", end: "bottom", perpStart: "left", perpEnd: "right" };
2420
+ let start;
2421
+ let end;
2422
+ let fallbackPerp;
2423
+ if (measurement.internalSide) {
2424
+ start = measurement.internalSide === "before" ? from[props.start] : to[props.end];
2425
+ end = measurement.internalSide === "before" ? to[props.start] : from[props.end];
2426
+ fallbackPerp = (to[props.perpStart] + to[props.perpEnd]) / 2;
2427
+ } else {
2428
+ start = from[props.end];
2429
+ end = to[props.start];
2430
+ fallbackPerp = (Math.max(Math.min(from[props.perpStart], to[props.perpStart]), 0) + Math.max(from[props.perpEnd], to[props.perpEnd])) / 2;
2431
+ }
2432
+ const overlapStart = Math.max(from[props.perpStart], to[props.perpStart]);
2433
+ const overlapEnd = Math.min(from[props.perpEnd], to[props.perpEnd]);
2434
+ const perp = line?.perp ?? (overlapEnd > overlapStart ? (overlapStart + overlapEnd) / 2 : fallbackPerp);
2435
+ return end > start ? { start, end, perp } : null;
401
2436
  }
402
2437
  function rectFromBand(axis, start, end, perp, thickness) {
403
- if (axis === "horizontal") {
404
- return {
405
- left: start,
406
- right: end,
407
- top: perp - thickness / 2,
408
- bottom: perp + thickness / 2,
409
- width: end - start,
410
- height: thickness
411
- };
412
- }
2438
+ if (axis === "horizontal") {
413
2439
  return {
414
- left: perp - thickness / 2,
415
- right: perp + thickness / 2,
416
- top: start,
417
- bottom: end,
418
- width: thickness,
419
- height: end - start
2440
+ left: start,
2441
+ right: end,
2442
+ top: perp - thickness / 2,
2443
+ bottom: perp + thickness / 2,
2444
+ width: end - start,
2445
+ height: thickness
420
2446
  };
2447
+ }
2448
+ return {
2449
+ left: perp - thickness / 2,
2450
+ right: perp + thickness / 2,
2451
+ top: start,
2452
+ bottom: end,
2453
+ width: thickness,
2454
+ height: end - start
2455
+ };
421
2456
  }
422
- // Contributions are in geometric order, so stacking their values from the gap's
423
- // start edge places each strip where that spacing actually renders. The array
424
- // is index-aligned with measurement.contributions (null for sub-pixel strips).
425
2457
  function contributionStrips(measurement, geometry) {
426
- const strips = [];
427
- let cursor = geometry.start;
428
- for (const contribution of measurement.contributions) {
429
- const next = Math.min(cursor + contribution.valuePx, geometry.end);
430
- strips.push(next - cursor > 0.1
431
- ? rectFromBand(measurement.axis, cursor, next, geometry.perp, GAP_BAND_THICKNESS)
432
- : null);
433
- cursor = next;
434
- }
435
- const remainder = geometry.end - cursor > 0.1
436
- ? rectFromBand(measurement.axis, cursor, geometry.end, geometry.perp, GAP_BAND_THICKNESS)
437
- : null;
438
- return { strips, remainder };
439
- }
440
- // The hovered contribution's slice along the axis, extended across its
441
- // element's full perpendicular extent — the actual spacing region (a padding
442
- // slice spans the container's full height, a margin slice its element's, etc.).
443
- // Falls back to a thickened band slice when there is no live element (e.g. the
444
- // unattributed remainder).
2458
+ const strips = [];
2459
+ let cursor = geometry.start;
2460
+ for (const contribution of measurement.contributions) {
2461
+ const next = Math.min(cursor + contribution.valuePx, geometry.end);
2462
+ strips.push(
2463
+ next - cursor > 0.1 ? rectFromBand(measurement.axis, cursor, next, geometry.perp, GAP_BAND_THICKNESS) : null
2464
+ );
2465
+ cursor = next;
2466
+ }
2467
+ const remainder = geometry.end - cursor > 0.1 ? rectFromBand(measurement.axis, cursor, geometry.end, geometry.perp, GAP_BAND_THICKNESS) : null;
2468
+ return { strips, remainder };
2469
+ }
445
2470
  function hoverHighlightRect(axis, strip, hover) {
446
- const blockRect = hover.element ? liveRect(hover.element) : null;
447
- if (!blockRect) {
448
- return expandAcross(strip, axis, 4);
449
- }
450
- return axis === "horizontal"
451
- ? {
452
- left: strip.left,
453
- right: strip.right,
454
- top: blockRect.top,
455
- bottom: blockRect.bottom,
456
- width: strip.width,
457
- height: blockRect.bottom - blockRect.top
458
- }
459
- : {
460
- left: blockRect.left,
461
- right: blockRect.right,
462
- top: strip.top,
463
- bottom: strip.bottom,
464
- width: blockRect.right - blockRect.left,
465
- height: strip.height
466
- };
2471
+ const blockRect = hover.element ? liveRect(hover.element) : null;
2472
+ if (!blockRect) {
2473
+ return expandAcross(strip, axis, 4);
2474
+ }
2475
+ return axis === "horizontal" ? {
2476
+ left: strip.left,
2477
+ right: strip.right,
2478
+ top: blockRect.top,
2479
+ bottom: blockRect.bottom,
2480
+ width: strip.width,
2481
+ height: blockRect.bottom - blockRect.top
2482
+ } : {
2483
+ left: blockRect.left,
2484
+ right: blockRect.right,
2485
+ top: strip.top,
2486
+ bottom: strip.bottom,
2487
+ width: blockRect.right - blockRect.left,
2488
+ height: strip.height
2489
+ };
467
2490
  }
468
2491
  function expandAcross(rect, axis, amount) {
469
- return axis === "horizontal"
470
- ? { ...rect, top: rect.top - amount, bottom: rect.bottom + amount, height: rect.height + amount * 2 }
471
- : { ...rect, left: rect.left - amount, right: rect.right + amount, width: rect.width + amount * 2 };
2492
+ return axis === "horizontal" ? { ...rect, top: rect.top - amount, bottom: rect.bottom + amount, height: rect.height + amount * 2 } : { ...rect, left: rect.left - amount, right: rect.right + amount, width: rect.width + amount * 2 };
472
2493
  }
473
- function PointInspectionRects({ inspection, snapshot }) {
474
- const kindClass = pointKindClass(inspection.region);
475
- return (_jsxs(_Fragment, { children: [snapshot.from ? _jsx("rect", { className: "gi-element-box gi-committed-box", ...svgRect(snapshot.from) }) : null, snapshot.to ? _jsx("rect", { className: "gi-element-box gi-committed-box", ...svgRect(snapshot.to) }) : null, snapshot.element ? _jsx("rect", { className: "gi-element-box gi-committed-box", ...svgRect(snapshot.element) }) : null, snapshot.rect ? _jsx("rect", { className: `gi-point-region ${kindClass}`, ...svgRect(snapshot.rect) }) : null, _jsx("circle", { className: `gi-point-dot ${kindClass}`, cx: inspection.point.x, cy: inspection.point.y, r: 4 })] }));
2494
+ function PointInspectionRects({
2495
+ inspection,
2496
+ snapshot
2497
+ }) {
2498
+ const kindClass = pointKindClass(inspection.region);
2499
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2500
+ snapshot.from ? /* @__PURE__ */ jsx("rect", { className: "gi-element-box gi-committed-box", ...svgRect(snapshot.from) }) : null,
2501
+ snapshot.to ? /* @__PURE__ */ jsx("rect", { className: "gi-element-box gi-committed-box", ...svgRect(snapshot.to) }) : null,
2502
+ snapshot.element ? /* @__PURE__ */ jsx("rect", { className: "gi-element-box gi-committed-box", ...svgRect(snapshot.element) }) : null,
2503
+ snapshot.rect ? /* @__PURE__ */ jsx("rect", { className: `gi-point-region ${kindClass}`, ...svgRect(snapshot.rect) }) : null,
2504
+ /* @__PURE__ */ jsx("circle", { className: `gi-point-dot ${kindClass}`, cx: inspection.point.x, cy: inspection.point.y, r: 4 })
2505
+ ] });
476
2506
  }
477
2507
  function useLivePointSnapshot(inspection) {
478
- return useLiveSnapshot(inspection, buildPointSnapshot);
2508
+ return useLiveSnapshot(inspection, buildPointSnapshot);
479
2509
  }
480
2510
  function buildPointSnapshot(inspection) {
481
- const rect = inspection.rect;
482
- const element = inspection.element ? liveRect(inspection.element) ?? undefined : undefined;
483
- const from = inspection.from ? liveRect(inspection.from) ?? undefined : undefined;
484
- const to = inspection.to ? liveRect(inspection.to) ?? undefined : undefined;
485
- const key = [
486
- rect ? rectKey(rect) : "",
487
- element ? rectKey(element) : "",
488
- from ? rectKey(from) : "",
489
- to ? rectKey(to) : ""
490
- ].join("::");
491
- if (!rect && !element && !from && !to) {
492
- return null;
493
- }
494
- return { key, rect, element, from, to };
2511
+ const rect = inspection.rect;
2512
+ const element = inspection.element ? liveRect(inspection.element) ?? void 0 : void 0;
2513
+ const from = inspection.from ? liveRect(inspection.from) ?? void 0 : void 0;
2514
+ const to = inspection.to ? liveRect(inspection.to) ?? void 0 : void 0;
2515
+ const key = [
2516
+ rect ? rectKey(rect) : "",
2517
+ element ? rectKey(element) : "",
2518
+ from ? rectKey(from) : "",
2519
+ to ? rectKey(to) : ""
2520
+ ].join("::");
2521
+ if (!rect && !element && !from && !to) {
2522
+ return null;
2523
+ }
2524
+ return { key, rect, element, from, to };
495
2525
  }
496
2526
  function useLiveOverlaySnapshot(measurement) {
497
- return useLiveSnapshot(measurement, buildOverlaySnapshot);
2527
+ return useLiveSnapshot(measurement, buildOverlaySnapshot);
498
2528
  }
499
- // Snapshots are in document coordinates, so page scroll moves the overlay natively
500
- // (no recompute needed for correctness there). Scroll events still trigger a recompute
501
- // because nested scrollers change clipping and sticky/fixed elements move in document
502
- // space; the key diff drops the update when nothing actually moved.
503
2529
  function useLiveSnapshot(input, build) {
504
- const [snapshot, setSnapshot] = useState(null);
505
- useEffect(() => {
506
- if (!input) {
507
- setSnapshot(null);
508
- return;
2530
+ const [snapshot, setSnapshot] = useState(null);
2531
+ useEffect(() => {
2532
+ if (!input) {
2533
+ setSnapshot(null);
2534
+ return;
2535
+ }
2536
+ let frame = 0;
2537
+ let lastKey = null;
2538
+ const recompute = () => {
2539
+ frame = 0;
2540
+ const nextSnapshot = build(input);
2541
+ if (!nextSnapshot) {
2542
+ if (lastKey !== "") {
2543
+ lastKey = "";
2544
+ setSnapshot(null);
509
2545
  }
510
- let frame = 0;
511
- // null = nothing emitted yet; "" = an empty result has been emitted.
512
- let lastKey = null;
513
- const recompute = () => {
514
- frame = 0;
515
- const nextSnapshot = build(input);
516
- if (!nextSnapshot) {
517
- if (lastKey !== "") {
518
- lastKey = "";
519
- setSnapshot(null);
520
- }
521
- return;
522
- }
523
- if (nextSnapshot.key !== lastKey) {
524
- lastKey = nextSnapshot.key;
525
- setSnapshot(nextSnapshot);
526
- }
527
- };
528
- const schedule = () => {
529
- if (!frame) {
530
- frame = requestAnimationFrame(recompute);
531
- }
532
- };
533
- recompute();
534
- const resizeObserver = new ResizeObserver(schedule);
535
- resizeObserver.observe(document.documentElement);
536
- resizeObserver.observe(document.body);
537
- const mutationObserver = new MutationObserver((records) => {
538
- if (records.every(isInspectorMutation)) {
539
- return;
540
- }
541
- schedule();
542
- });
543
- mutationObserver.observe(document.body, { attributes: true, childList: true, subtree: true });
544
- window.addEventListener("scroll", schedule, { capture: true, passive: true });
545
- window.addEventListener("resize", schedule);
546
- window.addEventListener("transitionend", schedule, true);
547
- window.addEventListener("animationend", schedule, true);
548
- return () => {
549
- if (frame) {
550
- cancelAnimationFrame(frame);
551
- }
552
- resizeObserver.disconnect();
553
- mutationObserver.disconnect();
554
- window.removeEventListener("scroll", schedule, true);
555
- window.removeEventListener("resize", schedule);
556
- window.removeEventListener("transitionend", schedule, true);
557
- window.removeEventListener("animationend", schedule, true);
558
- };
559
- }, [input, build]);
560
- return snapshot;
2546
+ return;
2547
+ }
2548
+ if (nextSnapshot.key !== lastKey) {
2549
+ lastKey = nextSnapshot.key;
2550
+ setSnapshot(nextSnapshot);
2551
+ }
2552
+ };
2553
+ const schedule = () => {
2554
+ if (!frame) {
2555
+ frame = requestAnimationFrame(recompute);
2556
+ }
2557
+ };
2558
+ recompute();
2559
+ const resizeObserver = new ResizeObserver(schedule);
2560
+ resizeObserver.observe(document.documentElement);
2561
+ resizeObserver.observe(document.body);
2562
+ const mutationObserver = new MutationObserver((records) => {
2563
+ if (records.every(isInspectorMutation)) {
2564
+ return;
2565
+ }
2566
+ schedule();
2567
+ });
2568
+ mutationObserver.observe(document.body, { attributes: true, childList: true, subtree: true });
2569
+ window.addEventListener("scroll", schedule, { capture: true, passive: true });
2570
+ window.addEventListener("resize", schedule);
2571
+ window.addEventListener("transitionend", schedule, true);
2572
+ window.addEventListener("animationend", schedule, true);
2573
+ return () => {
2574
+ if (frame) {
2575
+ cancelAnimationFrame(frame);
2576
+ }
2577
+ resizeObserver.disconnect();
2578
+ mutationObserver.disconnect();
2579
+ window.removeEventListener("scroll", schedule, true);
2580
+ window.removeEventListener("resize", schedule);
2581
+ window.removeEventListener("transitionend", schedule, true);
2582
+ window.removeEventListener("animationend", schedule, true);
2583
+ };
2584
+ }, [input, build]);
2585
+ return snapshot;
561
2586
  }
562
2587
  function buildOverlaySnapshot(measurement) {
563
- const from = liveRect(measurement.from);
564
- const to = liveRect(measurement.to);
565
- if (!from || !to) {
566
- return null;
567
- }
568
- const occupiedKeys = new Set([rectKey(from), rectKey(to)]);
569
- const series = repeatedSeriesRects(measurement, occupiedKeys);
570
- const key = [
571
- rectKey(from),
572
- rectKey(to),
573
- series.map((item) => `${item.kind}:${rectKey(item.rect)}`).join("|")
574
- ].join("::");
575
- return { key, from, to, series };
2588
+ const from = liveRect(measurement.from);
2589
+ const to = liveRect(measurement.to);
2590
+ if (!from || !to) {
2591
+ return null;
2592
+ }
2593
+ const occupiedKeys = /* @__PURE__ */ new Set([rectKey(from), rectKey(to)]);
2594
+ const series = repeatedSeriesRects(measurement, occupiedKeys);
2595
+ const key = [
2596
+ rectKey(from),
2597
+ rectKey(to),
2598
+ series.map((item) => `${item.kind}:${rectKey(item.rect)}`).join("|")
2599
+ ].join("::");
2600
+ return { key, from, to, series };
576
2601
  }
577
- const SERIES_SCAN_LIMIT = 80;
578
- const SERIES_RENDER_LIMIT = 28;
2602
+ var SERIES_SCAN_LIMIT = 80;
2603
+ var SERIES_RENDER_LIMIT = 28;
579
2604
  function repeatedSeriesRects(measurement, occupiedKeys) {
580
- const rects = [];
581
- const seen = new Set();
582
- for (const contribution of measurement.contributions) {
583
- const element = liveElement(contribution.element);
584
- if (!element) {
585
- continue;
586
- }
587
- const candidates = contribution.kind === "gap"
588
- ? gapSeriesRects(measurement.axis, element, contribution.kind, occupiedKeys)
589
- : siblingSeriesRects(element, contribution.kind, contribution.property, contribution.cssValue, occupiedKeys);
590
- for (const candidate of candidates) {
591
- const key = `${candidate.kind}:${rectKey(candidate.rect)}`;
592
- if (seen.has(key)) {
593
- continue;
594
- }
595
- seen.add(key);
596
- rects.push(candidate);
597
- if (rects.length >= SERIES_RENDER_LIMIT) {
598
- return rects;
599
- }
600
- }
2605
+ const rects = [];
2606
+ const seen = /* @__PURE__ */ new Set();
2607
+ for (const contribution of measurement.contributions) {
2608
+ const element = liveElement(contribution.element);
2609
+ if (!element) {
2610
+ continue;
601
2611
  }
602
- return rects;
2612
+ const candidates = contribution.kind === "gap" ? gapSeriesRects(measurement.axis, element, contribution.kind, occupiedKeys) : siblingSeriesRects(element, contribution.kind, contribution.property, contribution.cssValue, occupiedKeys);
2613
+ for (const candidate of candidates) {
2614
+ const key = `${candidate.kind}:${rectKey(candidate.rect)}`;
2615
+ if (seen.has(key)) {
2616
+ continue;
2617
+ }
2618
+ seen.add(key);
2619
+ rects.push(candidate);
2620
+ if (rects.length >= SERIES_RENDER_LIMIT) {
2621
+ return rects;
2622
+ }
2623
+ }
2624
+ }
2625
+ return rects;
603
2626
  }
604
2627
  function siblingSeriesRects(element, kind, property, cssValue, occupiedKeys) {
605
- if (kind === "unknown") {
606
- return [];
2628
+ if (kind === "unknown") {
2629
+ return [];
2630
+ }
2631
+ const parent = element.parentElement;
2632
+ const cssProperty = normalizeCssProperty(property);
2633
+ if (!parent || !cssProperty) {
2634
+ return [];
2635
+ }
2636
+ const style = getComputedStyle(element);
2637
+ const expectedValue = normalizeCssValue(cssValue ?? style.getPropertyValue(cssProperty));
2638
+ if (!expectedValue) {
2639
+ return [];
2640
+ }
2641
+ const signature = elementSeriesSignature(element);
2642
+ const candidates = siblingWindow(parent, element);
2643
+ const rects = [];
2644
+ for (const candidate of candidates) {
2645
+ if (candidate === element || elementSeriesSignature(candidate) !== signature) {
2646
+ continue;
607
2647
  }
608
- const parent = element.parentElement;
609
- const cssProperty = normalizeCssProperty(property);
610
- if (!parent || !cssProperty) {
611
- return [];
2648
+ const candidateValue = normalizeCssValue(getComputedStyle(candidate).getPropertyValue(cssProperty));
2649
+ if (candidateValue !== expectedValue) {
2650
+ continue;
612
2651
  }
613
- const style = getComputedStyle(element);
614
- const expectedValue = normalizeCssValue(cssValue ?? style.getPropertyValue(cssProperty));
615
- if (!expectedValue) {
616
- return [];
2652
+ const rect = visibleRectForElement(candidate);
2653
+ if (!rect || occupiedKeys.has(rectKey(rect))) {
2654
+ continue;
617
2655
  }
618
- const signature = elementSeriesSignature(element);
619
- const candidates = siblingWindow(parent, element);
620
- const rects = [];
621
- for (const candidate of candidates) {
622
- if (candidate === element || elementSeriesSignature(candidate) !== signature) {
623
- continue;
624
- }
625
- const candidateValue = normalizeCssValue(getComputedStyle(candidate).getPropertyValue(cssProperty));
626
- if (candidateValue !== expectedValue) {
627
- continue;
628
- }
629
- const rect = visibleRectForElement(candidate);
630
- if (!rect || occupiedKeys.has(rectKey(rect))) {
631
- continue;
632
- }
633
- rects.push({ kind, rect });
634
- if (rects.length >= SERIES_RENDER_LIMIT) {
635
- break;
636
- }
2656
+ rects.push({ kind, rect });
2657
+ if (rects.length >= SERIES_RENDER_LIMIT) {
2658
+ break;
637
2659
  }
638
- return rects;
2660
+ }
2661
+ return rects;
639
2662
  }
640
2663
  function gapSeriesRects(axis, container, kind, occupiedKeys) {
641
- const children = Array.from(container.children)
642
- .slice(0, SERIES_SCAN_LIMIT)
643
- .map((element) => ({ element, rect: visibleRectForElement(element) }))
644
- .filter((item) => Boolean(item.rect));
645
- if (children.length < 2) {
646
- return [];
647
- }
648
- const rects = [];
649
- const sorted = children.sort((a, b) => axisStart(axis, a.rect) - axisStart(axis, b.rect));
650
- for (const child of sorted) {
651
- if (!occupiedKeys.has(rectKey(child.rect))) {
652
- rects.push({ kind, rect: child.rect });
653
- }
654
- if (rects.length >= SERIES_RENDER_LIMIT) {
655
- return rects;
656
- }
2664
+ const children = Array.from(container.children).slice(0, SERIES_SCAN_LIMIT).map((element) => ({ element, rect: visibleRectForElement(element) })).filter((item) => Boolean(item.rect));
2665
+ if (children.length < 2) {
2666
+ return [];
2667
+ }
2668
+ const rects = [];
2669
+ const sorted = children.sort((a, b) => axisStart(axis, a.rect) - axisStart(axis, b.rect));
2670
+ for (const child of sorted) {
2671
+ if (!occupiedKeys.has(rectKey(child.rect))) {
2672
+ rects.push({ kind, rect: child.rect });
657
2673
  }
658
- for (let index = 0; index < sorted.length - 1; index += 1) {
659
- const before = sorted[index].rect;
660
- const after = sorted[index + 1].rect;
661
- const band = repeatedGapBandRect(axis, before, after);
662
- if (!band || occupiedKeys.has(rectKey(band))) {
663
- continue;
664
- }
665
- rects.push({ kind, rect: band });
666
- if (rects.length >= SERIES_RENDER_LIMIT) {
667
- break;
668
- }
2674
+ if (rects.length >= SERIES_RENDER_LIMIT) {
2675
+ return rects;
2676
+ }
2677
+ }
2678
+ for (let index = 0; index < sorted.length - 1; index += 1) {
2679
+ const before = sorted[index].rect;
2680
+ const after = sorted[index + 1].rect;
2681
+ const band = repeatedGapBandRect(axis, before, after);
2682
+ if (!band || occupiedKeys.has(rectKey(band))) {
2683
+ continue;
2684
+ }
2685
+ rects.push({ kind, rect: band });
2686
+ if (rects.length >= SERIES_RENDER_LIMIT) {
2687
+ break;
669
2688
  }
670
- return rects;
2689
+ }
2690
+ return rects;
671
2691
  }
672
2692
  function siblingWindow(parent, element) {
673
- const siblings = Array.from(parent.children);
674
- const index = siblings.indexOf(element);
675
- if (index === -1 || siblings.length <= SERIES_SCAN_LIMIT) {
676
- return siblings.slice(0, SERIES_SCAN_LIMIT);
677
- }
678
- const halfWindow = Math.floor(SERIES_SCAN_LIMIT / 2);
679
- const start = Math.max(0, Math.min(index - halfWindow, siblings.length - SERIES_SCAN_LIMIT));
680
- return siblings.slice(start, start + SERIES_SCAN_LIMIT);
2693
+ const siblings = Array.from(parent.children);
2694
+ const index = siblings.indexOf(element);
2695
+ if (index === -1 || siblings.length <= SERIES_SCAN_LIMIT) {
2696
+ return siblings.slice(0, SERIES_SCAN_LIMIT);
2697
+ }
2698
+ const halfWindow = Math.floor(SERIES_SCAN_LIMIT / 2);
2699
+ const start = Math.max(0, Math.min(index - halfWindow, siblings.length - SERIES_SCAN_LIMIT));
2700
+ return siblings.slice(start, start + SERIES_SCAN_LIMIT);
681
2701
  }
682
2702
  function normalizeCssProperty(property) {
683
- const normalized = property.trim().replace(/\s+/g, "-");
684
- if (!normalized || normalized.includes("layout") || normalized.includes("space-between")) {
685
- return null;
686
- }
687
- return normalized;
2703
+ const normalized = property.trim().replace(/\s+/g, "-");
2704
+ if (!normalized || normalized.includes("layout") || normalized.includes("space-between")) {
2705
+ return null;
2706
+ }
2707
+ return normalized;
688
2708
  }
689
2709
  function normalizeCssValue(value) {
690
- const normalized = value?.trim();
691
- return normalized && normalized !== "normal" && normalized !== "auto" ? normalized : null;
2710
+ const normalized = value?.trim();
2711
+ return normalized && normalized !== "normal" && normalized !== "auto" ? normalized : null;
692
2712
  }
693
2713
  function elementSeriesSignature(element) {
694
- return [
695
- element.tagName.toLowerCase(),
696
- Array.from(element.classList).sort().join(".")
697
- ].join(":");
2714
+ return [
2715
+ element.tagName.toLowerCase(),
2716
+ Array.from(element.classList).sort().join(".")
2717
+ ].join(":");
698
2718
  }
699
2719
  function axisStart(axis, rect) {
700
- return axis === "horizontal" ? rect.left : rect.top;
2720
+ return axis === "horizontal" ? rect.left : rect.top;
701
2721
  }
702
2722
  function repeatedGapBandRect(axis, before, after) {
703
- if (axis === "horizontal") {
704
- const left = before.right;
705
- const right = after.left;
706
- const top = Math.max(before.top, after.top);
707
- const bottom = Math.min(before.bottom, after.bottom);
708
- return right > left && bottom > top
709
- ? { left, right, top, bottom, width: roundPx(right - left), height: roundPx(bottom - top) }
710
- : null;
711
- }
712
- const top = before.bottom;
713
- const bottom = after.top;
714
- const left = Math.max(before.left, after.left);
715
- const right = Math.min(before.right, after.right);
716
- return bottom > top && right > left
717
- ? { left, right, top, bottom, width: roundPx(right - left), height: roundPx(bottom - top) }
718
- : null;
2723
+ if (axis === "horizontal") {
2724
+ const left2 = before.right;
2725
+ const right2 = after.left;
2726
+ const top2 = Math.max(before.top, after.top);
2727
+ const bottom2 = Math.min(before.bottom, after.bottom);
2728
+ return right2 > left2 && bottom2 > top2 ? { left: left2, right: right2, top: top2, bottom: bottom2, width: roundPx2(right2 - left2), height: roundPx2(bottom2 - top2) } : null;
2729
+ }
2730
+ const top = before.bottom;
2731
+ const bottom = after.top;
2732
+ const left = Math.max(before.left, after.left);
2733
+ const right = Math.min(before.right, after.right);
2734
+ return bottom > top && right > left ? { left, right, top, bottom, width: roundPx2(right - left), height: roundPx2(bottom - top) } : null;
719
2735
  }
720
2736
  function liveElement(info) {
721
- if (info.element?.isConnected) {
722
- return info.element;
723
- }
724
- // The stored node was removed (e.g. a framework re-render replaced it).
725
- // Re-resolve through the unique selector and drop the detached reference so
726
- // the old subtree can be garbage collected.
727
- const resolved = resolveSelector(info.selector);
728
- info.element = resolved ?? undefined;
729
- return resolved;
2737
+ if (info.element?.isConnected) {
2738
+ return info.element;
2739
+ }
2740
+ const resolved = resolveSelector(info.selector);
2741
+ info.element = resolved ?? void 0;
2742
+ return resolved;
730
2743
  }
731
2744
  function liveRect(info) {
732
- const element = liveElement(info);
733
- if (!element) {
734
- return null;
735
- }
736
- return visibleRectForElement(element);
2745
+ const element = liveElement(info);
2746
+ if (!element) {
2747
+ return null;
2748
+ }
2749
+ return visibleRectForElement(element);
737
2750
  }
738
2751
  function visibleRectForElement(element) {
739
- let rect = rectFromDomRect(element.getBoundingClientRect());
2752
+ let rect = rectFromDomRect(element.getBoundingClientRect());
2753
+ if (rect.width <= 0 || rect.height <= 0) {
2754
+ return null;
2755
+ }
2756
+ for (const ancestor of scrollAncestors(element)) {
2757
+ rect = intersectRects(rect, rectFromDomRect(ancestor.getBoundingClientRect()));
740
2758
  if (rect.width <= 0 || rect.height <= 0) {
741
- return null;
2759
+ return null;
742
2760
  }
743
- for (const ancestor of scrollAncestors(element)) {
744
- rect = intersectRects(rect, rectFromDomRect(ancestor.getBoundingClientRect()));
745
- if (rect.width <= 0 || rect.height <= 0) {
746
- return null;
747
- }
748
- }
749
- return offsetRect(rect, window.scrollX, window.scrollY);
2761
+ }
2762
+ return offsetRect(rect, window.scrollX, window.scrollY);
750
2763
  }
751
2764
  function offsetRect(rect, dx, dy) {
752
- return {
753
- top: roundPx(rect.top + dy),
754
- right: roundPx(rect.right + dx),
755
- bottom: roundPx(rect.bottom + dy),
756
- left: roundPx(rect.left + dx),
757
- width: rect.width,
758
- height: rect.height
759
- };
2765
+ return {
2766
+ top: roundPx2(rect.top + dy),
2767
+ right: roundPx2(rect.right + dx),
2768
+ bottom: roundPx2(rect.bottom + dy),
2769
+ left: roundPx2(rect.left + dx),
2770
+ width: rect.width,
2771
+ height: rect.height
2772
+ };
760
2773
  }
761
2774
  function rectFromDomRect(rect) {
762
- return {
763
- top: roundPx(rect.top),
764
- right: roundPx(rect.right),
765
- bottom: roundPx(rect.bottom),
766
- left: roundPx(rect.left),
767
- width: roundPx(rect.width),
768
- height: roundPx(rect.height)
769
- };
2775
+ return {
2776
+ top: roundPx2(rect.top),
2777
+ right: roundPx2(rect.right),
2778
+ bottom: roundPx2(rect.bottom),
2779
+ left: roundPx2(rect.left),
2780
+ width: roundPx2(rect.width),
2781
+ height: roundPx2(rect.height)
2782
+ };
770
2783
  }
771
2784
  function scrollAncestors(element) {
772
- const ancestors = [];
773
- let current = element.parentElement;
774
- while (current && current !== document.body && current !== document.documentElement) {
775
- const style = getComputedStyle(current);
776
- const overflow = `${style.overflow}${style.overflowX}${style.overflowY}`;
777
- if (/(auto|scroll|clip|hidden)/.test(overflow)) {
778
- ancestors.push(current);
779
- }
780
- current = current.parentElement;
2785
+ const ancestors = [];
2786
+ let current = element.parentElement;
2787
+ while (current && current !== document.body && current !== document.documentElement) {
2788
+ const style = getComputedStyle(current);
2789
+ const overflow = `${style.overflow}${style.overflowX}${style.overflowY}`;
2790
+ if (/(auto|scroll|clip|hidden)/.test(overflow)) {
2791
+ ancestors.push(current);
781
2792
  }
782
- return ancestors;
2793
+ current = current.parentElement;
2794
+ }
2795
+ return ancestors;
783
2796
  }
784
2797
  function intersectRects(a, b) {
785
- const left = Math.max(a.left, b.left);
786
- const right = Math.min(a.right, b.right);
787
- const top = Math.max(a.top, b.top);
788
- const bottom = Math.min(a.bottom, b.bottom);
789
- return {
790
- top: roundPx(top),
791
- right: roundPx(right),
792
- bottom: roundPx(bottom),
793
- left: roundPx(left),
794
- width: roundPx(Math.max(0, right - left)),
795
- height: roundPx(Math.max(0, bottom - top))
796
- };
2798
+ const left = Math.max(a.left, b.left);
2799
+ const right = Math.min(a.right, b.right);
2800
+ const top = Math.max(a.top, b.top);
2801
+ const bottom = Math.min(a.bottom, b.bottom);
2802
+ return {
2803
+ top: roundPx2(top),
2804
+ right: roundPx2(right),
2805
+ bottom: roundPx2(bottom),
2806
+ left: roundPx2(left),
2807
+ width: roundPx2(Math.max(0, right - left)),
2808
+ height: roundPx2(Math.max(0, bottom - top))
2809
+ };
797
2810
  }
798
2811
  function pointKindClass(region) {
799
- switch (region) {
800
- case "margin":
801
- return "gi-kind-margin";
802
- case "padding":
803
- return "gi-kind-padding";
804
- case "border":
805
- return "gi-kind-border";
806
- case "gap":
807
- return "gi-kind-gap";
808
- case "content":
809
- return "gi-kind-content";
810
- default:
811
- return "gi-kind-unknown";
812
- }
2812
+ switch (region) {
2813
+ case "margin":
2814
+ return "gi-kind-margin";
2815
+ case "padding":
2816
+ return "gi-kind-padding";
2817
+ case "border":
2818
+ return "gi-kind-border";
2819
+ case "gap":
2820
+ return "gi-kind-gap";
2821
+ case "content":
2822
+ return "gi-kind-content";
2823
+ default:
2824
+ return "gi-kind-unknown";
2825
+ }
813
2826
  }
814
2827
  function rectKey(rect) {
815
- return [rect.left, rect.top, rect.width, rect.height].join(":");
2828
+ return [rect.left, rect.top, rect.width, rect.height].join(":");
816
2829
  }
817
2830
  function edgeMarkerRect(axis, rect, side) {
818
- const thickness = 3;
819
- if (axis === "horizontal") {
820
- const left = side === "from" ? rect.right - thickness : rect.left;
821
- return {
822
- left,
823
- right: left + thickness,
824
- top: rect.top,
825
- bottom: rect.bottom,
826
- width: thickness,
827
- height: rect.height
828
- };
829
- }
830
- const top = side === "from" ? rect.bottom - thickness : rect.top;
2831
+ const thickness = 3;
2832
+ if (axis === "horizontal") {
2833
+ const left = side === "from" ? rect.right - thickness : rect.left;
831
2834
  return {
832
- left: rect.left,
833
- right: rect.right,
834
- top,
835
- bottom: top + thickness,
836
- width: rect.width,
837
- height: thickness
2835
+ left,
2836
+ right: left + thickness,
2837
+ top: rect.top,
2838
+ bottom: rect.bottom,
2839
+ width: thickness,
2840
+ height: rect.height
838
2841
  };
2842
+ }
2843
+ const top = side === "from" ? rect.bottom - thickness : rect.top;
2844
+ return {
2845
+ left: rect.left,
2846
+ right: rect.right,
2847
+ top,
2848
+ bottom: top + thickness,
2849
+ width: rect.width,
2850
+ height: thickness
2851
+ };
839
2852
  }
840
2853
  function internalEdgeMarkerRect(axis, rect, side, role) {
841
- const thickness = 3;
842
- if (axis === "horizontal") {
843
- const edge = side === "before"
844
- ? role === "container" ? rect.left : rect.left
845
- : role === "container" ? rect.right - thickness : rect.right - thickness;
846
- return {
847
- left: edge,
848
- right: edge + thickness,
849
- top: rect.top,
850
- bottom: rect.bottom,
851
- width: thickness,
852
- height: rect.height
853
- };
854
- }
855
- const edge = side === "before"
856
- ? role === "container" ? rect.top : rect.top
857
- : role === "container" ? rect.bottom - thickness : rect.bottom - thickness;
2854
+ const thickness = 3;
2855
+ if (axis === "horizontal") {
2856
+ const edge2 = side === "before" ? role === "container" ? rect.left : rect.left : role === "container" ? rect.right - thickness : rect.right - thickness;
858
2857
  return {
859
- left: rect.left,
860
- right: rect.right,
861
- top: edge,
862
- bottom: edge + thickness,
863
- width: rect.width,
864
- height: thickness
2858
+ left: edge2,
2859
+ right: edge2 + thickness,
2860
+ top: rect.top,
2861
+ bottom: rect.bottom,
2862
+ width: thickness,
2863
+ height: rect.height
865
2864
  };
2865
+ }
2866
+ const edge = side === "before" ? role === "container" ? rect.top : rect.top : role === "container" ? rect.bottom - thickness : rect.bottom - thickness;
2867
+ return {
2868
+ left: rect.left,
2869
+ right: rect.right,
2870
+ top: edge,
2871
+ bottom: edge + thickness,
2872
+ width: rect.width,
2873
+ height: thickness
2874
+ };
866
2875
  }
867
2876
  function svgRect(rect) {
868
- return {
869
- x: rect.left,
870
- y: rect.top,
871
- width: rect.width,
872
- height: rect.height
873
- };
2877
+ return {
2878
+ x: rect.left,
2879
+ y: rect.top,
2880
+ width: rect.width,
2881
+ height: rect.height
2882
+ };
874
2883
  }
875
2884
  function isInspectorMutation(record) {
876
- const target = record.target instanceof Element ? record.target : record.target.parentElement;
877
- return Boolean(target?.closest(".gi-root, .gi-svg"));
2885
+ const target = record.target instanceof Element ? record.target : record.target.parentElement;
2886
+ return Boolean(target?.closest(".gi-root, .gi-svg"));
878
2887
  }
879
2888
  function clampToViewport(x, y, target) {
880
- const rect = target.getBoundingClientRect();
881
- const margin = 8;
882
- return {
883
- x: Math.min(Math.max(x, margin), Math.max(margin, window.innerWidth - rect.width - margin)),
884
- y: Math.min(Math.max(y, margin), Math.max(margin, window.innerHeight - rect.height - margin))
885
- };
2889
+ const rect = target.getBoundingClientRect();
2890
+ const margin = 8;
2891
+ return {
2892
+ x: Math.min(Math.max(x, margin), Math.max(margin, window.innerWidth - rect.width - margin)),
2893
+ y: Math.min(Math.max(y, margin), Math.max(margin, window.innerHeight - rect.height - margin))
2894
+ };
886
2895
  }
887
2896
  function pointFromEvent(event) {
888
- return {
889
- x: event.clientX,
890
- y: event.clientY
891
- };
2897
+ return {
2898
+ x: event.clientX,
2899
+ y: event.clientY
2900
+ };
892
2901
  }
893
2902
  function toDocumentPoint(point) {
894
- return {
895
- x: point.x + window.scrollX,
896
- y: point.y + window.scrollY
897
- };
2903
+ return {
2904
+ x: point.x + window.scrollX,
2905
+ y: point.y + window.scrollY
2906
+ };
898
2907
  }
899
2908
  function toDocumentInspection(inspection) {
900
- return {
901
- ...inspection,
902
- point: toDocumentPoint(inspection.point),
903
- rect: inspection.rect ? offsetRect(inspection.rect, window.scrollX, window.scrollY) : inspection.rect
904
- };
2909
+ return {
2910
+ ...inspection,
2911
+ point: toDocumentPoint(inspection.point),
2912
+ rect: inspection.rect ? offsetRect(inspection.rect, window.scrollX, window.scrollY) : inspection.rect
2913
+ };
905
2914
  }
906
2915
  function dragDistance(drag) {
907
- return Math.hypot(drag.end.x - drag.start.x, drag.end.y - drag.start.y);
2916
+ return Math.hypot(drag.end.x - drag.start.x, drag.end.y - drag.start.y);
908
2917
  }
909
2918
  function lineFromDrag(axis, drag) {
910
- const start = toDocumentPoint(drag.start);
911
- const end = toDocumentPoint(drag.end);
912
- const startAlong = axis === "horizontal" ? start.x : start.y;
913
- const endAlong = axis === "horizontal" ? end.x : end.y;
914
- const perp = axis === "horizontal"
915
- ? (start.y + end.y) / 2
916
- : (start.x + end.x) / 2;
917
- return {
918
- min: Math.min(startAlong, endAlong),
919
- max: Math.max(startAlong, endAlong),
920
- perp
921
- };
2919
+ const start = toDocumentPoint(drag.start);
2920
+ const end = toDocumentPoint(drag.end);
2921
+ const startAlong = axis === "horizontal" ? start.x : start.y;
2922
+ const endAlong = axis === "horizontal" ? end.x : end.y;
2923
+ const perp = axis === "horizontal" ? (start.y + end.y) / 2 : (start.x + end.x) / 2;
2924
+ return {
2925
+ min: Math.min(startAlong, endAlong),
2926
+ max: Math.max(startAlong, endAlong),
2927
+ perp
2928
+ };
922
2929
  }
923
- function formatPx(value) {
924
- const rounded = roundPx(value);
925
- return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}px`;
2930
+ function formatPx2(value) {
2931
+ const rounded = roundPx2(value);
2932
+ return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}px`;
926
2933
  }
927
- function roundPx(value) {
928
- const nearest = Math.round(value);
929
- // Snap subpixel rendering noise (7.992px at fractional DPRs/zoom) to the integer.
930
- if (Math.abs(value - nearest) < 0.15) {
931
- return nearest;
932
- }
933
- return Math.round(value * 10) / 10;
2934
+ function roundPx2(value) {
2935
+ const nearest = Math.round(value);
2936
+ if (Math.abs(value - nearest) < 0.15) {
2937
+ return nearest;
2938
+ }
2939
+ return Math.round(value * 10) / 10;
934
2940
  }
935
- export { inferAxis, measureGap };
2941
+
2942
+ export { GapInspector, inferAxis, measureGap };