vite-plugin-specter 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ function getClientScript(options) {
13
13
  var LABEL = '#8B8D94';
14
14
  var MONO = "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace";
15
15
  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>';
16
+ 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>';
16
17
  var ACTIVATE = ${JSON.stringify(activateShortcut)};
17
18
 
18
19
  // \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
@@ -32,6 +33,9 @@ function getClientScript(options) {
32
33
  var lastMouse = { x: 0, y: 0 };
33
34
  var selectedEls = [];
34
35
  var selectedBadges = [];
36
+ var noteMap = new Map();
37
+ var noteInputEl = null;
38
+ var noteTargetEl = null;
35
39
 
36
40
  // \u2500\u2500\u2500 Tooltip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
37
41
  var tooltip = document.createElement('div');
@@ -191,11 +195,11 @@ function getClientScript(options) {
191
195
  pillExpanded = true;
192
196
  if (text) { pillText.textContent = text; return; }
193
197
  if (selectedEls.length > 0) {
194
- pillText.textContent = selectedEls.length + ' selected \xB7 Cmd+C copy all \xB7 Esc clear';
198
+ pillText.textContent = selectedEls.length + ' selected \xB7 N annotate \xB7 Cmd+C copy all \xB7 Esc clear';
195
199
  } else if (measureMode) {
196
200
  pillText.textContent = 'Measure \xB7 hover distances \xB7 M pin \xB7 Cmd+C copy \xB7 Option toggle';
197
201
  } else {
198
- pillText.textContent = 'Properties \xB7 hover inspect \xB7 P pick \xB7 Cmd+C copy \xB7 Option measure';
202
+ pillText.textContent = 'Properties \xB7 hover \xB7 P pick \xB7 N annotate \xB7 Cmd+C copy \xB7 Option measure';
199
203
  }
200
204
  }
201
205
 
@@ -276,17 +280,11 @@ function getClientScript(options) {
276
280
  return { hex: toHex(m[1], m[2], m[3]), alpha: m[4] !== undefined ? parseFloat(m[4]) : 1 };
277
281
  }
278
282
 
279
- function colorLabel(str) {
280
- var c = parseColor(str);
281
- if (!c) return null;
282
- if (c.alpha < 1) return c.hex + ' (' + Math.round(c.alpha * 100) + '% opacity)';
283
- return c.hex;
284
- }
285
-
286
283
  function colorObj(str) {
287
284
  var c = parseColor(str);
288
285
  if (!c) return null;
289
- return { raw: str, hex: c.hex, alpha: c.alpha, label: colorLabel(str) };
286
+ var label = c.alpha < 1 ? c.hex + ' (' + Math.round(c.alpha * 100) + '% opacity)' : c.hex;
287
+ return { raw: str, hex: c.hex, alpha: c.alpha, label: label };
290
288
  }
291
289
 
292
290
  function edge(t, r, b, l) {
@@ -321,6 +319,55 @@ function getClientScript(options) {
321
319
  return null;
322
320
  }
323
321
 
322
+ // Collapse a rule's declarations to their shortest lossless form: drop
323
+ // 'initial' (= default, no signal) and always-'normal' noise, and fold
324
+ // side/corner longhands back into shorthands (margin/padding/radius/gap/transition).
325
+ function cleanDecls(style) {
326
+ var m = {}, order = [];
327
+ for (var i = 0; i < style.length; i++) {
328
+ var p = style[i];
329
+ var v = style.getPropertyValue(p);
330
+ if (!v) continue;
331
+ if (v === 'initial') continue;
332
+ if (p === 'transition-behavior') continue;
333
+ if (!(p in m)) order.push(p);
334
+ m[p] = v;
335
+ }
336
+ var used = {}, collapses = {};
337
+ function four(name, t, r, b, l) {
338
+ if (m[t] === undefined || m[r] === undefined || m[b] === undefined || m[l] === undefined) return;
339
+ used[t] = used[r] = used[b] = used[l] = 1;
340
+ var a = m[t], c = m[r], d = m[b], e = m[l], sh;
341
+ if (a === c && c === d && d === e) sh = a;
342
+ else if (a === d && c === e) sh = a + ' ' + c;
343
+ else if (c === e) sh = a + ' ' + c + ' ' + d;
344
+ else sh = a + ' ' + c + ' ' + d + ' ' + e;
345
+ collapses[t] = name + ': ' + sh;
346
+ }
347
+ four('margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left');
348
+ four('padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left');
349
+ four('border-radius', 'border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius');
350
+ if (m['row-gap'] !== undefined && m['column-gap'] !== undefined) {
351
+ var g = m['row-gap'] === m['column-gap'] ? m['row-gap'] : m['row-gap'] + ' ' + m['column-gap'];
352
+ collapses['row-gap'] = 'gap: ' + g; used['row-gap'] = used['column-gap'] = 1;
353
+ }
354
+ if (m['transition-property'] !== undefined) {
355
+ var tr = 'transition: ' + m['transition-property'];
356
+ if (m['transition-duration'] !== undefined) tr += ' ' + m['transition-duration'];
357
+ if (m['transition-timing-function'] !== undefined) tr += ' ' + m['transition-timing-function'];
358
+ if (m['transition-delay'] !== undefined && m['transition-delay'] !== '0s') tr += ' ' + m['transition-delay'];
359
+ collapses['transition-property'] = tr;
360
+ used['transition-property'] = used['transition-duration'] = used['transition-timing-function'] = used['transition-delay'] = 1;
361
+ }
362
+ var out = [];
363
+ order.forEach(function (p) {
364
+ if (collapses[p]) { out.push(' ' + collapses[p] + ';'); return; }
365
+ if (used[p]) return;
366
+ out.push(' ' + p + ': ' + m[p] + ';');
367
+ });
368
+ return out;
369
+ }
370
+
324
371
  function getMatchingRules(el) {
325
372
  var results = [];
326
373
  try {
@@ -337,12 +384,7 @@ function getClientScript(options) {
337
384
  if (/^[*,]|^:root|^html|^body$|^::before|^::after/.test(sel.trim())) continue;
338
385
  try {
339
386
  if (el.matches(sel)) {
340
- var props = [];
341
- for (var i = 0; i < rule.style.length; i++) {
342
- var prop = rule.style[i];
343
- var val = rule.style.getPropertyValue(prop);
344
- if (val) props.push(' ' + prop + ': ' + val + ';');
345
- }
387
+ var props = cleanDecls(rule.style);
346
388
  if (props.length) results.push(sel + ' {\\n' + props.join('\\n') + '\\n}');
347
389
  }
348
390
  } catch (e) {}
@@ -462,9 +504,13 @@ function getClientScript(options) {
462
504
  }
463
505
 
464
506
  function buildMultiSelectCopyText() {
507
+ var n = selectedEls.length;
465
508
  return selectedEls.map(function (el, i) {
466
509
  var body = buildLLMClipboard(buildInfo(el));
467
- return body.replace('[Specter]', '[Specter ' + (i + 1) + '/' + selectedEls.length + ']');
510
+ var note = noteMap.get(el);
511
+ var header = n > 1 ? '[Specter ' + (i + 1) + '/' + n + ']' : '[Specter]';
512
+ if (note) header += '\\n\u270F\uFE0F CHANGE: ' + note;
513
+ return body.replace('[Specter]', header);
468
514
  }).join('\\n\\n' + Array(41).join('\u2500') + '\\n\\n');
469
515
  }
470
516
 
@@ -488,6 +534,7 @@ function getClientScript(options) {
488
534
  function clearHoverOutline() {
489
535
  if (outlinedEl) {
490
536
  outlinedEl.style.outline = prevOutline;
537
+ outlinedEl.style.outlineOffset = '';
491
538
  outlinedEl = null;
492
539
  }
493
540
  }
@@ -514,16 +561,25 @@ function getClientScript(options) {
514
561
  }
515
562
 
516
563
  // \u2500\u2500\u2500 Multi-select \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
517
- function makeBadge(el, index) {
564
+ function makeBadge(el, index, note) {
518
565
  var rect = el.getBoundingClientRect();
519
- var badge = document.createElement('div');
520
- badge.textContent = String(index + 1);
521
- Object.assign(badge.style, {
566
+ var wrap = document.createElement('div');
567
+ Object.assign(wrap.style, {
522
568
  position: 'fixed',
523
569
  left: (rect.left - 4) + 'px',
524
570
  top: (rect.top - 4) + 'px',
571
+ zIndex: '2147483644',
572
+ pointerEvents: 'none',
573
+ display: 'flex',
574
+ alignItems: 'flex-start',
575
+ gap: '4px',
576
+ });
577
+ var badge = document.createElement('div');
578
+ badge.textContent = String(index + 1);
579
+ Object.assign(badge.style, {
525
580
  width: '20px',
526
581
  height: '20px',
582
+ flexShrink: '0',
527
583
  background: PURPLE,
528
584
  color: '#fff',
529
585
  fontSize: '11px',
@@ -533,19 +589,38 @@ function getClientScript(options) {
533
589
  display: 'flex',
534
590
  alignItems: 'center',
535
591
  justifyContent: 'center',
536
- zIndex: '2147483644',
537
- pointerEvents: 'none',
538
592
  boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
539
593
  });
540
- document.body.appendChild(badge);
541
- return badge;
594
+ wrap.appendChild(badge);
595
+ if (note) {
596
+ var chip = document.createElement('div');
597
+ chip.textContent = '\u270F\uFE0F ' + (note.length > 32 ? note.slice(0, 32) + '\u2026' : note);
598
+ Object.assign(chip.style, {
599
+ maxWidth: '240px',
600
+ background: TIP_BG,
601
+ color: '#fff',
602
+ fontSize: '10px',
603
+ lineHeight: '16px',
604
+ fontFamily: MONO,
605
+ padding: '2px 6px',
606
+ borderRadius: '4px',
607
+ whiteSpace: 'nowrap',
608
+ overflow: 'hidden',
609
+ textOverflow: 'ellipsis',
610
+ border: '1px solid ' + PURPLE,
611
+ boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
612
+ });
613
+ wrap.appendChild(chip);
614
+ }
615
+ document.body.appendChild(wrap);
616
+ return wrap;
542
617
  }
543
618
 
544
619
  function rebuildBadges() {
545
620
  selectedBadges.forEach(function (b) { b.remove(); });
546
621
  selectedBadges.length = 0;
547
622
  selectedEls.forEach(function (el, i) {
548
- selectedBadges.push(makeBadge(el, i));
623
+ selectedBadges.push(makeBadge(el, i, noteMap.get(el)));
549
624
  });
550
625
  }
551
626
 
@@ -554,6 +629,7 @@ function getClientScript(options) {
554
629
  if (idx >= 0) {
555
630
  selectedEls.splice(idx, 1);
556
631
  el.style.outline = '';
632
+ noteMap.delete(el);
557
633
  } else {
558
634
  selectedEls.push(el);
559
635
  el.style.outline = '2px solid ' + PURPLE;
@@ -564,10 +640,12 @@ function getClientScript(options) {
564
640
  }
565
641
 
566
642
  function clearSelection() {
567
- selectedEls.forEach(function (el) { el.style.outline = ''; });
643
+ closeNoteInput();
644
+ selectedEls.forEach(function (el) { el.style.outline = ''; el.style.outlineOffset = ''; });
568
645
  selectedEls.length = 0;
569
646
  selectedBadges.forEach(function (b) { b.remove(); });
570
647
  selectedBadges.length = 0;
648
+ noteMap.clear();
571
649
  }
572
650
 
573
651
  function updatePillForSelection() {
@@ -575,6 +653,142 @@ function getClientScript(options) {
575
653
  else if (!pillWrap.matches(':hover')) collapsePill();
576
654
  }
577
655
 
656
+ // \u2500\u2500\u2500 Annotations (inline change notes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
657
+ function openNoteInput(el) {
658
+ closeNoteInput();
659
+ if (selectedEls.indexOf(el) < 0) {
660
+ clearHoverOutline();
661
+ hideTooltip();
662
+ selectedEls.push(el);
663
+ el.style.outline = '2px solid ' + PURPLE;
664
+ el.style.outlineOffset = '-1px';
665
+ }
666
+ noteTargetEl = el;
667
+ var rect = el.getBoundingClientRect();
668
+
669
+ var box = document.createElement('div');
670
+ Object.assign(box.style, {
671
+ position: 'fixed',
672
+ zIndex: '2147483647',
673
+ display: 'inline-flex',
674
+ alignItems: 'flex-start',
675
+ gap: '8px',
676
+ boxSizing: 'border-box',
677
+ background: TIP_BG,
678
+ border: '1px solid ' + PURPLE,
679
+ borderRadius: '8px',
680
+ padding: '11px 12px',
681
+ boxShadow: '0 4px 16px rgba(0,0,0,0.35)',
682
+ fontFamily: MONO,
683
+ });
684
+ var pen = document.createElement('span');
685
+ pen.innerHTML = PENCIL;
686
+ Object.assign(pen.style, {
687
+ display: 'flex',
688
+ alignItems: 'center',
689
+ flexShrink: '0',
690
+ marginTop: '2px',
691
+ color: '#E0A3F5',
692
+ });
693
+ var input = document.createElement('textarea');
694
+ input.rows = 1;
695
+ input.placeholder = 'Describe the change\u2026 (Enter to save)';
696
+ input.value = noteMap.get(el) || '';
697
+ Object.assign(input.style, {
698
+ flexShrink: '0',
699
+ background: 'transparent',
700
+ border: 'none',
701
+ outline: 'none',
702
+ resize: 'none',
703
+ overflow: 'hidden',
704
+ color: '#fff',
705
+ fontFamily: MONO,
706
+ fontSize: '12px',
707
+ lineHeight: '18px',
708
+ padding: '0',
709
+ margin: '0',
710
+ whiteSpace: 'pre-wrap',
711
+ overflowWrap: 'anywhere',
712
+ });
713
+ // Hidden mirror to measure single-line text width so the field grows as you type.
714
+ var meas = document.createElement('span');
715
+ Object.assign(meas.style, {
716
+ position: 'absolute',
717
+ visibility: 'hidden',
718
+ whiteSpace: 'pre',
719
+ pointerEvents: 'none',
720
+ fontFamily: MONO,
721
+ fontSize: '12px',
722
+ left: '-9999px',
723
+ top: '0',
724
+ });
725
+ box.appendChild(pen);
726
+ box.appendChild(input);
727
+ box.appendChild(meas);
728
+ document.body.appendChild(box);
729
+ noteInputEl = box;
730
+
731
+ var margin = 8;
732
+ function textW(t) { meas.textContent = t; return meas.offsetWidth; }
733
+
734
+ // Anchor to the element; the box is repositioned around this as it grows.
735
+ var anchorL = rect.left, anchorT = rect.top, anchorB = rect.bottom;
736
+
737
+ function sizeAndPosition() {
738
+ var vw = window.innerWidth, vh = window.innerHeight;
739
+ var maxBoxW = Math.min(560, vw - margin * 2);
740
+ var maxTextW = Math.max(120, maxBoxW - 55); // room for icon + gaps + padding
741
+ var placeholderW = textW(input.placeholder);
742
+ var minTextW = Math.min(placeholderW, maxTextW);
743
+ var maxTaH = Math.min(14 * 18, Math.max(18, (vh - margin * 2) - 24)); // cap ~14 lines, never past viewport
744
+
745
+ // Width: grow with content up to the max, then wrapping takes over.
746
+ var single = textW(input.value || input.placeholder) + 3;
747
+ input.style.width = Math.min(Math.max(single, minTextW), maxTextW) + 'px';
748
+ // Height: fit wrapped content, scroll only in the extreme case.
749
+ input.style.height = 'auto';
750
+ var h = input.scrollHeight;
751
+ if (h > maxTaH) { input.style.height = maxTaH + 'px'; input.style.overflowY = 'auto'; }
752
+ else { input.style.height = h + 'px'; input.style.overflowY = 'hidden'; }
753
+
754
+ // Keep the whole box on screen.
755
+ var bw = box.offsetWidth, bh = box.offsetHeight;
756
+ var left = Math.min(Math.max(anchorL, margin), Math.max(margin, vw - bw - margin));
757
+ var top = anchorT - bh - 8; // prefer sitting above the element
758
+ if (top < margin) top = anchorB + 8; // otherwise below it
759
+ if (top + bh > vh - margin) top = Math.max(margin, vh - bh - margin);
760
+ box.style.left = left + 'px';
761
+ box.style.top = top + 'px';
762
+ }
763
+ sizeAndPosition();
764
+
765
+ input.addEventListener('input', sizeAndPosition);
766
+ input.addEventListener('keydown', function (ev) {
767
+ ev.stopPropagation();
768
+ if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); commitNote(input.value); }
769
+ else if (ev.key === 'Escape') { ev.preventDefault(); closeNoteInput(); }
770
+ });
771
+
772
+ rebuildBadges();
773
+ updatePillForSelection();
774
+ setTimeout(function () { input.focus(); }, 0);
775
+ }
776
+
777
+ function commitNote(val) {
778
+ val = (val || '').trim();
779
+ if (noteTargetEl) {
780
+ if (val) noteMap.set(noteTargetEl, val);
781
+ else noteMap.delete(noteTargetEl);
782
+ }
783
+ closeNoteInput();
784
+ rebuildBadges();
785
+ }
786
+
787
+ function closeNoteInput() {
788
+ if (noteInputEl) { noteInputEl.remove(); noteInputEl = null; }
789
+ noteTargetEl = null;
790
+ }
791
+
578
792
  // \u2500\u2500\u2500 Pin (measure) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
579
793
  function setPin(el) {
580
794
  pinEl = el;
@@ -871,6 +1085,7 @@ function getClientScript(options) {
871
1085
  var target = e.target;
872
1086
  if (!target || target === pillWrap || pillWrap.contains(target)) return;
873
1087
  if (target === tooltip || tooltip.contains(target)) return;
1088
+ if (noteInputEl && (target === noteInputEl || noteInputEl.contains(target))) return;
874
1089
 
875
1090
  lastHovered = target;
876
1091
 
@@ -905,6 +1120,9 @@ function getClientScript(options) {
905
1120
 
906
1121
  if (!fiActive) return;
907
1122
 
1123
+ // Note editor open: let the field handle its own keys (Enter/Esc) and native copy/paste.
1124
+ if (noteInputEl) return;
1125
+
908
1126
  if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
909
1127
  optionHeld = true;
910
1128
  return;
@@ -935,6 +1153,12 @@ function getClientScript(options) {
935
1153
  return;
936
1154
  }
937
1155
 
1156
+ if (e.key === 'n' || e.key === 'N') {
1157
+ if (!lastHovered) return;
1158
+ openNoteInput(lastHovered);
1159
+ return;
1160
+ }
1161
+
938
1162
  if (e.key === 'm' || e.key === 'M') {
939
1163
  if (!measureMode || !lastHovered) return;
940
1164
  if (pinEl) {
@@ -979,8 +1203,9 @@ function getClientScript(options) {
979
1203
 
980
1204
  window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
981
1205
 
982
- if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
983
- browser.runtime.onMessage.addListener(function(msg) {
1206
+ var _rt = (typeof browser !== 'undefined' && browser.runtime) || (typeof chrome !== 'undefined' && chrome.runtime);
1207
+ if (_rt && _rt.onMessage) {
1208
+ _rt.onMessage.addListener(function(msg) {
984
1209
  if (msg && msg.type === 'specter-toggle') window.__specterToggle();
985
1210
  });
986
1211
  }
@@ -1,8 +1,4 @@
1
1
  // Chrome MV3 service worker
2
- // Toolbar click injects Specter on any page (not just localhost)
3
2
  chrome.action.onClicked.addListener((tab) => {
4
- chrome.scripting.executeScript({
5
- target: { tabId: tab.id },
6
- files: ['content.js'],
7
- });
3
+ chrome.tabs.sendMessage(tab.id, { type: 'specter-toggle' });
8
4
  });