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.js
CHANGED
|
@@ -19,6 +19,7 @@ function getClientScript(options) {
|
|
|
19
19
|
var TIP_BG = '#292B32';
|
|
20
20
|
var GREEN = '#22C55E';
|
|
21
21
|
var LABEL = '#8B8D94';
|
|
22
|
+
var BADGE_IDLE = '#6B6D75'; // panel row-number badges sit greyscale; the hovered row's badge colors to PURPLE
|
|
22
23
|
var MONO = "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace";
|
|
23
24
|
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>';
|
|
24
25
|
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>';
|
|
@@ -33,6 +34,7 @@ function getClientScript(options) {
|
|
|
33
34
|
// \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
|
|
34
35
|
var fiActive = false;
|
|
35
36
|
var measureMode = false;
|
|
37
|
+
var commentMode = false; // C: hide props/measure overlays for design review \u2014 Specs still capture them
|
|
36
38
|
var pinEl = null;
|
|
37
39
|
var pinHighlight = null;
|
|
38
40
|
var lastHovered = null;
|
|
@@ -198,7 +200,7 @@ function getClientScript(options) {
|
|
|
198
200
|
hoverFx(listBtn, { background: 'rgba(255,255,255,0.32)' }, { background: 'rgba(255,255,255,0.16)' });
|
|
199
201
|
|
|
200
202
|
var clearBtn = document.createElement('span');
|
|
201
|
-
clearBtn.textContent = '\u2715 Delete all
|
|
203
|
+
clearBtn.textContent = '\u2715 Delete all';
|
|
202
204
|
clearBtn.title = 'Delete all annotations';
|
|
203
205
|
Object.assign(clearBtn.style, {
|
|
204
206
|
display: 'none',
|
|
@@ -230,7 +232,18 @@ function getClientScript(options) {
|
|
|
230
232
|
chevron.addEventListener('click', function (e) { e.stopPropagation(); moveSide(); });
|
|
231
233
|
hoverFx(chevron, { opacity: '1', transform: 'scale(1.2)' }, { opacity: '0.8', transform: 'scale(1)' });
|
|
232
234
|
|
|
235
|
+
// Sync status in the control bar (only when a bridge is configured): spinner while
|
|
236
|
+
// syncing, green when synced, red on error \u2014 so you always know what's staged.
|
|
237
|
+
var pillSync = document.createElement('span');
|
|
238
|
+
Object.assign(pillSync.style, {
|
|
239
|
+
display: 'none', width: '8px', height: '8px', borderRadius: '999px',
|
|
240
|
+
background: '#6B7280', flexShrink: '0', boxSizing: 'border-box',
|
|
241
|
+
});
|
|
242
|
+
pillSync.title = 'Sync status \u2014 click to re-sync';
|
|
243
|
+
pillSync.addEventListener('click', function (e) { e.stopPropagation(); if (BRIDGE) doSync(); });
|
|
244
|
+
|
|
233
245
|
pill.appendChild(iconBtn);
|
|
246
|
+
pill.appendChild(pillSync);
|
|
234
247
|
pill.appendChild(pillText);
|
|
235
248
|
pill.appendChild(listBtn);
|
|
236
249
|
pill.appendChild(clearBtn);
|
|
@@ -238,6 +251,30 @@ function getClientScript(options) {
|
|
|
238
251
|
pillWrap.appendChild(pill);
|
|
239
252
|
document.body.appendChild(pillWrap);
|
|
240
253
|
|
|
254
|
+
// Keyframes for the sync spinner (injected once).
|
|
255
|
+
var spinStyle = document.createElement('style');
|
|
256
|
+
spinStyle.textContent = '@keyframes __specterSpin{to{transform:rotate(360deg)}}';
|
|
257
|
+
markUI(spinStyle);
|
|
258
|
+
document.head.appendChild(spinStyle);
|
|
259
|
+
|
|
260
|
+
// Shared dot styling for the control-bar AND side-panel sync indicators, so they
|
|
261
|
+
// always match: spinner while syncing, green when synced, red on error.
|
|
262
|
+
function applyDotState(el, state) {
|
|
263
|
+
el.style.boxSizing = 'border-box';
|
|
264
|
+
if (state === 'syncing') {
|
|
265
|
+
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' });
|
|
266
|
+
} else if (state === 'synced') {
|
|
267
|
+
Object.assign(el.style, { width: '8px', height: '8px', background: GREEN, border: 'none', animation: 'none' });
|
|
268
|
+
} else { // offline / error
|
|
269
|
+
Object.assign(el.style, { width: '8px', height: '8px', background: '#F26D6D', border: 'none', animation: 'none' });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function setPillSync(state) {
|
|
273
|
+
if (!BRIDGE) { pillSync.style.display = 'none'; return; }
|
|
274
|
+
pillSync.style.display = 'inline-block';
|
|
275
|
+
applyDotState(pillSync, state);
|
|
276
|
+
}
|
|
277
|
+
|
|
241
278
|
pillWrap.addEventListener('mouseenter', function () {
|
|
242
279
|
clearMeasureOverlay();
|
|
243
280
|
clearMeasureTargetHL();
|
|
@@ -267,6 +304,12 @@ function getClientScript(options) {
|
|
|
267
304
|
}
|
|
268
305
|
}
|
|
269
306
|
|
|
307
|
+
// The mode is shown at all times (persistent prefix), so you always know whether
|
|
308
|
+
// hovering shows properties, measurements, or nothing (Comment).
|
|
309
|
+
function modeLabel() {
|
|
310
|
+
return commentMode ? 'Comment' : (measureMode ? 'Measure' : 'Properties');
|
|
311
|
+
}
|
|
312
|
+
|
|
270
313
|
function expandPill(text) {
|
|
271
314
|
pill.style.maxWidth = '820px';
|
|
272
315
|
pillText.style.display = 'inline';
|
|
@@ -274,19 +317,17 @@ function getClientScript(options) {
|
|
|
274
317
|
listBtn.style.display = specs.length > 0 ? 'inline-flex' : 'none';
|
|
275
318
|
clearBtn.style.display = specs.length > 0 ? 'inline' : 'none';
|
|
276
319
|
pillExpanded = true;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
} else if (measureMode) {
|
|
281
|
-
pillText.textContent = 'Measure \xB7 hover distances \xB7 P mark \xB7 M pin \xB7 Cmd+C copy \xB7 Option toggle';
|
|
282
|
-
} else {
|
|
283
|
-
pillText.textContent = 'Properties \xB7 P mark / annotate \xB7 Cmd+C copy \xB7 Option measure';
|
|
284
|
-
}
|
|
320
|
+
// Just the mode (+ the action buttons when Specs exist). Shortcuts live in the
|
|
321
|
+
// side panel now, so the pill stays short.
|
|
322
|
+
pillText.textContent = text || modeLabel();
|
|
285
323
|
}
|
|
286
324
|
|
|
325
|
+
// "Collapsed" = the compact resting state: mode label only, no hints/buttons.
|
|
326
|
+
// Still shows the mode so it's visible at all times.
|
|
287
327
|
function collapsePill() {
|
|
288
|
-
|
|
289
|
-
pillText.style.display = '
|
|
328
|
+
pillText.textContent = modeLabel();
|
|
329
|
+
pillText.style.display = 'inline';
|
|
330
|
+
pill.style.maxWidth = '220px';
|
|
290
331
|
chevron.style.display = 'none';
|
|
291
332
|
listBtn.style.display = 'none';
|
|
292
333
|
clearBtn.style.display = 'none';
|
|
@@ -295,7 +336,7 @@ function getClientScript(options) {
|
|
|
295
336
|
|
|
296
337
|
function flashMode() {
|
|
297
338
|
if (pillExpanded && pillWrap.matches(':hover')) return;
|
|
298
|
-
var text = measureMode ? '
|
|
339
|
+
var text = commentMode ? 'Comment mode' : (measureMode ? 'Measure mode' : 'Properties mode');
|
|
299
340
|
expandPill(text);
|
|
300
341
|
clearTimeout(flashTimer);
|
|
301
342
|
flashTimer = setTimeout(function () {
|
|
@@ -316,11 +357,13 @@ function getClientScript(options) {
|
|
|
316
357
|
function activate() {
|
|
317
358
|
fiActive = true;
|
|
318
359
|
measureMode = false;
|
|
360
|
+
commentMode = false;
|
|
319
361
|
pillWrap.style.display = 'block';
|
|
320
362
|
document.body.style.cursor = 'crosshair';
|
|
321
363
|
updatePill();
|
|
322
364
|
startLoop();
|
|
323
365
|
reflowSpecs();
|
|
366
|
+
if (BRIDGE) doSync(); // resolve the control-bar sync dot (green if reachable, red if not)
|
|
324
367
|
}
|
|
325
368
|
|
|
326
369
|
function deactivate() {
|
|
@@ -604,24 +647,37 @@ function getClientScript(options) {
|
|
|
604
647
|
return ['[Specter]', head, 'find: ' + resolveLocator(data.el), props.join(' \xB7 ')].join('\\n');
|
|
605
648
|
}
|
|
606
649
|
|
|
650
|
+
// Group Specs on the SAME element so their identical properties aren't emitted
|
|
651
|
+
// twice \u2014 several comments on one thing share ONE property block, each with its
|
|
652
|
+
// own change note. Measure Specs always stand alone (each is a distinct reading).
|
|
653
|
+
function groupSpecs() {
|
|
654
|
+
var groups = [];
|
|
655
|
+
for (var i = 0; i < specs.length; i++) {
|
|
656
|
+
var s = specs[i], g = null;
|
|
657
|
+
if (s.kind === 'element' && s.el) {
|
|
658
|
+
for (var j = 0; j < groups.length; j++) { if (groups[j].kind === 'element' && groups[j].el === s.el) { g = groups[j]; break; } }
|
|
659
|
+
}
|
|
660
|
+
if (g) g.specs.push(s);
|
|
661
|
+
else groups.push({ kind: s.kind, el: s.el, specs: [s] });
|
|
662
|
+
}
|
|
663
|
+
return groups;
|
|
664
|
+
}
|
|
665
|
+
|
|
607
666
|
// Copy every Spec \u2014 using each Spec's body snapshotted at mark time, so Specs
|
|
608
667
|
// whose element is currently hidden (e.g. inside a closed modal) still copy.
|
|
668
|
+
// Same-element Specs collapse into one block (properties once, notes stacked).
|
|
609
669
|
function buildSpecsCopyText() {
|
|
610
|
-
var
|
|
611
|
-
|
|
612
|
-
|
|
670
|
+
var groups = groupSpecs();
|
|
671
|
+
var n = groups.length;
|
|
672
|
+
return groups.map(function (g, i) {
|
|
673
|
+
var tag = g.kind === 'measure' ? 'Specter Measure' : 'Specter';
|
|
613
674
|
var header = n > 1 ? '[' + tag + ' ' + (i + 1) + '/' + n + ']' : '[' + tag + ']';
|
|
614
|
-
|
|
615
|
-
|
|
675
|
+
var notes = g.specs.filter(function (s) { return s.note; }).map(function (s) { return '\u270F\uFE0F CHANGE: ' + s.note; });
|
|
676
|
+
if (notes.length) header += '\\n' + notes.join('\\n');
|
|
677
|
+
return g.specs[0].body.replace(/^\\[Specter[^\\]]*\\]/, function () { return header; });
|
|
616
678
|
}).join('\\n\\n---\\n\\n');
|
|
617
679
|
}
|
|
618
680
|
|
|
619
|
-
function linesToHTML(str) {
|
|
620
|
-
return str.split('\\n').map(function (l) {
|
|
621
|
-
return '<div style="margin-bottom:4px;color:#fff">' + esc(l) + '</div>';
|
|
622
|
-
}).join('');
|
|
623
|
-
}
|
|
624
|
-
|
|
625
681
|
// \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
|
|
626
682
|
function setHoverOutline(el) {
|
|
627
683
|
if (outlinedEl === el) return;
|
|
@@ -653,12 +709,14 @@ function getClientScript(options) {
|
|
|
653
709
|
}
|
|
654
710
|
|
|
655
711
|
function reRenderTooltip() {
|
|
712
|
+
if (commentMode) { hideTooltip(); return; }
|
|
656
713
|
if (measureMode) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
714
|
+
// Redraw the overlay on scroll/resize; readout box stays hidden (see hover handler).
|
|
715
|
+
if (pinEl && pinEl !== lastHovered) measureBetween(pinEl, lastHovered); else measureToNeighbor(lastHovered);
|
|
716
|
+
hideTooltip();
|
|
717
|
+
return;
|
|
661
718
|
}
|
|
719
|
+
tooltip.innerHTML = buildHumanDisplay(buildInfo(lastHovered));
|
|
662
720
|
positionTooltip(lastMouse.x, lastMouse.y);
|
|
663
721
|
}
|
|
664
722
|
|
|
@@ -814,7 +872,7 @@ function getClientScript(options) {
|
|
|
814
872
|
|
|
815
873
|
function updateBadgeContent(spec) {
|
|
816
874
|
spec.num.textContent = String(specs.indexOf(spec) + 1);
|
|
817
|
-
if (spec.note) spec.noteSpan.textContent =
|
|
875
|
+
if (spec.note) spec.noteSpan.textContent = spec.note;
|
|
818
876
|
else spec.noteSpan.textContent = spec.kind === 'measure' ? '\u2B21 measure' : '';
|
|
819
877
|
}
|
|
820
878
|
|
|
@@ -859,10 +917,6 @@ function getClientScript(options) {
|
|
|
859
917
|
updateBadgeContent(spec);
|
|
860
918
|
}
|
|
861
919
|
|
|
862
|
-
function findElementSpec(el) {
|
|
863
|
-
for (var i = 0; i < specs.length; i++) if (specs[i].kind === 'element' && specs[i].el === el) return specs[i];
|
|
864
|
-
return null;
|
|
865
|
-
}
|
|
866
920
|
|
|
867
921
|
// If the element sits inside a dialog/modal/drawer/menu, produce a hint for how to
|
|
868
922
|
// bring it back when it's later hidden \u2014 a declarative opener (aria-controls /
|
|
@@ -901,7 +955,9 @@ function getClientScript(options) {
|
|
|
901
955
|
if (pinEl && lastHovered && lastHovered !== pinEl) { el = pinEl; body = buildMeasureCopyText(pinEl, lastHovered); }
|
|
902
956
|
else { el = anchorEl; body = buildNeighborCopyText(anchorEl); }
|
|
903
957
|
} else {
|
|
904
|
-
|
|
958
|
+
// Multiple Specs per element are allowed in every mode \u2014 clustering fans the badges
|
|
959
|
+
// out and same-element output collapses to one property block. Mode only changes
|
|
960
|
+
// what's shown on screen, never whether you can add an annotation.
|
|
905
961
|
body = buildLLMClipboard(buildInfo(anchorEl));
|
|
906
962
|
}
|
|
907
963
|
clearHoverOutline();
|
|
@@ -1139,14 +1195,74 @@ function getClientScript(options) {
|
|
|
1139
1195
|
}
|
|
1140
1196
|
|
|
1141
1197
|
var panelList = document.createElement('div');
|
|
1142
|
-
|
|
1198
|
+
// overscroll-behavior:contain stops the panel's scroll from chaining into the page.
|
|
1199
|
+
Object.assign(panelList.style, { flex: '1', overflowY: 'auto', overflowX: 'hidden', overscrollBehavior: 'contain' });
|
|
1200
|
+
|
|
1201
|
+
// Keyboard shortcuts reference \u2014 collapsible, collapsed by default, lives here (not
|
|
1202
|
+
// in the pill) so the pill stays short.
|
|
1203
|
+
var panelKeys = document.createElement('div');
|
|
1204
|
+
Object.assign(panelKeys.style, {
|
|
1205
|
+
flexShrink: '0', fontSize: '11px', lineHeight: '18px', color: LABEL,
|
|
1206
|
+
borderTop: '1px solid rgba(255,255,255,0.08)',
|
|
1207
|
+
});
|
|
1208
|
+
(function () {
|
|
1209
|
+
var rows = [
|
|
1210
|
+
['P', 'mark / comment the hovered element'],
|
|
1211
|
+
['C', 'toggle Comment mode (hide properties)'],
|
|
1212
|
+
['\\u2325 Option', 'toggle Measure mode'],
|
|
1213
|
+
['M', 'pin an element to measure from'],
|
|
1214
|
+
['Cmd/Ctrl+C', 'copy all Specs'],
|
|
1215
|
+
['L', 'toggle this panel'],
|
|
1216
|
+
];
|
|
1217
|
+
var head = document.createElement('div');
|
|
1218
|
+
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' });
|
|
1219
|
+
var caret = document.createElement('span');
|
|
1220
|
+
caret.textContent = '\\u203A'; // \u203A
|
|
1221
|
+
Object.assign(caret.style, { display: 'inline-block', fontSize: '16px', lineHeight: '1', transition: 'transform 0.15s ease', transform: 'rotate(0deg)' });
|
|
1222
|
+
var headText = document.createElement('span');
|
|
1223
|
+
headText.textContent = 'Keyboard shortcuts';
|
|
1224
|
+
head.appendChild(caret);
|
|
1225
|
+
head.appendChild(headText);
|
|
1226
|
+
var body = document.createElement('div');
|
|
1227
|
+
Object.assign(body.style, { display: 'none', padding: '0 16px 12px' }); // collapsed by default
|
|
1228
|
+
rows.forEach(function (r) {
|
|
1229
|
+
var row = document.createElement('div');
|
|
1230
|
+
Object.assign(row.style, { display: 'flex', gap: '8px', marginBottom: '2px' });
|
|
1231
|
+
var k = document.createElement('span');
|
|
1232
|
+
k.textContent = r[0];
|
|
1233
|
+
Object.assign(k.style, { color: '#E0A3F5', fontWeight: '700', minWidth: '78px', flexShrink: '0' });
|
|
1234
|
+
var d = document.createElement('span');
|
|
1235
|
+
d.textContent = r[1];
|
|
1236
|
+
row.appendChild(k); row.appendChild(d);
|
|
1237
|
+
body.appendChild(row);
|
|
1238
|
+
});
|
|
1239
|
+
var open = false;
|
|
1240
|
+
head.addEventListener('click', function (e) {
|
|
1241
|
+
e.stopPropagation();
|
|
1242
|
+
open = !open;
|
|
1243
|
+
body.style.display = open ? 'block' : 'none';
|
|
1244
|
+
caret.style.transform = open ? 'rotate(90deg)' : 'rotate(0deg)';
|
|
1245
|
+
});
|
|
1246
|
+
panelKeys.appendChild(head);
|
|
1247
|
+
panelKeys.appendChild(body);
|
|
1248
|
+
})();
|
|
1143
1249
|
|
|
1144
1250
|
panelWrap.appendChild(panelHead);
|
|
1145
1251
|
panelWrap.appendChild(panelTools);
|
|
1146
1252
|
panelWrap.appendChild(panelHint);
|
|
1147
1253
|
panelWrap.appendChild(panelList);
|
|
1254
|
+
panelWrap.appendChild(panelKeys);
|
|
1148
1255
|
document.body.appendChild(panelWrap);
|
|
1149
1256
|
|
|
1257
|
+
// Panel scroll and page scroll are mutually exclusive: wheel over the scrollable list
|
|
1258
|
+
// scrolls it natively (overscroll-behavior:contain stops it chaining at the bounds);
|
|
1259
|
+
// wheel over any non-scrolling part of the panel never scrolls the page behind it.
|
|
1260
|
+
panelWrap.addEventListener('wheel', function (e) {
|
|
1261
|
+
var canScroll = panelList.scrollHeight > panelList.clientHeight;
|
|
1262
|
+
if (panelList.contains(e.target) && canScroll) return; // let the list scroll natively
|
|
1263
|
+
e.preventDefault();
|
|
1264
|
+
}, { passive: false });
|
|
1265
|
+
|
|
1150
1266
|
// \u2500\u2500 Auto-sync: mirror the browser's current Specs to the local bridge \u2500\u2500
|
|
1151
1267
|
// Debounced so rapid edits/typing collapse into one POST. The bridge replaces
|
|
1152
1268
|
// its snapshot for this URL, so it always reflects what's in the panel \u2014 no
|
|
@@ -1154,24 +1270,33 @@ function getClientScript(options) {
|
|
|
1154
1270
|
var syncTimer = null;
|
|
1155
1271
|
function setSyncState(s) {
|
|
1156
1272
|
if (!BRIDGE) return;
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1273
|
+
setPillSync(s); // control-bar dot
|
|
1274
|
+
applyDotState(dot, s); // side-panel dot \u2014 same spinner/green/red
|
|
1275
|
+
if (s === 'syncing') { dotLabel.textContent = 'Syncing\u2026'; }
|
|
1276
|
+
else if (s === 'synced') { dotLabel.textContent = specs.length ? (specs.length + (specs.length === 1 ? ' Spec synced' : ' Specs synced')) : 'Synced'; }
|
|
1277
|
+
else { dotLabel.textContent = 'Bridge offline'; }
|
|
1160
1278
|
}
|
|
1161
1279
|
function doSync() {
|
|
1162
1280
|
if (!BRIDGE) return;
|
|
1163
1281
|
setSyncState('syncing');
|
|
1282
|
+
// Keep the spinner up for at least 1s even on an instant localhost sync, so the
|
|
1283
|
+
// feedback is actually perceptible instead of flashing by.
|
|
1284
|
+
var started = Date.now();
|
|
1285
|
+
var settle = function (state) {
|
|
1286
|
+
var wait = Math.max(0, 1000 - (Date.now() - started));
|
|
1287
|
+
setTimeout(function () { setSyncState(state); }, wait);
|
|
1288
|
+
};
|
|
1164
1289
|
var payload = {
|
|
1165
1290
|
url: location.href,
|
|
1166
1291
|
text: buildSpecsCopyText(),
|
|
1167
|
-
specs:
|
|
1168
|
-
return { num: i + 1, note: s.note
|
|
1292
|
+
specs: groupSpecs().map(function (g, i) {
|
|
1293
|
+
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 };
|
|
1169
1294
|
}),
|
|
1170
1295
|
};
|
|
1171
1296
|
fetch(BRIDGE + '/specs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
|
|
1172
1297
|
.then(function (r) { if (!r.ok) throw 0; return r.json(); })
|
|
1173
|
-
.then(function () {
|
|
1174
|
-
.catch(function () {
|
|
1298
|
+
.then(function () { settle('synced'); })
|
|
1299
|
+
.catch(function () { settle('offline'); });
|
|
1175
1300
|
}
|
|
1176
1301
|
function scheduleSync() {
|
|
1177
1302
|
if (!BRIDGE) return;
|
|
@@ -1207,9 +1332,10 @@ function getClientScript(options) {
|
|
|
1207
1332
|
badge.textContent = String(i + 1);
|
|
1208
1333
|
Object.assign(badge.style, {
|
|
1209
1334
|
flexShrink: '0', width: '20px', height: '20px', borderRadius: '999px',
|
|
1210
|
-
background:
|
|
1211
|
-
border: '
|
|
1335
|
+
background: BADGE_IDLE, color: '#fff', fontSize: '11px', fontWeight: '700',
|
|
1336
|
+
border: '1px solid #fff', boxSizing: 'border-box',
|
|
1212
1337
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1338
|
+
transition: 'background 0.12s ease',
|
|
1213
1339
|
});
|
|
1214
1340
|
var content = document.createElement('div');
|
|
1215
1341
|
Object.assign(content.style, { flex: '1', minWidth: '0', display: 'flex', flexDirection: 'column', gap: '6px', paddingRight: '30px' });
|
|
@@ -1315,7 +1441,7 @@ function getClientScript(options) {
|
|
|
1315
1441
|
Object.assign(hbar.style, { display: 'flex', alignItems: 'center', gap: '7px', marginTop: '2px', flexWrap: 'wrap' });
|
|
1316
1442
|
var tag = document.createElement('span');
|
|
1317
1443
|
tag.textContent = 'HIDDEN';
|
|
1318
|
-
Object.assign(tag.style, { fontSize: '
|
|
1444
|
+
Object.assign(tag.style, { fontSize: '11px', fontWeight: '700', letterSpacing: '0.05em', color: '#3A2A05', background: '#F59E0B', borderRadius: '4px', padding: '2px 6px', flexShrink: '0' });
|
|
1319
1445
|
var hint = document.createElement('span');
|
|
1320
1446
|
hint.textContent = spec.locate || 'Not on the page right now';
|
|
1321
1447
|
Object.assign(hint.style, { fontSize: '11px', color: '#C9CBD2', overflow: 'hidden', textOverflow: 'ellipsis' });
|
|
@@ -1331,12 +1457,14 @@ function getClientScript(options) {
|
|
|
1331
1457
|
note.addEventListener('click', function (e) { if (spec.note) { e.stopPropagation(); spec._expanded = !spec._expanded; renderPanel(); } });
|
|
1332
1458
|
row.addEventListener('mouseenter', function () {
|
|
1333
1459
|
row.style.background = 'rgba(255,255,255,0.05)';
|
|
1460
|
+
badge.style.background = PURPLE;
|
|
1334
1461
|
editBtn.style.display = delBtn.style.display = 'flex';
|
|
1335
1462
|
actions.style.background = 'linear-gradient(to right, rgba(52,54,60,0) 0, rgba(52,54,60,1) 22px)';
|
|
1336
1463
|
if (visible) { highlightSpec = spec; revealSpec(spec); }
|
|
1337
1464
|
});
|
|
1338
1465
|
row.addEventListener('mouseleave', function () {
|
|
1339
1466
|
row.style.background = 'transparent';
|
|
1467
|
+
badge.style.background = BADGE_IDLE;
|
|
1340
1468
|
editBtn.style.display = delBtn.style.display = 'none';
|
|
1341
1469
|
actions.style.background = 'transparent';
|
|
1342
1470
|
if (highlightSpec === spec) highlightSpec = null;
|
|
@@ -1579,9 +1707,9 @@ function getClientScript(options) {
|
|
|
1579
1707
|
position: 'absolute',
|
|
1580
1708
|
background: RED,
|
|
1581
1709
|
color: '#fff',
|
|
1582
|
-
fontSize: '
|
|
1710
|
+
fontSize: '12px',
|
|
1583
1711
|
fontFamily: MONO,
|
|
1584
|
-
padding: '
|
|
1712
|
+
padding: '2px 5px',
|
|
1585
1713
|
borderRadius: '3px',
|
|
1586
1714
|
pointerEvents: 'none',
|
|
1587
1715
|
whiteSpace: 'nowrap',
|
|
@@ -1781,12 +1909,24 @@ function getClientScript(options) {
|
|
|
1781
1909
|
|
|
1782
1910
|
lastHovered = target;
|
|
1783
1911
|
|
|
1912
|
+
// Comment mode: outline only (so you know what you're commenting on), no
|
|
1913
|
+
// properties/measure overlay on screen. The Spec still captures everything.
|
|
1914
|
+
if (commentMode) {
|
|
1915
|
+
clearMeasureOverlay();
|
|
1916
|
+
clearMeasureTargetHL();
|
|
1917
|
+
setHoverOutline(target);
|
|
1918
|
+
hideTooltip();
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1784
1922
|
if (measureMode) {
|
|
1785
1923
|
clearHoverOutline();
|
|
1786
1924
|
showMeasureTargetHL(target);
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1925
|
+
// Draw the on-screen measurement overlay (the px badges + lines) but keep
|
|
1926
|
+
// the dark readout box hidden \u2014 it occludes the very measurements it reports.
|
|
1927
|
+
// The full readout still copies with Cmd+C. (Properties mode keeps its box.)
|
|
1928
|
+
if (pinEl && pinEl !== target) measureBetween(pinEl, target); else measureToNeighbor(target);
|
|
1929
|
+
hideTooltip();
|
|
1790
1930
|
if (pinEl) updatePinHL();
|
|
1791
1931
|
} else {
|
|
1792
1932
|
clearMeasureOverlay();
|
|
@@ -1868,6 +2008,21 @@ function getClientScript(options) {
|
|
|
1868
2008
|
return;
|
|
1869
2009
|
}
|
|
1870
2010
|
|
|
2011
|
+
// C \u2014 toggle Comment mode (outline only; props/measure hidden but still captured).
|
|
2012
|
+
// Plain c only \u2014 Cmd/Ctrl+C copy was handled above and returned.
|
|
2013
|
+
if ((e.key === 'c' || e.key === 'C') && !e.metaKey && !e.ctrlKey) {
|
|
2014
|
+
e.preventDefault();
|
|
2015
|
+
commentMode = !commentMode;
|
|
2016
|
+
if (commentMode && measureMode) { measureMode = false; clearPin(); }
|
|
2017
|
+
clearMeasureOverlay();
|
|
2018
|
+
clearMeasureTargetHL();
|
|
2019
|
+
clearHoverOutline();
|
|
2020
|
+
hideTooltip();
|
|
2021
|
+
if (lastHovered && commentMode) setHoverOutline(lastHovered);
|
|
2022
|
+
flashMode();
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1871
2026
|
// Esc only HIDES the plugin \u2014 Specs persist and return on reactivate.
|
|
1872
2027
|
if (e.key === 'Escape') {
|
|
1873
2028
|
if (panelOpen) { hidePanel(); return; }
|