vite-plugin-specter 0.5.0 → 0.6.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/README.md +15 -0
- package/dist/client.js +198 -47
- package/dist/extension-chrome/content.js +198 -47
- package/dist/extension-chrome/manifest.json +1 -1
- package/dist/extension-firefox/content.js +198 -47
- package/dist/extension-firefox/manifest.json +1 -1
- package/dist/index.cjs +198 -47
- package/dist/index.js +198 -47
- package/extension/content.js +198 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,21 @@ export default defineConfig({
|
|
|
27
27
|
|
|
28
28
|
Start your dev server and you're done. Specter is stripped from production builds automatically.
|
|
29
29
|
|
|
30
|
+
## ⚠️ Dev-only — never ship Specter to production (note for AI agents)
|
|
31
|
+
|
|
32
|
+
**Specter is a development-only tool. It must never end up in a customer-facing / production bundle.**
|
|
33
|
+
|
|
34
|
+
As a Vite plugin it is already safe by default: `specter()` runs with `apply: 'serve'`, so it is active on the dev server (`vite`) only and is completely excluded from `vite build`. Adding the plugin does **not** ship anything to your users.
|
|
35
|
+
|
|
36
|
+
If you are an AI agent (or automating a build), do **not** work around this guard:
|
|
37
|
+
|
|
38
|
+
- Do **not** call `getClientScript()` and inline the result into your app's HTML, templates, or components.
|
|
39
|
+
- Do **not** change `apply: 'serve'`, or otherwise force the plugin to run during `vite build`, `vite preview`, or SSR/production output.
|
|
40
|
+
- Do **not** copy the injected `<script>` from a dev page into the shipped app.
|
|
41
|
+
- Keep it in `devDependencies` (installed with `-D`); never move it to `dependencies`.
|
|
42
|
+
|
|
43
|
+
If you ever need Specter gone entirely, remove `specter()` from the Vite config (see [Uninstall](#uninstall)).
|
|
44
|
+
|
|
30
45
|
## How to use
|
|
31
46
|
|
|
32
47
|
### Toggle on/off
|
package/dist/client.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// ─── State ────────────────────────────────────────────────────────────────
|
|
29
29
|
var fiActive = false;
|
|
30
30
|
var measureMode = false;
|
|
31
|
+
var commentMode = false; // C: hide props/measure overlays for design review — Specs still capture them
|
|
31
32
|
var pinEl = null;
|
|
32
33
|
var pinHighlight = null;
|
|
33
34
|
var lastHovered = null;
|
|
@@ -193,7 +194,7 @@
|
|
|
193
194
|
hoverFx(listBtn, { background: 'rgba(255,255,255,0.32)' }, { background: 'rgba(255,255,255,0.16)' });
|
|
194
195
|
|
|
195
196
|
var clearBtn = document.createElement('span');
|
|
196
|
-
clearBtn.textContent = '✕ Delete all
|
|
197
|
+
clearBtn.textContent = '✕ Delete all';
|
|
197
198
|
clearBtn.title = 'Delete all annotations';
|
|
198
199
|
Object.assign(clearBtn.style, {
|
|
199
200
|
display: 'none',
|
|
@@ -225,7 +226,18 @@
|
|
|
225
226
|
chevron.addEventListener('click', function (e) { e.stopPropagation(); moveSide(); });
|
|
226
227
|
hoverFx(chevron, { opacity: '1', transform: 'scale(1.2)' }, { opacity: '0.8', transform: 'scale(1)' });
|
|
227
228
|
|
|
229
|
+
// Sync status in the control bar (only when a bridge is configured): spinner while
|
|
230
|
+
// syncing, green when synced, red on error — so you always know what's staged.
|
|
231
|
+
var pillSync = document.createElement('span');
|
|
232
|
+
Object.assign(pillSync.style, {
|
|
233
|
+
display: 'none', width: '8px', height: '8px', borderRadius: '999px',
|
|
234
|
+
background: '#6B7280', flexShrink: '0', boxSizing: 'border-box',
|
|
235
|
+
});
|
|
236
|
+
pillSync.title = 'Sync status — click to re-sync';
|
|
237
|
+
pillSync.addEventListener('click', function (e) { e.stopPropagation(); if (BRIDGE) doSync(); });
|
|
238
|
+
|
|
228
239
|
pill.appendChild(iconBtn);
|
|
240
|
+
pill.appendChild(pillSync);
|
|
229
241
|
pill.appendChild(pillText);
|
|
230
242
|
pill.appendChild(listBtn);
|
|
231
243
|
pill.appendChild(clearBtn);
|
|
@@ -233,6 +245,30 @@
|
|
|
233
245
|
pillWrap.appendChild(pill);
|
|
234
246
|
document.body.appendChild(pillWrap);
|
|
235
247
|
|
|
248
|
+
// Keyframes for the sync spinner (injected once).
|
|
249
|
+
var spinStyle = document.createElement('style');
|
|
250
|
+
spinStyle.textContent = '@keyframes __specterSpin{to{transform:rotate(360deg)}}';
|
|
251
|
+
markUI(spinStyle);
|
|
252
|
+
document.head.appendChild(spinStyle);
|
|
253
|
+
|
|
254
|
+
// Shared dot styling for the control-bar AND side-panel sync indicators, so they
|
|
255
|
+
// always match: spinner while syncing, green when synced, red on error.
|
|
256
|
+
function applyDotState(el, state) {
|
|
257
|
+
el.style.boxSizing = 'border-box';
|
|
258
|
+
if (state === 'syncing') {
|
|
259
|
+
Object.assign(el.style, { width: '10px', height: '10px', background: 'transparent', border: '2px solid rgba(255,255,255,0.35)', borderTopColor: '#fff', animation: '__specterSpin 0.6s linear infinite' });
|
|
260
|
+
} else if (state === 'synced') {
|
|
261
|
+
Object.assign(el.style, { width: '8px', height: '8px', background: GREEN, border: 'none', animation: 'none' });
|
|
262
|
+
} else { // offline / error
|
|
263
|
+
Object.assign(el.style, { width: '8px', height: '8px', background: '#F26D6D', border: 'none', animation: 'none' });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function setPillSync(state) {
|
|
267
|
+
if (!BRIDGE) { pillSync.style.display = 'none'; return; }
|
|
268
|
+
pillSync.style.display = 'inline-block';
|
|
269
|
+
applyDotState(pillSync, state);
|
|
270
|
+
}
|
|
271
|
+
|
|
236
272
|
pillWrap.addEventListener('mouseenter', function () {
|
|
237
273
|
clearMeasureOverlay();
|
|
238
274
|
clearMeasureTargetHL();
|
|
@@ -262,6 +298,12 @@
|
|
|
262
298
|
}
|
|
263
299
|
}
|
|
264
300
|
|
|
301
|
+
// The mode is shown at all times (persistent prefix), so you always know whether
|
|
302
|
+
// hovering shows properties, measurements, or nothing (Comment).
|
|
303
|
+
function modeLabel() {
|
|
304
|
+
return commentMode ? 'Comment' : (measureMode ? 'Measure' : 'Properties');
|
|
305
|
+
}
|
|
306
|
+
|
|
265
307
|
function expandPill(text) {
|
|
266
308
|
pill.style.maxWidth = '820px';
|
|
267
309
|
pillText.style.display = 'inline';
|
|
@@ -269,19 +311,17 @@
|
|
|
269
311
|
listBtn.style.display = specs.length > 0 ? 'inline-flex' : 'none';
|
|
270
312
|
clearBtn.style.display = specs.length > 0 ? 'inline' : 'none';
|
|
271
313
|
pillExpanded = true;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
} else if (measureMode) {
|
|
276
|
-
pillText.textContent = 'Measure · hover distances · P mark · M pin · Cmd+C copy · Option toggle';
|
|
277
|
-
} else {
|
|
278
|
-
pillText.textContent = 'Properties · P mark / annotate · Cmd+C copy · Option measure';
|
|
279
|
-
}
|
|
314
|
+
// Just the mode (+ the action buttons when Specs exist). Shortcuts live in the
|
|
315
|
+
// side panel now, so the pill stays short.
|
|
316
|
+
pillText.textContent = text || modeLabel();
|
|
280
317
|
}
|
|
281
318
|
|
|
319
|
+
// "Collapsed" = the compact resting state: mode label only, no hints/buttons.
|
|
320
|
+
// Still shows the mode so it's visible at all times.
|
|
282
321
|
function collapsePill() {
|
|
283
|
-
|
|
284
|
-
pillText.style.display = '
|
|
322
|
+
pillText.textContent = modeLabel();
|
|
323
|
+
pillText.style.display = 'inline';
|
|
324
|
+
pill.style.maxWidth = '220px';
|
|
285
325
|
chevron.style.display = 'none';
|
|
286
326
|
listBtn.style.display = 'none';
|
|
287
327
|
clearBtn.style.display = 'none';
|
|
@@ -290,7 +330,7 @@
|
|
|
290
330
|
|
|
291
331
|
function flashMode() {
|
|
292
332
|
if (pillExpanded && pillWrap.matches(':hover')) return;
|
|
293
|
-
var text = measureMode ? '
|
|
333
|
+
var text = commentMode ? 'Comment mode' : (measureMode ? 'Measure mode' : 'Properties mode');
|
|
294
334
|
expandPill(text);
|
|
295
335
|
clearTimeout(flashTimer);
|
|
296
336
|
flashTimer = setTimeout(function () {
|
|
@@ -311,11 +351,13 @@
|
|
|
311
351
|
function activate() {
|
|
312
352
|
fiActive = true;
|
|
313
353
|
measureMode = false;
|
|
354
|
+
commentMode = false;
|
|
314
355
|
pillWrap.style.display = 'block';
|
|
315
356
|
document.body.style.cursor = 'crosshair';
|
|
316
357
|
updatePill();
|
|
317
358
|
startLoop();
|
|
318
359
|
reflowSpecs();
|
|
360
|
+
if (BRIDGE) doSync(); // resolve the control-bar sync dot (green if reachable, red if not)
|
|
319
361
|
}
|
|
320
362
|
|
|
321
363
|
function deactivate() {
|
|
@@ -599,24 +641,37 @@
|
|
|
599
641
|
return ['[Specter]', head, 'find: ' + resolveLocator(data.el), props.join(' · ')].join('\n');
|
|
600
642
|
}
|
|
601
643
|
|
|
644
|
+
// Group Specs on the SAME element so their identical properties aren't emitted
|
|
645
|
+
// twice — several comments on one thing share ONE property block, each with its
|
|
646
|
+
// own change note. Measure Specs always stand alone (each is a distinct reading).
|
|
647
|
+
function groupSpecs() {
|
|
648
|
+
var groups = [];
|
|
649
|
+
for (var i = 0; i < specs.length; i++) {
|
|
650
|
+
var s = specs[i], g = null;
|
|
651
|
+
if (s.kind === 'element' && s.el) {
|
|
652
|
+
for (var j = 0; j < groups.length; j++) { if (groups[j].kind === 'element' && groups[j].el === s.el) { g = groups[j]; break; } }
|
|
653
|
+
}
|
|
654
|
+
if (g) g.specs.push(s);
|
|
655
|
+
else groups.push({ kind: s.kind, el: s.el, specs: [s] });
|
|
656
|
+
}
|
|
657
|
+
return groups;
|
|
658
|
+
}
|
|
659
|
+
|
|
602
660
|
// Copy every Spec — using each Spec's body snapshotted at mark time, so Specs
|
|
603
661
|
// whose element is currently hidden (e.g. inside a closed modal) still copy.
|
|
662
|
+
// Same-element Specs collapse into one block (properties once, notes stacked).
|
|
604
663
|
function buildSpecsCopyText() {
|
|
605
|
-
var
|
|
606
|
-
|
|
607
|
-
|
|
664
|
+
var groups = groupSpecs();
|
|
665
|
+
var n = groups.length;
|
|
666
|
+
return groups.map(function (g, i) {
|
|
667
|
+
var tag = g.kind === 'measure' ? 'Specter Measure' : 'Specter';
|
|
608
668
|
var header = n > 1 ? '[' + tag + ' ' + (i + 1) + '/' + n + ']' : '[' + tag + ']';
|
|
609
|
-
|
|
610
|
-
|
|
669
|
+
var notes = g.specs.filter(function (s) { return s.note; }).map(function (s) { return '✏️ CHANGE: ' + s.note; });
|
|
670
|
+
if (notes.length) header += '\n' + notes.join('\n');
|
|
671
|
+
return g.specs[0].body.replace(/^\[Specter[^\]]*\]/, function () { return header; });
|
|
611
672
|
}).join('\n\n---\n\n');
|
|
612
673
|
}
|
|
613
674
|
|
|
614
|
-
function linesToHTML(str) {
|
|
615
|
-
return str.split('\n').map(function (l) {
|
|
616
|
-
return '<div style="margin-bottom:4px;color:#fff">' + esc(l) + '</div>';
|
|
617
|
-
}).join('');
|
|
618
|
-
}
|
|
619
|
-
|
|
620
675
|
// ─── Hover outline ──────────────────────────────────────────────────────────
|
|
621
676
|
function setHoverOutline(el) {
|
|
622
677
|
if (outlinedEl === el) return;
|
|
@@ -648,12 +703,14 @@
|
|
|
648
703
|
}
|
|
649
704
|
|
|
650
705
|
function reRenderTooltip() {
|
|
706
|
+
if (commentMode) { hideTooltip(); return; }
|
|
651
707
|
if (measureMode) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
708
|
+
// Redraw the overlay on scroll/resize; readout box stays hidden (see hover handler).
|
|
709
|
+
if (pinEl && pinEl !== lastHovered) measureBetween(pinEl, lastHovered); else measureToNeighbor(lastHovered);
|
|
710
|
+
hideTooltip();
|
|
711
|
+
return;
|
|
656
712
|
}
|
|
713
|
+
tooltip.innerHTML = buildHumanDisplay(buildInfo(lastHovered));
|
|
657
714
|
positionTooltip(lastMouse.x, lastMouse.y);
|
|
658
715
|
}
|
|
659
716
|
|
|
@@ -809,7 +866,7 @@
|
|
|
809
866
|
|
|
810
867
|
function updateBadgeContent(spec) {
|
|
811
868
|
spec.num.textContent = String(specs.indexOf(spec) + 1);
|
|
812
|
-
if (spec.note) spec.noteSpan.textContent =
|
|
869
|
+
if (spec.note) spec.noteSpan.textContent = spec.note;
|
|
813
870
|
else spec.noteSpan.textContent = spec.kind === 'measure' ? '⬡ measure' : '';
|
|
814
871
|
}
|
|
815
872
|
|
|
@@ -854,10 +911,6 @@
|
|
|
854
911
|
updateBadgeContent(spec);
|
|
855
912
|
}
|
|
856
913
|
|
|
857
|
-
function findElementSpec(el) {
|
|
858
|
-
for (var i = 0; i < specs.length; i++) if (specs[i].kind === 'element' && specs[i].el === el) return specs[i];
|
|
859
|
-
return null;
|
|
860
|
-
}
|
|
861
914
|
|
|
862
915
|
// If the element sits inside a dialog/modal/drawer/menu, produce a hint for how to
|
|
863
916
|
// bring it back when it's later hidden — a declarative opener (aria-controls /
|
|
@@ -896,7 +949,9 @@
|
|
|
896
949
|
if (pinEl && lastHovered && lastHovered !== pinEl) { el = pinEl; body = buildMeasureCopyText(pinEl, lastHovered); }
|
|
897
950
|
else { el = anchorEl; body = buildNeighborCopyText(anchorEl); }
|
|
898
951
|
} else {
|
|
899
|
-
|
|
952
|
+
// Multiple Specs per element are allowed in every mode — clustering fans the badges
|
|
953
|
+
// out and same-element output collapses to one property block. Mode only changes
|
|
954
|
+
// what's shown on screen, never whether you can add an annotation.
|
|
900
955
|
body = buildLLMClipboard(buildInfo(anchorEl));
|
|
901
956
|
}
|
|
902
957
|
clearHoverOutline();
|
|
@@ -1134,14 +1189,74 @@
|
|
|
1134
1189
|
}
|
|
1135
1190
|
|
|
1136
1191
|
var panelList = document.createElement('div');
|
|
1137
|
-
|
|
1192
|
+
// overscroll-behavior:contain stops the panel's scroll from chaining into the page.
|
|
1193
|
+
Object.assign(panelList.style, { flex: '1', overflowY: 'auto', overflowX: 'hidden', overscrollBehavior: 'contain' });
|
|
1194
|
+
|
|
1195
|
+
// Keyboard shortcuts reference — collapsible, collapsed by default, lives here (not
|
|
1196
|
+
// in the pill) so the pill stays short.
|
|
1197
|
+
var panelKeys = document.createElement('div');
|
|
1198
|
+
Object.assign(panelKeys.style, {
|
|
1199
|
+
flexShrink: '0', fontSize: '11px', lineHeight: '18px', color: LABEL,
|
|
1200
|
+
borderTop: '1px solid rgba(255,255,255,0.08)',
|
|
1201
|
+
});
|
|
1202
|
+
(function () {
|
|
1203
|
+
var rows = [
|
|
1204
|
+
['P', 'mark / comment the hovered element'],
|
|
1205
|
+
['C', 'toggle Comment mode (hide properties)'],
|
|
1206
|
+
['\u2325 Option', 'toggle Measure mode'],
|
|
1207
|
+
['M', 'pin an element to measure from'],
|
|
1208
|
+
['Cmd/Ctrl+C', 'copy all Specs'],
|
|
1209
|
+
['L', 'toggle this panel'],
|
|
1210
|
+
];
|
|
1211
|
+
var head = document.createElement('div');
|
|
1212
|
+
Object.assign(head.style, { display: 'flex', alignItems: 'center', gap: '6px', color: '#8A8D96', fontWeight: '700', letterSpacing: '0.04em', textTransform: 'uppercase', fontSize: '11px', padding: '10px 16px', cursor: 'pointer', userSelect: 'none' });
|
|
1213
|
+
var caret = document.createElement('span');
|
|
1214
|
+
caret.textContent = '\u203A'; // ›
|
|
1215
|
+
Object.assign(caret.style, { display: 'inline-block', fontSize: '16px', lineHeight: '1', transition: 'transform 0.15s ease', transform: 'rotate(0deg)' });
|
|
1216
|
+
var headText = document.createElement('span');
|
|
1217
|
+
headText.textContent = 'Keyboard shortcuts';
|
|
1218
|
+
head.appendChild(caret);
|
|
1219
|
+
head.appendChild(headText);
|
|
1220
|
+
var body = document.createElement('div');
|
|
1221
|
+
Object.assign(body.style, { display: 'none', padding: '0 16px 12px' }); // collapsed by default
|
|
1222
|
+
rows.forEach(function (r) {
|
|
1223
|
+
var row = document.createElement('div');
|
|
1224
|
+
Object.assign(row.style, { display: 'flex', gap: '8px', marginBottom: '2px' });
|
|
1225
|
+
var k = document.createElement('span');
|
|
1226
|
+
k.textContent = r[0];
|
|
1227
|
+
Object.assign(k.style, { color: '#E0A3F5', fontWeight: '700', minWidth: '78px', flexShrink: '0' });
|
|
1228
|
+
var d = document.createElement('span');
|
|
1229
|
+
d.textContent = r[1];
|
|
1230
|
+
row.appendChild(k); row.appendChild(d);
|
|
1231
|
+
body.appendChild(row);
|
|
1232
|
+
});
|
|
1233
|
+
var open = false;
|
|
1234
|
+
head.addEventListener('click', function (e) {
|
|
1235
|
+
e.stopPropagation();
|
|
1236
|
+
open = !open;
|
|
1237
|
+
body.style.display = open ? 'block' : 'none';
|
|
1238
|
+
caret.style.transform = open ? 'rotate(90deg)' : 'rotate(0deg)';
|
|
1239
|
+
});
|
|
1240
|
+
panelKeys.appendChild(head);
|
|
1241
|
+
panelKeys.appendChild(body);
|
|
1242
|
+
})();
|
|
1138
1243
|
|
|
1139
1244
|
panelWrap.appendChild(panelHead);
|
|
1140
1245
|
panelWrap.appendChild(panelTools);
|
|
1141
1246
|
panelWrap.appendChild(panelHint);
|
|
1142
1247
|
panelWrap.appendChild(panelList);
|
|
1248
|
+
panelWrap.appendChild(panelKeys);
|
|
1143
1249
|
document.body.appendChild(panelWrap);
|
|
1144
1250
|
|
|
1251
|
+
// Panel scroll and page scroll are mutually exclusive: wheel over the scrollable list
|
|
1252
|
+
// scrolls it natively (overscroll-behavior:contain stops it chaining at the bounds);
|
|
1253
|
+
// wheel over any non-scrolling part of the panel never scrolls the page behind it.
|
|
1254
|
+
panelWrap.addEventListener('wheel', function (e) {
|
|
1255
|
+
var canScroll = panelList.scrollHeight > panelList.clientHeight;
|
|
1256
|
+
if (panelList.contains(e.target) && canScroll) return; // let the list scroll natively
|
|
1257
|
+
e.preventDefault();
|
|
1258
|
+
}, { passive: false });
|
|
1259
|
+
|
|
1145
1260
|
// ── Auto-sync: mirror the browser's current Specs to the local bridge ──
|
|
1146
1261
|
// Debounced so rapid edits/typing collapse into one POST. The bridge replaces
|
|
1147
1262
|
// its snapshot for this URL, so it always reflects what's in the panel — no
|
|
@@ -1149,24 +1264,33 @@
|
|
|
1149
1264
|
var syncTimer = null;
|
|
1150
1265
|
function setSyncState(s) {
|
|
1151
1266
|
if (!BRIDGE) return;
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1267
|
+
setPillSync(s); // control-bar dot
|
|
1268
|
+
applyDotState(dot, s); // side-panel dot — same spinner/green/red
|
|
1269
|
+
if (s === 'syncing') { dotLabel.textContent = 'Syncing…'; }
|
|
1270
|
+
else if (s === 'synced') { dotLabel.textContent = specs.length ? (specs.length + (specs.length === 1 ? ' Spec synced' : ' Specs synced')) : 'Synced'; }
|
|
1271
|
+
else { dotLabel.textContent = 'Bridge offline'; }
|
|
1155
1272
|
}
|
|
1156
1273
|
function doSync() {
|
|
1157
1274
|
if (!BRIDGE) return;
|
|
1158
1275
|
setSyncState('syncing');
|
|
1276
|
+
// Keep the spinner up for at least 1s even on an instant localhost sync, so the
|
|
1277
|
+
// feedback is actually perceptible instead of flashing by.
|
|
1278
|
+
var started = Date.now();
|
|
1279
|
+
var settle = function (state) {
|
|
1280
|
+
var wait = Math.max(0, 1000 - (Date.now() - started));
|
|
1281
|
+
setTimeout(function () { setSyncState(state); }, wait);
|
|
1282
|
+
};
|
|
1159
1283
|
var payload = {
|
|
1160
1284
|
url: location.href,
|
|
1161
1285
|
text: buildSpecsCopyText(),
|
|
1162
|
-
specs:
|
|
1163
|
-
return { num: i + 1, note: s.note
|
|
1286
|
+
specs: groupSpecs().map(function (g, i) {
|
|
1287
|
+
return { num: i + 1, note: g.specs.map(function (s) { return s.note; }).filter(Boolean).join('\n'), kind: g.kind, body: g.specs[0].body };
|
|
1164
1288
|
}),
|
|
1165
1289
|
};
|
|
1166
1290
|
fetch(BRIDGE + '/specs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
|
|
1167
1291
|
.then(function (r) { if (!r.ok) throw 0; return r.json(); })
|
|
1168
|
-
.then(function () {
|
|
1169
|
-
.catch(function () {
|
|
1292
|
+
.then(function () { settle('synced'); })
|
|
1293
|
+
.catch(function () { settle('offline'); });
|
|
1170
1294
|
}
|
|
1171
1295
|
function scheduleSync() {
|
|
1172
1296
|
if (!BRIDGE) return;
|
|
@@ -1310,7 +1434,7 @@
|
|
|
1310
1434
|
Object.assign(hbar.style, { display: 'flex', alignItems: 'center', gap: '7px', marginTop: '2px', flexWrap: 'wrap' });
|
|
1311
1435
|
var tag = document.createElement('span');
|
|
1312
1436
|
tag.textContent = 'HIDDEN';
|
|
1313
|
-
Object.assign(tag.style, { fontSize: '
|
|
1437
|
+
Object.assign(tag.style, { fontSize: '11px', fontWeight: '700', letterSpacing: '0.05em', color: '#3A2A05', background: '#F59E0B', borderRadius: '4px', padding: '2px 6px', flexShrink: '0' });
|
|
1314
1438
|
var hint = document.createElement('span');
|
|
1315
1439
|
hint.textContent = spec.locate || 'Not on the page right now';
|
|
1316
1440
|
Object.assign(hint.style, { fontSize: '11px', color: '#C9CBD2', overflow: 'hidden', textOverflow: 'ellipsis' });
|
|
@@ -1574,9 +1698,9 @@
|
|
|
1574
1698
|
position: 'absolute',
|
|
1575
1699
|
background: RED,
|
|
1576
1700
|
color: '#fff',
|
|
1577
|
-
fontSize: '
|
|
1701
|
+
fontSize: '12px',
|
|
1578
1702
|
fontFamily: MONO,
|
|
1579
|
-
padding: '
|
|
1703
|
+
padding: '2px 5px',
|
|
1580
1704
|
borderRadius: '3px',
|
|
1581
1705
|
pointerEvents: 'none',
|
|
1582
1706
|
whiteSpace: 'nowrap',
|
|
@@ -1776,12 +1900,24 @@
|
|
|
1776
1900
|
|
|
1777
1901
|
lastHovered = target;
|
|
1778
1902
|
|
|
1903
|
+
// Comment mode: outline only (so you know what you're commenting on), no
|
|
1904
|
+
// properties/measure overlay on screen. The Spec still captures everything.
|
|
1905
|
+
if (commentMode) {
|
|
1906
|
+
clearMeasureOverlay();
|
|
1907
|
+
clearMeasureTargetHL();
|
|
1908
|
+
setHoverOutline(target);
|
|
1909
|
+
hideTooltip();
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1779
1913
|
if (measureMode) {
|
|
1780
1914
|
clearHoverOutline();
|
|
1781
1915
|
showMeasureTargetHL(target);
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1916
|
+
// Draw the on-screen measurement overlay (the px badges + lines) but keep
|
|
1917
|
+
// the dark readout box hidden — it occludes the very measurements it reports.
|
|
1918
|
+
// The full readout still copies with Cmd+C. (Properties mode keeps its box.)
|
|
1919
|
+
if (pinEl && pinEl !== target) measureBetween(pinEl, target); else measureToNeighbor(target);
|
|
1920
|
+
hideTooltip();
|
|
1785
1921
|
if (pinEl) updatePinHL();
|
|
1786
1922
|
} else {
|
|
1787
1923
|
clearMeasureOverlay();
|
|
@@ -1863,6 +1999,21 @@
|
|
|
1863
1999
|
return;
|
|
1864
2000
|
}
|
|
1865
2001
|
|
|
2002
|
+
// C — toggle Comment mode (outline only; props/measure hidden but still captured).
|
|
2003
|
+
// Plain c only — Cmd/Ctrl+C copy was handled above and returned.
|
|
2004
|
+
if ((e.key === 'c' || e.key === 'C') && !e.metaKey && !e.ctrlKey) {
|
|
2005
|
+
e.preventDefault();
|
|
2006
|
+
commentMode = !commentMode;
|
|
2007
|
+
if (commentMode && measureMode) { measureMode = false; clearPin(); }
|
|
2008
|
+
clearMeasureOverlay();
|
|
2009
|
+
clearMeasureTargetHL();
|
|
2010
|
+
clearHoverOutline();
|
|
2011
|
+
hideTooltip();
|
|
2012
|
+
if (lastHovered && commentMode) setHoverOutline(lastHovered);
|
|
2013
|
+
flashMode();
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1866
2017
|
// Esc only HIDES the plugin — Specs persist and return on reactivate.
|
|
1867
2018
|
if (e.key === 'Escape') {
|
|
1868
2019
|
if (panelOpen) { hidePanel(); return; }
|