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