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.cjs CHANGED
@@ -40,6 +40,7 @@ function getClientScript(options) {
40
40
  var LABEL = '#8B8D94';
41
41
  var MONO = "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace";
42
42
  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>';
43
+ 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>';
43
44
  var ACTIVATE = ${JSON.stringify(activateShortcut)};
44
45
 
45
46
  // \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
@@ -59,6 +60,9 @@ function getClientScript(options) {
59
60
  var lastMouse = { x: 0, y: 0 };
60
61
  var selectedEls = [];
61
62
  var selectedBadges = [];
63
+ var noteMap = new Map();
64
+ var noteInputEl = null;
65
+ var noteTargetEl = null;
62
66
 
63
67
  // \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
64
68
  var tooltip = document.createElement('div');
@@ -218,11 +222,11 @@ function getClientScript(options) {
218
222
  pillExpanded = true;
219
223
  if (text) { pillText.textContent = text; return; }
220
224
  if (selectedEls.length > 0) {
221
- pillText.textContent = selectedEls.length + ' selected \xB7 Cmd+C copy all \xB7 Esc clear';
225
+ pillText.textContent = selectedEls.length + ' selected \xB7 N annotate \xB7 Cmd+C copy all \xB7 Esc clear';
222
226
  } else if (measureMode) {
223
227
  pillText.textContent = 'Measure \xB7 hover distances \xB7 M pin \xB7 Cmd+C copy \xB7 Option toggle';
224
228
  } else {
225
- pillText.textContent = 'Properties \xB7 hover inspect \xB7 P pick \xB7 Cmd+C copy \xB7 Option measure';
229
+ pillText.textContent = 'Properties \xB7 hover \xB7 P pick \xB7 N annotate \xB7 Cmd+C copy \xB7 Option measure';
226
230
  }
227
231
  }
228
232
 
@@ -303,17 +307,11 @@ function getClientScript(options) {
303
307
  return { hex: toHex(m[1], m[2], m[3]), alpha: m[4] !== undefined ? parseFloat(m[4]) : 1 };
304
308
  }
305
309
 
306
- function colorLabel(str) {
307
- var c = parseColor(str);
308
- if (!c) return null;
309
- if (c.alpha < 1) return c.hex + ' (' + Math.round(c.alpha * 100) + '% opacity)';
310
- return c.hex;
311
- }
312
-
313
310
  function colorObj(str) {
314
311
  var c = parseColor(str);
315
312
  if (!c) return null;
316
- return { raw: str, hex: c.hex, alpha: c.alpha, label: colorLabel(str) };
313
+ var label = c.alpha < 1 ? c.hex + ' (' + Math.round(c.alpha * 100) + '% opacity)' : c.hex;
314
+ return { raw: str, hex: c.hex, alpha: c.alpha, label: label };
317
315
  }
318
316
 
319
317
  function edge(t, r, b, l) {
@@ -348,6 +346,55 @@ function getClientScript(options) {
348
346
  return null;
349
347
  }
350
348
 
349
+ // Collapse a rule's declarations to their shortest lossless form: drop
350
+ // 'initial' (= default, no signal) and always-'normal' noise, and fold
351
+ // side/corner longhands back into shorthands (margin/padding/radius/gap/transition).
352
+ function cleanDecls(style) {
353
+ var m = {}, order = [];
354
+ for (var i = 0; i < style.length; i++) {
355
+ var p = style[i];
356
+ var v = style.getPropertyValue(p);
357
+ if (!v) continue;
358
+ if (v === 'initial') continue;
359
+ if (p === 'transition-behavior') continue;
360
+ if (!(p in m)) order.push(p);
361
+ m[p] = v;
362
+ }
363
+ var used = {}, collapses = {};
364
+ function four(name, t, r, b, l) {
365
+ if (m[t] === undefined || m[r] === undefined || m[b] === undefined || m[l] === undefined) return;
366
+ used[t] = used[r] = used[b] = used[l] = 1;
367
+ var a = m[t], c = m[r], d = m[b], e = m[l], sh;
368
+ if (a === c && c === d && d === e) sh = a;
369
+ else if (a === d && c === e) sh = a + ' ' + c;
370
+ else if (c === e) sh = a + ' ' + c + ' ' + d;
371
+ else sh = a + ' ' + c + ' ' + d + ' ' + e;
372
+ collapses[t] = name + ': ' + sh;
373
+ }
374
+ four('margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left');
375
+ four('padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left');
376
+ four('border-radius', 'border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius');
377
+ if (m['row-gap'] !== undefined && m['column-gap'] !== undefined) {
378
+ var g = m['row-gap'] === m['column-gap'] ? m['row-gap'] : m['row-gap'] + ' ' + m['column-gap'];
379
+ collapses['row-gap'] = 'gap: ' + g; used['row-gap'] = used['column-gap'] = 1;
380
+ }
381
+ if (m['transition-property'] !== undefined) {
382
+ var tr = 'transition: ' + m['transition-property'];
383
+ if (m['transition-duration'] !== undefined) tr += ' ' + m['transition-duration'];
384
+ if (m['transition-timing-function'] !== undefined) tr += ' ' + m['transition-timing-function'];
385
+ if (m['transition-delay'] !== undefined && m['transition-delay'] !== '0s') tr += ' ' + m['transition-delay'];
386
+ collapses['transition-property'] = tr;
387
+ used['transition-property'] = used['transition-duration'] = used['transition-timing-function'] = used['transition-delay'] = 1;
388
+ }
389
+ var out = [];
390
+ order.forEach(function (p) {
391
+ if (collapses[p]) { out.push(' ' + collapses[p] + ';'); return; }
392
+ if (used[p]) return;
393
+ out.push(' ' + p + ': ' + m[p] + ';');
394
+ });
395
+ return out;
396
+ }
397
+
351
398
  function getMatchingRules(el) {
352
399
  var results = [];
353
400
  try {
@@ -364,12 +411,7 @@ function getClientScript(options) {
364
411
  if (/^[*,]|^:root|^html|^body$|^::before|^::after/.test(sel.trim())) continue;
365
412
  try {
366
413
  if (el.matches(sel)) {
367
- var props = [];
368
- for (var i = 0; i < rule.style.length; i++) {
369
- var prop = rule.style[i];
370
- var val = rule.style.getPropertyValue(prop);
371
- if (val) props.push(' ' + prop + ': ' + val + ';');
372
- }
414
+ var props = cleanDecls(rule.style);
373
415
  if (props.length) results.push(sel + ' {\\n' + props.join('\\n') + '\\n}');
374
416
  }
375
417
  } catch (e) {}
@@ -489,9 +531,13 @@ function getClientScript(options) {
489
531
  }
490
532
 
491
533
  function buildMultiSelectCopyText() {
534
+ var n = selectedEls.length;
492
535
  return selectedEls.map(function (el, i) {
493
536
  var body = buildLLMClipboard(buildInfo(el));
494
- return body.replace('[Specter]', '[Specter ' + (i + 1) + '/' + selectedEls.length + ']');
537
+ var note = noteMap.get(el);
538
+ var header = n > 1 ? '[Specter ' + (i + 1) + '/' + n + ']' : '[Specter]';
539
+ if (note) header += '\\n\u270F\uFE0F CHANGE: ' + note;
540
+ return body.replace('[Specter]', header);
495
541
  }).join('\\n\\n' + Array(41).join('\u2500') + '\\n\\n');
496
542
  }
497
543
 
@@ -515,6 +561,7 @@ function getClientScript(options) {
515
561
  function clearHoverOutline() {
516
562
  if (outlinedEl) {
517
563
  outlinedEl.style.outline = prevOutline;
564
+ outlinedEl.style.outlineOffset = '';
518
565
  outlinedEl = null;
519
566
  }
520
567
  }
@@ -541,16 +588,25 @@ function getClientScript(options) {
541
588
  }
542
589
 
543
590
  // \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
544
- function makeBadge(el, index) {
591
+ function makeBadge(el, index, note) {
545
592
  var rect = el.getBoundingClientRect();
546
- var badge = document.createElement('div');
547
- badge.textContent = String(index + 1);
548
- Object.assign(badge.style, {
593
+ var wrap = document.createElement('div');
594
+ Object.assign(wrap.style, {
549
595
  position: 'fixed',
550
596
  left: (rect.left - 4) + 'px',
551
597
  top: (rect.top - 4) + 'px',
598
+ zIndex: '2147483644',
599
+ pointerEvents: 'none',
600
+ display: 'flex',
601
+ alignItems: 'flex-start',
602
+ gap: '4px',
603
+ });
604
+ var badge = document.createElement('div');
605
+ badge.textContent = String(index + 1);
606
+ Object.assign(badge.style, {
552
607
  width: '20px',
553
608
  height: '20px',
609
+ flexShrink: '0',
554
610
  background: PURPLE,
555
611
  color: '#fff',
556
612
  fontSize: '11px',
@@ -560,19 +616,38 @@ function getClientScript(options) {
560
616
  display: 'flex',
561
617
  alignItems: 'center',
562
618
  justifyContent: 'center',
563
- zIndex: '2147483644',
564
- pointerEvents: 'none',
565
619
  boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
566
620
  });
567
- document.body.appendChild(badge);
568
- return badge;
621
+ wrap.appendChild(badge);
622
+ if (note) {
623
+ var chip = document.createElement('div');
624
+ chip.textContent = '\u270F\uFE0F ' + (note.length > 32 ? note.slice(0, 32) + '\u2026' : note);
625
+ Object.assign(chip.style, {
626
+ maxWidth: '240px',
627
+ background: TIP_BG,
628
+ color: '#fff',
629
+ fontSize: '10px',
630
+ lineHeight: '16px',
631
+ fontFamily: MONO,
632
+ padding: '2px 6px',
633
+ borderRadius: '4px',
634
+ whiteSpace: 'nowrap',
635
+ overflow: 'hidden',
636
+ textOverflow: 'ellipsis',
637
+ border: '1px solid ' + PURPLE,
638
+ boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
639
+ });
640
+ wrap.appendChild(chip);
641
+ }
642
+ document.body.appendChild(wrap);
643
+ return wrap;
569
644
  }
570
645
 
571
646
  function rebuildBadges() {
572
647
  selectedBadges.forEach(function (b) { b.remove(); });
573
648
  selectedBadges.length = 0;
574
649
  selectedEls.forEach(function (el, i) {
575
- selectedBadges.push(makeBadge(el, i));
650
+ selectedBadges.push(makeBadge(el, i, noteMap.get(el)));
576
651
  });
577
652
  }
578
653
 
@@ -581,6 +656,7 @@ function getClientScript(options) {
581
656
  if (idx >= 0) {
582
657
  selectedEls.splice(idx, 1);
583
658
  el.style.outline = '';
659
+ noteMap.delete(el);
584
660
  } else {
585
661
  selectedEls.push(el);
586
662
  el.style.outline = '2px solid ' + PURPLE;
@@ -591,10 +667,12 @@ function getClientScript(options) {
591
667
  }
592
668
 
593
669
  function clearSelection() {
594
- selectedEls.forEach(function (el) { el.style.outline = ''; });
670
+ closeNoteInput();
671
+ selectedEls.forEach(function (el) { el.style.outline = ''; el.style.outlineOffset = ''; });
595
672
  selectedEls.length = 0;
596
673
  selectedBadges.forEach(function (b) { b.remove(); });
597
674
  selectedBadges.length = 0;
675
+ noteMap.clear();
598
676
  }
599
677
 
600
678
  function updatePillForSelection() {
@@ -602,6 +680,142 @@ function getClientScript(options) {
602
680
  else if (!pillWrap.matches(':hover')) collapsePill();
603
681
  }
604
682
 
683
+ // \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
684
+ function openNoteInput(el) {
685
+ closeNoteInput();
686
+ if (selectedEls.indexOf(el) < 0) {
687
+ clearHoverOutline();
688
+ hideTooltip();
689
+ selectedEls.push(el);
690
+ el.style.outline = '2px solid ' + PURPLE;
691
+ el.style.outlineOffset = '-1px';
692
+ }
693
+ noteTargetEl = el;
694
+ var rect = el.getBoundingClientRect();
695
+
696
+ var box = document.createElement('div');
697
+ Object.assign(box.style, {
698
+ position: 'fixed',
699
+ zIndex: '2147483647',
700
+ display: 'inline-flex',
701
+ alignItems: 'flex-start',
702
+ gap: '8px',
703
+ boxSizing: 'border-box',
704
+ background: TIP_BG,
705
+ border: '1px solid ' + PURPLE,
706
+ borderRadius: '8px',
707
+ padding: '11px 12px',
708
+ boxShadow: '0 4px 16px rgba(0,0,0,0.35)',
709
+ fontFamily: MONO,
710
+ });
711
+ var pen = document.createElement('span');
712
+ pen.innerHTML = PENCIL;
713
+ Object.assign(pen.style, {
714
+ display: 'flex',
715
+ alignItems: 'center',
716
+ flexShrink: '0',
717
+ marginTop: '2px',
718
+ color: '#E0A3F5',
719
+ });
720
+ var input = document.createElement('textarea');
721
+ input.rows = 1;
722
+ input.placeholder = 'Describe the change\u2026 (Enter to save)';
723
+ input.value = noteMap.get(el) || '';
724
+ Object.assign(input.style, {
725
+ flexShrink: '0',
726
+ background: 'transparent',
727
+ border: 'none',
728
+ outline: 'none',
729
+ resize: 'none',
730
+ overflow: 'hidden',
731
+ color: '#fff',
732
+ fontFamily: MONO,
733
+ fontSize: '12px',
734
+ lineHeight: '18px',
735
+ padding: '0',
736
+ margin: '0',
737
+ whiteSpace: 'pre-wrap',
738
+ overflowWrap: 'anywhere',
739
+ });
740
+ // Hidden mirror to measure single-line text width so the field grows as you type.
741
+ var meas = document.createElement('span');
742
+ Object.assign(meas.style, {
743
+ position: 'absolute',
744
+ visibility: 'hidden',
745
+ whiteSpace: 'pre',
746
+ pointerEvents: 'none',
747
+ fontFamily: MONO,
748
+ fontSize: '12px',
749
+ left: '-9999px',
750
+ top: '0',
751
+ });
752
+ box.appendChild(pen);
753
+ box.appendChild(input);
754
+ box.appendChild(meas);
755
+ document.body.appendChild(box);
756
+ noteInputEl = box;
757
+
758
+ var margin = 8;
759
+ function textW(t) { meas.textContent = t; return meas.offsetWidth; }
760
+
761
+ // Anchor to the element; the box is repositioned around this as it grows.
762
+ var anchorL = rect.left, anchorT = rect.top, anchorB = rect.bottom;
763
+
764
+ function sizeAndPosition() {
765
+ var vw = window.innerWidth, vh = window.innerHeight;
766
+ var maxBoxW = Math.min(560, vw - margin * 2);
767
+ var maxTextW = Math.max(120, maxBoxW - 55); // room for icon + gaps + padding
768
+ var placeholderW = textW(input.placeholder);
769
+ var minTextW = Math.min(placeholderW, maxTextW);
770
+ var maxTaH = Math.min(14 * 18, Math.max(18, (vh - margin * 2) - 24)); // cap ~14 lines, never past viewport
771
+
772
+ // Width: grow with content up to the max, then wrapping takes over.
773
+ var single = textW(input.value || input.placeholder) + 3;
774
+ input.style.width = Math.min(Math.max(single, minTextW), maxTextW) + 'px';
775
+ // Height: fit wrapped content, scroll only in the extreme case.
776
+ input.style.height = 'auto';
777
+ var h = input.scrollHeight;
778
+ if (h > maxTaH) { input.style.height = maxTaH + 'px'; input.style.overflowY = 'auto'; }
779
+ else { input.style.height = h + 'px'; input.style.overflowY = 'hidden'; }
780
+
781
+ // Keep the whole box on screen.
782
+ var bw = box.offsetWidth, bh = box.offsetHeight;
783
+ var left = Math.min(Math.max(anchorL, margin), Math.max(margin, vw - bw - margin));
784
+ var top = anchorT - bh - 8; // prefer sitting above the element
785
+ if (top < margin) top = anchorB + 8; // otherwise below it
786
+ if (top + bh > vh - margin) top = Math.max(margin, vh - bh - margin);
787
+ box.style.left = left + 'px';
788
+ box.style.top = top + 'px';
789
+ }
790
+ sizeAndPosition();
791
+
792
+ input.addEventListener('input', sizeAndPosition);
793
+ input.addEventListener('keydown', function (ev) {
794
+ ev.stopPropagation();
795
+ if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); commitNote(input.value); }
796
+ else if (ev.key === 'Escape') { ev.preventDefault(); closeNoteInput(); }
797
+ });
798
+
799
+ rebuildBadges();
800
+ updatePillForSelection();
801
+ setTimeout(function () { input.focus(); }, 0);
802
+ }
803
+
804
+ function commitNote(val) {
805
+ val = (val || '').trim();
806
+ if (noteTargetEl) {
807
+ if (val) noteMap.set(noteTargetEl, val);
808
+ else noteMap.delete(noteTargetEl);
809
+ }
810
+ closeNoteInput();
811
+ rebuildBadges();
812
+ }
813
+
814
+ function closeNoteInput() {
815
+ if (noteInputEl) { noteInputEl.remove(); noteInputEl = null; }
816
+ noteTargetEl = null;
817
+ }
818
+
605
819
  // \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
606
820
  function setPin(el) {
607
821
  pinEl = el;
@@ -898,6 +1112,7 @@ function getClientScript(options) {
898
1112
  var target = e.target;
899
1113
  if (!target || target === pillWrap || pillWrap.contains(target)) return;
900
1114
  if (target === tooltip || tooltip.contains(target)) return;
1115
+ if (noteInputEl && (target === noteInputEl || noteInputEl.contains(target))) return;
901
1116
 
902
1117
  lastHovered = target;
903
1118
 
@@ -932,6 +1147,9 @@ function getClientScript(options) {
932
1147
 
933
1148
  if (!fiActive) return;
934
1149
 
1150
+ // Note editor open: let the field handle its own keys (Enter/Esc) and native copy/paste.
1151
+ if (noteInputEl) return;
1152
+
935
1153
  if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
936
1154
  optionHeld = true;
937
1155
  return;
@@ -962,6 +1180,12 @@ function getClientScript(options) {
962
1180
  return;
963
1181
  }
964
1182
 
1183
+ if (e.key === 'n' || e.key === 'N') {
1184
+ if (!lastHovered) return;
1185
+ openNoteInput(lastHovered);
1186
+ return;
1187
+ }
1188
+
965
1189
  if (e.key === 'm' || e.key === 'M') {
966
1190
  if (!measureMode || !lastHovered) return;
967
1191
  if (pinEl) {
@@ -1006,8 +1230,9 @@ function getClientScript(options) {
1006
1230
 
1007
1231
  window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
1008
1232
 
1009
- if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.onMessage) {
1010
- browser.runtime.onMessage.addListener(function(msg) {
1233
+ var _rt = (typeof browser !== 'undefined' && browser.runtime) || (typeof chrome !== 'undefined' && chrome.runtime);
1234
+ if (_rt && _rt.onMessage) {
1235
+ _rt.onMessage.addListener(function(msg) {
1011
1236
  if (msg && msg.type === 'specter-toggle') window.__specterToggle();
1012
1237
  });
1013
1238
  }