vite-plugin-specter 0.1.2 → 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/client.js +588 -344
- package/dist/index.cjs +588 -344
- package/dist/index.js +588 -344
- package/extension/content.js +588 -344
- package/package.json +1 -1
package/extension/content.js
CHANGED
|
@@ -2,41 +2,55 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
if (window.__specter) return; window.__specter = true;
|
|
4
4
|
|
|
5
|
-
// ───
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
6
|
+
var PURPLE = '#AD24D3';
|
|
7
|
+
var RED = '#ED3E61';
|
|
8
|
+
var TIP_BG = '#292B32';
|
|
9
|
+
var GREEN = '#22C55E';
|
|
10
|
+
var LABEL = '#8B8D94';
|
|
11
|
+
var MONO = "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace";
|
|
12
|
+
var ZAP = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>';
|
|
13
|
+
var ACTIVATE = "ctrl+alt+z";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
// ─── State ────────────────────────────────────────────────────────────────
|
|
16
|
+
var fiActive = false;
|
|
17
|
+
var measureMode = false;
|
|
18
|
+
var pinEl = null;
|
|
19
|
+
var pinHighlight = null;
|
|
20
|
+
var lastHovered = null;
|
|
21
|
+
var optionHeld = false;
|
|
22
|
+
var pillExpanded = false;
|
|
23
|
+
var sideRight = false;
|
|
24
|
+
var outlinedEl = null;
|
|
25
|
+
var prevOutline = '';
|
|
26
|
+
var measureHL = null;
|
|
27
|
+
var copiedTimer = null;
|
|
28
|
+
var flashTimer = null;
|
|
29
|
+
var lastMouse = { x: 0, y: 0 };
|
|
30
|
+
var selectedEls = [];
|
|
31
|
+
var selectedBadges = [];
|
|
15
32
|
|
|
16
33
|
// ─── Tooltip ──────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
34
|
+
var tooltip = document.createElement('div');
|
|
18
35
|
Object.assign(tooltip.style, {
|
|
19
36
|
position: 'fixed',
|
|
20
37
|
zIndex: '2147483646',
|
|
21
38
|
pointerEvents: 'none',
|
|
22
|
-
fontFamily:
|
|
23
|
-
fontSize: '
|
|
24
|
-
lineHeight: '
|
|
25
|
-
background:
|
|
26
|
-
color: '#
|
|
27
|
-
padding: '
|
|
39
|
+
fontFamily: MONO,
|
|
40
|
+
fontSize: '13px',
|
|
41
|
+
lineHeight: '20px',
|
|
42
|
+
background: TIP_BG,
|
|
43
|
+
color: '#FFFFFF',
|
|
44
|
+
padding: '24px',
|
|
28
45
|
borderRadius: '8px',
|
|
29
|
-
maxWidth: '
|
|
30
|
-
boxShadow: '0
|
|
31
|
-
whiteSpace: 'pre-wrap',
|
|
32
|
-
wordBreak: 'break-word',
|
|
46
|
+
maxWidth: '480px',
|
|
47
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
|
|
33
48
|
display: 'none',
|
|
34
|
-
border: '1px solid rgba(255,255,255,0.08)',
|
|
35
49
|
});
|
|
36
50
|
document.body.appendChild(tooltip);
|
|
37
51
|
|
|
38
52
|
// Measure overlay
|
|
39
|
-
|
|
53
|
+
var measureOverlay = document.createElement('div');
|
|
40
54
|
Object.assign(measureOverlay.style, {
|
|
41
55
|
position: 'fixed',
|
|
42
56
|
inset: '0',
|
|
@@ -47,102 +61,156 @@
|
|
|
47
61
|
document.body.appendChild(measureOverlay);
|
|
48
62
|
|
|
49
63
|
// ─── Pill ─────────────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
Object.assign(
|
|
64
|
+
var pillWrap = document.createElement('div');
|
|
65
|
+
Object.assign(pillWrap.style, {
|
|
52
66
|
position: 'fixed',
|
|
53
|
-
bottom: '
|
|
54
|
-
left: '
|
|
67
|
+
bottom: '4px',
|
|
68
|
+
left: '4px',
|
|
55
69
|
zIndex: '2147483647',
|
|
70
|
+
padding: '12px',
|
|
56
71
|
display: 'none',
|
|
72
|
+
boxSizing: 'border-box',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
var pill = document.createElement('div');
|
|
76
|
+
Object.assign(pill.style, {
|
|
77
|
+
display: 'flex',
|
|
57
78
|
alignItems: 'center',
|
|
58
79
|
gap: '8px',
|
|
59
|
-
background:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
padding: '
|
|
63
|
-
fontFamily:
|
|
80
|
+
background: PURPLE,
|
|
81
|
+
borderRadius: '16px',
|
|
82
|
+
height: '32px',
|
|
83
|
+
padding: '0 9px',
|
|
84
|
+
fontFamily: MONO,
|
|
64
85
|
fontSize: '11px',
|
|
65
|
-
|
|
86
|
+
fontWeight: '400',
|
|
87
|
+
color: '#fff',
|
|
66
88
|
cursor: 'default',
|
|
67
89
|
userSelect: 'none',
|
|
68
|
-
|
|
90
|
+
boxShadow: '0 4px 12px rgba(173,36,211,0.4)',
|
|
69
91
|
overflow: 'hidden',
|
|
70
92
|
maxWidth: '32px',
|
|
71
|
-
|
|
93
|
+
transition: 'max-width 0.2s ease',
|
|
94
|
+
boxSizing: 'border-box',
|
|
95
|
+
whiteSpace: 'nowrap',
|
|
72
96
|
});
|
|
73
97
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
var iconBtn = document.createElement('div');
|
|
99
|
+
Object.assign(iconBtn.style, {
|
|
100
|
+
position: 'relative',
|
|
101
|
+
width: '14px',
|
|
102
|
+
height: '14px',
|
|
103
|
+
flexShrink: '0',
|
|
104
|
+
display: 'flex',
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
});
|
|
77
108
|
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
var zapEl = document.createElement('span');
|
|
110
|
+
zapEl.innerHTML = ZAP;
|
|
111
|
+
Object.assign(zapEl.style, {
|
|
112
|
+
position: 'absolute',
|
|
113
|
+
display: 'flex',
|
|
114
|
+
transition: 'transform 0.4s ease, opacity 0.2s ease',
|
|
115
|
+
});
|
|
80
116
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
color: '#666',
|
|
87
|
-
fontSize: '15px',
|
|
117
|
+
var closeEl = document.createElement('span');
|
|
118
|
+
closeEl.textContent = '×';
|
|
119
|
+
Object.assign(closeEl.style, {
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
fontSize: '17px',
|
|
88
122
|
lineHeight: '1',
|
|
89
|
-
|
|
123
|
+
opacity: '0',
|
|
124
|
+
transition: 'opacity 0.2s ease',
|
|
125
|
+
cursor: 'pointer',
|
|
126
|
+
});
|
|
127
|
+
closeEl.addEventListener('click', function (e) { e.stopPropagation(); deactivate(); });
|
|
128
|
+
|
|
129
|
+
iconBtn.appendChild(zapEl);
|
|
130
|
+
iconBtn.appendChild(closeEl);
|
|
131
|
+
|
|
132
|
+
var pillText = document.createElement('span');
|
|
133
|
+
Object.assign(pillText.style, { display: 'none', color: '#f3d9fb' });
|
|
134
|
+
|
|
135
|
+
var chevron = document.createElement('span');
|
|
136
|
+
chevron.textContent = '›';
|
|
137
|
+
chevron.title = 'Move to other side';
|
|
138
|
+
Object.assign(chevron.style, {
|
|
139
|
+
display: 'none',
|
|
140
|
+
cursor: 'pointer',
|
|
141
|
+
color: '#fff',
|
|
142
|
+
fontSize: '14px',
|
|
90
143
|
flexShrink: '0',
|
|
91
144
|
padding: '0 2px',
|
|
145
|
+
opacity: '0.8',
|
|
92
146
|
});
|
|
93
|
-
|
|
147
|
+
chevron.addEventListener('click', function (e) { e.stopPropagation(); moveSide(); });
|
|
94
148
|
|
|
95
|
-
pill.appendChild(
|
|
149
|
+
pill.appendChild(iconBtn);
|
|
96
150
|
pill.appendChild(pillText);
|
|
97
|
-
pill.appendChild(
|
|
98
|
-
|
|
151
|
+
pill.appendChild(chevron);
|
|
152
|
+
pillWrap.appendChild(pill);
|
|
153
|
+
document.body.appendChild(pillWrap);
|
|
99
154
|
|
|
100
|
-
|
|
155
|
+
pillWrap.addEventListener('mouseenter', function () {
|
|
101
156
|
clearMeasureOverlay();
|
|
157
|
+
clearMeasureTargetHL();
|
|
102
158
|
hideTooltip();
|
|
103
159
|
expandPill();
|
|
160
|
+
zapEl.style.transform = 'rotate(360deg)';
|
|
161
|
+
zapEl.style.opacity = '0';
|
|
162
|
+
closeEl.style.opacity = '1';
|
|
104
163
|
});
|
|
105
|
-
|
|
164
|
+
pillWrap.addEventListener('mouseleave', function () {
|
|
106
165
|
if (selectedEls.length === 0 && !pinEl) collapsePill();
|
|
166
|
+
zapEl.style.transform = 'rotate(0deg)';
|
|
167
|
+
zapEl.style.opacity = '1';
|
|
168
|
+
closeEl.style.opacity = '0';
|
|
107
169
|
});
|
|
108
170
|
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
171
|
+
function moveSide() {
|
|
172
|
+
sideRight = !sideRight;
|
|
173
|
+
if (sideRight) {
|
|
174
|
+
pillWrap.style.left = 'auto';
|
|
175
|
+
pillWrap.style.right = '4px';
|
|
176
|
+
chevron.textContent = '‹';
|
|
177
|
+
} else {
|
|
178
|
+
pillWrap.style.right = 'auto';
|
|
179
|
+
pillWrap.style.left = '4px';
|
|
180
|
+
chevron.textContent = '›';
|
|
116
181
|
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function expandPill(text) {
|
|
185
|
+
pill.style.maxWidth = '760px';
|
|
186
|
+
pillText.style.display = 'inline';
|
|
187
|
+
chevron.style.display = 'inline';
|
|
188
|
+
pillExpanded = true;
|
|
189
|
+
if (text) { pillText.textContent = text; return; }
|
|
117
190
|
if (selectedEls.length > 0) {
|
|
118
|
-
pillText.textContent =
|
|
191
|
+
pillText.textContent = selectedEls.length + ' selected · Cmd+C copy all · Esc clear';
|
|
119
192
|
} else if (measureMode) {
|
|
120
|
-
pillText.textContent = 'Measure · hover
|
|
193
|
+
pillText.textContent = 'Measure · hover distances · M pin · Cmd+C copy · Option toggle';
|
|
121
194
|
} else {
|
|
122
|
-
pillText.textContent = 'Properties · hover
|
|
195
|
+
pillText.textContent = 'Properties · hover inspect · P pick · Cmd+C copy · Option measure';
|
|
123
196
|
}
|
|
124
197
|
}
|
|
125
198
|
|
|
126
199
|
function collapsePill() {
|
|
127
200
|
pill.style.maxWidth = '32px';
|
|
128
|
-
pillText.style.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
function updatePillForSelection() {
|
|
133
|
-
if (selectedEls.length > 0) {
|
|
134
|
-
expandPill();
|
|
135
|
-
} else if (!pill.matches(':hover')) {
|
|
136
|
-
collapsePill();
|
|
137
|
-
}
|
|
201
|
+
pillText.style.display = 'none';
|
|
202
|
+
chevron.style.display = 'none';
|
|
203
|
+
pillExpanded = false;
|
|
138
204
|
}
|
|
139
205
|
|
|
140
206
|
function flashMode() {
|
|
141
|
-
|
|
207
|
+
if (pillExpanded && pillWrap.matches(':hover')) return;
|
|
208
|
+
var text = measureMode ? '⬡ Measure mode' : '◉ Properties mode';
|
|
142
209
|
expandPill(text);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
210
|
+
clearTimeout(flashTimer);
|
|
211
|
+
flashTimer = setTimeout(function () {
|
|
212
|
+
if (!pillWrap.matches(':hover') && selectedEls.length === 0 && !pinEl) collapsePill();
|
|
213
|
+
else expandPill();
|
|
146
214
|
}, 1200);
|
|
147
215
|
}
|
|
148
216
|
|
|
@@ -150,7 +218,8 @@
|
|
|
150
218
|
function activate() {
|
|
151
219
|
fiActive = true;
|
|
152
220
|
measureMode = false;
|
|
153
|
-
|
|
221
|
+
pillWrap.style.display = 'block';
|
|
222
|
+
document.body.style.cursor = 'crosshair';
|
|
154
223
|
collapsePill();
|
|
155
224
|
}
|
|
156
225
|
|
|
@@ -161,7 +230,10 @@
|
|
|
161
230
|
lastHovered = null;
|
|
162
231
|
clearSelection();
|
|
163
232
|
clearPin();
|
|
164
|
-
|
|
233
|
+
clearHoverOutline();
|
|
234
|
+
clearMeasureTargetHL();
|
|
235
|
+
pillWrap.style.display = 'none';
|
|
236
|
+
document.body.style.cursor = '';
|
|
165
237
|
hideTooltip();
|
|
166
238
|
clearMeasureOverlay();
|
|
167
239
|
}
|
|
@@ -169,15 +241,19 @@
|
|
|
169
241
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
170
242
|
function hideTooltip() { tooltip.style.display = 'none'; }
|
|
171
243
|
|
|
244
|
+
function esc(s) {
|
|
245
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
246
|
+
}
|
|
247
|
+
|
|
172
248
|
function positionTooltip(x, y) {
|
|
173
249
|
tooltip.style.display = 'block';
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
250
|
+
var tw = tooltip.offsetWidth;
|
|
251
|
+
var th = tooltip.offsetHeight;
|
|
252
|
+
var vw = window.innerWidth;
|
|
253
|
+
var vh = window.innerHeight;
|
|
254
|
+
var margin = 16;
|
|
255
|
+
var left = x + 16;
|
|
256
|
+
var top = y + 16;
|
|
181
257
|
if (left + tw > vw - margin) left = x - tw - 16;
|
|
182
258
|
if (top + th > vh - margin) top = y - th - 16;
|
|
183
259
|
if (left < margin) left = margin;
|
|
@@ -187,204 +263,341 @@
|
|
|
187
263
|
}
|
|
188
264
|
|
|
189
265
|
function toHex(r, g, b) {
|
|
190
|
-
return '#' + [r, g, b].map(v
|
|
266
|
+
return '#' + [r, g, b].map(function (v) { return Math.round(v).toString(16).padStart(2, '0'); }).join('');
|
|
191
267
|
}
|
|
192
268
|
|
|
193
269
|
function parseColor(str) {
|
|
194
270
|
if (!str || str === 'transparent' || str === 'rgba(0, 0, 0, 0)') return null;
|
|
195
|
-
|
|
271
|
+
var m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
196
272
|
if (!m) return { hex: str, alpha: 1 };
|
|
197
|
-
|
|
198
|
-
return { hex: toHex(r, g, b), alpha: parseFloat(a) };
|
|
273
|
+
return { hex: toHex(m[1], m[2], m[3]), alpha: m[4] !== undefined ? parseFloat(m[4]) : 1 };
|
|
199
274
|
}
|
|
200
275
|
|
|
201
276
|
function colorLabel(str) {
|
|
202
|
-
|
|
277
|
+
var c = parseColor(str);
|
|
203
278
|
if (!c) return null;
|
|
204
|
-
if (c.alpha < 1) return
|
|
279
|
+
if (c.alpha < 1) return c.hex + ' (' + Math.round(c.alpha * 100) + '% opacity)';
|
|
205
280
|
return c.hex;
|
|
206
281
|
}
|
|
207
282
|
|
|
283
|
+
function colorObj(str) {
|
|
284
|
+
var c = parseColor(str);
|
|
285
|
+
if (!c) return null;
|
|
286
|
+
return { raw: str, hex: c.hex, alpha: c.alpha, label: colorLabel(str) };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function edge(t, r, b, l) {
|
|
290
|
+
if ([t, r, b, l].every(function (v) { return v === '0px'; })) return null;
|
|
291
|
+
return { value: t + ' ' + r + ' ' + b + ' ' + l };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function weightName(w) {
|
|
295
|
+
var m = { '100': 'Thin', '200': 'ExtraLight', '300': 'Light', '400': 'Regular', '500': 'Medium', '600': 'Semibold', '700': 'Bold', '800': 'ExtraBold', '900': 'Black', 'normal': 'Regular', 'bold': 'Bold' };
|
|
296
|
+
return m[w] || w;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function swatch(hex) {
|
|
300
|
+
return '<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:' + hex + ';margin-right:6px;vertical-align:middle;border:1px solid rgba(255,255,255,0.2)"></span>';
|
|
301
|
+
}
|
|
302
|
+
|
|
208
303
|
function getComponentName(el) {
|
|
209
|
-
for (
|
|
304
|
+
for (var key in el) {
|
|
210
305
|
if (key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$')) {
|
|
211
|
-
|
|
306
|
+
var fiber = el[key];
|
|
212
307
|
while (fiber) {
|
|
213
|
-
|
|
308
|
+
var name = (fiber.type && (fiber.type.displayName || fiber.type.name));
|
|
214
309
|
if (name && name.length > 3 && /^[A-Z]/.test(name)) return name;
|
|
215
310
|
fiber = fiber.return;
|
|
216
311
|
}
|
|
217
312
|
}
|
|
218
313
|
}
|
|
219
314
|
if (el.__vueParentComponent) {
|
|
220
|
-
|
|
221
|
-
if (
|
|
315
|
+
var vname = el.__vueParentComponent.type && (el.__vueParentComponent.type.name || el.__vueParentComponent.type.__name);
|
|
316
|
+
if (vname && vname.length > 3) return vname;
|
|
222
317
|
}
|
|
223
318
|
return null;
|
|
224
319
|
}
|
|
225
320
|
|
|
226
321
|
function getMatchingRules(el) {
|
|
227
|
-
|
|
322
|
+
var results = [];
|
|
228
323
|
try {
|
|
229
|
-
for (
|
|
230
|
-
|
|
231
|
-
|
|
324
|
+
for (var s = 0; s < document.styleSheets.length; s++) {
|
|
325
|
+
var sheet = document.styleSheets[s];
|
|
326
|
+
var rules;
|
|
327
|
+
try { rules = sheet.cssRules; } catch (e) { continue; }
|
|
232
328
|
if (!rules) continue;
|
|
233
|
-
for (
|
|
329
|
+
for (var r = 0; r < rules.length; r++) {
|
|
330
|
+
var rule = rules[r];
|
|
234
331
|
if (rule.type !== 1) continue;
|
|
235
|
-
|
|
332
|
+
var sel = rule.selectorText;
|
|
236
333
|
if (!sel) continue;
|
|
237
334
|
if (/^[*,]|^:root|^html|^body$|^::before|^::after/.test(sel.trim())) continue;
|
|
238
335
|
try {
|
|
239
336
|
if (el.matches(sel)) {
|
|
240
|
-
|
|
241
|
-
for (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (val) props.push(
|
|
337
|
+
var props = [];
|
|
338
|
+
for (var i = 0; i < rule.style.length; i++) {
|
|
339
|
+
var prop = rule.style[i];
|
|
340
|
+
var val = rule.style.getPropertyValue(prop);
|
|
341
|
+
if (val) props.push(' ' + prop + ': ' + val + ';');
|
|
245
342
|
}
|
|
246
|
-
if (props.length) results.push(
|
|
343
|
+
if (props.length) results.push(sel + ' {\n' + props.join('\n') + '\n}');
|
|
247
344
|
}
|
|
248
|
-
} catch {}
|
|
345
|
+
} catch (e) {}
|
|
249
346
|
}
|
|
250
347
|
}
|
|
251
|
-
} catch {}
|
|
348
|
+
} catch (e) {}
|
|
252
349
|
return results;
|
|
253
350
|
}
|
|
254
351
|
|
|
255
352
|
function getSelector(el) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
for (
|
|
259
|
-
|
|
353
|
+
var parts = [];
|
|
354
|
+
var cur = el;
|
|
355
|
+
for (var i = 0; i < 3 && cur && cur !== document.body; i++) {
|
|
356
|
+
var part = cur.tagName.toLowerCase();
|
|
260
357
|
if (cur.id) { part += '#' + cur.id; parts.unshift(part); break; }
|
|
261
|
-
|
|
358
|
+
var cls = Array.prototype.slice.call(cur.classList).filter(function (c) { return c.indexOf('__specter') !== 0; });
|
|
359
|
+
if (cls.length) {
|
|
360
|
+
part += '.' + cls.slice(0, 2).join('.');
|
|
361
|
+
} else if (cur.parentElement) {
|
|
362
|
+
var sameTag = Array.prototype.slice.call(cur.parentElement.children).filter(function (c) { return c.tagName === cur.tagName; });
|
|
363
|
+
if (sameTag.length > 1) {
|
|
364
|
+
part += ':nth-child(' + (Array.prototype.indexOf.call(cur.parentElement.children, cur) + 1) + ')';
|
|
365
|
+
}
|
|
366
|
+
}
|
|
262
367
|
parts.unshift(part);
|
|
263
368
|
cur = cur.parentElement;
|
|
264
369
|
}
|
|
265
370
|
return parts.join(' > ');
|
|
266
371
|
}
|
|
267
372
|
|
|
268
|
-
// ───
|
|
373
|
+
// ─── Structured data model ──────────────────────────────────────────────────
|
|
269
374
|
function buildInfo(el) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
375
|
+
var cs = getComputedStyle(el);
|
|
376
|
+
var rect = el.getBoundingClientRect();
|
|
377
|
+
var ff = cs.fontFamily.split(',')[0].replace(/['"]/g, '').trim();
|
|
378
|
+
var raw = el.textContent ? el.textContent.trim() : '';
|
|
379
|
+
return {
|
|
380
|
+
el: el,
|
|
381
|
+
tag: el.tagName.toLowerCase(),
|
|
382
|
+
id: el.id || null,
|
|
383
|
+
classes: Array.prototype.slice.call(el.classList).filter(function (c) { return c.indexOf('__specter') !== 0; }),
|
|
384
|
+
component: getComponentName(el),
|
|
385
|
+
styleKey: el.dataset ? el.dataset.style : null,
|
|
386
|
+
width: Math.round(rect.width),
|
|
387
|
+
height: Math.round(rect.height),
|
|
388
|
+
text: raw.slice(0, 40),
|
|
389
|
+
textTrunc: raw.length > 40,
|
|
390
|
+
font: { family: ff, weight: cs.fontWeight, size: cs.fontSize, lineHeight: cs.lineHeight },
|
|
391
|
+
color: colorObj(cs.color),
|
|
392
|
+
bg: colorObj(cs.backgroundColor),
|
|
393
|
+
padding: edge(cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft),
|
|
394
|
+
margin: edge(cs.marginTop, cs.marginRight, cs.marginBottom, cs.marginLeft),
|
|
395
|
+
radius: (cs.borderRadius && cs.borderRadius !== '0px') ? cs.borderRadius : null,
|
|
396
|
+
display: ['flex', 'grid', 'inline-flex', 'inline-grid'].indexOf(cs.display) >= 0 ? cs.display : null,
|
|
397
|
+
gap: (cs.gap && cs.gap !== 'normal') ? cs.gap : null,
|
|
398
|
+
flexDir: (cs.display.indexOf('flex') >= 0 && cs.flexDirection !== 'row') ? cs.flexDirection : null,
|
|
399
|
+
rules: getMatchingRules(el),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
285
402
|
|
|
286
|
-
|
|
287
|
-
|
|
403
|
+
function row(label, val) {
|
|
404
|
+
return '<div style="display:flex;margin-bottom:8px"><span style="color:' + LABEL + ';min-width:76px;flex-shrink:0">' + label + '</span><span style="color:#fff;word-break:break-word">' + val + '</span></div>';
|
|
405
|
+
}
|
|
288
406
|
|
|
289
|
-
|
|
290
|
-
|
|
407
|
+
function buildHumanDisplay(data) {
|
|
408
|
+
var h = '';
|
|
409
|
+
var id = '<span style="color:#fff"><' + data.tag + '></span>';
|
|
410
|
+
if (data.id) id += '<span style="color:#F0B86E">#' + esc(data.id) + '</span>';
|
|
411
|
+
if (data.classes.length) id += '<span style="color:#5FD3C0">.' + data.classes.map(esc).join('.') + '</span>';
|
|
412
|
+
if (data.component) id += ' <span style="color:#E0A3F5;font-weight:600">' + esc(data.component) + '</span>';
|
|
413
|
+
if (data.styleKey) id += ' <span style="color:' + LABEL + '">[' + esc(data.styleKey) + ']</span>';
|
|
414
|
+
id += ' <span style="color:' + LABEL + '">' + data.width + '×' + data.height + '</span>';
|
|
415
|
+
h += '<div style="margin-bottom:8px">' + id + '</div>';
|
|
416
|
+
if (data.text) h += '<div style="color:' + LABEL + ';font-style:italic;margin-bottom:8px">"' + esc(data.text) + (data.textTrunc ? '…' : '') + '"</div>';
|
|
417
|
+
h += '<div style="height:8px"></div>';
|
|
418
|
+
h += row('Font', esc(data.font.family) + ' · ' + weightName(data.font.weight) + ' · ' + data.font.size + '/' + data.font.lineHeight);
|
|
419
|
+
if (data.color) h += row('Color', swatch(data.color.hex) + data.color.label);
|
|
420
|
+
if (data.bg) h += row('Bg', swatch(data.bg.hex) + data.bg.label);
|
|
421
|
+
if (data.padding) h += row('Padding', data.padding.value);
|
|
422
|
+
if (data.margin) h += row('Margin', data.margin.value);
|
|
423
|
+
if (data.radius) h += row('Radius', data.radius);
|
|
424
|
+
if (data.display) {
|
|
425
|
+
var d = data.display;
|
|
426
|
+
if (data.gap) d += ' · gap ' + data.gap;
|
|
427
|
+
if (data.flexDir) d += ' · ' + data.flexDir;
|
|
428
|
+
h += row('Display', d);
|
|
429
|
+
}
|
|
430
|
+
return h;
|
|
431
|
+
}
|
|
291
432
|
|
|
292
|
-
|
|
293
|
-
|
|
433
|
+
function buildLLMClipboard(data) {
|
|
434
|
+
var lines = ['[Specter]'];
|
|
435
|
+
var head = '<' + data.tag + '>';
|
|
436
|
+
if (data.id) head += '#' + data.id;
|
|
437
|
+
if (data.classes.length) head += '.' + data.classes.join('.');
|
|
438
|
+
if (data.component) head += ' ' + data.component;
|
|
439
|
+
if (data.styleKey) head += ' [' + data.styleKey + ']';
|
|
440
|
+
head += ' ' + data.width + '×' + data.height;
|
|
441
|
+
lines.push(head);
|
|
442
|
+
if (data.text) lines.push('"' + data.text + (data.textTrunc ? '…' : '') + '"');
|
|
443
|
+
lines.push('font: ' + data.font.family + ' ' + data.font.weight + ' ' + data.font.size + '/' + data.font.lineHeight);
|
|
444
|
+
if (data.color) lines.push('color: ' + data.color.label);
|
|
445
|
+
if (data.bg) lines.push('bg: ' + data.bg.label);
|
|
446
|
+
if (data.padding) lines.push('padding: ' + data.padding.value);
|
|
447
|
+
if (data.margin) lines.push('margin: ' + data.margin.value);
|
|
448
|
+
if (data.radius) lines.push('radius: ' + data.radius);
|
|
449
|
+
if (data.display) {
|
|
450
|
+
var d = 'display: ' + data.display;
|
|
451
|
+
if (data.gap) d += ' gap: ' + data.gap;
|
|
452
|
+
if (data.flexDir) d += ' dir: ' + data.flexDir;
|
|
453
|
+
lines.push(d);
|
|
454
|
+
}
|
|
455
|
+
lines.push('selector: ' + getSelector(data.el));
|
|
456
|
+
var out = lines.join('\n');
|
|
457
|
+
if (data.rules && data.rules.length) out += '\n\n--- CSS Rules ---\n' + data.rules.join('\n\n');
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
294
460
|
|
|
295
|
-
|
|
296
|
-
|
|
461
|
+
function buildMultiSelectCopyText() {
|
|
462
|
+
return selectedEls.map(function (el, i) {
|
|
463
|
+
var body = buildLLMClipboard(buildInfo(el));
|
|
464
|
+
return body.replace('[Specter]', '[Specter ' + (i + 1) + '/' + selectedEls.length + ']');
|
|
465
|
+
}).join('\n\n' + Array(41).join('─') + '\n\n');
|
|
466
|
+
}
|
|
297
467
|
|
|
298
|
-
|
|
299
|
-
|
|
468
|
+
function linesToHTML(str) {
|
|
469
|
+
return str.split('\n').map(function (l) {
|
|
470
|
+
return '<div style="margin-bottom:4px;color:#fff">' + esc(l) + '</div>';
|
|
471
|
+
}).join('');
|
|
472
|
+
}
|
|
300
473
|
|
|
301
|
-
|
|
474
|
+
// ─── Hover outline ──────────────────────────────────────────────────────────
|
|
475
|
+
function setHoverOutline(el) {
|
|
476
|
+
if (outlinedEl === el) return;
|
|
477
|
+
clearHoverOutline();
|
|
478
|
+
if (!el) return;
|
|
479
|
+
outlinedEl = el;
|
|
480
|
+
prevOutline = el.style.outline;
|
|
481
|
+
el.style.outline = '2px solid ' + PURPLE;
|
|
482
|
+
el.style.outlineOffset = '-1px';
|
|
483
|
+
}
|
|
302
484
|
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (disp.includes('flex') && cs.flexDirection !== 'row') d += ` dir: ${cs.flexDirection}`;
|
|
308
|
-
lines.push(d);
|
|
485
|
+
function clearHoverOutline() {
|
|
486
|
+
if (outlinedEl) {
|
|
487
|
+
outlinedEl.style.outline = prevOutline;
|
|
488
|
+
outlinedEl = null;
|
|
309
489
|
}
|
|
310
|
-
|
|
311
|
-
return lines.join('\n');
|
|
312
490
|
}
|
|
313
491
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
492
|
+
// ─── Copied feedback ────────────────────────────────────────────────────────
|
|
493
|
+
function showCopied() {
|
|
494
|
+
tooltip.innerHTML = '<div style="color:' + GREEN + ';font-weight:600;display:flex;align-items:center;gap:6px"><span>✓</span><span>Copied!</span></div>';
|
|
495
|
+
tooltip.style.display = 'block';
|
|
496
|
+
clearTimeout(copiedTimer);
|
|
497
|
+
copiedTimer = setTimeout(function () {
|
|
498
|
+
if (fiActive && lastHovered) reRenderTooltip();
|
|
499
|
+
else hideTooltip();
|
|
500
|
+
}, 1200);
|
|
320
501
|
}
|
|
321
502
|
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
503
|
+
function reRenderTooltip() {
|
|
504
|
+
if (measureMode) {
|
|
505
|
+
var text = (pinEl && pinEl !== lastHovered) ? measureBetween(pinEl, lastHovered) : measureToNeighbor(lastHovered);
|
|
506
|
+
tooltip.innerHTML = linesToHTML(text);
|
|
507
|
+
} else {
|
|
508
|
+
tooltip.innerHTML = buildHumanDisplay(buildInfo(lastHovered));
|
|
509
|
+
}
|
|
510
|
+
positionTooltip(lastMouse.x, lastMouse.y);
|
|
330
511
|
}
|
|
331
512
|
|
|
332
513
|
// ─── Multi-select ─────────────────────────────────────────────────────────
|
|
333
514
|
function makeBadge(el, index) {
|
|
334
|
-
|
|
335
|
-
|
|
515
|
+
var rect = el.getBoundingClientRect();
|
|
516
|
+
var badge = document.createElement('div');
|
|
336
517
|
badge.textContent = String(index + 1);
|
|
337
518
|
Object.assign(badge.style, {
|
|
338
519
|
position: 'fixed',
|
|
339
|
-
left: rect.left + 'px',
|
|
340
|
-
top: rect.top + 'px',
|
|
341
|
-
width: '
|
|
342
|
-
height: '
|
|
343
|
-
background:
|
|
520
|
+
left: (rect.left - 4) + 'px',
|
|
521
|
+
top: (rect.top - 4) + 'px',
|
|
522
|
+
width: '20px',
|
|
523
|
+
height: '20px',
|
|
524
|
+
background: PURPLE,
|
|
344
525
|
color: '#fff',
|
|
345
|
-
fontSize: '
|
|
526
|
+
fontSize: '11px',
|
|
346
527
|
fontWeight: '700',
|
|
347
|
-
fontFamily:
|
|
528
|
+
fontFamily: MONO,
|
|
348
529
|
borderRadius: '50%',
|
|
349
530
|
display: 'flex',
|
|
350
531
|
alignItems: 'center',
|
|
351
532
|
justifyContent: 'center',
|
|
352
533
|
zIndex: '2147483644',
|
|
353
534
|
pointerEvents: 'none',
|
|
354
|
-
boxShadow: '0
|
|
355
|
-
transform: 'translate(-50%, -50%)',
|
|
535
|
+
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
|
356
536
|
});
|
|
357
537
|
document.body.appendChild(badge);
|
|
358
538
|
return badge;
|
|
359
539
|
}
|
|
360
540
|
|
|
361
541
|
function rebuildBadges() {
|
|
362
|
-
selectedBadges.forEach(b
|
|
542
|
+
selectedBadges.forEach(function (b) { b.remove(); });
|
|
363
543
|
selectedBadges.length = 0;
|
|
364
|
-
selectedEls.forEach((el, i)
|
|
544
|
+
selectedEls.forEach(function (el, i) {
|
|
365
545
|
selectedBadges.push(makeBadge(el, i));
|
|
366
546
|
});
|
|
367
547
|
}
|
|
368
548
|
|
|
369
549
|
function toggleSelection(el) {
|
|
370
|
-
|
|
550
|
+
var idx = selectedEls.indexOf(el);
|
|
371
551
|
if (idx >= 0) {
|
|
372
552
|
selectedEls.splice(idx, 1);
|
|
553
|
+
el.style.outline = '';
|
|
373
554
|
} else {
|
|
374
555
|
selectedEls.push(el);
|
|
556
|
+
el.style.outline = '2px solid ' + PURPLE;
|
|
557
|
+
el.style.outlineOffset = '-1px';
|
|
375
558
|
}
|
|
376
559
|
rebuildBadges();
|
|
377
560
|
updatePillForSelection();
|
|
378
561
|
}
|
|
379
562
|
|
|
380
563
|
function clearSelection() {
|
|
564
|
+
selectedEls.forEach(function (el) { el.style.outline = ''; });
|
|
381
565
|
selectedEls.length = 0;
|
|
382
|
-
selectedBadges.forEach(b
|
|
566
|
+
selectedBadges.forEach(function (b) { b.remove(); });
|
|
383
567
|
selectedBadges.length = 0;
|
|
384
568
|
}
|
|
385
569
|
|
|
386
|
-
|
|
387
|
-
|
|
570
|
+
function updatePillForSelection() {
|
|
571
|
+
if (selectedEls.length > 0) expandPill();
|
|
572
|
+
else if (!pillWrap.matches(':hover')) collapsePill();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ─── Pin (measure) ──────────────────────────────────────────────────────────
|
|
576
|
+
function setPin(el) {
|
|
577
|
+
pinEl = el;
|
|
578
|
+
updatePinHL();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function updatePinHL() {
|
|
582
|
+
if (!pinEl) return;
|
|
583
|
+
var r = pinEl.getBoundingClientRect();
|
|
584
|
+
if (!pinHighlight) {
|
|
585
|
+
pinHighlight = document.createElement('div');
|
|
586
|
+
Object.assign(pinHighlight.style, {
|
|
587
|
+
position: 'fixed',
|
|
588
|
+
border: '2px dashed ' + RED,
|
|
589
|
+
background: 'rgba(237,62,97,0.05)',
|
|
590
|
+
boxSizing: 'border-box',
|
|
591
|
+
pointerEvents: 'none',
|
|
592
|
+
zIndex: '2147483643',
|
|
593
|
+
});
|
|
594
|
+
document.body.appendChild(pinHighlight);
|
|
595
|
+
}
|
|
596
|
+
pinHighlight.style.left = r.left + 'px';
|
|
597
|
+
pinHighlight.style.top = r.top + 'px';
|
|
598
|
+
pinHighlight.style.width = r.width + 'px';
|
|
599
|
+
pinHighlight.style.height = r.height + 'px';
|
|
600
|
+
}
|
|
388
601
|
|
|
389
602
|
function clearPin() {
|
|
390
603
|
if (pinHighlight) { pinHighlight.remove(); pinHighlight = null; }
|
|
@@ -396,14 +609,51 @@
|
|
|
396
609
|
measureOverlay.style.display = 'none';
|
|
397
610
|
}
|
|
398
611
|
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
612
|
+
function showMeasureTargetHL(el) {
|
|
613
|
+
clearMeasureTargetHL();
|
|
614
|
+
var r = el.getBoundingClientRect();
|
|
615
|
+
measureHL = document.createElement('div');
|
|
616
|
+
Object.assign(measureHL.style, {
|
|
617
|
+
position: 'fixed',
|
|
618
|
+
left: r.left + 'px',
|
|
619
|
+
top: r.top + 'px',
|
|
620
|
+
width: r.width + 'px',
|
|
621
|
+
height: r.height + 'px',
|
|
622
|
+
background: 'rgba(173,36,211,0.08)',
|
|
623
|
+
border: '1.5px solid ' + PURPLE,
|
|
624
|
+
boxSizing: 'border-box',
|
|
405
625
|
pointerEvents: 'none',
|
|
626
|
+
zIndex: '2147483643',
|
|
406
627
|
});
|
|
628
|
+
document.body.appendChild(measureHL);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function clearMeasureTargetHL() {
|
|
632
|
+
if (measureHL) { measureHL.remove(); measureHL = null; }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ─── Measurement drawing ────────────────────────────────────────────────────
|
|
636
|
+
function cap(x, y, mainHoriz) {
|
|
637
|
+
var c = document.createElement('div');
|
|
638
|
+
Object.assign(c.style, { position: 'absolute', background: RED, pointerEvents: 'none' });
|
|
639
|
+
if (mainHoriz) {
|
|
640
|
+
c.style.left = (x - 0.5) + 'px';
|
|
641
|
+
c.style.top = (y - 4) + 'px';
|
|
642
|
+
c.style.width = '1px';
|
|
643
|
+
c.style.height = '8px';
|
|
644
|
+
} else {
|
|
645
|
+
c.style.left = (x - 4) + 'px';
|
|
646
|
+
c.style.top = (y - 0.5) + 'px';
|
|
647
|
+
c.style.width = '8px';
|
|
648
|
+
c.style.height = '1px';
|
|
649
|
+
}
|
|
650
|
+
measureOverlay.appendChild(c);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function drawLine(x1, y1, x2, y2, label) {
|
|
654
|
+
var isHoriz = Math.abs(y2 - y1) < 1;
|
|
655
|
+
var line = document.createElement('div');
|
|
656
|
+
Object.assign(line.style, { position: 'absolute', background: RED, pointerEvents: 'none' });
|
|
407
657
|
if (isHoriz) {
|
|
408
658
|
Object.assign(line.style, {
|
|
409
659
|
left: Math.min(x1, x2) + 'px', top: (y1 - 0.5) + 'px',
|
|
@@ -416,156 +666,149 @@
|
|
|
416
666
|
});
|
|
417
667
|
}
|
|
418
668
|
measureOverlay.appendChild(line);
|
|
669
|
+
cap(x1, y1, isHoriz);
|
|
670
|
+
cap(x2, y2, isHoriz);
|
|
419
671
|
|
|
420
672
|
if (label && label !== '0px') {
|
|
421
|
-
|
|
673
|
+
var lbl = document.createElement('div');
|
|
422
674
|
lbl.textContent = label;
|
|
423
675
|
Object.assign(lbl.style, {
|
|
424
676
|
position: 'absolute',
|
|
425
|
-
background:
|
|
677
|
+
background: RED,
|
|
426
678
|
color: '#fff',
|
|
427
679
|
fontSize: '10px',
|
|
428
|
-
fontFamily:
|
|
680
|
+
fontFamily: MONO,
|
|
429
681
|
padding: '1px 4px',
|
|
430
682
|
borderRadius: '3px',
|
|
431
683
|
pointerEvents: 'none',
|
|
432
684
|
whiteSpace: 'nowrap',
|
|
433
685
|
});
|
|
434
686
|
if (isHoriz) {
|
|
435
|
-
|
|
436
|
-
lbl.style.left =
|
|
437
|
-
lbl.style.top = (y1 -
|
|
687
|
+
var cxl = (Math.min(x1, x2) + Math.max(x1, x2)) / 2;
|
|
688
|
+
lbl.style.left = cxl + 'px';
|
|
689
|
+
lbl.style.top = (y1 - 16) + 'px';
|
|
438
690
|
lbl.style.transform = 'translateX(-50%)';
|
|
439
691
|
} else {
|
|
440
|
-
|
|
692
|
+
var cyl = (Math.min(y1, y2) + Math.max(y1, y2)) / 2;
|
|
441
693
|
lbl.style.left = (x1 + 6) + 'px';
|
|
442
|
-
lbl.style.top =
|
|
694
|
+
lbl.style.top = cyl + 'px';
|
|
443
695
|
lbl.style.transform = 'translateY(-50%)';
|
|
444
696
|
}
|
|
445
697
|
measureOverlay.appendChild(lbl);
|
|
446
698
|
}
|
|
447
699
|
}
|
|
448
700
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
clearMeasureOverlay();
|
|
468
|
-
const tr = targetEl.getBoundingClientRect();
|
|
469
|
-
let measured = false;
|
|
470
|
-
let cur = targetEl.parentElement;
|
|
471
|
-
|
|
472
|
-
for (let depth = 0; depth < 3 && cur && !measured; depth++) {
|
|
473
|
-
const children = Array.from(cur.children).filter(c => c !== targetEl && c.offsetParent !== null);
|
|
474
|
-
if (children.length === 0) { cur = cur.parentElement; continue; }
|
|
475
|
-
|
|
476
|
-
const closest = { top: null, bottom: null, left: null, right: null };
|
|
477
|
-
const gaps = { top: Infinity, bottom: Infinity, left: Infinity, right: Infinity };
|
|
478
|
-
|
|
479
|
-
for (const sib of children) {
|
|
480
|
-
const sr = sib.getBoundingClientRect();
|
|
481
|
-
if (sr.bottom <= tr.top) {
|
|
482
|
-
const g = tr.top - sr.bottom;
|
|
483
|
-
if (g < gaps.top) { gaps.top = g; closest.top = sib; }
|
|
484
|
-
}
|
|
485
|
-
if (sr.top >= tr.bottom) {
|
|
486
|
-
const g = sr.top - tr.bottom;
|
|
487
|
-
if (g < gaps.bottom) { gaps.bottom = g; closest.bottom = sib; }
|
|
488
|
-
}
|
|
489
|
-
if (sr.right <= tr.left) {
|
|
490
|
-
const g = tr.left - sr.right;
|
|
491
|
-
if (g < gaps.left) { gaps.left = g; closest.left = sib; }
|
|
492
|
-
}
|
|
493
|
-
if (sr.left >= tr.right) {
|
|
494
|
-
const g = sr.left - tr.right;
|
|
495
|
-
if (g < gaps.right) { gaps.right = g; closest.right = sib; }
|
|
496
|
-
}
|
|
701
|
+
// ─── Neighbor computation (parent-first w/ container fallback) ───────────────
|
|
702
|
+
function edgeToAncestor(targetEl, tr, dir) {
|
|
703
|
+
var cur = targetEl.parentElement;
|
|
704
|
+
var ctx = 'parent';
|
|
705
|
+
for (var i = 0; i < 4 && cur && cur !== document.body; i++) {
|
|
706
|
+
var pr = cur.getBoundingClientRect();
|
|
707
|
+
var gap;
|
|
708
|
+
if (dir === 'top') gap = tr.top - pr.top;
|
|
709
|
+
else if (dir === 'bottom') gap = pr.bottom - tr.bottom;
|
|
710
|
+
else if (dir === 'left') gap = tr.left - pr.left;
|
|
711
|
+
else gap = pr.right - tr.right;
|
|
712
|
+
if (gap > 0.5) {
|
|
713
|
+
var ed;
|
|
714
|
+
if (dir === 'top') ed = pr.top;
|
|
715
|
+
else if (dir === 'bottom') ed = pr.bottom;
|
|
716
|
+
else if (dir === 'left') ed = pr.left;
|
|
717
|
+
else ed = pr.right;
|
|
718
|
+
return { gap: gap, ctx: ctx, edge: ed };
|
|
497
719
|
}
|
|
498
|
-
|
|
499
|
-
if (closest.top || closest.bottom || closest.left || closest.right) {
|
|
500
|
-
measured = true;
|
|
501
|
-
const cx = (tr.left + tr.right) / 2;
|
|
502
|
-
const cy = (tr.top + tr.bottom) / 2;
|
|
503
|
-
if (closest.top) drawLine(cx, closest.top.getBoundingClientRect().bottom, cx, tr.top, Math.round(gaps.top) + 'px');
|
|
504
|
-
if (closest.bottom) drawLine(cx, tr.bottom, cx, closest.bottom.getBoundingClientRect().top, Math.round(gaps.bottom) + 'px');
|
|
505
|
-
if (closest.left) drawLine(closest.left.getBoundingClientRect().right, cy, tr.left, cy, Math.round(gaps.left) + 'px');
|
|
506
|
-
if (closest.right) drawLine(tr.right, cy, closest.right.getBoundingClientRect().left, cy, Math.round(gaps.right) + 'px');
|
|
507
|
-
} else {
|
|
508
|
-
const pr = cur.getBoundingClientRect();
|
|
509
|
-
const cx = (tr.left + tr.right) / 2;
|
|
510
|
-
const cy = (tr.top + tr.bottom) / 2;
|
|
511
|
-
const gT = tr.top - pr.top, gB = pr.bottom - tr.bottom;
|
|
512
|
-
const gL = tr.left - pr.left, gR = pr.right - tr.right;
|
|
513
|
-
if (gT > 0) drawLine(cx, pr.top, cx, tr.top, Math.round(gT) + 'px');
|
|
514
|
-
if (gB > 0) drawLine(cx, tr.bottom, cx, pr.bottom, Math.round(gB) + 'px');
|
|
515
|
-
if (gL > 0) drawLine(pr.left, cy, tr.left, cy, Math.round(gL) + 'px');
|
|
516
|
-
if (gR > 0) drawLine(tr.right, cy, pr.right, cy, Math.round(gR) + 'px');
|
|
517
|
-
measured = true;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
720
|
cur = cur.parentElement;
|
|
721
|
+
ctx = 'container';
|
|
521
722
|
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function computeNeighbors(targetEl, tr) {
|
|
727
|
+
var out = { top: null, bottom: null, left: null, right: null };
|
|
728
|
+
var parent = targetEl.parentElement;
|
|
729
|
+
if (parent) {
|
|
730
|
+
var sibs = Array.prototype.slice.call(parent.children).filter(function (c) { return c !== targetEl && c.offsetParent !== null; });
|
|
731
|
+
sibs.forEach(function (sib) {
|
|
732
|
+
var sr = sib.getBoundingClientRect();
|
|
733
|
+
if (sr.bottom <= tr.top) { var g = tr.top - sr.bottom; if (!out.top || g < out.top.gap) out.top = { gap: g, ctx: 'sibling', edge: sr.bottom }; }
|
|
734
|
+
if (sr.top >= tr.bottom) { var g2 = sr.top - tr.bottom; if (!out.bottom || g2 < out.bottom.gap) out.bottom = { gap: g2, ctx: 'sibling', edge: sr.top }; }
|
|
735
|
+
if (sr.right <= tr.left) { var g3 = tr.left - sr.right; if (!out.left || g3 < out.left.gap) out.left = { gap: g3, ctx: 'sibling', edge: sr.right }; }
|
|
736
|
+
if (sr.left >= tr.right) { var g4 = sr.left - tr.right; if (!out.right || g4 < out.right.gap) out.right = { gap: g4, ctx: 'sibling', edge: sr.left }; }
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
['top', 'bottom', 'left', 'right'].forEach(function (k) { if (!out[k]) out[k] = edgeToAncestor(targetEl, tr, k); });
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
522
742
|
|
|
743
|
+
function measureToNeighbor(targetEl) {
|
|
744
|
+
clearMeasureOverlay();
|
|
745
|
+
var tr = targetEl.getBoundingClientRect();
|
|
746
|
+
var dirs = computeNeighbors(targetEl, tr);
|
|
747
|
+
var cx = (tr.left + tr.right) / 2;
|
|
748
|
+
var cy = (tr.top + tr.bottom) / 2;
|
|
749
|
+
if (dirs.top) drawLine(cx, dirs.top.edge, cx, tr.top, Math.round(dirs.top.gap) + 'px');
|
|
750
|
+
if (dirs.bottom) drawLine(cx, tr.bottom, cx, dirs.bottom.edge, Math.round(dirs.bottom.gap) + 'px');
|
|
751
|
+
if (dirs.left) drawLine(dirs.left.edge, cy, tr.left, cy, Math.round(dirs.left.gap) + 'px');
|
|
752
|
+
if (dirs.right) drawLine(tr.right, cy, dirs.right.edge, cy, Math.round(dirs.right.gap) + 'px');
|
|
523
753
|
measureOverlay.style.display = 'block';
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
754
|
+
var tag = targetEl.tagName.toLowerCase();
|
|
755
|
+
var lines = ['⬡ Measure (M pin · Cmd+C copy)', '<' + tag + '> ' + Math.round(tr.width) + '×' + Math.round(tr.height)];
|
|
756
|
+
['top', 'right', 'bottom', 'left'].forEach(function (k) {
|
|
757
|
+
if (dirs[k]) lines.push(k + ': ' + Math.round(dirs[k].gap) + 'px (to ' + dirs[k].ctx + ')');
|
|
758
|
+
});
|
|
759
|
+
return lines.join('\n');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function buildNeighborCopyText(el) {
|
|
763
|
+
var tr = el.getBoundingClientRect();
|
|
764
|
+
var dirs = computeNeighbors(el, tr);
|
|
765
|
+
var lines = ['[Specter Measure]', '<' + el.tagName.toLowerCase() + '> ' + Math.round(tr.width) + '×' + Math.round(tr.height), 'selector: ' + getSelector(el)];
|
|
766
|
+
['top', 'right', 'bottom', 'left'].forEach(function (k) {
|
|
767
|
+
if (dirs[k]) lines.push(k + ': ' + Math.round(dirs[k].gap) + 'px (to ' + dirs[k].ctx + ')');
|
|
768
|
+
});
|
|
769
|
+
return lines.join('\n');
|
|
527
770
|
}
|
|
528
771
|
|
|
529
772
|
function measureBetween(fromEl, toEl) {
|
|
530
773
|
clearMeasureOverlay();
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
774
|
+
var fr = fromEl.getBoundingClientRect();
|
|
775
|
+
var tr = toEl.getBoundingClientRect();
|
|
776
|
+
var fTag = fromEl.tagName.toLowerCase(), tTag = toEl.tagName.toLowerCase();
|
|
777
|
+
var fComp = getComponentName(fromEl), tComp = getComponentName(toEl);
|
|
535
778
|
|
|
536
|
-
|
|
537
|
-
lines.push(
|
|
538
|
-
lines.push(
|
|
779
|
+
var lines = ['⬡ Measure (Cmd+C copy)'];
|
|
780
|
+
lines.push('From: <' + fTag + '>' + (fComp ? ' ' + fComp : '') + ' ' + Math.round(fr.width) + '×' + Math.round(fr.height));
|
|
781
|
+
lines.push('To: <' + tTag + '>' + (tComp ? ' ' + tComp : '') + ' ' + Math.round(tr.width) + '×' + Math.round(tr.height));
|
|
539
782
|
|
|
540
|
-
|
|
541
|
-
|
|
783
|
+
var fromContainsTo = fr.left <= tr.left && fr.top <= tr.top && fr.right >= tr.right && fr.bottom >= tr.bottom;
|
|
784
|
+
var toContainsFrom = tr.left <= fr.left && tr.top <= fr.top && tr.right >= fr.right && tr.bottom >= fr.bottom;
|
|
542
785
|
|
|
543
786
|
if (fromContainsTo || toContainsFrom) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
787
|
+
var outer = fromContainsTo ? fr : tr;
|
|
788
|
+
var inner = fromContainsTo ? tr : fr;
|
|
789
|
+
var cx = (inner.left + inner.right) / 2, cy = (inner.top + inner.bottom) / 2;
|
|
790
|
+
var iT = inner.top - outer.top, iB = outer.bottom - inner.bottom;
|
|
791
|
+
var iL = inner.left - outer.left, iR = outer.right - inner.right;
|
|
549
792
|
if (iT > 0) drawLine(cx, outer.top, cx, inner.top, Math.round(iT) + 'px');
|
|
550
793
|
if (iB > 0) drawLine(cx, inner.bottom, cx, outer.bottom, Math.round(iB) + 'px');
|
|
551
794
|
if (iL > 0) drawLine(outer.left, cy, inner.left, cy, Math.round(iL) + 'px');
|
|
552
795
|
if (iR > 0) drawLine(inner.right, cy, outer.right, cy, Math.round(iR) + 'px');
|
|
553
|
-
lines.push(
|
|
796
|
+
lines.push('inset: ' + Math.round(iT) + 'px ' + Math.round(iR) + 'px ' + Math.round(iB) + 'px ' + Math.round(iL) + 'px');
|
|
554
797
|
} else {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
798
|
+
var vGap = fr.bottom <= tr.top ? tr.top - fr.bottom : tr.bottom <= fr.top ? fr.top - tr.bottom : 0;
|
|
799
|
+
var hGap = fr.right <= tr.left ? tr.left - fr.right : tr.right <= fr.left ? fr.left - tr.right : 0;
|
|
800
|
+
var cx2 = (fr.left + fr.right) / 2, cy2 = (fr.top + fr.bottom) / 2;
|
|
558
801
|
if (vGap > 0) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
drawLine(
|
|
562
|
-
lines.push(
|
|
802
|
+
var y1 = fr.bottom <= tr.top ? fr.bottom : tr.bottom;
|
|
803
|
+
var y2 = fr.bottom <= tr.top ? tr.top : fr.top;
|
|
804
|
+
drawLine(cx2, y1, cx2, y2, Math.round(vGap) + 'px');
|
|
805
|
+
lines.push('vertical gap: ' + Math.round(vGap) + 'px');
|
|
563
806
|
}
|
|
564
807
|
if (hGap > 0) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
drawLine(x1,
|
|
568
|
-
lines.push(
|
|
808
|
+
var x1 = fr.right <= tr.left ? fr.right : tr.right;
|
|
809
|
+
var x2 = fr.right <= tr.left ? tr.left : fr.left;
|
|
810
|
+
drawLine(x1, cy2, x2, cy2, Math.round(hGap) + 'px');
|
|
811
|
+
lines.push('horizontal gap: ' + Math.round(hGap) + 'px');
|
|
569
812
|
}
|
|
570
813
|
if (vGap === 0 && hGap === 0) lines.push('Elements overlap');
|
|
571
814
|
}
|
|
@@ -575,28 +818,26 @@
|
|
|
575
818
|
}
|
|
576
819
|
|
|
577
820
|
function buildMeasureCopyText(fromEl, toEl) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
lines.push(
|
|
588
|
-
lines.push(
|
|
589
|
-
lines.push(`To: <${tTag}>${tComp ? ' ' + tComp : ''} ${Math.round(tr.width)}×${Math.round(tr.height)}${tText ? ' "' + tText + '"' : ''}`);
|
|
590
|
-
lines.push(` selector: ${getSelector(toEl)}`);
|
|
821
|
+
var fr = fromEl.getBoundingClientRect(), tr = toEl.getBoundingClientRect();
|
|
822
|
+
var fromContainsTo = fr.left <= tr.left && fr.top <= tr.top && fr.right >= tr.right && fr.bottom >= tr.bottom;
|
|
823
|
+
var toContainsFrom = tr.left <= fr.left && tr.top <= fr.top && tr.right >= fr.right && tr.bottom >= fr.bottom;
|
|
824
|
+
var fTag = fromEl.tagName.toLowerCase(), tTag = toEl.tagName.toLowerCase();
|
|
825
|
+
var fComp = getComponentName(fromEl), tComp = getComponentName(toEl);
|
|
826
|
+
|
|
827
|
+
var lines = ['[Specter Measure]'];
|
|
828
|
+
lines.push('From: <' + fTag + '>' + (fComp ? ' ' + fComp : '') + ' ' + Math.round(fr.width) + '×' + Math.round(fr.height));
|
|
829
|
+
lines.push(' selector: ' + getSelector(fromEl));
|
|
830
|
+
lines.push('To: <' + tTag + '>' + (tComp ? ' ' + tComp : '') + ' ' + Math.round(tr.width) + '×' + Math.round(tr.height));
|
|
831
|
+
lines.push(' selector: ' + getSelector(toEl));
|
|
591
832
|
|
|
592
833
|
if (fromContainsTo || toContainsFrom) {
|
|
593
|
-
|
|
594
|
-
lines.push(
|
|
834
|
+
var outer = fromContainsTo ? fr : tr, inner = fromContainsTo ? tr : fr;
|
|
835
|
+
lines.push('inset: ' + Math.round(inner.top - outer.top) + 'px ' + Math.round(outer.right - inner.right) + 'px ' + Math.round(outer.bottom - inner.bottom) + 'px ' + Math.round(inner.left - outer.left) + 'px');
|
|
595
836
|
} else {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (vGap > 0) lines.push(
|
|
599
|
-
if (hGap > 0) lines.push(
|
|
837
|
+
var vGap = fr.bottom <= tr.top ? tr.top - fr.bottom : tr.bottom <= fr.top ? fr.top - tr.bottom : 0;
|
|
838
|
+
var hGap = fr.right <= tr.left ? tr.left - fr.right : tr.right <= fr.left ? fr.left - tr.right : 0;
|
|
839
|
+
if (vGap > 0) lines.push('vertical gap: ' + Math.round(vGap) + 'px');
|
|
840
|
+
if (hGap > 0) lines.push('horizontal gap: ' + Math.round(hGap) + 'px');
|
|
600
841
|
if (vGap === 0 && hGap === 0) lines.push('Elements overlap');
|
|
601
842
|
}
|
|
602
843
|
return lines.join('\n');
|
|
@@ -604,51 +845,55 @@
|
|
|
604
845
|
|
|
605
846
|
// ─── Shortcut parser ──────────────────────────────────────────────────────
|
|
606
847
|
function matchesActivate(e) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
848
|
+
var parts = ACTIVATE.toLowerCase().split('+');
|
|
849
|
+
var needCtrl = parts.indexOf('ctrl') >= 0;
|
|
850
|
+
var needAlt = parts.indexOf('alt') >= 0;
|
|
851
|
+
var needShift = parts.indexOf('shift') >= 0;
|
|
852
|
+
var needMeta = parts.indexOf('meta') >= 0 || parts.indexOf('cmd') >= 0;
|
|
853
|
+
var key = parts.filter(function (p) { return ['ctrl', 'alt', 'shift', 'meta', 'cmd'].indexOf(p) < 0; })[0];
|
|
613
854
|
if (needCtrl !== e.ctrlKey) return false;
|
|
614
855
|
if (needAlt !== e.altKey) return false;
|
|
615
856
|
if (needShift !== e.shiftKey) return false;
|
|
616
857
|
if (needMeta !== e.metaKey) return false;
|
|
617
858
|
if (!key) return false;
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return eKey === key || eCode ===
|
|
859
|
+
var eKey = e.key.toLowerCase();
|
|
860
|
+
var eCode = e.code.toLowerCase();
|
|
861
|
+
return eKey === key || eCode === 'key' + key || (key === 'period' && (eKey === '.' || eCode === 'period'));
|
|
621
862
|
}
|
|
622
863
|
|
|
623
864
|
// ─── Mouse ────────────────────────────────────────────────────────────────
|
|
624
865
|
function onMouseMove(e) {
|
|
625
866
|
if (!fiActive) return;
|
|
626
|
-
|
|
627
|
-
|
|
867
|
+
lastMouse.x = e.clientX; lastMouse.y = e.clientY;
|
|
868
|
+
var target = e.target;
|
|
869
|
+
if (!target || target === pillWrap || pillWrap.contains(target)) return;
|
|
628
870
|
if (target === tooltip || tooltip.contains(target)) return;
|
|
629
871
|
|
|
630
872
|
lastHovered = target;
|
|
631
873
|
|
|
632
874
|
if (measureMode) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
tooltip.
|
|
875
|
+
clearHoverOutline();
|
|
876
|
+
showMeasureTargetHL(target);
|
|
877
|
+
var text = (pinEl && pinEl !== target) ? measureBetween(pinEl, target) : measureToNeighbor(target);
|
|
878
|
+
tooltip.innerHTML = linesToHTML(text);
|
|
637
879
|
positionTooltip(e.clientX, e.clientY);
|
|
880
|
+
if (pinEl) updatePinHL();
|
|
638
881
|
} else {
|
|
639
882
|
clearMeasureOverlay();
|
|
640
|
-
|
|
883
|
+
clearMeasureTargetHL();
|
|
884
|
+
setHoverOutline(target);
|
|
885
|
+
tooltip.innerHTML = buildHumanDisplay(buildInfo(target));
|
|
641
886
|
positionTooltip(e.clientX, e.clientY);
|
|
642
887
|
}
|
|
643
888
|
}
|
|
644
889
|
|
|
645
890
|
// ─── Keyboard ─────────────────────────────────────────────────────────────
|
|
646
891
|
function isInputFocused() {
|
|
647
|
-
|
|
892
|
+
var el = document.activeElement;
|
|
648
893
|
return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable);
|
|
649
894
|
}
|
|
650
895
|
|
|
651
|
-
document.addEventListener('keydown', (e)
|
|
896
|
+
document.addEventListener('keydown', function (e) {
|
|
652
897
|
if (matchesActivate(e)) {
|
|
653
898
|
e.preventDefault();
|
|
654
899
|
if (fiActive) deactivate(); else activate();
|
|
@@ -657,51 +902,48 @@
|
|
|
657
902
|
|
|
658
903
|
if (!fiActive) return;
|
|
659
904
|
|
|
660
|
-
// Option tap (keydown only — actual toggle fires on keyup)
|
|
661
905
|
if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
662
906
|
optionHeld = true;
|
|
663
907
|
return;
|
|
664
908
|
}
|
|
665
909
|
|
|
666
|
-
// Cmd+C — copy
|
|
667
910
|
if ((e.metaKey || e.ctrlKey) && e.key === 'c' && !e.shiftKey && !e.altKey) {
|
|
668
911
|
e.preventDefault();
|
|
669
|
-
|
|
912
|
+
var text;
|
|
670
913
|
if (selectedEls.length > 0) {
|
|
671
914
|
text = buildMultiSelectCopyText();
|
|
672
915
|
} else if (measureMode && pinEl && lastHovered && lastHovered !== pinEl) {
|
|
673
916
|
text = buildMeasureCopyText(pinEl, lastHovered);
|
|
917
|
+
} else if (measureMode && lastHovered) {
|
|
918
|
+
text = buildNeighborCopyText(lastHovered);
|
|
674
919
|
} else if (lastHovered) {
|
|
675
|
-
text =
|
|
920
|
+
text = buildLLMClipboard(buildInfo(lastHovered));
|
|
676
921
|
}
|
|
677
|
-
if (text) navigator.clipboard.writeText(text).catch(()
|
|
922
|
+
if (text) navigator.clipboard.writeText(text).then(showCopied).catch(function () {});
|
|
678
923
|
return;
|
|
679
924
|
}
|
|
680
925
|
|
|
681
926
|
if (isInputFocused()) return;
|
|
682
927
|
|
|
683
|
-
// P — pick/unpick element (multi-select), Properties mode only
|
|
684
928
|
if (e.key === 'p' || e.key === 'P') {
|
|
685
929
|
if (measureMode || !lastHovered) return;
|
|
930
|
+
clearHoverOutline();
|
|
686
931
|
toggleSelection(lastHovered);
|
|
687
932
|
return;
|
|
688
933
|
}
|
|
689
934
|
|
|
690
|
-
// M — pin element for measure-from, Measure mode only
|
|
691
935
|
if (e.key === 'm' || e.key === 'M') {
|
|
692
936
|
if (!measureMode || !lastHovered) return;
|
|
693
937
|
if (pinEl) {
|
|
694
938
|
clearPin();
|
|
695
939
|
collapsePill();
|
|
696
940
|
} else {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
expandPill('Pinned · hover another element · Cmd+C → copy · Esc → clear');
|
|
941
|
+
setPin(lastHovered);
|
|
942
|
+
expandPill('Pinned · hover another element · Cmd+C copy · Esc clear');
|
|
700
943
|
}
|
|
701
944
|
return;
|
|
702
945
|
}
|
|
703
946
|
|
|
704
|
-
// Escape cascade: pin → selection → exit
|
|
705
947
|
if (e.key === 'Escape') {
|
|
706
948
|
if (pinEl) {
|
|
707
949
|
clearPin();
|
|
@@ -718,12 +960,14 @@
|
|
|
718
960
|
}
|
|
719
961
|
}, true);
|
|
720
962
|
|
|
721
|
-
document.addEventListener('keyup', (e)
|
|
963
|
+
document.addEventListener('keyup', function (e) {
|
|
722
964
|
if (!fiActive) return;
|
|
723
965
|
if (e.key === 'Alt' && optionHeld) {
|
|
724
966
|
optionHeld = false;
|
|
725
967
|
measureMode = !measureMode;
|
|
726
968
|
clearMeasureOverlay();
|
|
969
|
+
clearHoverOutline();
|
|
970
|
+
clearMeasureTargetHL();
|
|
727
971
|
if (!measureMode) clearPin();
|
|
728
972
|
hideTooltip();
|
|
729
973
|
flashMode();
|