gap-inspector 0.1.0
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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +878 -0
- package/dist/measurement.d.ts +79 -0
- package/dist/measurement.js +1209 -0
- package/dist/styles.d.ts +1 -0
- package/dist/styles.js +620 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, 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 activeAxis = drag ? inferAxis(drag.start, drag.end) : "horizontal";
|
|
26
|
+
// Computed in pointer handlers (not during render): measureGap reads layout,
|
|
27
|
+
// and the boundary-scan fallback is skipped so dragging stays cheap on big pages.
|
|
28
|
+
function buildPreview(nextDrag) {
|
|
29
|
+
if (dragDistance(nextDrag) < 8) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const previewReport = measureGap({
|
|
33
|
+
axis: inferAxis(nextDrag.start, nextDrag.end),
|
|
34
|
+
start: nextDrag.start,
|
|
35
|
+
end: nextDrag.end,
|
|
36
|
+
ignoreElements: [rootRef.current],
|
|
37
|
+
boundaryScan: false
|
|
38
|
+
});
|
|
39
|
+
if (!previewReport) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const snapshot = buildOverlaySnapshot(previewReport);
|
|
43
|
+
return snapshot ? { measurement: previewReport, snapshot } : null;
|
|
44
|
+
}
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!open) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
function handleKeyDown(event) {
|
|
50
|
+
if (event.key === "Alt") {
|
|
51
|
+
setPassThrough(true);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function handleKeyUp(event) {
|
|
55
|
+
if (event.key === "Alt") {
|
|
56
|
+
setPassThrough(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function handleBlur() {
|
|
60
|
+
setPassThrough(false);
|
|
61
|
+
}
|
|
62
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
63
|
+
window.addEventListener("keyup", handleKeyUp);
|
|
64
|
+
window.addEventListener("blur", handleBlur);
|
|
65
|
+
return () => {
|
|
66
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
67
|
+
window.removeEventListener("keyup", handleKeyUp);
|
|
68
|
+
window.removeEventListener("blur", handleBlur);
|
|
69
|
+
setPassThrough(false);
|
|
70
|
+
};
|
|
71
|
+
}, [open]);
|
|
72
|
+
function beginMeasure(event) {
|
|
73
|
+
if (event.button !== 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
77
|
+
const point = pointFromEvent(event);
|
|
78
|
+
const nextDrag = { start: point, end: point };
|
|
79
|
+
dragRef.current = nextDrag;
|
|
80
|
+
setDrag(nextDrag);
|
|
81
|
+
setPreview(null);
|
|
82
|
+
setCopyState("idle");
|
|
83
|
+
}
|
|
84
|
+
function updateMeasure(event) {
|
|
85
|
+
if (!dragRef.current) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const nextDrag = { ...dragRef.current, end: pointFromEvent(event) };
|
|
89
|
+
dragRef.current = nextDrag;
|
|
90
|
+
setDrag(nextDrag);
|
|
91
|
+
setPreview(buildPreview(nextDrag));
|
|
92
|
+
}
|
|
93
|
+
function finishMeasure(event) {
|
|
94
|
+
if (!dragRef.current) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const end = pointFromEvent(event);
|
|
98
|
+
const nextDrag = { ...dragRef.current, end };
|
|
99
|
+
dragRef.current = null;
|
|
100
|
+
const distance = dragDistance(nextDrag);
|
|
101
|
+
if (distance < 6) {
|
|
102
|
+
if (measurement || pointInspection) {
|
|
103
|
+
setMeasurement(null);
|
|
104
|
+
setPointInspection(null);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const inspection = inspectPoint({
|
|
108
|
+
point: end,
|
|
109
|
+
ignoreElements: [rootRef.current]
|
|
110
|
+
});
|
|
111
|
+
setPointInspection(inspection ? toDocumentInspection(inspection) : inspection);
|
|
112
|
+
}
|
|
113
|
+
setDrag(null);
|
|
114
|
+
setPreview(null);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const axis = inferAxis(nextDrag.start, nextDrag.end);
|
|
118
|
+
const report = measureGap({
|
|
119
|
+
axis,
|
|
120
|
+
start: nextDrag.start,
|
|
121
|
+
end,
|
|
122
|
+
ignoreElements: [rootRef.current]
|
|
123
|
+
});
|
|
124
|
+
if (report) {
|
|
125
|
+
setMeasurement(report);
|
|
126
|
+
setPointInspection(null);
|
|
127
|
+
onMeasure?.(report);
|
|
128
|
+
}
|
|
129
|
+
setDrag(null);
|
|
130
|
+
setPreview(null);
|
|
131
|
+
}
|
|
132
|
+
function cancelMeasure() {
|
|
133
|
+
dragRef.current = null;
|
|
134
|
+
setDrag(null);
|
|
135
|
+
setPreview(null);
|
|
136
|
+
}
|
|
137
|
+
// The pill and the panel share one position (their top-left corner), so the
|
|
138
|
+
// inspector stays where the user put it when collapsing or expanding.
|
|
139
|
+
function beginInspectorDrag(event, target) {
|
|
140
|
+
if (event.button !== 0 || !target) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (event.target instanceof Element && event.target.closest(".gi-close")) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const rect = target.getBoundingClientRect();
|
|
147
|
+
gripRef.current = {
|
|
148
|
+
dx: event.clientX - rect.left,
|
|
149
|
+
dy: event.clientY - rect.top,
|
|
150
|
+
startX: event.clientX,
|
|
151
|
+
startY: event.clientY,
|
|
152
|
+
target,
|
|
153
|
+
moved: false
|
|
154
|
+
};
|
|
155
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
156
|
+
}
|
|
157
|
+
function updateInspectorDrag(event) {
|
|
158
|
+
const grip = gripRef.current;
|
|
159
|
+
if (!grip) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!grip.moved && Math.hypot(event.clientX - grip.startX, event.clientY - grip.startY) < 4) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
grip.moved = true;
|
|
166
|
+
setPanelPosition(clampToViewport(event.clientX - grip.dx, event.clientY - grip.dy, grip.target));
|
|
167
|
+
}
|
|
168
|
+
function endInspectorDrag() {
|
|
169
|
+
suppressLauncherClickRef.current = Boolean(gripRef.current?.moved);
|
|
170
|
+
gripRef.current = null;
|
|
171
|
+
}
|
|
172
|
+
function handleLauncherClick() {
|
|
173
|
+
if (suppressLauncherClickRef.current) {
|
|
174
|
+
suppressLauncherClickRef.current = false;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
setOpen(true);
|
|
178
|
+
}
|
|
179
|
+
// Re-clamp whenever the visible element changes (pill <-> panel swap, window
|
|
180
|
+
// resize): the panel is much larger than the pill, so a position that suits
|
|
181
|
+
// one can put the other off screen.
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!panelPosition) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const target = open ? panelRef.current : launcherRef.current;
|
|
187
|
+
if (!target) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const clampNow = () => {
|
|
191
|
+
setPanelPosition((current) => current ? clampToViewport(current.x, current.y, target) : current);
|
|
192
|
+
};
|
|
193
|
+
clampNow();
|
|
194
|
+
window.addEventListener("resize", clampNow);
|
|
195
|
+
return () => {
|
|
196
|
+
window.removeEventListener("resize", clampNow);
|
|
197
|
+
};
|
|
198
|
+
}, [panelPosition !== null, open]);
|
|
199
|
+
async function copyMeasurement() {
|
|
200
|
+
const markdown = measurement?.markdown ?? pointInspection?.markdown;
|
|
201
|
+
if (!markdown) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
await navigator.clipboard.writeText(markdown);
|
|
206
|
+
setCopyState("copied");
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
setCopyState("failed");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!open) {
|
|
213
|
+
return (_jsxs("div", { className: "gi-root", ref: rootRef, children: [_jsx("style", { children: gapInspectorStyles }), _jsxs("button", { className: "gi-button", type: "button", ref: launcherRef, style: panelPosition
|
|
214
|
+
? { left: panelPosition.x, top: panelPosition.y, right: "auto", bottom: "auto" }
|
|
215
|
+
: 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"] })] }));
|
|
216
|
+
}
|
|
217
|
+
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"
|
|
218
|
+
? null
|
|
219
|
+
: 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
|
|
220
|
+
? { left: panelPosition.x, top: panelPosition.y, right: "auto", bottom: "auto" }
|
|
221
|
+
: 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: () => setOpen(false), 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: () => {
|
|
222
|
+
setMeasurement(null);
|
|
223
|
+
setPointInspection(null);
|
|
224
|
+
}, children: "Clear" }), _jsx("button", { className: "gi-primary", "data-state": copyState, type: "button", onClick: copyMeasurement, children: copyState === "copied" ? "Copied" : copyState === "failed" ? "Failed" : "Copy report" })] })) : null] })] }));
|
|
225
|
+
}
|
|
226
|
+
// A miniature of the measured gap: contributions are in geometric order along
|
|
227
|
+
// the axis, so each segment sits where that spacing actually renders.
|
|
228
|
+
function SpacingBar({ measurement, onHover }) {
|
|
229
|
+
if (measurement.totalPx < 0.5) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
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] }));
|
|
233
|
+
}
|
|
234
|
+
function contributionHover(contribution, index, target) {
|
|
235
|
+
const element = liveElement(contribution.element);
|
|
236
|
+
let meta;
|
|
237
|
+
if (element) {
|
|
238
|
+
const rect = element.getBoundingClientRect();
|
|
239
|
+
meta = `${element.tagName.toLowerCase()} \u00b7 ${getComputedStyle(element).display} \u00b7 ${roundPx(rect.width)} \u00d7 ${roundPx(rect.height)}px`;
|
|
240
|
+
}
|
|
241
|
+
const anchor = target.getBoundingClientRect();
|
|
242
|
+
return {
|
|
243
|
+
kind: contribution.kind,
|
|
244
|
+
index,
|
|
245
|
+
property: contribution.property,
|
|
246
|
+
valuePx: contribution.valuePx,
|
|
247
|
+
cssValue: contribution.cssValue,
|
|
248
|
+
selector: contribution.selector,
|
|
249
|
+
meta,
|
|
250
|
+
note: contribution.note,
|
|
251
|
+
element: contribution.element,
|
|
252
|
+
anchor: { left: anchor.left, top: anchor.top, bottom: anchor.bottom }
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function unattributedHover(measurement, target) {
|
|
256
|
+
const anchor = target.getBoundingClientRect();
|
|
257
|
+
return {
|
|
258
|
+
kind: "unknown",
|
|
259
|
+
index: measurement.contributions.length,
|
|
260
|
+
property: "unattributed space",
|
|
261
|
+
valuePx: measurement.unattributedPx,
|
|
262
|
+
note: "Rendered space not tied to a direct margin, padding, border, or gap declaration.",
|
|
263
|
+
anchor: { left: anchor.left, top: anchor.top, bottom: anchor.bottom }
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function HoverCard({ info }) {
|
|
267
|
+
const width = 280;
|
|
268
|
+
const left = Math.min(Math.max(info.anchor.left, 8), Math.max(8, window.innerWidth - width - 8));
|
|
269
|
+
const placeBelow = info.anchor.top < 240;
|
|
270
|
+
const style = placeBelow
|
|
271
|
+
? { left, top: info.anchor.bottom + 8, width }
|
|
272
|
+
: { left, bottom: window.innerHeight - info.anchor.top + 8, width };
|
|
273
|
+
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] }));
|
|
274
|
+
}
|
|
275
|
+
function Report({ measurement, hover, onHover }) {
|
|
276
|
+
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] }));
|
|
277
|
+
}
|
|
278
|
+
function PointReport({ inspection }) {
|
|
279
|
+
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] }));
|
|
280
|
+
}
|
|
281
|
+
function MeasurementOverlay({ drag, axis, measurement, pointInspection, preview, hover }) {
|
|
282
|
+
const snapshot = useLiveOverlaySnapshot(measurement);
|
|
283
|
+
const pointSnapshot = useLivePointSnapshot(pointInspection);
|
|
284
|
+
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] }));
|
|
285
|
+
}
|
|
286
|
+
function DragLine({ drag, axis }) {
|
|
287
|
+
const start = toDocumentPoint(drag.start);
|
|
288
|
+
const end = toDocumentPoint(drag.end);
|
|
289
|
+
const x1 = axis === "horizontal" ? start.x : (start.x + end.x) / 2;
|
|
290
|
+
const x2 = axis === "horizontal" ? end.x : (start.x + end.x) / 2;
|
|
291
|
+
const y1 = axis === "horizontal" ? (start.y + end.y) / 2 : start.y;
|
|
292
|
+
const y2 = axis === "horizontal" ? (start.y + end.y) / 2 : end.y;
|
|
293
|
+
return _jsx("line", { className: "gi-line", x1: x1, y1: y1, x2: x2, y2: y2 });
|
|
294
|
+
}
|
|
295
|
+
function MeasurementRects({ measurement, snapshot, variant, line, hover }) {
|
|
296
|
+
const from = snapshot.from;
|
|
297
|
+
const to = snapshot.to;
|
|
298
|
+
const geometry = bandGeometry(measurement, from, to, line);
|
|
299
|
+
const band = geometry
|
|
300
|
+
? rectFromBand(measurement.axis, geometry.start, geometry.end, geometry.perp, GAP_BAND_THICKNESS)
|
|
301
|
+
: null;
|
|
302
|
+
const { strips, remainder } = geometry
|
|
303
|
+
? contributionStrips(measurement, geometry)
|
|
304
|
+
: { strips: [], remainder: null };
|
|
305
|
+
const hoverStrip = hover
|
|
306
|
+
? hover.index === measurement.contributions.length
|
|
307
|
+
? remainder
|
|
308
|
+
: strips[hover.index] ?? null
|
|
309
|
+
: null;
|
|
310
|
+
const hoverRect = hover && hoverStrip
|
|
311
|
+
? hoverHighlightRect(measurement.axis, hoverStrip, hover)
|
|
312
|
+
: null;
|
|
313
|
+
const fromEdge = measurement.internalSide
|
|
314
|
+
? internalEdgeMarkerRect(measurement.axis, from, measurement.internalSide, "container")
|
|
315
|
+
: edgeMarkerRect(measurement.axis, from, "from");
|
|
316
|
+
const toEdge = measurement.internalSide
|
|
317
|
+
? internalEdgeMarkerRect(measurement.axis, to, measurement.internalSide, "child")
|
|
318
|
+
: edgeMarkerRect(measurement.axis, to, "to");
|
|
319
|
+
const prefix = variant === "preview" ? "gi-preview" : "gi-committed";
|
|
320
|
+
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) })] }));
|
|
321
|
+
}
|
|
322
|
+
const GAP_BAND_THICKNESS = 12;
|
|
323
|
+
// The span of the measured gap along the axis plus the perpendicular center of
|
|
324
|
+
// the drawn line (or the from/to overlap when re-rendered without a line).
|
|
325
|
+
function bandGeometry(measurement, from, to, line) {
|
|
326
|
+
const horizontal = measurement.axis === "horizontal";
|
|
327
|
+
const props = horizontal
|
|
328
|
+
? { start: "left", end: "right", perpStart: "top", perpEnd: "bottom" }
|
|
329
|
+
: { start: "top", end: "bottom", perpStart: "left", perpEnd: "right" };
|
|
330
|
+
let start;
|
|
331
|
+
let end;
|
|
332
|
+
let fallbackPerp;
|
|
333
|
+
if (measurement.internalSide) {
|
|
334
|
+
start = measurement.internalSide === "before" ? from[props.start] : to[props.end];
|
|
335
|
+
end = measurement.internalSide === "before" ? to[props.start] : from[props.end];
|
|
336
|
+
fallbackPerp = (to[props.perpStart] + to[props.perpEnd]) / 2;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
start = from[props.end];
|
|
340
|
+
end = to[props.start];
|
|
341
|
+
fallbackPerp =
|
|
342
|
+
(Math.max(Math.min(from[props.perpStart], to[props.perpStart]), 0)
|
|
343
|
+
+ Math.max(from[props.perpEnd], to[props.perpEnd])) / 2;
|
|
344
|
+
}
|
|
345
|
+
const overlapStart = Math.max(from[props.perpStart], to[props.perpStart]);
|
|
346
|
+
const overlapEnd = Math.min(from[props.perpEnd], to[props.perpEnd]);
|
|
347
|
+
const perp = line?.perp ?? (overlapEnd > overlapStart ? (overlapStart + overlapEnd) / 2 : fallbackPerp);
|
|
348
|
+
return end > start ? { start, end, perp } : null;
|
|
349
|
+
}
|
|
350
|
+
function rectFromBand(axis, start, end, perp, thickness) {
|
|
351
|
+
if (axis === "horizontal") {
|
|
352
|
+
return {
|
|
353
|
+
left: start,
|
|
354
|
+
right: end,
|
|
355
|
+
top: perp - thickness / 2,
|
|
356
|
+
bottom: perp + thickness / 2,
|
|
357
|
+
width: end - start,
|
|
358
|
+
height: thickness
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
left: perp - thickness / 2,
|
|
363
|
+
right: perp + thickness / 2,
|
|
364
|
+
top: start,
|
|
365
|
+
bottom: end,
|
|
366
|
+
width: thickness,
|
|
367
|
+
height: end - start
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// Contributions are in geometric order, so stacking their values from the gap's
|
|
371
|
+
// start edge places each strip where that spacing actually renders. The array
|
|
372
|
+
// is index-aligned with measurement.contributions (null for sub-pixel strips).
|
|
373
|
+
function contributionStrips(measurement, geometry) {
|
|
374
|
+
const strips = [];
|
|
375
|
+
let cursor = geometry.start;
|
|
376
|
+
for (const contribution of measurement.contributions) {
|
|
377
|
+
const next = Math.min(cursor + contribution.valuePx, geometry.end);
|
|
378
|
+
strips.push(next - cursor > 0.1
|
|
379
|
+
? rectFromBand(measurement.axis, cursor, next, geometry.perp, GAP_BAND_THICKNESS)
|
|
380
|
+
: null);
|
|
381
|
+
cursor = next;
|
|
382
|
+
}
|
|
383
|
+
const remainder = geometry.end - cursor > 0.1
|
|
384
|
+
? rectFromBand(measurement.axis, cursor, geometry.end, geometry.perp, GAP_BAND_THICKNESS)
|
|
385
|
+
: null;
|
|
386
|
+
return { strips, remainder };
|
|
387
|
+
}
|
|
388
|
+
// The hovered contribution's slice along the axis, extended across its
|
|
389
|
+
// element's full perpendicular extent — the actual spacing region (a padding
|
|
390
|
+
// slice spans the container's full height, a margin slice its element's, etc.).
|
|
391
|
+
// Falls back to a thickened band slice when there is no live element (e.g. the
|
|
392
|
+
// unattributed remainder).
|
|
393
|
+
function hoverHighlightRect(axis, strip, hover) {
|
|
394
|
+
const blockRect = hover.element ? liveRect(hover.element) : null;
|
|
395
|
+
if (!blockRect) {
|
|
396
|
+
return expandAcross(strip, axis, 4);
|
|
397
|
+
}
|
|
398
|
+
return axis === "horizontal"
|
|
399
|
+
? {
|
|
400
|
+
left: strip.left,
|
|
401
|
+
right: strip.right,
|
|
402
|
+
top: blockRect.top,
|
|
403
|
+
bottom: blockRect.bottom,
|
|
404
|
+
width: strip.width,
|
|
405
|
+
height: blockRect.bottom - blockRect.top
|
|
406
|
+
}
|
|
407
|
+
: {
|
|
408
|
+
left: blockRect.left,
|
|
409
|
+
right: blockRect.right,
|
|
410
|
+
top: strip.top,
|
|
411
|
+
bottom: strip.bottom,
|
|
412
|
+
width: blockRect.right - blockRect.left,
|
|
413
|
+
height: strip.height
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function expandAcross(rect, axis, amount) {
|
|
417
|
+
return axis === "horizontal"
|
|
418
|
+
? { ...rect, top: rect.top - amount, bottom: rect.bottom + amount, height: rect.height + amount * 2 }
|
|
419
|
+
: { ...rect, left: rect.left - amount, right: rect.right + amount, width: rect.width + amount * 2 };
|
|
420
|
+
}
|
|
421
|
+
function PointInspectionRects({ inspection, snapshot }) {
|
|
422
|
+
const kindClass = pointKindClass(inspection.region);
|
|
423
|
+
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 })] }));
|
|
424
|
+
}
|
|
425
|
+
function useLivePointSnapshot(inspection) {
|
|
426
|
+
return useLiveSnapshot(inspection, buildPointSnapshot);
|
|
427
|
+
}
|
|
428
|
+
function buildPointSnapshot(inspection) {
|
|
429
|
+
const rect = inspection.rect;
|
|
430
|
+
const element = inspection.element ? liveRect(inspection.element) ?? undefined : undefined;
|
|
431
|
+
const from = inspection.from ? liveRect(inspection.from) ?? undefined : undefined;
|
|
432
|
+
const to = inspection.to ? liveRect(inspection.to) ?? undefined : undefined;
|
|
433
|
+
const key = [
|
|
434
|
+
rect ? rectKey(rect) : "",
|
|
435
|
+
element ? rectKey(element) : "",
|
|
436
|
+
from ? rectKey(from) : "",
|
|
437
|
+
to ? rectKey(to) : ""
|
|
438
|
+
].join("::");
|
|
439
|
+
if (!rect && !element && !from && !to) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return { key, rect, element, from, to };
|
|
443
|
+
}
|
|
444
|
+
function useLiveOverlaySnapshot(measurement) {
|
|
445
|
+
return useLiveSnapshot(measurement, buildOverlaySnapshot);
|
|
446
|
+
}
|
|
447
|
+
// Snapshots are in document coordinates, so page scroll moves the overlay natively
|
|
448
|
+
// (no recompute needed for correctness there). Scroll events still trigger a recompute
|
|
449
|
+
// because nested scrollers change clipping and sticky/fixed elements move in document
|
|
450
|
+
// space; the key diff drops the update when nothing actually moved.
|
|
451
|
+
function useLiveSnapshot(input, build) {
|
|
452
|
+
const [snapshot, setSnapshot] = useState(null);
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (!input) {
|
|
455
|
+
setSnapshot(null);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
let frame = 0;
|
|
459
|
+
// null = nothing emitted yet; "" = an empty result has been emitted.
|
|
460
|
+
let lastKey = null;
|
|
461
|
+
const recompute = () => {
|
|
462
|
+
frame = 0;
|
|
463
|
+
const nextSnapshot = build(input);
|
|
464
|
+
if (!nextSnapshot) {
|
|
465
|
+
if (lastKey !== "") {
|
|
466
|
+
lastKey = "";
|
|
467
|
+
setSnapshot(null);
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (nextSnapshot.key !== lastKey) {
|
|
472
|
+
lastKey = nextSnapshot.key;
|
|
473
|
+
setSnapshot(nextSnapshot);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const schedule = () => {
|
|
477
|
+
if (!frame) {
|
|
478
|
+
frame = requestAnimationFrame(recompute);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
recompute();
|
|
482
|
+
const resizeObserver = new ResizeObserver(schedule);
|
|
483
|
+
resizeObserver.observe(document.documentElement);
|
|
484
|
+
resizeObserver.observe(document.body);
|
|
485
|
+
const mutationObserver = new MutationObserver((records) => {
|
|
486
|
+
if (records.every(isInspectorMutation)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
schedule();
|
|
490
|
+
});
|
|
491
|
+
mutationObserver.observe(document.body, { attributes: true, childList: true, subtree: true });
|
|
492
|
+
window.addEventListener("scroll", schedule, { capture: true, passive: true });
|
|
493
|
+
window.addEventListener("resize", schedule);
|
|
494
|
+
window.addEventListener("transitionend", schedule, true);
|
|
495
|
+
window.addEventListener("animationend", schedule, true);
|
|
496
|
+
return () => {
|
|
497
|
+
if (frame) {
|
|
498
|
+
cancelAnimationFrame(frame);
|
|
499
|
+
}
|
|
500
|
+
resizeObserver.disconnect();
|
|
501
|
+
mutationObserver.disconnect();
|
|
502
|
+
window.removeEventListener("scroll", schedule, true);
|
|
503
|
+
window.removeEventListener("resize", schedule);
|
|
504
|
+
window.removeEventListener("transitionend", schedule, true);
|
|
505
|
+
window.removeEventListener("animationend", schedule, true);
|
|
506
|
+
};
|
|
507
|
+
}, [input, build]);
|
|
508
|
+
return snapshot;
|
|
509
|
+
}
|
|
510
|
+
function buildOverlaySnapshot(measurement) {
|
|
511
|
+
const from = liveRect(measurement.from);
|
|
512
|
+
const to = liveRect(measurement.to);
|
|
513
|
+
if (!from || !to) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
const occupiedKeys = new Set([rectKey(from), rectKey(to)]);
|
|
517
|
+
const series = repeatedSeriesRects(measurement, occupiedKeys);
|
|
518
|
+
const key = [
|
|
519
|
+
rectKey(from),
|
|
520
|
+
rectKey(to),
|
|
521
|
+
series.map((item) => `${item.kind}:${rectKey(item.rect)}`).join("|")
|
|
522
|
+
].join("::");
|
|
523
|
+
return { key, from, to, series };
|
|
524
|
+
}
|
|
525
|
+
const SERIES_SCAN_LIMIT = 80;
|
|
526
|
+
const SERIES_RENDER_LIMIT = 28;
|
|
527
|
+
function repeatedSeriesRects(measurement, occupiedKeys) {
|
|
528
|
+
const rects = [];
|
|
529
|
+
const seen = new Set();
|
|
530
|
+
for (const contribution of measurement.contributions) {
|
|
531
|
+
const element = liveElement(contribution.element);
|
|
532
|
+
if (!element) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const candidates = contribution.kind === "gap"
|
|
536
|
+
? gapSeriesRects(measurement.axis, element, contribution.kind, occupiedKeys)
|
|
537
|
+
: siblingSeriesRects(element, contribution.kind, contribution.property, contribution.cssValue, occupiedKeys);
|
|
538
|
+
for (const candidate of candidates) {
|
|
539
|
+
const key = `${candidate.kind}:${rectKey(candidate.rect)}`;
|
|
540
|
+
if (seen.has(key)) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
seen.add(key);
|
|
544
|
+
rects.push(candidate);
|
|
545
|
+
if (rects.length >= SERIES_RENDER_LIMIT) {
|
|
546
|
+
return rects;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return rects;
|
|
551
|
+
}
|
|
552
|
+
function siblingSeriesRects(element, kind, property, cssValue, occupiedKeys) {
|
|
553
|
+
if (kind === "unknown") {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
const parent = element.parentElement;
|
|
557
|
+
const cssProperty = normalizeCssProperty(property);
|
|
558
|
+
if (!parent || !cssProperty) {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
const style = getComputedStyle(element);
|
|
562
|
+
const expectedValue = normalizeCssValue(cssValue ?? style.getPropertyValue(cssProperty));
|
|
563
|
+
if (!expectedValue) {
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
const signature = elementSeriesSignature(element);
|
|
567
|
+
const candidates = siblingWindow(parent, element);
|
|
568
|
+
const rects = [];
|
|
569
|
+
for (const candidate of candidates) {
|
|
570
|
+
if (candidate === element || elementSeriesSignature(candidate) !== signature) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const candidateValue = normalizeCssValue(getComputedStyle(candidate).getPropertyValue(cssProperty));
|
|
574
|
+
if (candidateValue !== expectedValue) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const rect = visibleRectForElement(candidate);
|
|
578
|
+
if (!rect || occupiedKeys.has(rectKey(rect))) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
rects.push({ kind, rect });
|
|
582
|
+
if (rects.length >= SERIES_RENDER_LIMIT) {
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return rects;
|
|
587
|
+
}
|
|
588
|
+
function gapSeriesRects(axis, container, kind, occupiedKeys) {
|
|
589
|
+
const children = Array.from(container.children)
|
|
590
|
+
.slice(0, SERIES_SCAN_LIMIT)
|
|
591
|
+
.map((element) => ({ element, rect: visibleRectForElement(element) }))
|
|
592
|
+
.filter((item) => Boolean(item.rect));
|
|
593
|
+
if (children.length < 2) {
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
const rects = [];
|
|
597
|
+
const sorted = children.sort((a, b) => axisStart(axis, a.rect) - axisStart(axis, b.rect));
|
|
598
|
+
for (const child of sorted) {
|
|
599
|
+
if (!occupiedKeys.has(rectKey(child.rect))) {
|
|
600
|
+
rects.push({ kind, rect: child.rect });
|
|
601
|
+
}
|
|
602
|
+
if (rects.length >= SERIES_RENDER_LIMIT) {
|
|
603
|
+
return rects;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
for (let index = 0; index < sorted.length - 1; index += 1) {
|
|
607
|
+
const before = sorted[index].rect;
|
|
608
|
+
const after = sorted[index + 1].rect;
|
|
609
|
+
const band = repeatedGapBandRect(axis, before, after);
|
|
610
|
+
if (!band || occupiedKeys.has(rectKey(band))) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
rects.push({ kind, rect: band });
|
|
614
|
+
if (rects.length >= SERIES_RENDER_LIMIT) {
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return rects;
|
|
619
|
+
}
|
|
620
|
+
function siblingWindow(parent, element) {
|
|
621
|
+
const siblings = Array.from(parent.children);
|
|
622
|
+
const index = siblings.indexOf(element);
|
|
623
|
+
if (index === -1 || siblings.length <= SERIES_SCAN_LIMIT) {
|
|
624
|
+
return siblings.slice(0, SERIES_SCAN_LIMIT);
|
|
625
|
+
}
|
|
626
|
+
const halfWindow = Math.floor(SERIES_SCAN_LIMIT / 2);
|
|
627
|
+
const start = Math.max(0, Math.min(index - halfWindow, siblings.length - SERIES_SCAN_LIMIT));
|
|
628
|
+
return siblings.slice(start, start + SERIES_SCAN_LIMIT);
|
|
629
|
+
}
|
|
630
|
+
function normalizeCssProperty(property) {
|
|
631
|
+
const normalized = property.trim().replace(/\s+/g, "-");
|
|
632
|
+
if (!normalized || normalized.includes("layout") || normalized.includes("space-between")) {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
return normalized;
|
|
636
|
+
}
|
|
637
|
+
function normalizeCssValue(value) {
|
|
638
|
+
const normalized = value?.trim();
|
|
639
|
+
return normalized && normalized !== "normal" && normalized !== "auto" ? normalized : null;
|
|
640
|
+
}
|
|
641
|
+
function elementSeriesSignature(element) {
|
|
642
|
+
return [
|
|
643
|
+
element.tagName.toLowerCase(),
|
|
644
|
+
Array.from(element.classList).sort().join(".")
|
|
645
|
+
].join(":");
|
|
646
|
+
}
|
|
647
|
+
function axisStart(axis, rect) {
|
|
648
|
+
return axis === "horizontal" ? rect.left : rect.top;
|
|
649
|
+
}
|
|
650
|
+
function repeatedGapBandRect(axis, before, after) {
|
|
651
|
+
if (axis === "horizontal") {
|
|
652
|
+
const left = before.right;
|
|
653
|
+
const right = after.left;
|
|
654
|
+
const top = Math.max(before.top, after.top);
|
|
655
|
+
const bottom = Math.min(before.bottom, after.bottom);
|
|
656
|
+
return right > left && bottom > top
|
|
657
|
+
? { left, right, top, bottom, width: roundPx(right - left), height: roundPx(bottom - top) }
|
|
658
|
+
: null;
|
|
659
|
+
}
|
|
660
|
+
const top = before.bottom;
|
|
661
|
+
const bottom = after.top;
|
|
662
|
+
const left = Math.max(before.left, after.left);
|
|
663
|
+
const right = Math.min(before.right, after.right);
|
|
664
|
+
return bottom > top && right > left
|
|
665
|
+
? { left, right, top, bottom, width: roundPx(right - left), height: roundPx(bottom - top) }
|
|
666
|
+
: null;
|
|
667
|
+
}
|
|
668
|
+
function liveElement(info) {
|
|
669
|
+
if (info.element?.isConnected) {
|
|
670
|
+
return info.element;
|
|
671
|
+
}
|
|
672
|
+
// The stored node was removed (e.g. a framework re-render replaced it).
|
|
673
|
+
// Re-resolve through the unique selector and drop the detached reference so
|
|
674
|
+
// the old subtree can be garbage collected.
|
|
675
|
+
const resolved = resolveSelector(info.selector);
|
|
676
|
+
info.element = resolved ?? undefined;
|
|
677
|
+
return resolved;
|
|
678
|
+
}
|
|
679
|
+
function liveRect(info) {
|
|
680
|
+
const element = liveElement(info);
|
|
681
|
+
if (!element) {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
return visibleRectForElement(element);
|
|
685
|
+
}
|
|
686
|
+
function visibleRectForElement(element) {
|
|
687
|
+
let rect = rectFromDomRect(element.getBoundingClientRect());
|
|
688
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
for (const ancestor of scrollAncestors(element)) {
|
|
692
|
+
rect = intersectRects(rect, rectFromDomRect(ancestor.getBoundingClientRect()));
|
|
693
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return offsetRect(rect, window.scrollX, window.scrollY);
|
|
698
|
+
}
|
|
699
|
+
function offsetRect(rect, dx, dy) {
|
|
700
|
+
return {
|
|
701
|
+
top: roundPx(rect.top + dy),
|
|
702
|
+
right: roundPx(rect.right + dx),
|
|
703
|
+
bottom: roundPx(rect.bottom + dy),
|
|
704
|
+
left: roundPx(rect.left + dx),
|
|
705
|
+
width: rect.width,
|
|
706
|
+
height: rect.height
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function rectFromDomRect(rect) {
|
|
710
|
+
return {
|
|
711
|
+
top: roundPx(rect.top),
|
|
712
|
+
right: roundPx(rect.right),
|
|
713
|
+
bottom: roundPx(rect.bottom),
|
|
714
|
+
left: roundPx(rect.left),
|
|
715
|
+
width: roundPx(rect.width),
|
|
716
|
+
height: roundPx(rect.height)
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function scrollAncestors(element) {
|
|
720
|
+
const ancestors = [];
|
|
721
|
+
let current = element.parentElement;
|
|
722
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
723
|
+
const style = getComputedStyle(current);
|
|
724
|
+
const overflow = `${style.overflow}${style.overflowX}${style.overflowY}`;
|
|
725
|
+
if (/(auto|scroll|clip|hidden)/.test(overflow)) {
|
|
726
|
+
ancestors.push(current);
|
|
727
|
+
}
|
|
728
|
+
current = current.parentElement;
|
|
729
|
+
}
|
|
730
|
+
return ancestors;
|
|
731
|
+
}
|
|
732
|
+
function intersectRects(a, b) {
|
|
733
|
+
const left = Math.max(a.left, b.left);
|
|
734
|
+
const right = Math.min(a.right, b.right);
|
|
735
|
+
const top = Math.max(a.top, b.top);
|
|
736
|
+
const bottom = Math.min(a.bottom, b.bottom);
|
|
737
|
+
return {
|
|
738
|
+
top: roundPx(top),
|
|
739
|
+
right: roundPx(right),
|
|
740
|
+
bottom: roundPx(bottom),
|
|
741
|
+
left: roundPx(left),
|
|
742
|
+
width: roundPx(Math.max(0, right - left)),
|
|
743
|
+
height: roundPx(Math.max(0, bottom - top))
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function pointKindClass(region) {
|
|
747
|
+
switch (region) {
|
|
748
|
+
case "margin":
|
|
749
|
+
return "gi-kind-margin";
|
|
750
|
+
case "padding":
|
|
751
|
+
return "gi-kind-padding";
|
|
752
|
+
case "border":
|
|
753
|
+
return "gi-kind-border";
|
|
754
|
+
case "gap":
|
|
755
|
+
return "gi-kind-gap";
|
|
756
|
+
case "content":
|
|
757
|
+
return "gi-kind-content";
|
|
758
|
+
default:
|
|
759
|
+
return "gi-kind-unknown";
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function rectKey(rect) {
|
|
763
|
+
return [rect.left, rect.top, rect.width, rect.height].join(":");
|
|
764
|
+
}
|
|
765
|
+
function edgeMarkerRect(axis, rect, side) {
|
|
766
|
+
const thickness = 3;
|
|
767
|
+
if (axis === "horizontal") {
|
|
768
|
+
const left = side === "from" ? rect.right - thickness : rect.left;
|
|
769
|
+
return {
|
|
770
|
+
left,
|
|
771
|
+
right: left + thickness,
|
|
772
|
+
top: rect.top,
|
|
773
|
+
bottom: rect.bottom,
|
|
774
|
+
width: thickness,
|
|
775
|
+
height: rect.height
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const top = side === "from" ? rect.bottom - thickness : rect.top;
|
|
779
|
+
return {
|
|
780
|
+
left: rect.left,
|
|
781
|
+
right: rect.right,
|
|
782
|
+
top,
|
|
783
|
+
bottom: top + thickness,
|
|
784
|
+
width: rect.width,
|
|
785
|
+
height: thickness
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function internalEdgeMarkerRect(axis, rect, side, role) {
|
|
789
|
+
const thickness = 3;
|
|
790
|
+
if (axis === "horizontal") {
|
|
791
|
+
const edge = side === "before"
|
|
792
|
+
? role === "container" ? rect.left : rect.left
|
|
793
|
+
: role === "container" ? rect.right - thickness : rect.right - thickness;
|
|
794
|
+
return {
|
|
795
|
+
left: edge,
|
|
796
|
+
right: edge + thickness,
|
|
797
|
+
top: rect.top,
|
|
798
|
+
bottom: rect.bottom,
|
|
799
|
+
width: thickness,
|
|
800
|
+
height: rect.height
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const edge = side === "before"
|
|
804
|
+
? role === "container" ? rect.top : rect.top
|
|
805
|
+
: role === "container" ? rect.bottom - thickness : rect.bottom - thickness;
|
|
806
|
+
return {
|
|
807
|
+
left: rect.left,
|
|
808
|
+
right: rect.right,
|
|
809
|
+
top: edge,
|
|
810
|
+
bottom: edge + thickness,
|
|
811
|
+
width: rect.width,
|
|
812
|
+
height: thickness
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
function svgRect(rect) {
|
|
816
|
+
return {
|
|
817
|
+
x: rect.left,
|
|
818
|
+
y: rect.top,
|
|
819
|
+
width: rect.width,
|
|
820
|
+
height: rect.height
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function isInspectorMutation(record) {
|
|
824
|
+
const target = record.target instanceof Element ? record.target : record.target.parentElement;
|
|
825
|
+
return Boolean(target?.closest(".gi-root, .gi-svg"));
|
|
826
|
+
}
|
|
827
|
+
function clampToViewport(x, y, target) {
|
|
828
|
+
const rect = target.getBoundingClientRect();
|
|
829
|
+
const margin = 8;
|
|
830
|
+
return {
|
|
831
|
+
x: Math.min(Math.max(x, margin), Math.max(margin, window.innerWidth - rect.width - margin)),
|
|
832
|
+
y: Math.min(Math.max(y, margin), Math.max(margin, window.innerHeight - rect.height - margin))
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function pointFromEvent(event) {
|
|
836
|
+
return {
|
|
837
|
+
x: event.clientX,
|
|
838
|
+
y: event.clientY
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function toDocumentPoint(point) {
|
|
842
|
+
return {
|
|
843
|
+
x: point.x + window.scrollX,
|
|
844
|
+
y: point.y + window.scrollY
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function toDocumentInspection(inspection) {
|
|
848
|
+
return {
|
|
849
|
+
...inspection,
|
|
850
|
+
point: toDocumentPoint(inspection.point),
|
|
851
|
+
rect: inspection.rect ? offsetRect(inspection.rect, window.scrollX, window.scrollY) : inspection.rect
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function dragDistance(drag) {
|
|
855
|
+
return Math.hypot(drag.end.x - drag.start.x, drag.end.y - drag.start.y);
|
|
856
|
+
}
|
|
857
|
+
function lineFromDrag(axis, drag) {
|
|
858
|
+
const start = toDocumentPoint(drag.start);
|
|
859
|
+
const end = toDocumentPoint(drag.end);
|
|
860
|
+
const startAlong = axis === "horizontal" ? start.x : start.y;
|
|
861
|
+
const endAlong = axis === "horizontal" ? end.x : end.y;
|
|
862
|
+
const perp = axis === "horizontal"
|
|
863
|
+
? (start.y + end.y) / 2
|
|
864
|
+
: (start.x + end.x) / 2;
|
|
865
|
+
return {
|
|
866
|
+
min: Math.min(startAlong, endAlong),
|
|
867
|
+
max: Math.max(startAlong, endAlong),
|
|
868
|
+
perp
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function formatPx(value) {
|
|
872
|
+
const rounded = Math.round(value * 100) / 100;
|
|
873
|
+
return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(2)}px`;
|
|
874
|
+
}
|
|
875
|
+
function roundPx(value) {
|
|
876
|
+
return Math.round(value * 100) / 100;
|
|
877
|
+
}
|
|
878
|
+
export { inferAxis, measureGap };
|