vite-plugin-specter 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +582 -344
- package/dist/index.cjs +583 -345
- package/dist/index.js +583 -345
- package/extension/content.js +582 -344
- package/package.json +1 -1
package/dist/client.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,335 @@
|
|
|
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
|
+
component: getComponentName(el),
|
|
383
|
+
styleKey: el.dataset ? el.dataset.style : null,
|
|
384
|
+
width: Math.round(rect.width),
|
|
385
|
+
height: Math.round(rect.height),
|
|
386
|
+
text: raw.slice(0, 40),
|
|
387
|
+
textTrunc: raw.length > 40,
|
|
388
|
+
font: { family: ff, weight: cs.fontWeight, size: cs.fontSize, lineHeight: cs.lineHeight },
|
|
389
|
+
color: colorObj(cs.color),
|
|
390
|
+
bg: colorObj(cs.backgroundColor),
|
|
391
|
+
padding: edge(cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft),
|
|
392
|
+
margin: edge(cs.marginTop, cs.marginRight, cs.marginBottom, cs.marginLeft),
|
|
393
|
+
radius: (cs.borderRadius && cs.borderRadius !== '0px') ? cs.borderRadius : null,
|
|
394
|
+
display: ['flex', 'grid', 'inline-flex', 'inline-grid'].indexOf(cs.display) >= 0 ? cs.display : null,
|
|
395
|
+
gap: (cs.gap && cs.gap !== 'normal') ? cs.gap : null,
|
|
396
|
+
flexDir: (cs.display.indexOf('flex') >= 0 && cs.flexDirection !== 'row') ? cs.flexDirection : null,
|
|
397
|
+
rules: getMatchingRules(el),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
285
400
|
|
|
286
|
-
|
|
287
|
-
|
|
401
|
+
function row(label, val) {
|
|
402
|
+
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>';
|
|
403
|
+
}
|
|
288
404
|
|
|
289
|
-
|
|
290
|
-
|
|
405
|
+
function buildHumanDisplay(data) {
|
|
406
|
+
var h = '';
|
|
407
|
+
var id = '<span style="color:#fff"><' + data.tag + '></span>';
|
|
408
|
+
if (data.component) id += ' <span style="color:#E0A3F5;font-weight:600">' + esc(data.component) + '</span>';
|
|
409
|
+
if (data.styleKey) id += ' <span style="color:' + LABEL + '">.' + esc(data.styleKey) + '</span>';
|
|
410
|
+
id += ' <span style="color:' + LABEL + '">' + data.width + '×' + data.height + '</span>';
|
|
411
|
+
h += '<div style="margin-bottom:8px">' + id + '</div>';
|
|
412
|
+
if (data.text) h += '<div style="color:' + LABEL + ';font-style:italic;margin-bottom:8px">"' + esc(data.text) + (data.textTrunc ? '…' : '') + '"</div>';
|
|
413
|
+
h += '<div style="height:8px"></div>';
|
|
414
|
+
h += row('Font', esc(data.font.family) + ' · ' + weightName(data.font.weight) + ' · ' + data.font.size + '/' + data.font.lineHeight);
|
|
415
|
+
if (data.color) h += row('Color', swatch(data.color.hex) + data.color.label);
|
|
416
|
+
if (data.bg) h += row('Bg', swatch(data.bg.hex) + data.bg.label);
|
|
417
|
+
if (data.padding) h += row('Padding', data.padding.value);
|
|
418
|
+
if (data.margin) h += row('Margin', data.margin.value);
|
|
419
|
+
if (data.radius) h += row('Radius', data.radius);
|
|
420
|
+
if (data.display) {
|
|
421
|
+
var d = data.display;
|
|
422
|
+
if (data.gap) d += ' · gap ' + data.gap;
|
|
423
|
+
if (data.flexDir) d += ' · ' + data.flexDir;
|
|
424
|
+
h += row('Display', d);
|
|
425
|
+
}
|
|
426
|
+
return h;
|
|
427
|
+
}
|
|
291
428
|
|
|
292
|
-
|
|
293
|
-
|
|
429
|
+
function buildLLMClipboard(data) {
|
|
430
|
+
var lines = ['[Specter]'];
|
|
431
|
+
var head = '<' + data.tag + '>';
|
|
432
|
+
if (data.component) head += ' ' + data.component;
|
|
433
|
+
if (data.styleKey) head += ' .' + data.styleKey;
|
|
434
|
+
head += ' ' + data.width + '×' + data.height;
|
|
435
|
+
lines.push(head);
|
|
436
|
+
if (data.text) lines.push('"' + data.text + (data.textTrunc ? '…' : '') + '"');
|
|
437
|
+
lines.push('font: ' + data.font.family + ' ' + data.font.weight + ' ' + data.font.size + '/' + data.font.lineHeight);
|
|
438
|
+
if (data.color) lines.push('color: ' + data.color.label);
|
|
439
|
+
if (data.bg) lines.push('bg: ' + data.bg.label);
|
|
440
|
+
if (data.padding) lines.push('padding: ' + data.padding.value);
|
|
441
|
+
if (data.margin) lines.push('margin: ' + data.margin.value);
|
|
442
|
+
if (data.radius) lines.push('radius: ' + data.radius);
|
|
443
|
+
if (data.display) {
|
|
444
|
+
var d = 'display: ' + data.display;
|
|
445
|
+
if (data.gap) d += ' gap: ' + data.gap;
|
|
446
|
+
if (data.flexDir) d += ' dir: ' + data.flexDir;
|
|
447
|
+
lines.push(d);
|
|
448
|
+
}
|
|
449
|
+
lines.push('selector: ' + getSelector(data.el));
|
|
450
|
+
var out = lines.join('\n');
|
|
451
|
+
if (data.rules && data.rules.length) out += '\n\n--- CSS Rules ---\n' + data.rules.join('\n\n');
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
294
454
|
|
|
295
|
-
|
|
296
|
-
|
|
455
|
+
function buildMultiSelectCopyText() {
|
|
456
|
+
return selectedEls.map(function (el, i) {
|
|
457
|
+
var body = buildLLMClipboard(buildInfo(el));
|
|
458
|
+
return body.replace('[Specter]', '[Specter ' + (i + 1) + '/' + selectedEls.length + ']');
|
|
459
|
+
}).join('\n\n' + Array(41).join('─') + '\n\n');
|
|
460
|
+
}
|
|
297
461
|
|
|
298
|
-
|
|
299
|
-
|
|
462
|
+
function linesToHTML(str) {
|
|
463
|
+
return str.split('\n').map(function (l) {
|
|
464
|
+
return '<div style="margin-bottom:4px;color:#fff">' + esc(l) + '</div>';
|
|
465
|
+
}).join('');
|
|
466
|
+
}
|
|
300
467
|
|
|
301
|
-
|
|
468
|
+
// ─── Hover outline ──────────────────────────────────────────────────────────
|
|
469
|
+
function setHoverOutline(el) {
|
|
470
|
+
if (outlinedEl === el) return;
|
|
471
|
+
clearHoverOutline();
|
|
472
|
+
if (!el) return;
|
|
473
|
+
outlinedEl = el;
|
|
474
|
+
prevOutline = el.style.outline;
|
|
475
|
+
el.style.outline = '2px solid ' + PURPLE;
|
|
476
|
+
el.style.outlineOffset = '-1px';
|
|
477
|
+
}
|
|
302
478
|
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (disp.includes('flex') && cs.flexDirection !== 'row') d += ` dir: ${cs.flexDirection}`;
|
|
308
|
-
lines.push(d);
|
|
479
|
+
function clearHoverOutline() {
|
|
480
|
+
if (outlinedEl) {
|
|
481
|
+
outlinedEl.style.outline = prevOutline;
|
|
482
|
+
outlinedEl = null;
|
|
309
483
|
}
|
|
310
|
-
|
|
311
|
-
return lines.join('\n');
|
|
312
484
|
}
|
|
313
485
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
486
|
+
// ─── Copied feedback ────────────────────────────────────────────────────────
|
|
487
|
+
function showCopied() {
|
|
488
|
+
tooltip.innerHTML = '<div style="color:' + GREEN + ';font-weight:600;display:flex;align-items:center;gap:6px"><span>✓</span><span>Copied!</span></div>';
|
|
489
|
+
tooltip.style.display = 'block';
|
|
490
|
+
clearTimeout(copiedTimer);
|
|
491
|
+
copiedTimer = setTimeout(function () {
|
|
492
|
+
if (fiActive && lastHovered) reRenderTooltip();
|
|
493
|
+
else hideTooltip();
|
|
494
|
+
}, 1200);
|
|
320
495
|
}
|
|
321
496
|
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
497
|
+
function reRenderTooltip() {
|
|
498
|
+
if (measureMode) {
|
|
499
|
+
var text = (pinEl && pinEl !== lastHovered) ? measureBetween(pinEl, lastHovered) : measureToNeighbor(lastHovered);
|
|
500
|
+
tooltip.innerHTML = linesToHTML(text);
|
|
501
|
+
} else {
|
|
502
|
+
tooltip.innerHTML = buildHumanDisplay(buildInfo(lastHovered));
|
|
503
|
+
}
|
|
504
|
+
positionTooltip(lastMouse.x, lastMouse.y);
|
|
330
505
|
}
|
|
331
506
|
|
|
332
507
|
// ─── Multi-select ─────────────────────────────────────────────────────────
|
|
333
508
|
function makeBadge(el, index) {
|
|
334
|
-
|
|
335
|
-
|
|
509
|
+
var rect = el.getBoundingClientRect();
|
|
510
|
+
var badge = document.createElement('div');
|
|
336
511
|
badge.textContent = String(index + 1);
|
|
337
512
|
Object.assign(badge.style, {
|
|
338
513
|
position: 'fixed',
|
|
339
|
-
left: rect.left + 'px',
|
|
340
|
-
top: rect.top + 'px',
|
|
341
|
-
width: '
|
|
342
|
-
height: '
|
|
343
|
-
background:
|
|
514
|
+
left: (rect.left - 4) + 'px',
|
|
515
|
+
top: (rect.top - 4) + 'px',
|
|
516
|
+
width: '20px',
|
|
517
|
+
height: '20px',
|
|
518
|
+
background: PURPLE,
|
|
344
519
|
color: '#fff',
|
|
345
|
-
fontSize: '
|
|
520
|
+
fontSize: '11px',
|
|
346
521
|
fontWeight: '700',
|
|
347
|
-
fontFamily:
|
|
522
|
+
fontFamily: MONO,
|
|
348
523
|
borderRadius: '50%',
|
|
349
524
|
display: 'flex',
|
|
350
525
|
alignItems: 'center',
|
|
351
526
|
justifyContent: 'center',
|
|
352
527
|
zIndex: '2147483644',
|
|
353
528
|
pointerEvents: 'none',
|
|
354
|
-
boxShadow: '0
|
|
355
|
-
transform: 'translate(-50%, -50%)',
|
|
529
|
+
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
|
356
530
|
});
|
|
357
531
|
document.body.appendChild(badge);
|
|
358
532
|
return badge;
|
|
359
533
|
}
|
|
360
534
|
|
|
361
535
|
function rebuildBadges() {
|
|
362
|
-
selectedBadges.forEach(b
|
|
536
|
+
selectedBadges.forEach(function (b) { b.remove(); });
|
|
363
537
|
selectedBadges.length = 0;
|
|
364
|
-
selectedEls.forEach((el, i)
|
|
538
|
+
selectedEls.forEach(function (el, i) {
|
|
365
539
|
selectedBadges.push(makeBadge(el, i));
|
|
366
540
|
});
|
|
367
541
|
}
|
|
368
542
|
|
|
369
543
|
function toggleSelection(el) {
|
|
370
|
-
|
|
544
|
+
var idx = selectedEls.indexOf(el);
|
|
371
545
|
if (idx >= 0) {
|
|
372
546
|
selectedEls.splice(idx, 1);
|
|
547
|
+
el.style.outline = '';
|
|
373
548
|
} else {
|
|
374
549
|
selectedEls.push(el);
|
|
550
|
+
el.style.outline = '2px solid ' + PURPLE;
|
|
551
|
+
el.style.outlineOffset = '-1px';
|
|
375
552
|
}
|
|
376
553
|
rebuildBadges();
|
|
377
554
|
updatePillForSelection();
|
|
378
555
|
}
|
|
379
556
|
|
|
380
557
|
function clearSelection() {
|
|
558
|
+
selectedEls.forEach(function (el) { el.style.outline = ''; });
|
|
381
559
|
selectedEls.length = 0;
|
|
382
|
-
selectedBadges.forEach(b
|
|
560
|
+
selectedBadges.forEach(function (b) { b.remove(); });
|
|
383
561
|
selectedBadges.length = 0;
|
|
384
562
|
}
|
|
385
563
|
|
|
386
|
-
|
|
387
|
-
|
|
564
|
+
function updatePillForSelection() {
|
|
565
|
+
if (selectedEls.length > 0) expandPill();
|
|
566
|
+
else if (!pillWrap.matches(':hover')) collapsePill();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ─── Pin (measure) ──────────────────────────────────────────────────────────
|
|
570
|
+
function setPin(el) {
|
|
571
|
+
pinEl = el;
|
|
572
|
+
updatePinHL();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function updatePinHL() {
|
|
576
|
+
if (!pinEl) return;
|
|
577
|
+
var r = pinEl.getBoundingClientRect();
|
|
578
|
+
if (!pinHighlight) {
|
|
579
|
+
pinHighlight = document.createElement('div');
|
|
580
|
+
Object.assign(pinHighlight.style, {
|
|
581
|
+
position: 'fixed',
|
|
582
|
+
border: '2px dashed ' + RED,
|
|
583
|
+
background: 'rgba(237,62,97,0.05)',
|
|
584
|
+
boxSizing: 'border-box',
|
|
585
|
+
pointerEvents: 'none',
|
|
586
|
+
zIndex: '2147483643',
|
|
587
|
+
});
|
|
588
|
+
document.body.appendChild(pinHighlight);
|
|
589
|
+
}
|
|
590
|
+
pinHighlight.style.left = r.left + 'px';
|
|
591
|
+
pinHighlight.style.top = r.top + 'px';
|
|
592
|
+
pinHighlight.style.width = r.width + 'px';
|
|
593
|
+
pinHighlight.style.height = r.height + 'px';
|
|
594
|
+
}
|
|
388
595
|
|
|
389
596
|
function clearPin() {
|
|
390
597
|
if (pinHighlight) { pinHighlight.remove(); pinHighlight = null; }
|
|
@@ -396,14 +603,51 @@
|
|
|
396
603
|
measureOverlay.style.display = 'none';
|
|
397
604
|
}
|
|
398
605
|
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
606
|
+
function showMeasureTargetHL(el) {
|
|
607
|
+
clearMeasureTargetHL();
|
|
608
|
+
var r = el.getBoundingClientRect();
|
|
609
|
+
measureHL = document.createElement('div');
|
|
610
|
+
Object.assign(measureHL.style, {
|
|
611
|
+
position: 'fixed',
|
|
612
|
+
left: r.left + 'px',
|
|
613
|
+
top: r.top + 'px',
|
|
614
|
+
width: r.width + 'px',
|
|
615
|
+
height: r.height + 'px',
|
|
616
|
+
background: 'rgba(173,36,211,0.08)',
|
|
617
|
+
border: '1.5px solid ' + PURPLE,
|
|
618
|
+
boxSizing: 'border-box',
|
|
405
619
|
pointerEvents: 'none',
|
|
620
|
+
zIndex: '2147483643',
|
|
406
621
|
});
|
|
622
|
+
document.body.appendChild(measureHL);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function clearMeasureTargetHL() {
|
|
626
|
+
if (measureHL) { measureHL.remove(); measureHL = null; }
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ─── Measurement drawing ────────────────────────────────────────────────────
|
|
630
|
+
function cap(x, y, mainHoriz) {
|
|
631
|
+
var c = document.createElement('div');
|
|
632
|
+
Object.assign(c.style, { position: 'absolute', background: RED, pointerEvents: 'none' });
|
|
633
|
+
if (mainHoriz) {
|
|
634
|
+
c.style.left = (x - 0.5) + 'px';
|
|
635
|
+
c.style.top = (y - 4) + 'px';
|
|
636
|
+
c.style.width = '1px';
|
|
637
|
+
c.style.height = '8px';
|
|
638
|
+
} else {
|
|
639
|
+
c.style.left = (x - 4) + 'px';
|
|
640
|
+
c.style.top = (y - 0.5) + 'px';
|
|
641
|
+
c.style.width = '8px';
|
|
642
|
+
c.style.height = '1px';
|
|
643
|
+
}
|
|
644
|
+
measureOverlay.appendChild(c);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function drawLine(x1, y1, x2, y2, label) {
|
|
648
|
+
var isHoriz = Math.abs(y2 - y1) < 1;
|
|
649
|
+
var line = document.createElement('div');
|
|
650
|
+
Object.assign(line.style, { position: 'absolute', background: RED, pointerEvents: 'none' });
|
|
407
651
|
if (isHoriz) {
|
|
408
652
|
Object.assign(line.style, {
|
|
409
653
|
left: Math.min(x1, x2) + 'px', top: (y1 - 0.5) + 'px',
|
|
@@ -416,156 +660,149 @@
|
|
|
416
660
|
});
|
|
417
661
|
}
|
|
418
662
|
measureOverlay.appendChild(line);
|
|
663
|
+
cap(x1, y1, isHoriz);
|
|
664
|
+
cap(x2, y2, isHoriz);
|
|
419
665
|
|
|
420
666
|
if (label && label !== '0px') {
|
|
421
|
-
|
|
667
|
+
var lbl = document.createElement('div');
|
|
422
668
|
lbl.textContent = label;
|
|
423
669
|
Object.assign(lbl.style, {
|
|
424
670
|
position: 'absolute',
|
|
425
|
-
background:
|
|
671
|
+
background: RED,
|
|
426
672
|
color: '#fff',
|
|
427
673
|
fontSize: '10px',
|
|
428
|
-
fontFamily:
|
|
674
|
+
fontFamily: MONO,
|
|
429
675
|
padding: '1px 4px',
|
|
430
676
|
borderRadius: '3px',
|
|
431
677
|
pointerEvents: 'none',
|
|
432
678
|
whiteSpace: 'nowrap',
|
|
433
679
|
});
|
|
434
680
|
if (isHoriz) {
|
|
435
|
-
|
|
436
|
-
lbl.style.left =
|
|
437
|
-
lbl.style.top = (y1 -
|
|
681
|
+
var cxl = (Math.min(x1, x2) + Math.max(x1, x2)) / 2;
|
|
682
|
+
lbl.style.left = cxl + 'px';
|
|
683
|
+
lbl.style.top = (y1 - 16) + 'px';
|
|
438
684
|
lbl.style.transform = 'translateX(-50%)';
|
|
439
685
|
} else {
|
|
440
|
-
|
|
686
|
+
var cyl = (Math.min(y1, y2) + Math.max(y1, y2)) / 2;
|
|
441
687
|
lbl.style.left = (x1 + 6) + 'px';
|
|
442
|
-
lbl.style.top =
|
|
688
|
+
lbl.style.top = cyl + 'px';
|
|
443
689
|
lbl.style.transform = 'translateY(-50%)';
|
|
444
690
|
}
|
|
445
691
|
measureOverlay.appendChild(lbl);
|
|
446
692
|
}
|
|
447
693
|
}
|
|
448
694
|
|
|
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
|
-
}
|
|
695
|
+
// ─── Neighbor computation (parent-first w/ container fallback) ───────────────
|
|
696
|
+
function edgeToAncestor(targetEl, tr, dir) {
|
|
697
|
+
var cur = targetEl.parentElement;
|
|
698
|
+
var ctx = 'parent';
|
|
699
|
+
for (var i = 0; i < 4 && cur && cur !== document.body; i++) {
|
|
700
|
+
var pr = cur.getBoundingClientRect();
|
|
701
|
+
var gap;
|
|
702
|
+
if (dir === 'top') gap = tr.top - pr.top;
|
|
703
|
+
else if (dir === 'bottom') gap = pr.bottom - tr.bottom;
|
|
704
|
+
else if (dir === 'left') gap = tr.left - pr.left;
|
|
705
|
+
else gap = pr.right - tr.right;
|
|
706
|
+
if (gap > 0.5) {
|
|
707
|
+
var ed;
|
|
708
|
+
if (dir === 'top') ed = pr.top;
|
|
709
|
+
else if (dir === 'bottom') ed = pr.bottom;
|
|
710
|
+
else if (dir === 'left') ed = pr.left;
|
|
711
|
+
else ed = pr.right;
|
|
712
|
+
return { gap: gap, ctx: ctx, edge: ed };
|
|
497
713
|
}
|
|
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
714
|
cur = cur.parentElement;
|
|
715
|
+
ctx = 'container';
|
|
521
716
|
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function computeNeighbors(targetEl, tr) {
|
|
721
|
+
var out = { top: null, bottom: null, left: null, right: null };
|
|
722
|
+
var parent = targetEl.parentElement;
|
|
723
|
+
if (parent) {
|
|
724
|
+
var sibs = Array.prototype.slice.call(parent.children).filter(function (c) { return c !== targetEl && c.offsetParent !== null; });
|
|
725
|
+
sibs.forEach(function (sib) {
|
|
726
|
+
var sr = sib.getBoundingClientRect();
|
|
727
|
+
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 }; }
|
|
728
|
+
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 }; }
|
|
729
|
+
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 }; }
|
|
730
|
+
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 }; }
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
['top', 'bottom', 'left', 'right'].forEach(function (k) { if (!out[k]) out[k] = edgeToAncestor(targetEl, tr, k); });
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
522
736
|
|
|
737
|
+
function measureToNeighbor(targetEl) {
|
|
738
|
+
clearMeasureOverlay();
|
|
739
|
+
var tr = targetEl.getBoundingClientRect();
|
|
740
|
+
var dirs = computeNeighbors(targetEl, tr);
|
|
741
|
+
var cx = (tr.left + tr.right) / 2;
|
|
742
|
+
var cy = (tr.top + tr.bottom) / 2;
|
|
743
|
+
if (dirs.top) drawLine(cx, dirs.top.edge, cx, tr.top, Math.round(dirs.top.gap) + 'px');
|
|
744
|
+
if (dirs.bottom) drawLine(cx, tr.bottom, cx, dirs.bottom.edge, Math.round(dirs.bottom.gap) + 'px');
|
|
745
|
+
if (dirs.left) drawLine(dirs.left.edge, cy, tr.left, cy, Math.round(dirs.left.gap) + 'px');
|
|
746
|
+
if (dirs.right) drawLine(tr.right, cy, dirs.right.edge, cy, Math.round(dirs.right.gap) + 'px');
|
|
523
747
|
measureOverlay.style.display = 'block';
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
748
|
+
var tag = targetEl.tagName.toLowerCase();
|
|
749
|
+
var lines = ['⬡ Measure (M pin · Cmd+C copy)', '<' + tag + '> ' + Math.round(tr.width) + '×' + Math.round(tr.height)];
|
|
750
|
+
['top', 'right', 'bottom', 'left'].forEach(function (k) {
|
|
751
|
+
if (dirs[k]) lines.push(k + ': ' + Math.round(dirs[k].gap) + 'px (to ' + dirs[k].ctx + ')');
|
|
752
|
+
});
|
|
753
|
+
return lines.join('\n');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function buildNeighborCopyText(el) {
|
|
757
|
+
var tr = el.getBoundingClientRect();
|
|
758
|
+
var dirs = computeNeighbors(el, tr);
|
|
759
|
+
var lines = ['[Specter Measure]', '<' + el.tagName.toLowerCase() + '> ' + Math.round(tr.width) + '×' + Math.round(tr.height), 'selector: ' + getSelector(el)];
|
|
760
|
+
['top', 'right', 'bottom', 'left'].forEach(function (k) {
|
|
761
|
+
if (dirs[k]) lines.push(k + ': ' + Math.round(dirs[k].gap) + 'px (to ' + dirs[k].ctx + ')');
|
|
762
|
+
});
|
|
763
|
+
return lines.join('\n');
|
|
527
764
|
}
|
|
528
765
|
|
|
529
766
|
function measureBetween(fromEl, toEl) {
|
|
530
767
|
clearMeasureOverlay();
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
768
|
+
var fr = fromEl.getBoundingClientRect();
|
|
769
|
+
var tr = toEl.getBoundingClientRect();
|
|
770
|
+
var fTag = fromEl.tagName.toLowerCase(), tTag = toEl.tagName.toLowerCase();
|
|
771
|
+
var fComp = getComponentName(fromEl), tComp = getComponentName(toEl);
|
|
535
772
|
|
|
536
|
-
|
|
537
|
-
lines.push(
|
|
538
|
-
lines.push(
|
|
773
|
+
var lines = ['⬡ Measure (Cmd+C copy)'];
|
|
774
|
+
lines.push('From: <' + fTag + '>' + (fComp ? ' ' + fComp : '') + ' ' + Math.round(fr.width) + '×' + Math.round(fr.height));
|
|
775
|
+
lines.push('To: <' + tTag + '>' + (tComp ? ' ' + tComp : '') + ' ' + Math.round(tr.width) + '×' + Math.round(tr.height));
|
|
539
776
|
|
|
540
|
-
|
|
541
|
-
|
|
777
|
+
var fromContainsTo = fr.left <= tr.left && fr.top <= tr.top && fr.right >= tr.right && fr.bottom >= tr.bottom;
|
|
778
|
+
var toContainsFrom = tr.left <= fr.left && tr.top <= fr.top && tr.right >= fr.right && tr.bottom >= fr.bottom;
|
|
542
779
|
|
|
543
780
|
if (fromContainsTo || toContainsFrom) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
781
|
+
var outer = fromContainsTo ? fr : tr;
|
|
782
|
+
var inner = fromContainsTo ? tr : fr;
|
|
783
|
+
var cx = (inner.left + inner.right) / 2, cy = (inner.top + inner.bottom) / 2;
|
|
784
|
+
var iT = inner.top - outer.top, iB = outer.bottom - inner.bottom;
|
|
785
|
+
var iL = inner.left - outer.left, iR = outer.right - inner.right;
|
|
549
786
|
if (iT > 0) drawLine(cx, outer.top, cx, inner.top, Math.round(iT) + 'px');
|
|
550
787
|
if (iB > 0) drawLine(cx, inner.bottom, cx, outer.bottom, Math.round(iB) + 'px');
|
|
551
788
|
if (iL > 0) drawLine(outer.left, cy, inner.left, cy, Math.round(iL) + 'px');
|
|
552
789
|
if (iR > 0) drawLine(inner.right, cy, outer.right, cy, Math.round(iR) + 'px');
|
|
553
|
-
lines.push(
|
|
790
|
+
lines.push('inset: ' + Math.round(iT) + 'px ' + Math.round(iR) + 'px ' + Math.round(iB) + 'px ' + Math.round(iL) + 'px');
|
|
554
791
|
} else {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
792
|
+
var vGap = fr.bottom <= tr.top ? tr.top - fr.bottom : tr.bottom <= fr.top ? fr.top - tr.bottom : 0;
|
|
793
|
+
var hGap = fr.right <= tr.left ? tr.left - fr.right : tr.right <= fr.left ? fr.left - tr.right : 0;
|
|
794
|
+
var cx2 = (fr.left + fr.right) / 2, cy2 = (fr.top + fr.bottom) / 2;
|
|
558
795
|
if (vGap > 0) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
drawLine(
|
|
562
|
-
lines.push(
|
|
796
|
+
var y1 = fr.bottom <= tr.top ? fr.bottom : tr.bottom;
|
|
797
|
+
var y2 = fr.bottom <= tr.top ? tr.top : fr.top;
|
|
798
|
+
drawLine(cx2, y1, cx2, y2, Math.round(vGap) + 'px');
|
|
799
|
+
lines.push('vertical gap: ' + Math.round(vGap) + 'px');
|
|
563
800
|
}
|
|
564
801
|
if (hGap > 0) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
drawLine(x1,
|
|
568
|
-
lines.push(
|
|
802
|
+
var x1 = fr.right <= tr.left ? fr.right : tr.right;
|
|
803
|
+
var x2 = fr.right <= tr.left ? tr.left : fr.left;
|
|
804
|
+
drawLine(x1, cy2, x2, cy2, Math.round(hGap) + 'px');
|
|
805
|
+
lines.push('horizontal gap: ' + Math.round(hGap) + 'px');
|
|
569
806
|
}
|
|
570
807
|
if (vGap === 0 && hGap === 0) lines.push('Elements overlap');
|
|
571
808
|
}
|
|
@@ -575,28 +812,26 @@
|
|
|
575
812
|
}
|
|
576
813
|
|
|
577
814
|
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)}`);
|
|
815
|
+
var fr = fromEl.getBoundingClientRect(), tr = toEl.getBoundingClientRect();
|
|
816
|
+
var fromContainsTo = fr.left <= tr.left && fr.top <= tr.top && fr.right >= tr.right && fr.bottom >= tr.bottom;
|
|
817
|
+
var toContainsFrom = tr.left <= fr.left && tr.top <= fr.top && tr.right >= fr.right && tr.bottom >= fr.bottom;
|
|
818
|
+
var fTag = fromEl.tagName.toLowerCase(), tTag = toEl.tagName.toLowerCase();
|
|
819
|
+
var fComp = getComponentName(fromEl), tComp = getComponentName(toEl);
|
|
820
|
+
|
|
821
|
+
var lines = ['[Specter Measure]'];
|
|
822
|
+
lines.push('From: <' + fTag + '>' + (fComp ? ' ' + fComp : '') + ' ' + Math.round(fr.width) + '×' + Math.round(fr.height));
|
|
823
|
+
lines.push(' selector: ' + getSelector(fromEl));
|
|
824
|
+
lines.push('To: <' + tTag + '>' + (tComp ? ' ' + tComp : '') + ' ' + Math.round(tr.width) + '×' + Math.round(tr.height));
|
|
825
|
+
lines.push(' selector: ' + getSelector(toEl));
|
|
591
826
|
|
|
592
827
|
if (fromContainsTo || toContainsFrom) {
|
|
593
|
-
|
|
594
|
-
lines.push(
|
|
828
|
+
var outer = fromContainsTo ? fr : tr, inner = fromContainsTo ? tr : fr;
|
|
829
|
+
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
830
|
} else {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (vGap > 0) lines.push(
|
|
599
|
-
if (hGap > 0) lines.push(
|
|
831
|
+
var vGap = fr.bottom <= tr.top ? tr.top - fr.bottom : tr.bottom <= fr.top ? fr.top - tr.bottom : 0;
|
|
832
|
+
var hGap = fr.right <= tr.left ? tr.left - fr.right : tr.right <= fr.left ? fr.left - tr.right : 0;
|
|
833
|
+
if (vGap > 0) lines.push('vertical gap: ' + Math.round(vGap) + 'px');
|
|
834
|
+
if (hGap > 0) lines.push('horizontal gap: ' + Math.round(hGap) + 'px');
|
|
600
835
|
if (vGap === 0 && hGap === 0) lines.push('Elements overlap');
|
|
601
836
|
}
|
|
602
837
|
return lines.join('\n');
|
|
@@ -604,51 +839,55 @@
|
|
|
604
839
|
|
|
605
840
|
// ─── Shortcut parser ──────────────────────────────────────────────────────
|
|
606
841
|
function matchesActivate(e) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
842
|
+
var parts = ACTIVATE.toLowerCase().split('+');
|
|
843
|
+
var needCtrl = parts.indexOf('ctrl') >= 0;
|
|
844
|
+
var needAlt = parts.indexOf('alt') >= 0;
|
|
845
|
+
var needShift = parts.indexOf('shift') >= 0;
|
|
846
|
+
var needMeta = parts.indexOf('meta') >= 0 || parts.indexOf('cmd') >= 0;
|
|
847
|
+
var key = parts.filter(function (p) { return ['ctrl', 'alt', 'shift', 'meta', 'cmd'].indexOf(p) < 0; })[0];
|
|
613
848
|
if (needCtrl !== e.ctrlKey) return false;
|
|
614
849
|
if (needAlt !== e.altKey) return false;
|
|
615
850
|
if (needShift !== e.shiftKey) return false;
|
|
616
851
|
if (needMeta !== e.metaKey) return false;
|
|
617
852
|
if (!key) return false;
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return eKey === key || eCode ===
|
|
853
|
+
var eKey = e.key.toLowerCase();
|
|
854
|
+
var eCode = e.code.toLowerCase();
|
|
855
|
+
return eKey === key || eCode === 'key' + key || (key === 'period' && (eKey === '.' || eCode === 'period'));
|
|
621
856
|
}
|
|
622
857
|
|
|
623
858
|
// ─── Mouse ────────────────────────────────────────────────────────────────
|
|
624
859
|
function onMouseMove(e) {
|
|
625
860
|
if (!fiActive) return;
|
|
626
|
-
|
|
627
|
-
|
|
861
|
+
lastMouse.x = e.clientX; lastMouse.y = e.clientY;
|
|
862
|
+
var target = e.target;
|
|
863
|
+
if (!target || target === pillWrap || pillWrap.contains(target)) return;
|
|
628
864
|
if (target === tooltip || tooltip.contains(target)) return;
|
|
629
865
|
|
|
630
866
|
lastHovered = target;
|
|
631
867
|
|
|
632
868
|
if (measureMode) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
tooltip.
|
|
869
|
+
clearHoverOutline();
|
|
870
|
+
showMeasureTargetHL(target);
|
|
871
|
+
var text = (pinEl && pinEl !== target) ? measureBetween(pinEl, target) : measureToNeighbor(target);
|
|
872
|
+
tooltip.innerHTML = linesToHTML(text);
|
|
637
873
|
positionTooltip(e.clientX, e.clientY);
|
|
874
|
+
if (pinEl) updatePinHL();
|
|
638
875
|
} else {
|
|
639
876
|
clearMeasureOverlay();
|
|
640
|
-
|
|
877
|
+
clearMeasureTargetHL();
|
|
878
|
+
setHoverOutline(target);
|
|
879
|
+
tooltip.innerHTML = buildHumanDisplay(buildInfo(target));
|
|
641
880
|
positionTooltip(e.clientX, e.clientY);
|
|
642
881
|
}
|
|
643
882
|
}
|
|
644
883
|
|
|
645
884
|
// ─── Keyboard ─────────────────────────────────────────────────────────────
|
|
646
885
|
function isInputFocused() {
|
|
647
|
-
|
|
886
|
+
var el = document.activeElement;
|
|
648
887
|
return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable);
|
|
649
888
|
}
|
|
650
889
|
|
|
651
|
-
document.addEventListener('keydown', (e)
|
|
890
|
+
document.addEventListener('keydown', function (e) {
|
|
652
891
|
if (matchesActivate(e)) {
|
|
653
892
|
e.preventDefault();
|
|
654
893
|
if (fiActive) deactivate(); else activate();
|
|
@@ -657,51 +896,48 @@
|
|
|
657
896
|
|
|
658
897
|
if (!fiActive) return;
|
|
659
898
|
|
|
660
|
-
// Option tap (keydown only — actual toggle fires on keyup)
|
|
661
899
|
if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
662
900
|
optionHeld = true;
|
|
663
901
|
return;
|
|
664
902
|
}
|
|
665
903
|
|
|
666
|
-
// Cmd+C — copy
|
|
667
904
|
if ((e.metaKey || e.ctrlKey) && e.key === 'c' && !e.shiftKey && !e.altKey) {
|
|
668
905
|
e.preventDefault();
|
|
669
|
-
|
|
906
|
+
var text;
|
|
670
907
|
if (selectedEls.length > 0) {
|
|
671
908
|
text = buildMultiSelectCopyText();
|
|
672
909
|
} else if (measureMode && pinEl && lastHovered && lastHovered !== pinEl) {
|
|
673
910
|
text = buildMeasureCopyText(pinEl, lastHovered);
|
|
911
|
+
} else if (measureMode && lastHovered) {
|
|
912
|
+
text = buildNeighborCopyText(lastHovered);
|
|
674
913
|
} else if (lastHovered) {
|
|
675
|
-
text =
|
|
914
|
+
text = buildLLMClipboard(buildInfo(lastHovered));
|
|
676
915
|
}
|
|
677
|
-
if (text) navigator.clipboard.writeText(text).catch(()
|
|
916
|
+
if (text) navigator.clipboard.writeText(text).then(showCopied).catch(function () {});
|
|
678
917
|
return;
|
|
679
918
|
}
|
|
680
919
|
|
|
681
920
|
if (isInputFocused()) return;
|
|
682
921
|
|
|
683
|
-
// P — pick/unpick element (multi-select), Properties mode only
|
|
684
922
|
if (e.key === 'p' || e.key === 'P') {
|
|
685
923
|
if (measureMode || !lastHovered) return;
|
|
924
|
+
clearHoverOutline();
|
|
686
925
|
toggleSelection(lastHovered);
|
|
687
926
|
return;
|
|
688
927
|
}
|
|
689
928
|
|
|
690
|
-
// M — pin element for measure-from, Measure mode only
|
|
691
929
|
if (e.key === 'm' || e.key === 'M') {
|
|
692
930
|
if (!measureMode || !lastHovered) return;
|
|
693
931
|
if (pinEl) {
|
|
694
932
|
clearPin();
|
|
695
933
|
collapsePill();
|
|
696
934
|
} else {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
expandPill('Pinned · hover another element · Cmd+C → copy · Esc → clear');
|
|
935
|
+
setPin(lastHovered);
|
|
936
|
+
expandPill('Pinned · hover another element · Cmd+C copy · Esc clear');
|
|
700
937
|
}
|
|
701
938
|
return;
|
|
702
939
|
}
|
|
703
940
|
|
|
704
|
-
// Escape cascade: pin → selection → exit
|
|
705
941
|
if (e.key === 'Escape') {
|
|
706
942
|
if (pinEl) {
|
|
707
943
|
clearPin();
|
|
@@ -718,12 +954,14 @@
|
|
|
718
954
|
}
|
|
719
955
|
}, true);
|
|
720
956
|
|
|
721
|
-
document.addEventListener('keyup', (e)
|
|
957
|
+
document.addEventListener('keyup', function (e) {
|
|
722
958
|
if (!fiActive) return;
|
|
723
959
|
if (e.key === 'Alt' && optionHeld) {
|
|
724
960
|
optionHeld = false;
|
|
725
961
|
measureMode = !measureMode;
|
|
726
962
|
clearMeasureOverlay();
|
|
963
|
+
clearHoverOutline();
|
|
964
|
+
clearMeasureTargetHL();
|
|
727
965
|
if (!measureMode) clearPin();
|
|
728
966
|
hideTooltip();
|
|
729
967
|
flashMode();
|