vite-plugin-specter 0.1.2 → 0.2.1

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