vite-plugin-specter 0.1.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/bookmarklet.txt +1 -0
- package/dist/client.js +735 -0
- package/dist/index.cjs +786 -0
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +758 -0
- package/extension/background.firefox.js +4 -0
- package/extension/background.js +8 -0
- package/extension/content.js +735 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon32.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.firefox.json +42 -0
- package/extension/manifest.json +36 -0
- package/package.json +43 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
getClientScript: () => getClientScript,
|
|
24
|
+
specter: () => specter
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/client.ts
|
|
29
|
+
function getClientScript(options) {
|
|
30
|
+
const activateShortcut = options.shortcuts?.activate ?? "ctrl+alt+z";
|
|
31
|
+
return `(function() {
|
|
32
|
+
'use strict';
|
|
33
|
+
if (window.__specter) return; window.__specter = true;
|
|
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 = [];
|
|
43
|
+
|
|
44
|
+
const ACTIVATE = ${JSON.stringify(activateShortcut)};
|
|
45
|
+
|
|
46
|
+
// \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');
|
|
48
|
+
Object.assign(tooltip.style, {
|
|
49
|
+
position: 'fixed',
|
|
50
|
+
zIndex: '2147483646',
|
|
51
|
+
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',
|
|
58
|
+
borderRadius: '8px',
|
|
59
|
+
maxWidth: '560px',
|
|
60
|
+
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
|
|
61
|
+
whiteSpace: 'pre-wrap',
|
|
62
|
+
wordBreak: 'break-word',
|
|
63
|
+
display: 'none',
|
|
64
|
+
border: '1px solid rgba(255,255,255,0.08)',
|
|
65
|
+
});
|
|
66
|
+
document.body.appendChild(tooltip);
|
|
67
|
+
|
|
68
|
+
// Measure overlay
|
|
69
|
+
const measureOverlay = document.createElement('div');
|
|
70
|
+
Object.assign(measureOverlay.style, {
|
|
71
|
+
position: 'fixed',
|
|
72
|
+
inset: '0',
|
|
73
|
+
zIndex: '2147483645',
|
|
74
|
+
pointerEvents: 'none',
|
|
75
|
+
display: 'none',
|
|
76
|
+
});
|
|
77
|
+
document.body.appendChild(measureOverlay);
|
|
78
|
+
|
|
79
|
+
// \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, {
|
|
82
|
+
position: 'fixed',
|
|
83
|
+
bottom: '16px',
|
|
84
|
+
left: '16px',
|
|
85
|
+
zIndex: '2147483647',
|
|
86
|
+
display: 'none',
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
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',
|
|
94
|
+
fontSize: '11px',
|
|
95
|
+
color: '#e8e8e8',
|
|
96
|
+
cursor: 'default',
|
|
97
|
+
userSelect: 'none',
|
|
98
|
+
transition: 'max-width 0.2s ease',
|
|
99
|
+
overflow: 'hidden',
|
|
100
|
+
maxWidth: '32px',
|
|
101
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const pillIcon = document.createElement('span');
|
|
105
|
+
pillIcon.textContent = '\u26A1';
|
|
106
|
+
Object.assign(pillIcon.style, { fontSize: '13px', flexShrink: '0' });
|
|
107
|
+
|
|
108
|
+
const pillText = document.createElement('span');
|
|
109
|
+
Object.assign(pillText.style, { visibility: 'hidden', whiteSpace: 'nowrap', color: '#aaa' });
|
|
110
|
+
|
|
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',
|
|
118
|
+
lineHeight: '1',
|
|
119
|
+
visibility: 'hidden',
|
|
120
|
+
flexShrink: '0',
|
|
121
|
+
padding: '0 2px',
|
|
122
|
+
});
|
|
123
|
+
pillClose.addEventListener('click', (e) => { e.stopPropagation(); deactivate(); });
|
|
124
|
+
|
|
125
|
+
pill.appendChild(pillIcon);
|
|
126
|
+
pill.appendChild(pillText);
|
|
127
|
+
pill.appendChild(pillClose);
|
|
128
|
+
document.body.appendChild(pill);
|
|
129
|
+
|
|
130
|
+
pill.addEventListener('mouseenter', () => {
|
|
131
|
+
clearMeasureOverlay();
|
|
132
|
+
hideTooltip();
|
|
133
|
+
expandPill();
|
|
134
|
+
});
|
|
135
|
+
pill.addEventListener('mouseleave', () => {
|
|
136
|
+
if (selectedEls.length === 0 && !pinEl) collapsePill();
|
|
137
|
+
});
|
|
138
|
+
|
|
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;
|
|
146
|
+
}
|
|
147
|
+
if (selectedEls.length > 0) {
|
|
148
|
+
pillText.textContent = \`\${selectedEls.length} selected \xB7 Cmd+C \u2192 copy all \xB7 Esc \u2192 clear\`;
|
|
149
|
+
} 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';
|
|
151
|
+
} else {
|
|
152
|
+
pillText.textContent = 'Properties \xB7 hover \u2192 inspect \xB7 P \u2192 pick \xB7 Cmd+C \u2192 copy \xB7 Option \u2192 measure mode';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function collapsePill() {
|
|
157
|
+
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
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function flashMode() {
|
|
171
|
+
const text = measureMode ? '\u2B21 Measure mode' : '\u25C9 Properties mode';
|
|
172
|
+
expandPill(text);
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
if (!pill.matches(':hover') && selectedEls.length === 0 && !pinEl) collapsePill();
|
|
175
|
+
else if (selectedEls.length > 0 || pinEl) expandPill();
|
|
176
|
+
}, 1200);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// \u2500\u2500\u2500 Activate / Deactivate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
180
|
+
function activate() {
|
|
181
|
+
fiActive = true;
|
|
182
|
+
measureMode = false;
|
|
183
|
+
pill.style.display = 'flex';
|
|
184
|
+
collapsePill();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function deactivate() {
|
|
188
|
+
fiActive = false;
|
|
189
|
+
measureMode = false;
|
|
190
|
+
optionHeld = false;
|
|
191
|
+
lastHovered = null;
|
|
192
|
+
clearSelection();
|
|
193
|
+
clearPin();
|
|
194
|
+
pill.style.display = 'none';
|
|
195
|
+
hideTooltip();
|
|
196
|
+
clearMeasureOverlay();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// \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
|
+
function hideTooltip() { tooltip.style.display = 'none'; }
|
|
201
|
+
|
|
202
|
+
function positionTooltip(x, y) {
|
|
203
|
+
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;
|
|
211
|
+
if (left + tw > vw - margin) left = x - tw - 16;
|
|
212
|
+
if (top + th > vh - margin) top = y - th - 16;
|
|
213
|
+
if (left < margin) left = margin;
|
|
214
|
+
if (top < margin) top = margin;
|
|
215
|
+
tooltip.style.left = left + 'px';
|
|
216
|
+
tooltip.style.top = top + 'px';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function toHex(r, g, b) {
|
|
220
|
+
return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseColor(str) {
|
|
224
|
+
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.]+))?\\)/);
|
|
226
|
+
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) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function colorLabel(str) {
|
|
232
|
+
const c = parseColor(str);
|
|
233
|
+
if (!c) return null;
|
|
234
|
+
if (c.alpha < 1) return \`\${c.hex} (\${Math.round(c.alpha * 100)}% opacity)\`;
|
|
235
|
+
return c.hex;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getComponentName(el) {
|
|
239
|
+
for (const key of Object.keys(el)) {
|
|
240
|
+
if (key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$')) {
|
|
241
|
+
let fiber = el[key];
|
|
242
|
+
while (fiber) {
|
|
243
|
+
const name = fiber.type?.displayName || fiber.type?.name;
|
|
244
|
+
if (name && name.length > 3 && /^[A-Z]/.test(name)) return name;
|
|
245
|
+
fiber = fiber.return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (el.__vueParentComponent) {
|
|
250
|
+
const name = el.__vueParentComponent.type?.name || el.__vueParentComponent.type?.__name;
|
|
251
|
+
if (name && name.length > 3) return name;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getMatchingRules(el) {
|
|
257
|
+
const results = [];
|
|
258
|
+
try {
|
|
259
|
+
for (const sheet of document.styleSheets) {
|
|
260
|
+
let rules;
|
|
261
|
+
try { rules = sheet.cssRules; } catch { continue; }
|
|
262
|
+
if (!rules) continue;
|
|
263
|
+
for (const rule of rules) {
|
|
264
|
+
if (rule.type !== 1) continue;
|
|
265
|
+
const sel = rule.selectorText;
|
|
266
|
+
if (!sel) continue;
|
|
267
|
+
if (/^[*,]|^:root|^html|^body$|^::before|^::after/.test(sel.trim())) continue;
|
|
268
|
+
try {
|
|
269
|
+
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};\`);
|
|
275
|
+
}
|
|
276
|
+
if (props.length) results.push(\`\${sel} {\\n\${props.join('\\n')}\\n}\`);
|
|
277
|
+
}
|
|
278
|
+
} catch {}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch {}
|
|
282
|
+
return results;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
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();
|
|
290
|
+
if (cur.id) { part += '#' + cur.id; parts.unshift(part); break; }
|
|
291
|
+
if (cur.classList.length) part += '.' + Array.from(cur.classList).slice(0, 2).join('.');
|
|
292
|
+
parts.unshift(part);
|
|
293
|
+
cur = cur.parentElement;
|
|
294
|
+
}
|
|
295
|
+
return parts.join(' > ');
|
|
296
|
+
}
|
|
297
|
+
|
|
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
|
|
299
|
+
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' : ''}"\`);
|
|
315
|
+
|
|
316
|
+
const ff = cs.fontFamily.split(',')[0].replace(/['"]/g, '').trim();
|
|
317
|
+
lines.push(\`font: \${ff} \${cs.fontWeight} \${cs.fontSize}/\${cs.lineHeight}\`);
|
|
318
|
+
|
|
319
|
+
const color = colorLabel(cs.color);
|
|
320
|
+
if (color) lines.push(\`color: \${color}\`);
|
|
321
|
+
|
|
322
|
+
const bg = colorLabel(cs.backgroundColor);
|
|
323
|
+
if (bg) lines.push(\`bg: \${bg}\`);
|
|
324
|
+
|
|
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}\`);
|
|
327
|
+
|
|
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}\`);
|
|
330
|
+
|
|
331
|
+
if (cs.borderRadius && cs.borderRadius !== '0px') lines.push(\`radius: \${cs.borderRadius}\`);
|
|
332
|
+
|
|
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);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return lines.join('\\n');
|
|
342
|
+
}
|
|
343
|
+
|
|
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;
|
|
350
|
+
}
|
|
351
|
+
|
|
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');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// \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
|
+
function makeBadge(el, index) {
|
|
364
|
+
const rect = el.getBoundingClientRect();
|
|
365
|
+
const badge = document.createElement('div');
|
|
366
|
+
badge.textContent = String(index + 1);
|
|
367
|
+
Object.assign(badge.style, {
|
|
368
|
+
position: 'fixed',
|
|
369
|
+
left: rect.left + 'px',
|
|
370
|
+
top: rect.top + 'px',
|
|
371
|
+
width: '18px',
|
|
372
|
+
height: '18px',
|
|
373
|
+
background: '#9b59b6',
|
|
374
|
+
color: '#fff',
|
|
375
|
+
fontSize: '10px',
|
|
376
|
+
fontWeight: '700',
|
|
377
|
+
fontFamily: 'ui-monospace, monospace',
|
|
378
|
+
borderRadius: '50%',
|
|
379
|
+
display: 'flex',
|
|
380
|
+
alignItems: 'center',
|
|
381
|
+
justifyContent: 'center',
|
|
382
|
+
zIndex: '2147483644',
|
|
383
|
+
pointerEvents: 'none',
|
|
384
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.4)',
|
|
385
|
+
transform: 'translate(-50%, -50%)',
|
|
386
|
+
});
|
|
387
|
+
document.body.appendChild(badge);
|
|
388
|
+
return badge;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function rebuildBadges() {
|
|
392
|
+
selectedBadges.forEach(b => b.remove());
|
|
393
|
+
selectedBadges.length = 0;
|
|
394
|
+
selectedEls.forEach((el, i) => {
|
|
395
|
+
selectedBadges.push(makeBadge(el, i));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function toggleSelection(el) {
|
|
400
|
+
const idx = selectedEls.indexOf(el);
|
|
401
|
+
if (idx >= 0) {
|
|
402
|
+
selectedEls.splice(idx, 1);
|
|
403
|
+
} else {
|
|
404
|
+
selectedEls.push(el);
|
|
405
|
+
}
|
|
406
|
+
rebuildBadges();
|
|
407
|
+
updatePillForSelection();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function clearSelection() {
|
|
411
|
+
selectedEls.length = 0;
|
|
412
|
+
selectedBadges.forEach(b => b.remove());
|
|
413
|
+
selectedBadges.length = 0;
|
|
414
|
+
}
|
|
415
|
+
|
|
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;
|
|
418
|
+
|
|
419
|
+
function clearPin() {
|
|
420
|
+
if (pinHighlight) { pinHighlight.remove(); pinHighlight = null; }
|
|
421
|
+
pinEl = null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function clearMeasureOverlay() {
|
|
425
|
+
measureOverlay.innerHTML = '';
|
|
426
|
+
measureOverlay.style.display = 'none';
|
|
427
|
+
}
|
|
428
|
+
|
|
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',
|
|
435
|
+
pointerEvents: 'none',
|
|
436
|
+
});
|
|
437
|
+
if (isHoriz) {
|
|
438
|
+
Object.assign(line.style, {
|
|
439
|
+
left: Math.min(x1, x2) + 'px', top: (y1 - 0.5) + 'px',
|
|
440
|
+
width: Math.abs(x2 - x1) + 'px', height: '1px',
|
|
441
|
+
});
|
|
442
|
+
} else {
|
|
443
|
+
Object.assign(line.style, {
|
|
444
|
+
left: (x1 - 0.5) + 'px', top: Math.min(y1, y2) + 'px',
|
|
445
|
+
width: '1px', height: Math.abs(y2 - y1) + 'px',
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
measureOverlay.appendChild(line);
|
|
449
|
+
|
|
450
|
+
if (label && label !== '0px') {
|
|
451
|
+
const lbl = document.createElement('div');
|
|
452
|
+
lbl.textContent = label;
|
|
453
|
+
Object.assign(lbl.style, {
|
|
454
|
+
position: 'absolute',
|
|
455
|
+
background: '#e53e3e',
|
|
456
|
+
color: '#fff',
|
|
457
|
+
fontSize: '10px',
|
|
458
|
+
fontFamily: 'ui-monospace, monospace',
|
|
459
|
+
padding: '1px 4px',
|
|
460
|
+
borderRadius: '3px',
|
|
461
|
+
pointerEvents: 'none',
|
|
462
|
+
whiteSpace: 'nowrap',
|
|
463
|
+
});
|
|
464
|
+
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';
|
|
468
|
+
lbl.style.transform = 'translateX(-50%)';
|
|
469
|
+
} else {
|
|
470
|
+
const cy = (Math.min(y1, y2) + Math.max(y1, y2)) / 2;
|
|
471
|
+
lbl.style.left = (x1 + 6) + 'px';
|
|
472
|
+
lbl.style.top = cy + 'px';
|
|
473
|
+
lbl.style.transform = 'translateY(-50%)';
|
|
474
|
+
}
|
|
475
|
+
measureOverlay.appendChild(lbl);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
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
|
+
}
|
|
527
|
+
}
|
|
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
|
+
cur = cur.parentElement;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
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)}\`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function measureBetween(fromEl, toEl) {
|
|
560
|
+
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);
|
|
565
|
+
|
|
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)}\`);
|
|
569
|
+
|
|
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;
|
|
572
|
+
|
|
573
|
+
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;
|
|
579
|
+
if (iT > 0) drawLine(cx, outer.top, cx, inner.top, Math.round(iT) + 'px');
|
|
580
|
+
if (iB > 0) drawLine(cx, inner.bottom, cx, outer.bottom, Math.round(iB) + 'px');
|
|
581
|
+
if (iL > 0) drawLine(outer.left, cy, inner.left, cy, Math.round(iL) + 'px');
|
|
582
|
+
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\`);
|
|
584
|
+
} 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;
|
|
588
|
+
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'})\`);
|
|
593
|
+
}
|
|
594
|
+
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\`);
|
|
599
|
+
}
|
|
600
|
+
if (vGap === 0 && hGap === 0) lines.push('Elements overlap');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
measureOverlay.style.display = 'block';
|
|
604
|
+
return lines.join('\\n');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
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)}\`);
|
|
621
|
+
|
|
622
|
+
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\`);
|
|
625
|
+
} 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\`);
|
|
630
|
+
if (vGap === 0 && hGap === 0) lines.push('Elements overlap');
|
|
631
|
+
}
|
|
632
|
+
return lines.join('\\n');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// \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
|
+
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));
|
|
643
|
+
if (needCtrl !== e.ctrlKey) return false;
|
|
644
|
+
if (needAlt !== e.altKey) return false;
|
|
645
|
+
if (needShift !== e.shiftKey) return false;
|
|
646
|
+
if (needMeta !== e.metaKey) return false;
|
|
647
|
+
if (!key) return false;
|
|
648
|
+
const eKey = e.key.toLowerCase();
|
|
649
|
+
return eKey === key || (key === 'period' && eKey === '.');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// \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
|
|
653
|
+
function onMouseMove(e) {
|
|
654
|
+
if (!fiActive) return;
|
|
655
|
+
const target = e.target;
|
|
656
|
+
if (!target || target === pill || pill.contains(target)) return;
|
|
657
|
+
if (target === tooltip || tooltip.contains(target)) return;
|
|
658
|
+
|
|
659
|
+
lastHovered = target;
|
|
660
|
+
|
|
661
|
+
if (measureMode) {
|
|
662
|
+
const text = (pinEl && pinEl !== target)
|
|
663
|
+
? measureBetween(pinEl, target)
|
|
664
|
+
: measureToNeighbor(target);
|
|
665
|
+
tooltip.textContent = text;
|
|
666
|
+
positionTooltip(e.clientX, e.clientY);
|
|
667
|
+
} else {
|
|
668
|
+
clearMeasureOverlay();
|
|
669
|
+
tooltip.textContent = buildInfo(target);
|
|
670
|
+
positionTooltip(e.clientX, e.clientY);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// \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
|
|
675
|
+
function isInputFocused() {
|
|
676
|
+
const el = document.activeElement;
|
|
677
|
+
return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
document.addEventListener('keydown', (e) => {
|
|
681
|
+
if (matchesActivate(e)) {
|
|
682
|
+
e.preventDefault();
|
|
683
|
+
if (fiActive) deactivate(); else activate();
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!fiActive) return;
|
|
688
|
+
|
|
689
|
+
// Option tap (keydown only \u2014 actual toggle fires on keyup)
|
|
690
|
+
if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
691
|
+
optionHeld = true;
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Cmd+C \u2014 copy
|
|
696
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'c' && !e.shiftKey && !e.altKey) {
|
|
697
|
+
e.preventDefault();
|
|
698
|
+
let text;
|
|
699
|
+
if (selectedEls.length > 0) {
|
|
700
|
+
text = buildMultiSelectCopyText();
|
|
701
|
+
} else if (measureMode && pinEl && lastHovered && lastHovered !== pinEl) {
|
|
702
|
+
text = buildMeasureCopyText(pinEl, lastHovered);
|
|
703
|
+
} else if (lastHovered) {
|
|
704
|
+
text = buildCopyText(lastHovered);
|
|
705
|
+
}
|
|
706
|
+
if (text) navigator.clipboard.writeText(text).catch(() => {});
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (isInputFocused()) return;
|
|
711
|
+
|
|
712
|
+
// P \u2014 pick/unpick element (multi-select), Properties mode only
|
|
713
|
+
if (e.key === 'p' || e.key === 'P') {
|
|
714
|
+
if (measureMode || !lastHovered) return;
|
|
715
|
+
toggleSelection(lastHovered);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// M \u2014 pin element for measure-from, Measure mode only
|
|
720
|
+
if (e.key === 'm' || e.key === 'M') {
|
|
721
|
+
if (!measureMode || !lastHovered) return;
|
|
722
|
+
if (pinEl) {
|
|
723
|
+
clearPin();
|
|
724
|
+
collapsePill();
|
|
725
|
+
} else {
|
|
726
|
+
pinEl = lastHovered;
|
|
727
|
+
pinHighlight = highlightEl(pinEl, '#9b59b6');
|
|
728
|
+
expandPill('Pinned \xB7 hover another element \xB7 Cmd+C \u2192 copy \xB7 Esc \u2192 clear');
|
|
729
|
+
}
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Escape cascade: pin \u2192 selection \u2192 exit
|
|
734
|
+
if (e.key === 'Escape') {
|
|
735
|
+
if (pinEl) {
|
|
736
|
+
clearPin();
|
|
737
|
+
clearMeasureOverlay();
|
|
738
|
+
hideTooltip();
|
|
739
|
+
updatePillForSelection();
|
|
740
|
+
} else if (selectedEls.length > 0) {
|
|
741
|
+
clearSelection();
|
|
742
|
+
collapsePill();
|
|
743
|
+
} else {
|
|
744
|
+
deactivate();
|
|
745
|
+
}
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
}, true);
|
|
749
|
+
|
|
750
|
+
document.addEventListener('keyup', (e) => {
|
|
751
|
+
if (!fiActive) return;
|
|
752
|
+
if (e.key === 'Alt' && optionHeld) {
|
|
753
|
+
optionHeld = false;
|
|
754
|
+
measureMode = !measureMode;
|
|
755
|
+
clearMeasureOverlay();
|
|
756
|
+
if (!measureMode) clearPin();
|
|
757
|
+
hideTooltip();
|
|
758
|
+
flashMode();
|
|
759
|
+
}
|
|
760
|
+
}, true);
|
|
761
|
+
|
|
762
|
+
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
763
|
+
|
|
764
|
+
console.log('%c\u{1F47B} Specter \u2014 Ctrl+Option+Z to toggle', 'color:#aaa;font-size:11px;');
|
|
765
|
+
})();`;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/index.ts
|
|
769
|
+
function specter(options = {}) {
|
|
770
|
+
return {
|
|
771
|
+
name: "vite-plugin-specter",
|
|
772
|
+
apply: "serve",
|
|
773
|
+
transformIndexHtml(html) {
|
|
774
|
+
const script = getClientScript(options);
|
|
775
|
+
return html.replace("</body>", `<script>
|
|
776
|
+
${script}
|
|
777
|
+
</script>
|
|
778
|
+
</body>`);
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
783
|
+
0 && (module.exports = {
|
|
784
|
+
getClientScript,
|
|
785
|
+
specter
|
|
786
|
+
});
|