gap-inspector 0.1.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.cjs +2946 -0
- package/dist/{measurement.d.ts → index.d.cts} +21 -19
- package/dist/index.d.ts +78 -5
- package/dist/index.js +2826 -762
- package/package.json +7 -4
- package/dist/measurement.js +0 -1209
- package/dist/styles.d.ts +0 -1
- package/dist/styles.js +0 -620
package/dist/index.js
CHANGED
|
@@ -1,878 +1,2942 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
);
|
|
44
76
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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);
|
|
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;
|
|
156
148
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
167
241
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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;
|
|
171
247
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
197
394
|
};
|
|
198
|
-
|
|
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
|
-
}
|
|
395
|
+
}
|
|
211
396
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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) {
|
|
230
876
|
return null;
|
|
877
|
+
}
|
|
878
|
+
scope = element.shadowRoot;
|
|
231
879
|
}
|
|
232
|
-
|
|
880
|
+
}
|
|
881
|
+
return element;
|
|
233
882
|
}
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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;
|
|
240
1863
|
}
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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);
|
|
253
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
|
+
};
|
|
254
2247
|
}
|
|
255
2248
|
function unattributedHover(measurement, target) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
};
|
|
265
2258
|
}
|
|
266
2259
|
function HoverCard({ info }) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
] });
|
|
274
2280
|
}
|
|
275
|
-
function Report({
|
|
276
|
-
|
|
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
|
+
] });
|
|
277
2323
|
}
|
|
278
2324
|
function PointReport({ inspection }) {
|
|
279
|
-
|
|
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
|
+
] });
|
|
280
2337
|
}
|
|
281
|
-
function MeasurementOverlay({
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
+
] });
|
|
285
2362
|
}
|
|
286
2363
|
function DragLine({ drag, axis }) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
function MeasurementRects({
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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;
|
|
325
2417
|
function bandGeometry(measurement, from, to, line) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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;
|
|
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;
|
|
349
2436
|
}
|
|
350
2437
|
function rectFromBand(axis, start, end, perp, thickness) {
|
|
351
|
-
|
|
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
|
-
}
|
|
2438
|
+
if (axis === "horizontal") {
|
|
361
2439
|
return {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
2440
|
+
left: start,
|
|
2441
|
+
right: end,
|
|
2442
|
+
top: perp - thickness / 2,
|
|
2443
|
+
bottom: perp + thickness / 2,
|
|
2444
|
+
width: end - start,
|
|
2445
|
+
height: thickness
|
|
368
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
|
+
};
|
|
369
2456
|
}
|
|
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
2457
|
function contributionStrips(measurement, geometry) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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).
|
|
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
|
+
}
|
|
393
2470
|
function hoverHighlightRect(axis, strip, hover) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
height: strip.height
|
|
414
|
-
};
|
|
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
|
+
};
|
|
415
2490
|
}
|
|
416
2491
|
function expandAcross(rect, axis, amount) {
|
|
417
|
-
|
|
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 };
|
|
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 };
|
|
420
2493
|
}
|
|
421
|
-
function PointInspectionRects({
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
] });
|
|
424
2506
|
}
|
|
425
2507
|
function useLivePointSnapshot(inspection) {
|
|
426
|
-
|
|
2508
|
+
return useLiveSnapshot(inspection, buildPointSnapshot);
|
|
427
2509
|
}
|
|
428
2510
|
function buildPointSnapshot(inspection) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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 };
|
|
443
2525
|
}
|
|
444
2526
|
function useLiveOverlaySnapshot(measurement) {
|
|
445
|
-
|
|
2527
|
+
return useLiveSnapshot(measurement, buildOverlaySnapshot);
|
|
446
2528
|
}
|
|
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
2529
|
function useLiveSnapshot(input, build) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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);
|
|
457
2545
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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;
|
|
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;
|
|
509
2586
|
}
|
|
510
2587
|
function buildOverlaySnapshot(measurement) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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 };
|
|
524
2601
|
}
|
|
525
|
-
|
|
526
|
-
|
|
2602
|
+
var SERIES_SCAN_LIMIT = 80;
|
|
2603
|
+
var SERIES_RENDER_LIMIT = 28;
|
|
527
2604
|
function repeatedSeriesRects(measurement, occupiedKeys) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
}
|
|
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;
|
|
549
2611
|
}
|
|
550
|
-
|
|
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;
|
|
551
2626
|
}
|
|
552
2627
|
function siblingSeriesRects(element, kind, property, cssValue, occupiedKeys) {
|
|
553
|
-
|
|
554
|
-
|
|
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;
|
|
555
2647
|
}
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return [];
|
|
2648
|
+
const candidateValue = normalizeCssValue(getComputedStyle(candidate).getPropertyValue(cssProperty));
|
|
2649
|
+
if (candidateValue !== expectedValue) {
|
|
2650
|
+
continue;
|
|
560
2651
|
}
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return [];
|
|
2652
|
+
const rect = visibleRectForElement(candidate);
|
|
2653
|
+
if (!rect || occupiedKeys.has(rectKey(rect))) {
|
|
2654
|
+
continue;
|
|
565
2655
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
}
|
|
2656
|
+
rects.push({ kind, rect });
|
|
2657
|
+
if (rects.length >= SERIES_RENDER_LIMIT) {
|
|
2658
|
+
break;
|
|
585
2659
|
}
|
|
586
|
-
|
|
2660
|
+
}
|
|
2661
|
+
return rects;
|
|
587
2662
|
}
|
|
588
2663
|
function gapSeriesRects(axis, container, kind, occupiedKeys) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
}
|
|
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 });
|
|
605
2673
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
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;
|
|
617
2684
|
}
|
|
618
|
-
|
|
2685
|
+
rects.push({ kind, rect: band });
|
|
2686
|
+
if (rects.length >= SERIES_RENDER_LIMIT) {
|
|
2687
|
+
break;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
return rects;
|
|
619
2691
|
}
|
|
620
2692
|
function siblingWindow(parent, element) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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);
|
|
629
2701
|
}
|
|
630
2702
|
function normalizeCssProperty(property) {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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;
|
|
636
2708
|
}
|
|
637
2709
|
function normalizeCssValue(value) {
|
|
638
|
-
|
|
639
|
-
|
|
2710
|
+
const normalized = value?.trim();
|
|
2711
|
+
return normalized && normalized !== "normal" && normalized !== "auto" ? normalized : null;
|
|
640
2712
|
}
|
|
641
2713
|
function elementSeriesSignature(element) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
2714
|
+
return [
|
|
2715
|
+
element.tagName.toLowerCase(),
|
|
2716
|
+
Array.from(element.classList).sort().join(".")
|
|
2717
|
+
].join(":");
|
|
646
2718
|
}
|
|
647
2719
|
function axisStart(axis, rect) {
|
|
648
|
-
|
|
2720
|
+
return axis === "horizontal" ? rect.left : rect.top;
|
|
649
2721
|
}
|
|
650
2722
|
function repeatedGapBandRect(axis, before, after) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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;
|
|
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;
|
|
667
2735
|
}
|
|
668
2736
|
function liveElement(info) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const resolved = resolveSelector(info.selector);
|
|
676
|
-
info.element = resolved ?? undefined;
|
|
677
|
-
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;
|
|
678
2743
|
}
|
|
679
2744
|
function liveRect(info) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
2745
|
+
const element = liveElement(info);
|
|
2746
|
+
if (!element) {
|
|
2747
|
+
return null;
|
|
2748
|
+
}
|
|
2749
|
+
return visibleRectForElement(element);
|
|
685
2750
|
}
|
|
686
2751
|
function visibleRectForElement(element) {
|
|
687
|
-
|
|
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()));
|
|
688
2758
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
689
|
-
|
|
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
|
-
}
|
|
2759
|
+
return null;
|
|
696
2760
|
}
|
|
697
|
-
|
|
2761
|
+
}
|
|
2762
|
+
return offsetRect(rect, window.scrollX, window.scrollY);
|
|
698
2763
|
}
|
|
699
2764
|
function offsetRect(rect, dx, dy) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
+
};
|
|
708
2773
|
}
|
|
709
2774
|
function rectFromDomRect(rect) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
+
};
|
|
718
2783
|
}
|
|
719
2784
|
function scrollAncestors(element) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
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);
|
|
729
2792
|
}
|
|
730
|
-
|
|
2793
|
+
current = current.parentElement;
|
|
2794
|
+
}
|
|
2795
|
+
return ancestors;
|
|
731
2796
|
}
|
|
732
2797
|
function intersectRects(a, b) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
+
};
|
|
745
2810
|
}
|
|
746
2811
|
function pointKindClass(region) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
+
}
|
|
761
2826
|
}
|
|
762
2827
|
function rectKey(rect) {
|
|
763
|
-
|
|
2828
|
+
return [rect.left, rect.top, rect.width, rect.height].join(":");
|
|
764
2829
|
}
|
|
765
2830
|
function edgeMarkerRect(axis, rect, side) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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;
|
|
2831
|
+
const thickness = 3;
|
|
2832
|
+
if (axis === "horizontal") {
|
|
2833
|
+
const left = side === "from" ? rect.right - thickness : rect.left;
|
|
779
2834
|
return {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
2835
|
+
left,
|
|
2836
|
+
right: left + thickness,
|
|
2837
|
+
top: rect.top,
|
|
2838
|
+
bottom: rect.bottom,
|
|
2839
|
+
width: thickness,
|
|
2840
|
+
height: rect.height
|
|
786
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
|
+
};
|
|
787
2852
|
}
|
|
788
2853
|
function internalEdgeMarkerRect(axis, rect, side, role) {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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;
|
|
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;
|
|
806
2857
|
return {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
2858
|
+
left: edge2,
|
|
2859
|
+
right: edge2 + thickness,
|
|
2860
|
+
top: rect.top,
|
|
2861
|
+
bottom: rect.bottom,
|
|
2862
|
+
width: thickness,
|
|
2863
|
+
height: rect.height
|
|
813
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
|
+
};
|
|
814
2875
|
}
|
|
815
2876
|
function svgRect(rect) {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
2877
|
+
return {
|
|
2878
|
+
x: rect.left,
|
|
2879
|
+
y: rect.top,
|
|
2880
|
+
width: rect.width,
|
|
2881
|
+
height: rect.height
|
|
2882
|
+
};
|
|
822
2883
|
}
|
|
823
2884
|
function isInspectorMutation(record) {
|
|
824
|
-
|
|
825
|
-
|
|
2885
|
+
const target = record.target instanceof Element ? record.target : record.target.parentElement;
|
|
2886
|
+
return Boolean(target?.closest(".gi-root, .gi-svg"));
|
|
826
2887
|
}
|
|
827
2888
|
function clampToViewport(x, y, target) {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
+
};
|
|
834
2895
|
}
|
|
835
2896
|
function pointFromEvent(event) {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
2897
|
+
return {
|
|
2898
|
+
x: event.clientX,
|
|
2899
|
+
y: event.clientY
|
|
2900
|
+
};
|
|
840
2901
|
}
|
|
841
2902
|
function toDocumentPoint(point) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
2903
|
+
return {
|
|
2904
|
+
x: point.x + window.scrollX,
|
|
2905
|
+
y: point.y + window.scrollY
|
|
2906
|
+
};
|
|
846
2907
|
}
|
|
847
2908
|
function toDocumentInspection(inspection) {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
2909
|
+
return {
|
|
2910
|
+
...inspection,
|
|
2911
|
+
point: toDocumentPoint(inspection.point),
|
|
2912
|
+
rect: inspection.rect ? offsetRect(inspection.rect, window.scrollX, window.scrollY) : inspection.rect
|
|
2913
|
+
};
|
|
853
2914
|
}
|
|
854
2915
|
function dragDistance(drag) {
|
|
855
|
-
|
|
2916
|
+
return Math.hypot(drag.end.x - drag.start.x, drag.end.y - drag.start.y);
|
|
856
2917
|
}
|
|
857
2918
|
function lineFromDrag(axis, drag) {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
perp
|
|
869
|
-
};
|
|
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
|
+
};
|
|
870
2929
|
}
|
|
871
|
-
function
|
|
872
|
-
|
|
873
|
-
|
|
2930
|
+
function formatPx2(value) {
|
|
2931
|
+
const rounded = roundPx2(value);
|
|
2932
|
+
return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}px`;
|
|
874
2933
|
}
|
|
875
|
-
function
|
|
876
|
-
|
|
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;
|
|
877
2940
|
}
|
|
878
|
-
|
|
2941
|
+
|
|
2942
|
+
export { GapInspector, inferAxis, measureGap };
|