vite-plugin-specter 0.5.0 → 0.6.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/README.md +79 -41
- package/dist/client.js +204 -49
- package/dist/extension-chrome/content.js +204 -49
- package/dist/extension-chrome/manifest.json +1 -1
- package/dist/extension-firefox/content.js +204 -49
- package/dist/extension-firefox/manifest.json +1 -1
- package/dist/index.cjs +204 -49
- package/dist/index.js +204 -49
- package/extension/content.js +204 -49
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -46,6 +46,7 @@ function getClientScript(options) {
|
|
|
46
46
|
var TIP_BG = '#292B32';
|
|
47
47
|
var GREEN = '#22C55E';
|
|
48
48
|
var LABEL = '#8B8D94';
|
|
49
|
+
var BADGE_IDLE = '#6B6D75'; // panel row-number badges sit greyscale; the hovered row's badge colors to PURPLE
|
|
49
50
|
var MONO = "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace";
|
|
50
51
|
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>';
|
|
51
52
|
var PENCIL = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"></path><path d="m15 5 4 4"></path></svg>';
|
|
@@ -60,6 +61,7 @@ function getClientScript(options) {
|
|
|
60
61
|
// \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
|
|
61
62
|
var fiActive = false;
|
|
62
63
|
var measureMode = false;
|
|
64
|
+
var commentMode = false; // C: hide props/measure overlays for design review \u2014 Specs still capture them
|
|
63
65
|
var pinEl = null;
|
|
64
66
|
var pinHighlight = null;
|
|
65
67
|
var lastHovered = null;
|
|
@@ -225,7 +227,7 @@ function getClientScript(options) {
|
|
|
225
227
|
hoverFx(listBtn, { background: 'rgba(255,255,255,0.32)' }, { background: 'rgba(255,255,255,0.16)' });
|
|
226
228
|
|
|
227
229
|
var clearBtn = document.createElement('span');
|
|
228
|
-
clearBtn.textContent = '\u2715 Delete all
|
|
230
|
+
clearBtn.textContent = '\u2715 Delete all';
|
|
229
231
|
clearBtn.title = 'Delete all annotations';
|
|
230
232
|
Object.assign(clearBtn.style, {
|
|
231
233
|
display: 'none',
|
|
@@ -257,7 +259,18 @@ function getClientScript(options) {
|
|
|
257
259
|
chevron.addEventListener('click', function (e) { e.stopPropagation(); moveSide(); });
|
|
258
260
|
hoverFx(chevron, { opacity: '1', transform: 'scale(1.2)' }, { opacity: '0.8', transform: 'scale(1)' });
|
|
259
261
|
|
|
262
|
+
// Sync status in the control bar (only when a bridge is configured): spinner while
|
|
263
|
+
// syncing, green when synced, red on error \u2014 so you always know what's staged.
|
|
264
|
+
var pillSync = document.createElement('span');
|
|
265
|
+
Object.assign(pillSync.style, {
|
|
266
|
+
display: 'none', width: '8px', height: '8px', borderRadius: '999px',
|
|
267
|
+
background: '#6B7280', flexShrink: '0', boxSizing: 'border-box',
|
|
268
|
+
});
|
|
269
|
+
pillSync.title = 'Sync status \u2014 click to re-sync';
|
|
270
|
+
pillSync.addEventListener('click', function (e) { e.stopPropagation(); if (BRIDGE) doSync(); });
|
|
271
|
+
|
|
260
272
|
pill.appendChild(iconBtn);
|
|
273
|
+
pill.appendChild(pillSync);
|
|
261
274
|
pill.appendChild(pillText);
|
|
262
275
|
pill.appendChild(listBtn);
|
|
263
276
|
pill.appendChild(clearBtn);
|
|
@@ -265,6 +278,30 @@ function getClientScript(options) {
|
|
|
265
278
|
pillWrap.appendChild(pill);
|
|
266
279
|
document.body.appendChild(pillWrap);
|
|
267
280
|
|
|
281
|
+
// Keyframes for the sync spinner (injected once).
|
|
282
|
+
var spinStyle = document.createElement('style');
|
|
283
|
+
spinStyle.textContent = '@keyframes __specterSpin{to{transform:rotate(360deg)}}';
|
|
284
|
+
markUI(spinStyle);
|
|
285
|
+
document.head.appendChild(spinStyle);
|
|
286
|
+
|
|
287
|
+
// Shared dot styling for the control-bar AND side-panel sync indicators, so they
|
|
288
|
+
// always match: spinner while syncing, green when synced, red on error.
|
|
289
|
+
function applyDotState(el, state) {
|
|
290
|
+
el.style.boxSizing = 'border-box';
|
|
291
|
+
if (state === 'syncing') {
|
|
292
|
+
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' });
|
|
293
|
+
} else if (state === 'synced') {
|
|
294
|
+
Object.assign(el.style, { width: '8px', height: '8px', background: GREEN, border: 'none', animation: 'none' });
|
|
295
|
+
} else { // offline / error
|
|
296
|
+
Object.assign(el.style, { width: '8px', height: '8px', background: '#F26D6D', border: 'none', animation: 'none' });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function setPillSync(state) {
|
|
300
|
+
if (!BRIDGE) { pillSync.style.display = 'none'; return; }
|
|
301
|
+
pillSync.style.display = 'inline-block';
|
|
302
|
+
applyDotState(pillSync, state);
|
|
303
|
+
}
|
|
304
|
+
|
|
268
305
|
pillWrap.addEventListener('mouseenter', function () {
|
|
269
306
|
clearMeasureOverlay();
|
|
270
307
|
clearMeasureTargetHL();
|
|
@@ -294,6 +331,12 @@ function getClientScript(options) {
|
|
|
294
331
|
}
|
|
295
332
|
}
|
|
296
333
|
|
|
334
|
+
// The mode is shown at all times (persistent prefix), so you always know whether
|
|
335
|
+
// hovering shows properties, measurements, or nothing (Comment).
|
|
336
|
+
function modeLabel() {
|
|
337
|
+
return commentMode ? 'Comment' : (measureMode ? 'Measure' : 'Properties');
|
|
338
|
+
}
|
|
339
|
+
|
|
297
340
|
function expandPill(text) {
|
|
298
341
|
pill.style.maxWidth = '820px';
|
|
299
342
|
pillText.style.display = 'inline';
|
|
@@ -301,19 +344,17 @@ function getClientScript(options) {
|
|
|
301
344
|
listBtn.style.display = specs.length > 0 ? 'inline-flex' : 'none';
|
|
302
345
|
clearBtn.style.display = specs.length > 0 ? 'inline' : 'none';
|
|
303
346
|
pillExpanded = true;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
} else if (measureMode) {
|
|
308
|
-
pillText.textContent = 'Measure \xB7 hover distances \xB7 P mark \xB7 M pin \xB7 Cmd+C copy \xB7 Option toggle';
|
|
309
|
-
} else {
|
|
310
|
-
pillText.textContent = 'Properties \xB7 P mark / annotate \xB7 Cmd+C copy \xB7 Option measure';
|
|
311
|
-
}
|
|
347
|
+
// Just the mode (+ the action buttons when Specs exist). Shortcuts live in the
|
|
348
|
+
// side panel now, so the pill stays short.
|
|
349
|
+
pillText.textContent = text || modeLabel();
|
|
312
350
|
}
|
|
313
351
|
|
|
352
|
+
// "Collapsed" = the compact resting state: mode label only, no hints/buttons.
|
|
353
|
+
// Still shows the mode so it's visible at all times.
|
|
314
354
|
function collapsePill() {
|
|
315
|
-
|
|
316
|
-
pillText.style.display = '
|
|
355
|
+
pillText.textContent = modeLabel();
|
|
356
|
+
pillText.style.display = 'inline';
|
|
357
|
+
pill.style.maxWidth = '220px';
|
|
317
358
|
chevron.style.display = 'none';
|
|
318
359
|
listBtn.style.display = 'none';
|
|
319
360
|
clearBtn.style.display = 'none';
|
|
@@ -322,7 +363,7 @@ function getClientScript(options) {
|
|
|
322
363
|
|
|
323
364
|
function flashMode() {
|
|
324
365
|
if (pillExpanded && pillWrap.matches(':hover')) return;
|
|
325
|
-
var text = measureMode ? '
|
|
366
|
+
var text = commentMode ? 'Comment mode' : (measureMode ? 'Measure mode' : 'Properties mode');
|
|
326
367
|
expandPill(text);
|
|
327
368
|
clearTimeout(flashTimer);
|
|
328
369
|
flashTimer = setTimeout(function () {
|
|
@@ -343,11 +384,13 @@ function getClientScript(options) {
|
|
|
343
384
|
function activate() {
|
|
344
385
|
fiActive = true;
|
|
345
386
|
measureMode = false;
|
|
387
|
+
commentMode = false;
|
|
346
388
|
pillWrap.style.display = 'block';
|
|
347
389
|
document.body.style.cursor = 'crosshair';
|
|
348
390
|
updatePill();
|
|
349
391
|
startLoop();
|
|
350
392
|
reflowSpecs();
|
|
393
|
+
if (BRIDGE) doSync(); // resolve the control-bar sync dot (green if reachable, red if not)
|
|
351
394
|
}
|
|
352
395
|
|
|
353
396
|
function deactivate() {
|
|
@@ -631,24 +674,37 @@ function getClientScript(options) {
|
|
|
631
674
|
return ['[Specter]', head, 'find: ' + resolveLocator(data.el), props.join(' \xB7 ')].join('\\n');
|
|
632
675
|
}
|
|
633
676
|
|
|
677
|
+
// Group Specs on the SAME element so their identical properties aren't emitted
|
|
678
|
+
// twice \u2014 several comments on one thing share ONE property block, each with its
|
|
679
|
+
// own change note. Measure Specs always stand alone (each is a distinct reading).
|
|
680
|
+
function groupSpecs() {
|
|
681
|
+
var groups = [];
|
|
682
|
+
for (var i = 0; i < specs.length; i++) {
|
|
683
|
+
var s = specs[i], g = null;
|
|
684
|
+
if (s.kind === 'element' && s.el) {
|
|
685
|
+
for (var j = 0; j < groups.length; j++) { if (groups[j].kind === 'element' && groups[j].el === s.el) { g = groups[j]; break; } }
|
|
686
|
+
}
|
|
687
|
+
if (g) g.specs.push(s);
|
|
688
|
+
else groups.push({ kind: s.kind, el: s.el, specs: [s] });
|
|
689
|
+
}
|
|
690
|
+
return groups;
|
|
691
|
+
}
|
|
692
|
+
|
|
634
693
|
// Copy every Spec \u2014 using each Spec's body snapshotted at mark time, so Specs
|
|
635
694
|
// whose element is currently hidden (e.g. inside a closed modal) still copy.
|
|
695
|
+
// Same-element Specs collapse into one block (properties once, notes stacked).
|
|
636
696
|
function buildSpecsCopyText() {
|
|
637
|
-
var
|
|
638
|
-
|
|
639
|
-
|
|
697
|
+
var groups = groupSpecs();
|
|
698
|
+
var n = groups.length;
|
|
699
|
+
return groups.map(function (g, i) {
|
|
700
|
+
var tag = g.kind === 'measure' ? 'Specter Measure' : 'Specter';
|
|
640
701
|
var header = n > 1 ? '[' + tag + ' ' + (i + 1) + '/' + n + ']' : '[' + tag + ']';
|
|
641
|
-
|
|
642
|
-
|
|
702
|
+
var notes = g.specs.filter(function (s) { return s.note; }).map(function (s) { return '\u270F\uFE0F CHANGE: ' + s.note; });
|
|
703
|
+
if (notes.length) header += '\\n' + notes.join('\\n');
|
|
704
|
+
return g.specs[0].body.replace(/^\\[Specter[^\\]]*\\]/, function () { return header; });
|
|
643
705
|
}).join('\\n\\n---\\n\\n');
|
|
644
706
|
}
|
|
645
707
|
|
|
646
|
-
function linesToHTML(str) {
|
|
647
|
-
return str.split('\\n').map(function (l) {
|
|
648
|
-
return '<div style="margin-bottom:4px;color:#fff">' + esc(l) + '</div>';
|
|
649
|
-
}).join('');
|
|
650
|
-
}
|
|
651
|
-
|
|
652
708
|
// \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
|
|
653
709
|
function setHoverOutline(el) {
|
|
654
710
|
if (outlinedEl === el) return;
|
|
@@ -680,12 +736,14 @@ function getClientScript(options) {
|
|
|
680
736
|
}
|
|
681
737
|
|
|
682
738
|
function reRenderTooltip() {
|
|
739
|
+
if (commentMode) { hideTooltip(); return; }
|
|
683
740
|
if (measureMode) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
741
|
+
// Redraw the overlay on scroll/resize; readout box stays hidden (see hover handler).
|
|
742
|
+
if (pinEl && pinEl !== lastHovered) measureBetween(pinEl, lastHovered); else measureToNeighbor(lastHovered);
|
|
743
|
+
hideTooltip();
|
|
744
|
+
return;
|
|
688
745
|
}
|
|
746
|
+
tooltip.innerHTML = buildHumanDisplay(buildInfo(lastHovered));
|
|
689
747
|
positionTooltip(lastMouse.x, lastMouse.y);
|
|
690
748
|
}
|
|
691
749
|
|
|
@@ -841,7 +899,7 @@ function getClientScript(options) {
|
|
|
841
899
|
|
|
842
900
|
function updateBadgeContent(spec) {
|
|
843
901
|
spec.num.textContent = String(specs.indexOf(spec) + 1);
|
|
844
|
-
if (spec.note) spec.noteSpan.textContent =
|
|
902
|
+
if (spec.note) spec.noteSpan.textContent = spec.note;
|
|
845
903
|
else spec.noteSpan.textContent = spec.kind === 'measure' ? '\u2B21 measure' : '';
|
|
846
904
|
}
|
|
847
905
|
|
|
@@ -886,10 +944,6 @@ function getClientScript(options) {
|
|
|
886
944
|
updateBadgeContent(spec);
|
|
887
945
|
}
|
|
888
946
|
|
|
889
|
-
function findElementSpec(el) {
|
|
890
|
-
for (var i = 0; i < specs.length; i++) if (specs[i].kind === 'element' && specs[i].el === el) return specs[i];
|
|
891
|
-
return null;
|
|
892
|
-
}
|
|
893
947
|
|
|
894
948
|
// If the element sits inside a dialog/modal/drawer/menu, produce a hint for how to
|
|
895
949
|
// bring it back when it's later hidden \u2014 a declarative opener (aria-controls /
|
|
@@ -928,7 +982,9 @@ function getClientScript(options) {
|
|
|
928
982
|
if (pinEl && lastHovered && lastHovered !== pinEl) { el = pinEl; body = buildMeasureCopyText(pinEl, lastHovered); }
|
|
929
983
|
else { el = anchorEl; body = buildNeighborCopyText(anchorEl); }
|
|
930
984
|
} else {
|
|
931
|
-
|
|
985
|
+
// Multiple Specs per element are allowed in every mode \u2014 clustering fans the badges
|
|
986
|
+
// out and same-element output collapses to one property block. Mode only changes
|
|
987
|
+
// what's shown on screen, never whether you can add an annotation.
|
|
932
988
|
body = buildLLMClipboard(buildInfo(anchorEl));
|
|
933
989
|
}
|
|
934
990
|
clearHoverOutline();
|
|
@@ -1166,14 +1222,74 @@ function getClientScript(options) {
|
|
|
1166
1222
|
}
|
|
1167
1223
|
|
|
1168
1224
|
var panelList = document.createElement('div');
|
|
1169
|
-
|
|
1225
|
+
// overscroll-behavior:contain stops the panel's scroll from chaining into the page.
|
|
1226
|
+
Object.assign(panelList.style, { flex: '1', overflowY: 'auto', overflowX: 'hidden', overscrollBehavior: 'contain' });
|
|
1227
|
+
|
|
1228
|
+
// Keyboard shortcuts reference \u2014 collapsible, collapsed by default, lives here (not
|
|
1229
|
+
// in the pill) so the pill stays short.
|
|
1230
|
+
var panelKeys = document.createElement('div');
|
|
1231
|
+
Object.assign(panelKeys.style, {
|
|
1232
|
+
flexShrink: '0', fontSize: '11px', lineHeight: '18px', color: LABEL,
|
|
1233
|
+
borderTop: '1px solid rgba(255,255,255,0.08)',
|
|
1234
|
+
});
|
|
1235
|
+
(function () {
|
|
1236
|
+
var rows = [
|
|
1237
|
+
['P', 'mark / comment the hovered element'],
|
|
1238
|
+
['C', 'toggle Comment mode (hide properties)'],
|
|
1239
|
+
['\\u2325 Option', 'toggle Measure mode'],
|
|
1240
|
+
['M', 'pin an element to measure from'],
|
|
1241
|
+
['Cmd/Ctrl+C', 'copy all Specs'],
|
|
1242
|
+
['L', 'toggle this panel'],
|
|
1243
|
+
];
|
|
1244
|
+
var head = document.createElement('div');
|
|
1245
|
+
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' });
|
|
1246
|
+
var caret = document.createElement('span');
|
|
1247
|
+
caret.textContent = '\\u203A'; // \u203A
|
|
1248
|
+
Object.assign(caret.style, { display: 'inline-block', fontSize: '16px', lineHeight: '1', transition: 'transform 0.15s ease', transform: 'rotate(0deg)' });
|
|
1249
|
+
var headText = document.createElement('span');
|
|
1250
|
+
headText.textContent = 'Keyboard shortcuts';
|
|
1251
|
+
head.appendChild(caret);
|
|
1252
|
+
head.appendChild(headText);
|
|
1253
|
+
var body = document.createElement('div');
|
|
1254
|
+
Object.assign(body.style, { display: 'none', padding: '0 16px 12px' }); // collapsed by default
|
|
1255
|
+
rows.forEach(function (r) {
|
|
1256
|
+
var row = document.createElement('div');
|
|
1257
|
+
Object.assign(row.style, { display: 'flex', gap: '8px', marginBottom: '2px' });
|
|
1258
|
+
var k = document.createElement('span');
|
|
1259
|
+
k.textContent = r[0];
|
|
1260
|
+
Object.assign(k.style, { color: '#E0A3F5', fontWeight: '700', minWidth: '78px', flexShrink: '0' });
|
|
1261
|
+
var d = document.createElement('span');
|
|
1262
|
+
d.textContent = r[1];
|
|
1263
|
+
row.appendChild(k); row.appendChild(d);
|
|
1264
|
+
body.appendChild(row);
|
|
1265
|
+
});
|
|
1266
|
+
var open = false;
|
|
1267
|
+
head.addEventListener('click', function (e) {
|
|
1268
|
+
e.stopPropagation();
|
|
1269
|
+
open = !open;
|
|
1270
|
+
body.style.display = open ? 'block' : 'none';
|
|
1271
|
+
caret.style.transform = open ? 'rotate(90deg)' : 'rotate(0deg)';
|
|
1272
|
+
});
|
|
1273
|
+
panelKeys.appendChild(head);
|
|
1274
|
+
panelKeys.appendChild(body);
|
|
1275
|
+
})();
|
|
1170
1276
|
|
|
1171
1277
|
panelWrap.appendChild(panelHead);
|
|
1172
1278
|
panelWrap.appendChild(panelTools);
|
|
1173
1279
|
panelWrap.appendChild(panelHint);
|
|
1174
1280
|
panelWrap.appendChild(panelList);
|
|
1281
|
+
panelWrap.appendChild(panelKeys);
|
|
1175
1282
|
document.body.appendChild(panelWrap);
|
|
1176
1283
|
|
|
1284
|
+
// Panel scroll and page scroll are mutually exclusive: wheel over the scrollable list
|
|
1285
|
+
// scrolls it natively (overscroll-behavior:contain stops it chaining at the bounds);
|
|
1286
|
+
// wheel over any non-scrolling part of the panel never scrolls the page behind it.
|
|
1287
|
+
panelWrap.addEventListener('wheel', function (e) {
|
|
1288
|
+
var canScroll = panelList.scrollHeight > panelList.clientHeight;
|
|
1289
|
+
if (panelList.contains(e.target) && canScroll) return; // let the list scroll natively
|
|
1290
|
+
e.preventDefault();
|
|
1291
|
+
}, { passive: false });
|
|
1292
|
+
|
|
1177
1293
|
// \u2500\u2500 Auto-sync: mirror the browser's current Specs to the local bridge \u2500\u2500
|
|
1178
1294
|
// Debounced so rapid edits/typing collapse into one POST. The bridge replaces
|
|
1179
1295
|
// its snapshot for this URL, so it always reflects what's in the panel \u2014 no
|
|
@@ -1181,24 +1297,33 @@ function getClientScript(options) {
|
|
|
1181
1297
|
var syncTimer = null;
|
|
1182
1298
|
function setSyncState(s) {
|
|
1183
1299
|
if (!BRIDGE) return;
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1300
|
+
setPillSync(s); // control-bar dot
|
|
1301
|
+
applyDotState(dot, s); // side-panel dot \u2014 same spinner/green/red
|
|
1302
|
+
if (s === 'syncing') { dotLabel.textContent = 'Syncing\u2026'; }
|
|
1303
|
+
else if (s === 'synced') { dotLabel.textContent = specs.length ? (specs.length + (specs.length === 1 ? ' Spec synced' : ' Specs synced')) : 'Synced'; }
|
|
1304
|
+
else { dotLabel.textContent = 'Bridge offline'; }
|
|
1187
1305
|
}
|
|
1188
1306
|
function doSync() {
|
|
1189
1307
|
if (!BRIDGE) return;
|
|
1190
1308
|
setSyncState('syncing');
|
|
1309
|
+
// Keep the spinner up for at least 1s even on an instant localhost sync, so the
|
|
1310
|
+
// feedback is actually perceptible instead of flashing by.
|
|
1311
|
+
var started = Date.now();
|
|
1312
|
+
var settle = function (state) {
|
|
1313
|
+
var wait = Math.max(0, 1000 - (Date.now() - started));
|
|
1314
|
+
setTimeout(function () { setSyncState(state); }, wait);
|
|
1315
|
+
};
|
|
1191
1316
|
var payload = {
|
|
1192
1317
|
url: location.href,
|
|
1193
1318
|
text: buildSpecsCopyText(),
|
|
1194
|
-
specs:
|
|
1195
|
-
return { num: i + 1, note: s.note
|
|
1319
|
+
specs: groupSpecs().map(function (g, i) {
|
|
1320
|
+
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 };
|
|
1196
1321
|
}),
|
|
1197
1322
|
};
|
|
1198
1323
|
fetch(BRIDGE + '/specs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
|
|
1199
1324
|
.then(function (r) { if (!r.ok) throw 0; return r.json(); })
|
|
1200
|
-
.then(function () {
|
|
1201
|
-
.catch(function () {
|
|
1325
|
+
.then(function () { settle('synced'); })
|
|
1326
|
+
.catch(function () { settle('offline'); });
|
|
1202
1327
|
}
|
|
1203
1328
|
function scheduleSync() {
|
|
1204
1329
|
if (!BRIDGE) return;
|
|
@@ -1234,9 +1359,10 @@ function getClientScript(options) {
|
|
|
1234
1359
|
badge.textContent = String(i + 1);
|
|
1235
1360
|
Object.assign(badge.style, {
|
|
1236
1361
|
flexShrink: '0', width: '20px', height: '20px', borderRadius: '999px',
|
|
1237
|
-
background:
|
|
1238
|
-
border: '
|
|
1362
|
+
background: BADGE_IDLE, color: '#fff', fontSize: '11px', fontWeight: '700',
|
|
1363
|
+
border: '1px solid #fff', boxSizing: 'border-box',
|
|
1239
1364
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1365
|
+
transition: 'background 0.12s ease',
|
|
1240
1366
|
});
|
|
1241
1367
|
var content = document.createElement('div');
|
|
1242
1368
|
Object.assign(content.style, { flex: '1', minWidth: '0', display: 'flex', flexDirection: 'column', gap: '6px', paddingRight: '30px' });
|
|
@@ -1342,7 +1468,7 @@ function getClientScript(options) {
|
|
|
1342
1468
|
Object.assign(hbar.style, { display: 'flex', alignItems: 'center', gap: '7px', marginTop: '2px', flexWrap: 'wrap' });
|
|
1343
1469
|
var tag = document.createElement('span');
|
|
1344
1470
|
tag.textContent = 'HIDDEN';
|
|
1345
|
-
Object.assign(tag.style, { fontSize: '
|
|
1471
|
+
Object.assign(tag.style, { fontSize: '11px', fontWeight: '700', letterSpacing: '0.05em', color: '#3A2A05', background: '#F59E0B', borderRadius: '4px', padding: '2px 6px', flexShrink: '0' });
|
|
1346
1472
|
var hint = document.createElement('span');
|
|
1347
1473
|
hint.textContent = spec.locate || 'Not on the page right now';
|
|
1348
1474
|
Object.assign(hint.style, { fontSize: '11px', color: '#C9CBD2', overflow: 'hidden', textOverflow: 'ellipsis' });
|
|
@@ -1358,12 +1484,14 @@ function getClientScript(options) {
|
|
|
1358
1484
|
note.addEventListener('click', function (e) { if (spec.note) { e.stopPropagation(); spec._expanded = !spec._expanded; renderPanel(); } });
|
|
1359
1485
|
row.addEventListener('mouseenter', function () {
|
|
1360
1486
|
row.style.background = 'rgba(255,255,255,0.05)';
|
|
1487
|
+
badge.style.background = PURPLE;
|
|
1361
1488
|
editBtn.style.display = delBtn.style.display = 'flex';
|
|
1362
1489
|
actions.style.background = 'linear-gradient(to right, rgba(52,54,60,0) 0, rgba(52,54,60,1) 22px)';
|
|
1363
1490
|
if (visible) { highlightSpec = spec; revealSpec(spec); }
|
|
1364
1491
|
});
|
|
1365
1492
|
row.addEventListener('mouseleave', function () {
|
|
1366
1493
|
row.style.background = 'transparent';
|
|
1494
|
+
badge.style.background = BADGE_IDLE;
|
|
1367
1495
|
editBtn.style.display = delBtn.style.display = 'none';
|
|
1368
1496
|
actions.style.background = 'transparent';
|
|
1369
1497
|
if (highlightSpec === spec) highlightSpec = null;
|
|
@@ -1606,9 +1734,9 @@ function getClientScript(options) {
|
|
|
1606
1734
|
position: 'absolute',
|
|
1607
1735
|
background: RED,
|
|
1608
1736
|
color: '#fff',
|
|
1609
|
-
fontSize: '
|
|
1737
|
+
fontSize: '12px',
|
|
1610
1738
|
fontFamily: MONO,
|
|
1611
|
-
padding: '
|
|
1739
|
+
padding: '2px 5px',
|
|
1612
1740
|
borderRadius: '3px',
|
|
1613
1741
|
pointerEvents: 'none',
|
|
1614
1742
|
whiteSpace: 'nowrap',
|
|
@@ -1808,12 +1936,24 @@ function getClientScript(options) {
|
|
|
1808
1936
|
|
|
1809
1937
|
lastHovered = target;
|
|
1810
1938
|
|
|
1939
|
+
// Comment mode: outline only (so you know what you're commenting on), no
|
|
1940
|
+
// properties/measure overlay on screen. The Spec still captures everything.
|
|
1941
|
+
if (commentMode) {
|
|
1942
|
+
clearMeasureOverlay();
|
|
1943
|
+
clearMeasureTargetHL();
|
|
1944
|
+
setHoverOutline(target);
|
|
1945
|
+
hideTooltip();
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1811
1949
|
if (measureMode) {
|
|
1812
1950
|
clearHoverOutline();
|
|
1813
1951
|
showMeasureTargetHL(target);
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1952
|
+
// Draw the on-screen measurement overlay (the px badges + lines) but keep
|
|
1953
|
+
// the dark readout box hidden \u2014 it occludes the very measurements it reports.
|
|
1954
|
+
// The full readout still copies with Cmd+C. (Properties mode keeps its box.)
|
|
1955
|
+
if (pinEl && pinEl !== target) measureBetween(pinEl, target); else measureToNeighbor(target);
|
|
1956
|
+
hideTooltip();
|
|
1817
1957
|
if (pinEl) updatePinHL();
|
|
1818
1958
|
} else {
|
|
1819
1959
|
clearMeasureOverlay();
|
|
@@ -1895,6 +2035,21 @@ function getClientScript(options) {
|
|
|
1895
2035
|
return;
|
|
1896
2036
|
}
|
|
1897
2037
|
|
|
2038
|
+
// C \u2014 toggle Comment mode (outline only; props/measure hidden but still captured).
|
|
2039
|
+
// Plain c only \u2014 Cmd/Ctrl+C copy was handled above and returned.
|
|
2040
|
+
if ((e.key === 'c' || e.key === 'C') && !e.metaKey && !e.ctrlKey) {
|
|
2041
|
+
e.preventDefault();
|
|
2042
|
+
commentMode = !commentMode;
|
|
2043
|
+
if (commentMode && measureMode) { measureMode = false; clearPin(); }
|
|
2044
|
+
clearMeasureOverlay();
|
|
2045
|
+
clearMeasureTargetHL();
|
|
2046
|
+
clearHoverOutline();
|
|
2047
|
+
hideTooltip();
|
|
2048
|
+
if (lastHovered && commentMode) setHoverOutline(lastHovered);
|
|
2049
|
+
flashMode();
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1898
2053
|
// Esc only HIDES the plugin \u2014 Specs persist and return on reactivate.
|
|
1899
2054
|
if (e.key === 'Escape') {
|
|
1900
2055
|
if (panelOpen) { hidePanel(); return; }
|