vite-plugin-specter 0.2.3 → 0.3.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 +9 -1
- package/dist/extension-firefox/background.js +4 -0
- package/dist/extension-firefox/content.js +988 -0
- package/dist/extension-firefox/icons/icon128.png +0 -0
- package/dist/extension-firefox/icons/icon16.png +0 -0
- package/dist/extension-firefox/icons/icon32.png +0 -0
- package/dist/extension-firefox/icons/icon48.png +0 -0
- package/dist/extension-firefox/manifest.json +46 -0
- package/dist/index.cjs +10 -2
- package/dist/index.js +10 -2
- package/extension/background.firefox.js +1 -1
- package/extension/content.js +9 -1
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon32.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.firefox.json +4 -8
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -269,7 +269,7 @@
|
|
|
269
269
|
function parseColor(str) {
|
|
270
270
|
if (!str || str === 'transparent' || str === 'rgba(0, 0, 0, 0)') return null;
|
|
271
271
|
var m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
272
|
-
if (!m) return
|
|
272
|
+
if (!m) return null;
|
|
273
273
|
return { hex: toHex(m[1], m[2], m[3]), alpha: m[4] !== undefined ? parseFloat(m[4]) : 1 };
|
|
274
274
|
}
|
|
275
275
|
|
|
@@ -976,5 +976,13 @@
|
|
|
976
976
|
|
|
977
977
|
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
978
978
|
|
|
979
|
+
window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
|
|
980
|
+
|
|
981
|
+
if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
|
|
982
|
+
browser.runtime.onMessage.addListener(function(msg) {
|
|
983
|
+
if (msg && msg.type === 'specter-toggle') window.__specterToggle();
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
979
987
|
console.log('%c👻 Specter — Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
|
|
980
988
|
})();
|
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
'use strict';
|
|
3
|
+
if (window.__specter) return; window.__specter = true;
|
|
4
|
+
|
|
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";
|
|
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 = [];
|
|
32
|
+
|
|
33
|
+
// ─── Tooltip ──────────────────────────────────────────────────────────────
|
|
34
|
+
var tooltip = document.createElement('div');
|
|
35
|
+
Object.assign(tooltip.style, {
|
|
36
|
+
position: 'fixed',
|
|
37
|
+
zIndex: '2147483646',
|
|
38
|
+
pointerEvents: 'none',
|
|
39
|
+
fontFamily: MONO,
|
|
40
|
+
fontSize: '13px',
|
|
41
|
+
lineHeight: '20px',
|
|
42
|
+
background: TIP_BG,
|
|
43
|
+
color: '#FFFFFF',
|
|
44
|
+
padding: '24px',
|
|
45
|
+
borderRadius: '8px',
|
|
46
|
+
maxWidth: '480px',
|
|
47
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
|
|
48
|
+
display: 'none',
|
|
49
|
+
});
|
|
50
|
+
document.body.appendChild(tooltip);
|
|
51
|
+
|
|
52
|
+
// Measure overlay
|
|
53
|
+
var measureOverlay = document.createElement('div');
|
|
54
|
+
Object.assign(measureOverlay.style, {
|
|
55
|
+
position: 'fixed',
|
|
56
|
+
inset: '0',
|
|
57
|
+
zIndex: '2147483645',
|
|
58
|
+
pointerEvents: 'none',
|
|
59
|
+
display: 'none',
|
|
60
|
+
});
|
|
61
|
+
document.body.appendChild(measureOverlay);
|
|
62
|
+
|
|
63
|
+
// ─── Pill ─────────────────────────────────────────────────────────────────
|
|
64
|
+
var pillWrap = document.createElement('div');
|
|
65
|
+
Object.assign(pillWrap.style, {
|
|
66
|
+
position: 'fixed',
|
|
67
|
+
bottom: '4px',
|
|
68
|
+
left: '4px',
|
|
69
|
+
zIndex: '2147483647',
|
|
70
|
+
padding: '12px',
|
|
71
|
+
display: 'none',
|
|
72
|
+
boxSizing: 'border-box',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
var pill = document.createElement('div');
|
|
76
|
+
Object.assign(pill.style, {
|
|
77
|
+
display: 'flex',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
gap: '8px',
|
|
80
|
+
background: PURPLE,
|
|
81
|
+
borderRadius: '16px',
|
|
82
|
+
height: '32px',
|
|
83
|
+
padding: '0 9px',
|
|
84
|
+
fontFamily: MONO,
|
|
85
|
+
fontSize: '11px',
|
|
86
|
+
fontWeight: '400',
|
|
87
|
+
color: '#fff',
|
|
88
|
+
cursor: 'default',
|
|
89
|
+
userSelect: 'none',
|
|
90
|
+
boxShadow: '0 4px 12px rgba(173,36,211,0.4)',
|
|
91
|
+
overflow: 'hidden',
|
|
92
|
+
maxWidth: '32px',
|
|
93
|
+
transition: 'max-width 0.2s ease',
|
|
94
|
+
boxSizing: 'border-box',
|
|
95
|
+
whiteSpace: 'nowrap',
|
|
96
|
+
});
|
|
97
|
+
|
|
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
|
+
});
|
|
108
|
+
|
|
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
|
+
});
|
|
116
|
+
|
|
117
|
+
var closeEl = document.createElement('span');
|
|
118
|
+
closeEl.textContent = '×';
|
|
119
|
+
Object.assign(closeEl.style, {
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
fontSize: '17px',
|
|
122
|
+
lineHeight: '1',
|
|
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',
|
|
143
|
+
flexShrink: '0',
|
|
144
|
+
padding: '0 2px',
|
|
145
|
+
opacity: '0.8',
|
|
146
|
+
});
|
|
147
|
+
chevron.addEventListener('click', function (e) { e.stopPropagation(); moveSide(); });
|
|
148
|
+
|
|
149
|
+
pill.appendChild(iconBtn);
|
|
150
|
+
pill.appendChild(pillText);
|
|
151
|
+
pill.appendChild(chevron);
|
|
152
|
+
pillWrap.appendChild(pill);
|
|
153
|
+
document.body.appendChild(pillWrap);
|
|
154
|
+
|
|
155
|
+
pillWrap.addEventListener('mouseenter', function () {
|
|
156
|
+
clearMeasureOverlay();
|
|
157
|
+
clearMeasureTargetHL();
|
|
158
|
+
hideTooltip();
|
|
159
|
+
expandPill();
|
|
160
|
+
zapEl.style.transform = 'rotate(360deg)';
|
|
161
|
+
zapEl.style.opacity = '0';
|
|
162
|
+
closeEl.style.opacity = '1';
|
|
163
|
+
});
|
|
164
|
+
pillWrap.addEventListener('mouseleave', function () {
|
|
165
|
+
if (selectedEls.length === 0 && !pinEl) collapsePill();
|
|
166
|
+
zapEl.style.transform = 'rotate(0deg)';
|
|
167
|
+
zapEl.style.opacity = '1';
|
|
168
|
+
closeEl.style.opacity = '0';
|
|
169
|
+
});
|
|
170
|
+
|
|
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 = '›';
|
|
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; }
|
|
190
|
+
if (selectedEls.length > 0) {
|
|
191
|
+
pillText.textContent = selectedEls.length + ' selected · Cmd+C copy all · Esc clear';
|
|
192
|
+
} else if (measureMode) {
|
|
193
|
+
pillText.textContent = 'Measure · hover distances · M pin · Cmd+C copy · Option toggle';
|
|
194
|
+
} else {
|
|
195
|
+
pillText.textContent = 'Properties · hover inspect · P pick · Cmd+C copy · Option measure';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function collapsePill() {
|
|
200
|
+
pill.style.maxWidth = '32px';
|
|
201
|
+
pillText.style.display = 'none';
|
|
202
|
+
chevron.style.display = 'none';
|
|
203
|
+
pillExpanded = false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function flashMode() {
|
|
207
|
+
if (pillExpanded && pillWrap.matches(':hover')) return;
|
|
208
|
+
var text = measureMode ? '⬡ Measure mode' : '◉ Properties mode';
|
|
209
|
+
expandPill(text);
|
|
210
|
+
clearTimeout(flashTimer);
|
|
211
|
+
flashTimer = setTimeout(function () {
|
|
212
|
+
if (!pillWrap.matches(':hover') && selectedEls.length === 0 && !pinEl) collapsePill();
|
|
213
|
+
else expandPill();
|
|
214
|
+
}, 1200);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Activate / Deactivate ────────────────────────────────────────────────
|
|
218
|
+
function activate() {
|
|
219
|
+
fiActive = true;
|
|
220
|
+
measureMode = false;
|
|
221
|
+
pillWrap.style.display = 'block';
|
|
222
|
+
document.body.style.cursor = 'crosshair';
|
|
223
|
+
collapsePill();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function deactivate() {
|
|
227
|
+
fiActive = false;
|
|
228
|
+
measureMode = false;
|
|
229
|
+
optionHeld = false;
|
|
230
|
+
lastHovered = null;
|
|
231
|
+
clearSelection();
|
|
232
|
+
clearPin();
|
|
233
|
+
clearHoverOutline();
|
|
234
|
+
clearMeasureTargetHL();
|
|
235
|
+
pillWrap.style.display = 'none';
|
|
236
|
+
document.body.style.cursor = '';
|
|
237
|
+
hideTooltip();
|
|
238
|
+
clearMeasureOverlay();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
242
|
+
function hideTooltip() { tooltip.style.display = 'none'; }
|
|
243
|
+
|
|
244
|
+
function esc(s) {
|
|
245
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function positionTooltip(x, y) {
|
|
249
|
+
tooltip.style.display = 'block';
|
|
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;
|
|
257
|
+
if (left + tw > vw - margin) left = x - tw - 16;
|
|
258
|
+
if (top + th > vh - margin) top = y - th - 16;
|
|
259
|
+
if (left < margin) left = margin;
|
|
260
|
+
if (top < margin) top = margin;
|
|
261
|
+
tooltip.style.left = left + 'px';
|
|
262
|
+
tooltip.style.top = top + 'px';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function toHex(r, g, b) {
|
|
266
|
+
return '#' + [r, g, b].map(function (v) { return Math.round(v).toString(16).padStart(2, '0'); }).join('');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseColor(str) {
|
|
270
|
+
if (!str || str === 'transparent' || str === 'rgba(0, 0, 0, 0)') return null;
|
|
271
|
+
var m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
272
|
+
if (!m) return null;
|
|
273
|
+
return { hex: toHex(m[1], m[2], m[3]), alpha: m[4] !== undefined ? parseFloat(m[4]) : 1 };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function colorLabel(str) {
|
|
277
|
+
var c = parseColor(str);
|
|
278
|
+
if (!c) return null;
|
|
279
|
+
if (c.alpha < 1) return c.hex + ' (' + Math.round(c.alpha * 100) + '% opacity)';
|
|
280
|
+
return c.hex;
|
|
281
|
+
}
|
|
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
|
+
|
|
303
|
+
function getComponentName(el) {
|
|
304
|
+
for (var key in el) {
|
|
305
|
+
if (key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$')) {
|
|
306
|
+
var fiber = el[key];
|
|
307
|
+
while (fiber) {
|
|
308
|
+
var name = (fiber.type && (fiber.type.displayName || fiber.type.name));
|
|
309
|
+
if (name && name.length > 3 && /^[A-Z]/.test(name)) return name;
|
|
310
|
+
fiber = fiber.return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (el.__vueParentComponent) {
|
|
315
|
+
var vname = el.__vueParentComponent.type && (el.__vueParentComponent.type.name || el.__vueParentComponent.type.__name);
|
|
316
|
+
if (vname && vname.length > 3) return vname;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getMatchingRules(el) {
|
|
322
|
+
var results = [];
|
|
323
|
+
try {
|
|
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; }
|
|
328
|
+
if (!rules) continue;
|
|
329
|
+
for (var r = 0; r < rules.length; r++) {
|
|
330
|
+
var rule = rules[r];
|
|
331
|
+
if (rule.type !== 1) continue;
|
|
332
|
+
var sel = rule.selectorText;
|
|
333
|
+
if (!sel) continue;
|
|
334
|
+
if (/^[*,]|^:root|^html|^body$|^::before|^::after/.test(sel.trim())) continue;
|
|
335
|
+
try {
|
|
336
|
+
if (el.matches(sel)) {
|
|
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 + ';');
|
|
342
|
+
}
|
|
343
|
+
if (props.length) results.push(sel + ' {\n' + props.join('\n') + '\n}');
|
|
344
|
+
}
|
|
345
|
+
} catch (e) {}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch (e) {}
|
|
349
|
+
return results;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getSelector(el) {
|
|
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();
|
|
357
|
+
if (cur.id) { part += '#' + cur.id; parts.unshift(part); break; }
|
|
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
|
+
}
|
|
367
|
+
parts.unshift(part);
|
|
368
|
+
cur = cur.parentElement;
|
|
369
|
+
}
|
|
370
|
+
return parts.join(' > ');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Structured data model ──────────────────────────────────────────────────
|
|
374
|
+
function buildInfo(el) {
|
|
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
|
+
}
|
|
402
|
+
|
|
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
|
+
}
|
|
406
|
+
|
|
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
|
+
}
|
|
432
|
+
|
|
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
|
+
}
|
|
460
|
+
|
|
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
|
+
}
|
|
467
|
+
|
|
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
|
+
}
|
|
473
|
+
|
|
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
|
+
}
|
|
484
|
+
|
|
485
|
+
function clearHoverOutline() {
|
|
486
|
+
if (outlinedEl) {
|
|
487
|
+
outlinedEl.style.outline = prevOutline;
|
|
488
|
+
outlinedEl = null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
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);
|
|
501
|
+
}
|
|
502
|
+
|
|
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);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ─── Multi-select ─────────────────────────────────────────────────────────
|
|
514
|
+
function makeBadge(el, index) {
|
|
515
|
+
var rect = el.getBoundingClientRect();
|
|
516
|
+
var badge = document.createElement('div');
|
|
517
|
+
badge.textContent = String(index + 1);
|
|
518
|
+
Object.assign(badge.style, {
|
|
519
|
+
position: 'fixed',
|
|
520
|
+
left: (rect.left - 4) + 'px',
|
|
521
|
+
top: (rect.top - 4) + 'px',
|
|
522
|
+
width: '20px',
|
|
523
|
+
height: '20px',
|
|
524
|
+
background: PURPLE,
|
|
525
|
+
color: '#fff',
|
|
526
|
+
fontSize: '11px',
|
|
527
|
+
fontWeight: '700',
|
|
528
|
+
fontFamily: MONO,
|
|
529
|
+
borderRadius: '50%',
|
|
530
|
+
display: 'flex',
|
|
531
|
+
alignItems: 'center',
|
|
532
|
+
justifyContent: 'center',
|
|
533
|
+
zIndex: '2147483644',
|
|
534
|
+
pointerEvents: 'none',
|
|
535
|
+
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
|
536
|
+
});
|
|
537
|
+
document.body.appendChild(badge);
|
|
538
|
+
return badge;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function rebuildBadges() {
|
|
542
|
+
selectedBadges.forEach(function (b) { b.remove(); });
|
|
543
|
+
selectedBadges.length = 0;
|
|
544
|
+
selectedEls.forEach(function (el, i) {
|
|
545
|
+
selectedBadges.push(makeBadge(el, i));
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function toggleSelection(el) {
|
|
550
|
+
var idx = selectedEls.indexOf(el);
|
|
551
|
+
if (idx >= 0) {
|
|
552
|
+
selectedEls.splice(idx, 1);
|
|
553
|
+
el.style.outline = '';
|
|
554
|
+
} else {
|
|
555
|
+
selectedEls.push(el);
|
|
556
|
+
el.style.outline = '2px solid ' + PURPLE;
|
|
557
|
+
el.style.outlineOffset = '-1px';
|
|
558
|
+
}
|
|
559
|
+
rebuildBadges();
|
|
560
|
+
updatePillForSelection();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function clearSelection() {
|
|
564
|
+
selectedEls.forEach(function (el) { el.style.outline = ''; });
|
|
565
|
+
selectedEls.length = 0;
|
|
566
|
+
selectedBadges.forEach(function (b) { b.remove(); });
|
|
567
|
+
selectedBadges.length = 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
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
|
+
}
|
|
601
|
+
|
|
602
|
+
function clearPin() {
|
|
603
|
+
if (pinHighlight) { pinHighlight.remove(); pinHighlight = null; }
|
|
604
|
+
pinEl = null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function clearMeasureOverlay() {
|
|
608
|
+
measureOverlay.innerHTML = '';
|
|
609
|
+
measureOverlay.style.display = 'none';
|
|
610
|
+
}
|
|
611
|
+
|
|
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',
|
|
625
|
+
pointerEvents: 'none',
|
|
626
|
+
zIndex: '2147483643',
|
|
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' });
|
|
657
|
+
if (isHoriz) {
|
|
658
|
+
Object.assign(line.style, {
|
|
659
|
+
left: Math.min(x1, x2) + 'px', top: (y1 - 0.5) + 'px',
|
|
660
|
+
width: Math.abs(x2 - x1) + 'px', height: '1px',
|
|
661
|
+
});
|
|
662
|
+
} else {
|
|
663
|
+
Object.assign(line.style, {
|
|
664
|
+
left: (x1 - 0.5) + 'px', top: Math.min(y1, y2) + 'px',
|
|
665
|
+
width: '1px', height: Math.abs(y2 - y1) + 'px',
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
measureOverlay.appendChild(line);
|
|
669
|
+
cap(x1, y1, isHoriz);
|
|
670
|
+
cap(x2, y2, isHoriz);
|
|
671
|
+
|
|
672
|
+
if (label && label !== '0px') {
|
|
673
|
+
var lbl = document.createElement('div');
|
|
674
|
+
lbl.textContent = label;
|
|
675
|
+
Object.assign(lbl.style, {
|
|
676
|
+
position: 'absolute',
|
|
677
|
+
background: RED,
|
|
678
|
+
color: '#fff',
|
|
679
|
+
fontSize: '10px',
|
|
680
|
+
fontFamily: MONO,
|
|
681
|
+
padding: '1px 4px',
|
|
682
|
+
borderRadius: '3px',
|
|
683
|
+
pointerEvents: 'none',
|
|
684
|
+
whiteSpace: 'nowrap',
|
|
685
|
+
});
|
|
686
|
+
if (isHoriz) {
|
|
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';
|
|
690
|
+
lbl.style.transform = 'translateX(-50%)';
|
|
691
|
+
} else {
|
|
692
|
+
var cyl = (Math.min(y1, y2) + Math.max(y1, y2)) / 2;
|
|
693
|
+
lbl.style.left = (x1 + 6) + 'px';
|
|
694
|
+
lbl.style.top = cyl + 'px';
|
|
695
|
+
lbl.style.transform = 'translateY(-50%)';
|
|
696
|
+
}
|
|
697
|
+
measureOverlay.appendChild(lbl);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
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 };
|
|
719
|
+
}
|
|
720
|
+
cur = cur.parentElement;
|
|
721
|
+
ctx = 'container';
|
|
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
|
+
}
|
|
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');
|
|
753
|
+
measureOverlay.style.display = 'block';
|
|
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');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function measureBetween(fromEl, toEl) {
|
|
773
|
+
clearMeasureOverlay();
|
|
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);
|
|
778
|
+
|
|
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));
|
|
782
|
+
|
|
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;
|
|
785
|
+
|
|
786
|
+
if (fromContainsTo || toContainsFrom) {
|
|
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;
|
|
792
|
+
if (iT > 0) drawLine(cx, outer.top, cx, inner.top, Math.round(iT) + 'px');
|
|
793
|
+
if (iB > 0) drawLine(cx, inner.bottom, cx, outer.bottom, Math.round(iB) + 'px');
|
|
794
|
+
if (iL > 0) drawLine(outer.left, cy, inner.left, cy, Math.round(iL) + 'px');
|
|
795
|
+
if (iR > 0) drawLine(inner.right, cy, outer.right, cy, Math.round(iR) + 'px');
|
|
796
|
+
lines.push('inset: ' + Math.round(iT) + 'px ' + Math.round(iR) + 'px ' + Math.round(iB) + 'px ' + Math.round(iL) + 'px');
|
|
797
|
+
} else {
|
|
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;
|
|
801
|
+
if (vGap > 0) {
|
|
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');
|
|
806
|
+
}
|
|
807
|
+
if (hGap > 0) {
|
|
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');
|
|
812
|
+
}
|
|
813
|
+
if (vGap === 0 && hGap === 0) lines.push('Elements overlap');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
measureOverlay.style.display = 'block';
|
|
817
|
+
return lines.join('\n');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function buildMeasureCopyText(fromEl, 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));
|
|
832
|
+
|
|
833
|
+
if (fromContainsTo || toContainsFrom) {
|
|
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');
|
|
836
|
+
} else {
|
|
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');
|
|
841
|
+
if (vGap === 0 && hGap === 0) lines.push('Elements overlap');
|
|
842
|
+
}
|
|
843
|
+
return lines.join('\n');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ─── Shortcut parser ──────────────────────────────────────────────────────
|
|
847
|
+
function matchesActivate(e) {
|
|
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];
|
|
854
|
+
if (needCtrl !== e.ctrlKey) return false;
|
|
855
|
+
if (needAlt !== e.altKey) return false;
|
|
856
|
+
if (needShift !== e.shiftKey) return false;
|
|
857
|
+
if (needMeta !== e.metaKey) return false;
|
|
858
|
+
if (!key) return false;
|
|
859
|
+
var eKey = e.key.toLowerCase();
|
|
860
|
+
var eCode = e.code.toLowerCase();
|
|
861
|
+
return eKey === key || eCode === 'key' + key || (key === 'period' && (eKey === '.' || eCode === 'period'));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ─── Mouse ────────────────────────────────────────────────────────────────
|
|
865
|
+
function onMouseMove(e) {
|
|
866
|
+
if (!fiActive) return;
|
|
867
|
+
lastMouse.x = e.clientX; lastMouse.y = e.clientY;
|
|
868
|
+
var target = e.target;
|
|
869
|
+
if (!target || target === pillWrap || pillWrap.contains(target)) return;
|
|
870
|
+
if (target === tooltip || tooltip.contains(target)) return;
|
|
871
|
+
|
|
872
|
+
lastHovered = target;
|
|
873
|
+
|
|
874
|
+
if (measureMode) {
|
|
875
|
+
clearHoverOutline();
|
|
876
|
+
showMeasureTargetHL(target);
|
|
877
|
+
var text = (pinEl && pinEl !== target) ? measureBetween(pinEl, target) : measureToNeighbor(target);
|
|
878
|
+
tooltip.innerHTML = linesToHTML(text);
|
|
879
|
+
positionTooltip(e.clientX, e.clientY);
|
|
880
|
+
if (pinEl) updatePinHL();
|
|
881
|
+
} else {
|
|
882
|
+
clearMeasureOverlay();
|
|
883
|
+
clearMeasureTargetHL();
|
|
884
|
+
setHoverOutline(target);
|
|
885
|
+
tooltip.innerHTML = buildHumanDisplay(buildInfo(target));
|
|
886
|
+
positionTooltip(e.clientX, e.clientY);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ─── Keyboard ─────────────────────────────────────────────────────────────
|
|
891
|
+
function isInputFocused() {
|
|
892
|
+
var el = document.activeElement;
|
|
893
|
+
return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
document.addEventListener('keydown', function (e) {
|
|
897
|
+
if (matchesActivate(e)) {
|
|
898
|
+
e.preventDefault();
|
|
899
|
+
if (fiActive) deactivate(); else activate();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (!fiActive) return;
|
|
904
|
+
|
|
905
|
+
if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
906
|
+
optionHeld = true;
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'c' && !e.shiftKey && !e.altKey) {
|
|
911
|
+
e.preventDefault();
|
|
912
|
+
var text;
|
|
913
|
+
if (selectedEls.length > 0) {
|
|
914
|
+
text = buildMultiSelectCopyText();
|
|
915
|
+
} else if (measureMode && pinEl && lastHovered && lastHovered !== pinEl) {
|
|
916
|
+
text = buildMeasureCopyText(pinEl, lastHovered);
|
|
917
|
+
} else if (measureMode && lastHovered) {
|
|
918
|
+
text = buildNeighborCopyText(lastHovered);
|
|
919
|
+
} else if (lastHovered) {
|
|
920
|
+
text = buildLLMClipboard(buildInfo(lastHovered));
|
|
921
|
+
}
|
|
922
|
+
if (text) navigator.clipboard.writeText(text).then(showCopied).catch(function () {});
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (isInputFocused()) return;
|
|
927
|
+
|
|
928
|
+
if (e.key === 'p' || e.key === 'P') {
|
|
929
|
+
if (measureMode || !lastHovered) return;
|
|
930
|
+
clearHoverOutline();
|
|
931
|
+
toggleSelection(lastHovered);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (e.key === 'm' || e.key === 'M') {
|
|
936
|
+
if (!measureMode || !lastHovered) return;
|
|
937
|
+
if (pinEl) {
|
|
938
|
+
clearPin();
|
|
939
|
+
collapsePill();
|
|
940
|
+
} else {
|
|
941
|
+
setPin(lastHovered);
|
|
942
|
+
expandPill('Pinned · hover another element · Cmd+C copy · Esc clear');
|
|
943
|
+
}
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (e.key === 'Escape') {
|
|
948
|
+
if (pinEl) {
|
|
949
|
+
clearPin();
|
|
950
|
+
clearMeasureOverlay();
|
|
951
|
+
hideTooltip();
|
|
952
|
+
updatePillForSelection();
|
|
953
|
+
} else if (selectedEls.length > 0) {
|
|
954
|
+
clearSelection();
|
|
955
|
+
collapsePill();
|
|
956
|
+
} else {
|
|
957
|
+
deactivate();
|
|
958
|
+
}
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
}, true);
|
|
962
|
+
|
|
963
|
+
document.addEventListener('keyup', function (e) {
|
|
964
|
+
if (!fiActive) return;
|
|
965
|
+
if (e.key === 'Alt' && optionHeld) {
|
|
966
|
+
optionHeld = false;
|
|
967
|
+
measureMode = !measureMode;
|
|
968
|
+
clearMeasureOverlay();
|
|
969
|
+
clearHoverOutline();
|
|
970
|
+
clearMeasureTargetHL();
|
|
971
|
+
if (!measureMode) clearPin();
|
|
972
|
+
hideTooltip();
|
|
973
|
+
flashMode();
|
|
974
|
+
}
|
|
975
|
+
}, true);
|
|
976
|
+
|
|
977
|
+
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
978
|
+
|
|
979
|
+
window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
|
|
980
|
+
|
|
981
|
+
if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
|
|
982
|
+
browser.runtime.onMessage.addListener(function(msg) {
|
|
983
|
+
if (msg && msg.type === 'specter-toggle') window.__specterToggle();
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
console.log('%c👻 Specter — Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
|
|
988
|
+
})();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 2,
|
|
3
|
+
"name": "Specter",
|
|
4
|
+
"version": "0.3.0",
|
|
5
|
+
"description": "Element inspector for AI-assisted development. Ctrl+Option+Z to activate.",
|
|
6
|
+
"browser_specific_settings": {
|
|
7
|
+
"gecko": {
|
|
8
|
+
"id": "specter@setugk.com",
|
|
9
|
+
"strict_min_version": "109.0"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"icons": {
|
|
13
|
+
"16": "icons/icon16.png",
|
|
14
|
+
"32": "icons/icon32.png",
|
|
15
|
+
"48": "icons/icon48.png",
|
|
16
|
+
"128": "icons/icon128.png"
|
|
17
|
+
},
|
|
18
|
+
"browser_action": {
|
|
19
|
+
"default_icon": {
|
|
20
|
+
"16": "icons/icon16.png",
|
|
21
|
+
"48": "icons/icon48.png",
|
|
22
|
+
"128": "icons/icon128.png"
|
|
23
|
+
},
|
|
24
|
+
"default_title": "Toggle Specter"
|
|
25
|
+
},
|
|
26
|
+
"background": {
|
|
27
|
+
"scripts": [
|
|
28
|
+
"background.js"
|
|
29
|
+
],
|
|
30
|
+
"persistent": false
|
|
31
|
+
},
|
|
32
|
+
"content_scripts": [
|
|
33
|
+
{
|
|
34
|
+
"matches": [
|
|
35
|
+
"<all_urls>"
|
|
36
|
+
],
|
|
37
|
+
"js": [
|
|
38
|
+
"content.js"
|
|
39
|
+
],
|
|
40
|
+
"run_at": "document_end"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"permissions": [
|
|
44
|
+
"activeTab"
|
|
45
|
+
]
|
|
46
|
+
}
|
package/dist/index.cjs
CHANGED
|
@@ -299,7 +299,7 @@ function getClientScript(options) {
|
|
|
299
299
|
function parseColor(str) {
|
|
300
300
|
if (!str || str === 'transparent' || str === 'rgba(0, 0, 0, 0)') return null;
|
|
301
301
|
var m = str.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?\\)/);
|
|
302
|
-
if (!m) return
|
|
302
|
+
if (!m) return null;
|
|
303
303
|
return { hex: toHex(m[1], m[2], m[3]), alpha: m[4] !== undefined ? parseFloat(m[4]) : 1 };
|
|
304
304
|
}
|
|
305
305
|
|
|
@@ -1006,6 +1006,14 @@ function getClientScript(options) {
|
|
|
1006
1006
|
|
|
1007
1007
|
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
1008
1008
|
|
|
1009
|
+
window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
|
|
1010
|
+
|
|
1011
|
+
if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
|
|
1012
|
+
browser.runtime.onMessage.addListener(function(msg) {
|
|
1013
|
+
if (msg && msg.type === 'specter-toggle') window.__specterToggle();
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1009
1017
|
console.log('%c\u{1F47B} Specter \u2014 Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
|
|
1010
1018
|
})();`;
|
|
1011
1019
|
}
|
|
@@ -1016,7 +1024,7 @@ function specter(options = {}) {
|
|
|
1016
1024
|
name: "vite-plugin-specter",
|
|
1017
1025
|
apply: "serve",
|
|
1018
1026
|
transformIndexHtml(html) {
|
|
1019
|
-
const script = getClientScript(options);
|
|
1027
|
+
const script = getClientScript(options).replace(/<\/script>/gi, "<\\/script>");
|
|
1020
1028
|
return html.replace("</body>", () => `<script>
|
|
1021
1029
|
${script}
|
|
1022
1030
|
</script>
|
package/dist/index.js
CHANGED
|
@@ -272,7 +272,7 @@ function getClientScript(options) {
|
|
|
272
272
|
function parseColor(str) {
|
|
273
273
|
if (!str || str === 'transparent' || str === 'rgba(0, 0, 0, 0)') return null;
|
|
274
274
|
var m = str.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?\\)/);
|
|
275
|
-
if (!m) return
|
|
275
|
+
if (!m) return null;
|
|
276
276
|
return { hex: toHex(m[1], m[2], m[3]), alpha: m[4] !== undefined ? parseFloat(m[4]) : 1 };
|
|
277
277
|
}
|
|
278
278
|
|
|
@@ -979,6 +979,14 @@ function getClientScript(options) {
|
|
|
979
979
|
|
|
980
980
|
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
981
981
|
|
|
982
|
+
window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
|
|
983
|
+
|
|
984
|
+
if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
|
|
985
|
+
browser.runtime.onMessage.addListener(function(msg) {
|
|
986
|
+
if (msg && msg.type === 'specter-toggle') window.__specterToggle();
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
982
990
|
console.log('%c\u{1F47B} Specter \u2014 Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
|
|
983
991
|
})();`;
|
|
984
992
|
}
|
|
@@ -989,7 +997,7 @@ function specter(options = {}) {
|
|
|
989
997
|
name: "vite-plugin-specter",
|
|
990
998
|
apply: "serve",
|
|
991
999
|
transformIndexHtml(html) {
|
|
992
|
-
const script = getClientScript(options);
|
|
1000
|
+
const script = getClientScript(options).replace(/<\/script>/gi, "<\\/script>");
|
|
993
1001
|
return html.replace("</body>", () => `<script>
|
|
994
1002
|
${script}
|
|
995
1003
|
</script>
|
package/extension/content.js
CHANGED
|
@@ -269,7 +269,7 @@
|
|
|
269
269
|
function parseColor(str) {
|
|
270
270
|
if (!str || str === 'transparent' || str === 'rgba(0, 0, 0, 0)') return null;
|
|
271
271
|
var m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
272
|
-
if (!m) return
|
|
272
|
+
if (!m) return null;
|
|
273
273
|
return { hex: toHex(m[1], m[2], m[3]), alpha: m[4] !== undefined ? parseFloat(m[4]) : 1 };
|
|
274
274
|
}
|
|
275
275
|
|
|
@@ -976,5 +976,13 @@
|
|
|
976
976
|
|
|
977
977
|
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
978
978
|
|
|
979
|
+
window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
|
|
980
|
+
|
|
981
|
+
if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
|
|
982
|
+
browser.runtime.onMessage.addListener(function(msg) {
|
|
983
|
+
if (msg && msg.type === 'specter-toggle') window.__specterToggle();
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
979
987
|
console.log('%c👻 Specter — Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
|
|
980
988
|
})();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 2,
|
|
3
3
|
"name": "Specter",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Element inspector for AI-assisted development. Ctrl+Option+Z to activate.",
|
|
6
6
|
"browser_specific_settings": {
|
|
7
7
|
"gecko": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"48": "icons/icon48.png",
|
|
22
22
|
"128": "icons/icon128.png"
|
|
23
23
|
},
|
|
24
|
-
"default_title": "
|
|
24
|
+
"default_title": "Toggle Specter"
|
|
25
25
|
},
|
|
26
26
|
"background": {
|
|
27
27
|
"scripts": ["background.firefox.js"],
|
|
@@ -29,14 +29,10 @@
|
|
|
29
29
|
},
|
|
30
30
|
"content_scripts": [
|
|
31
31
|
{
|
|
32
|
-
"matches": [
|
|
33
|
-
"http://localhost/*",
|
|
34
|
-
"http://127.0.0.1/*",
|
|
35
|
-
"http://0.0.0.0/*"
|
|
36
|
-
],
|
|
32
|
+
"matches": ["<all_urls>"],
|
|
37
33
|
"js": ["content.js"],
|
|
38
34
|
"run_at": "document_end"
|
|
39
35
|
}
|
|
40
36
|
],
|
|
41
|
-
"permissions": ["activeTab"
|
|
37
|
+
"permissions": ["activeTab"]
|
|
42
38
|
}
|
package/extension/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-specter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Inspect elements and Figma-style measure spacing in your vibe-coded Vite projects. Give your AI exactly what it needs to make the right change.",
|
|
5
5
|
"author": "Setu Kathawate <dev@setugk.com>",
|
|
6
6
|
"homepage": "https://github.com/setugk/vite-plugin-specter#readme",
|