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 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 { hex: str, alpha: 1 };
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,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,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, '&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
+ 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
+ })();
@@ -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 { hex: str, alpha: 1 };
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 { hex: str, alpha: 1 };
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>
@@ -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
  });
@@ -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 { hex: str, alpha: 1 };
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.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.3",
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",