vite-plugin-specter 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -945,17 +945,7 @@
945
945
  }
946
946
 
947
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
- }
948
+ deactivate();
959
949
  return;
960
950
  }
961
951
  }, true);
@@ -976,5 +966,13 @@
976
966
 
977
967
  document.addEventListener('mousemove', onMouseMove, { passive: true });
978
968
 
969
+ window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
970
+
971
+ if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
972
+ browser.runtime.onMessage.addListener(function(msg) {
973
+ if (msg && msg.type === 'specter-toggle') window.__specterToggle();
974
+ });
975
+ }
976
+
979
977
  console.log('%c👻 Specter — Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
980
978
  })();
@@ -0,0 +1,4 @@
1
+ // Firefox MV2 background script
2
+ browser.browserAction.onClicked.addListener((tab) => {
3
+ browser.tabs.sendMessage(tab.id, { type: 'specter-toggle' });
4
+ });
@@ -0,0 +1,978 @@
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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">&lt;' + data.tag + '&gt;</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
+ deactivate();
949
+ return;
950
+ }
951
+ }, true);
952
+
953
+ document.addEventListener('keyup', function (e) {
954
+ if (!fiActive) return;
955
+ if (e.key === 'Alt' && optionHeld) {
956
+ optionHeld = false;
957
+ measureMode = !measureMode;
958
+ clearMeasureOverlay();
959
+ clearHoverOutline();
960
+ clearMeasureTargetHL();
961
+ if (!measureMode) clearPin();
962
+ hideTooltip();
963
+ flashMode();
964
+ }
965
+ }, true);
966
+
967
+ document.addEventListener('mousemove', onMouseMove, { passive: true });
968
+
969
+ window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
970
+
971
+ if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
972
+ browser.runtime.onMessage.addListener(function(msg) {
973
+ if (msg && msg.type === 'specter-toggle') window.__specterToggle();
974
+ });
975
+ }
976
+
977
+ console.log('%c👻 Specter — Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
978
+ })();
@@ -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
@@ -975,17 +975,7 @@ function getClientScript(options) {
975
975
  }
976
976
 
977
977
  if (e.key === 'Escape') {
978
- if (pinEl) {
979
- clearPin();
980
- clearMeasureOverlay();
981
- hideTooltip();
982
- updatePillForSelection();
983
- } else if (selectedEls.length > 0) {
984
- clearSelection();
985
- collapsePill();
986
- } else {
987
- deactivate();
988
- }
978
+ deactivate();
989
979
  return;
990
980
  }
991
981
  }, true);
@@ -1006,6 +996,14 @@ function getClientScript(options) {
1006
996
 
1007
997
  document.addEventListener('mousemove', onMouseMove, { passive: true });
1008
998
 
999
+ window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
1000
+
1001
+ if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
1002
+ browser.runtime.onMessage.addListener(function(msg) {
1003
+ if (msg && msg.type === 'specter-toggle') window.__specterToggle();
1004
+ });
1005
+ }
1006
+
1009
1007
  console.log('%c\u{1F47B} Specter \u2014 Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
1010
1008
  })();`;
1011
1009
  }
package/dist/index.js CHANGED
@@ -948,17 +948,7 @@ function getClientScript(options) {
948
948
  }
949
949
 
950
950
  if (e.key === 'Escape') {
951
- if (pinEl) {
952
- clearPin();
953
- clearMeasureOverlay();
954
- hideTooltip();
955
- updatePillForSelection();
956
- } else if (selectedEls.length > 0) {
957
- clearSelection();
958
- collapsePill();
959
- } else {
960
- deactivate();
961
- }
951
+ deactivate();
962
952
  return;
963
953
  }
964
954
  }, true);
@@ -979,6 +969,14 @@ function getClientScript(options) {
979
969
 
980
970
  document.addEventListener('mousemove', onMouseMove, { passive: true });
981
971
 
972
+ window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
973
+
974
+ if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
975
+ browser.runtime.onMessage.addListener(function(msg) {
976
+ if (msg && msg.type === 'specter-toggle') window.__specterToggle();
977
+ });
978
+ }
979
+
982
980
  console.log('%c\u{1F47B} Specter \u2014 Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
983
981
  })();`;
984
982
  }
@@ -1,4 +1,4 @@
1
1
  // Firefox MV2 background script
2
2
  browser.browserAction.onClicked.addListener((tab) => {
3
- browser.tabs.executeScript(tab.id, { file: 'content.js' });
3
+ browser.tabs.sendMessage(tab.id, { type: 'specter-toggle' });
4
4
  });
@@ -945,17 +945,7 @@
945
945
  }
946
946
 
947
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
- }
948
+ deactivate();
959
949
  return;
960
950
  }
961
951
  }, true);
@@ -976,5 +966,13 @@
976
966
 
977
967
  document.addEventListener('mousemove', onMouseMove, { passive: true });
978
968
 
969
+ window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
970
+
971
+ if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
972
+ browser.runtime.onMessage.addListener(function(msg) {
973
+ if (msg && msg.type === 'specter-toggle') window.__specterToggle();
974
+ });
975
+ }
976
+
979
977
  console.log('%c👻 Specter — Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
980
978
  })();
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.1.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": "Activate Specter on this page"
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", "tabs"]
37
+ "permissions": ["activeTab"]
42
38
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Specter",
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "description": "Element inspector for AI-assisted development. Ctrl+Option+Z to activate.",
6
6
  "icons": {
7
7
  "16": "icons/icon16.png",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-specter",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
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",