vite-plugin-specter 0.1.2 → 0.2.0

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