vite-plugin-specter 0.1.2 → 0.2.1

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