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/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 annotations';
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
- if (text) { pillText.textContent = text; return; }
305
- if (specs.length > 0) {
306
- pillText.textContent = specs.length + (specs.length === 1 ? ' Spec' : ' Specs') + ' \xB7 P add \xB7 L panel \xB7 Cmd+C copy';
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
- pill.style.maxWidth = '32px';
316
- pillText.style.display = 'none';
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 ? '\u2B21 Measure mode' : '\u25C9 Properties mode';
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 n = specs.length;
638
- return specs.map(function (spec, i) {
639
- var tag = spec.kind === 'measure' ? 'Specter Measure' : 'Specter';
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
- if (spec.note) header += '\\n\u270F\uFE0F CHANGE: ' + spec.note;
642
- return spec.body.replace(/^\\[Specter[^\\]]*\\]/, function () { return header; });
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
- var text = (pinEl && pinEl !== lastHovered) ? measureBetween(pinEl, lastHovered) : measureToNeighbor(lastHovered);
685
- tooltip.innerHTML = linesToHTML(text);
686
- } else {
687
- tooltip.innerHTML = buildHumanDisplay(buildInfo(lastHovered));
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 = '\u270F\uFE0F ' + spec.note;
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
- if (findElementSpec(anchorEl)) return; // P over an already-Spec'd element \u2192 nothing
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
- Object.assign(panelList.style, { flex: '1', overflowY: 'auto', overflowX: 'hidden' });
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
- if (s === 'syncing') { dot.style.background = '#F59E0B'; dotLabel.textContent = 'Syncing\u2026'; }
1185
- else if (s === 'synced') { dot.style.background = GREEN; dotLabel.textContent = specs.length ? (specs.length + (specs.length === 1 ? ' Spec synced' : ' Specs synced')) : 'Synced'; }
1186
- else { dot.style.background = '#6B7280'; dotLabel.textContent = 'Bridge offline'; }
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: specs.map(function (s, i) {
1195
- return { num: i + 1, note: s.note || '', kind: s.kind, body: s.body };
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 () { setSyncState('synced'); })
1201
- .catch(function () { setSyncState('offline'); });
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: PURPLE, color: '#fff', fontSize: '11px', fontWeight: '700',
1238
- border: '2px solid #fff', boxSizing: 'border-box',
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: '9px', fontWeight: '700', letterSpacing: '0.05em', color: '#3A2A05', background: '#F59E0B', borderRadius: '4px', padding: '2px 6px', flexShrink: '0' });
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: '10px',
1737
+ fontSize: '12px',
1610
1738
  fontFamily: MONO,
1611
- padding: '1px 4px',
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
- var text = (pinEl && pinEl !== target) ? measureBetween(pinEl, target) : measureToNeighbor(target);
1815
- tooltip.innerHTML = linesToHTML(text);
1816
- positionTooltip(e.clientX, e.clientY);
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; }