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