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