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.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 annotations';
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
- if (text) { pillText.textContent = text; return; }
278
- if (specs.length > 0) {
279
- pillText.textContent = specs.length + (specs.length === 1 ? ' Spec' : ' Specs') + ' \xB7 P add \xB7 L panel \xB7 Cmd+C copy';
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
- pill.style.maxWidth = '32px';
289
- pillText.style.display = 'none';
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 ? '\u2B21 Measure mode' : '\u25C9 Properties mode';
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 n = specs.length;
611
- return specs.map(function (spec, i) {
612
- var tag = spec.kind === 'measure' ? 'Specter Measure' : 'Specter';
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
- if (spec.note) header += '\\n\u270F\uFE0F CHANGE: ' + spec.note;
615
- return spec.body.replace(/^\\[Specter[^\\]]*\\]/, function () { return header; });
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
- var text = (pinEl && pinEl !== lastHovered) ? measureBetween(pinEl, lastHovered) : measureToNeighbor(lastHovered);
658
- tooltip.innerHTML = linesToHTML(text);
659
- } else {
660
- tooltip.innerHTML = buildHumanDisplay(buildInfo(lastHovered));
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 = '\u270F\uFE0F ' + spec.note;
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
- if (findElementSpec(anchorEl)) return; // P over an already-Spec'd element \u2192 nothing
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
- Object.assign(panelList.style, { flex: '1', overflowY: 'auto', overflowX: 'hidden' });
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
- if (s === 'syncing') { dot.style.background = '#F59E0B'; dotLabel.textContent = 'Syncing\u2026'; }
1158
- else if (s === 'synced') { dot.style.background = GREEN; dotLabel.textContent = specs.length ? (specs.length + (specs.length === 1 ? ' Spec synced' : ' Specs synced')) : 'Synced'; }
1159
- else { dot.style.background = '#6B7280'; dotLabel.textContent = 'Bridge offline'; }
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: specs.map(function (s, i) {
1168
- return { num: i + 1, note: s.note || '', kind: s.kind, body: s.body };
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 () { setSyncState('synced'); })
1174
- .catch(function () { setSyncState('offline'); });
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: PURPLE, color: '#fff', fontSize: '11px', fontWeight: '700',
1211
- border: '2px solid #fff', boxSizing: 'border-box',
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: '9px', fontWeight: '700', letterSpacing: '0.05em', color: '#3A2A05', background: '#F59E0B', borderRadius: '4px', padding: '2px 6px', flexShrink: '0' });
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: '10px',
1710
+ fontSize: '12px',
1583
1711
  fontFamily: MONO,
1584
- padding: '1px 4px',
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
- var text = (pinEl && pinEl !== target) ? measureBetween(pinEl, target) : measureToNeighbor(target);
1788
- tooltip.innerHTML = linesToHTML(text);
1789
- positionTooltip(e.clientX, e.clientY);
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; }