vite-plugin-specter 0.3.4 → 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
 
@@ -342,6 +346,55 @@ function getClientScript(options) {
342
346
  return null;
343
347
  }
344
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
+
345
398
  function getMatchingRules(el) {
346
399
  var results = [];
347
400
  try {
@@ -358,12 +411,7 @@ function getClientScript(options) {
358
411
  if (/^[*,]|^:root|^html|^body$|^::before|^::after/.test(sel.trim())) continue;
359
412
  try {
360
413
  if (el.matches(sel)) {
361
- var props = [];
362
- for (var i = 0; i < rule.style.length; i++) {
363
- var prop = rule.style[i];
364
- var val = rule.style.getPropertyValue(prop);
365
- if (val) props.push(' ' + prop + ': ' + val + ';');
366
- }
414
+ var props = cleanDecls(rule.style);
367
415
  if (props.length) results.push(sel + ' {\\n' + props.join('\\n') + '\\n}');
368
416
  }
369
417
  } catch (e) {}
@@ -483,9 +531,13 @@ function getClientScript(options) {
483
531
  }
484
532
 
485
533
  function buildMultiSelectCopyText() {
534
+ var n = selectedEls.length;
486
535
  return selectedEls.map(function (el, i) {
487
536
  var body = buildLLMClipboard(buildInfo(el));
488
- 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);
489
541
  }).join('\\n\\n' + Array(41).join('\u2500') + '\\n\\n');
490
542
  }
491
543
 
@@ -536,16 +588,25 @@ function getClientScript(options) {
536
588
  }
537
589
 
538
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
539
- function makeBadge(el, index) {
591
+ function makeBadge(el, index, note) {
540
592
  var rect = el.getBoundingClientRect();
541
- var badge = document.createElement('div');
542
- badge.textContent = String(index + 1);
543
- Object.assign(badge.style, {
593
+ var wrap = document.createElement('div');
594
+ Object.assign(wrap.style, {
544
595
  position: 'fixed',
545
596
  left: (rect.left - 4) + 'px',
546
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, {
547
607
  width: '20px',
548
608
  height: '20px',
609
+ flexShrink: '0',
549
610
  background: PURPLE,
550
611
  color: '#fff',
551
612
  fontSize: '11px',
@@ -555,19 +616,38 @@ function getClientScript(options) {
555
616
  display: 'flex',
556
617
  alignItems: 'center',
557
618
  justifyContent: 'center',
558
- zIndex: '2147483644',
559
- pointerEvents: 'none',
560
619
  boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
561
620
  });
562
- document.body.appendChild(badge);
563
- 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;
564
644
  }
565
645
 
566
646
  function rebuildBadges() {
567
647
  selectedBadges.forEach(function (b) { b.remove(); });
568
648
  selectedBadges.length = 0;
569
649
  selectedEls.forEach(function (el, i) {
570
- selectedBadges.push(makeBadge(el, i));
650
+ selectedBadges.push(makeBadge(el, i, noteMap.get(el)));
571
651
  });
572
652
  }
573
653
 
@@ -576,6 +656,7 @@ function getClientScript(options) {
576
656
  if (idx >= 0) {
577
657
  selectedEls.splice(idx, 1);
578
658
  el.style.outline = '';
659
+ noteMap.delete(el);
579
660
  } else {
580
661
  selectedEls.push(el);
581
662
  el.style.outline = '2px solid ' + PURPLE;
@@ -586,10 +667,12 @@ function getClientScript(options) {
586
667
  }
587
668
 
588
669
  function clearSelection() {
670
+ closeNoteInput();
589
671
  selectedEls.forEach(function (el) { el.style.outline = ''; el.style.outlineOffset = ''; });
590
672
  selectedEls.length = 0;
591
673
  selectedBadges.forEach(function (b) { b.remove(); });
592
674
  selectedBadges.length = 0;
675
+ noteMap.clear();
593
676
  }
594
677
 
595
678
  function updatePillForSelection() {
@@ -597,6 +680,142 @@ function getClientScript(options) {
597
680
  else if (!pillWrap.matches(':hover')) collapsePill();
598
681
  }
599
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
+
600
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
601
820
  function setPin(el) {
602
821
  pinEl = el;
@@ -893,6 +1112,7 @@ function getClientScript(options) {
893
1112
  var target = e.target;
894
1113
  if (!target || target === pillWrap || pillWrap.contains(target)) return;
895
1114
  if (target === tooltip || tooltip.contains(target)) return;
1115
+ if (noteInputEl && (target === noteInputEl || noteInputEl.contains(target))) return;
896
1116
 
897
1117
  lastHovered = target;
898
1118
 
@@ -927,6 +1147,9 @@ function getClientScript(options) {
927
1147
 
928
1148
  if (!fiActive) return;
929
1149
 
1150
+ // Note editor open: let the field handle its own keys (Enter/Esc) and native copy/paste.
1151
+ if (noteInputEl) return;
1152
+
930
1153
  if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
931
1154
  optionHeld = true;
932
1155
  return;
@@ -957,6 +1180,12 @@ function getClientScript(options) {
957
1180
  return;
958
1181
  }
959
1182
 
1183
+ if (e.key === 'n' || e.key === 'N') {
1184
+ if (!lastHovered) return;
1185
+ openNoteInput(lastHovered);
1186
+ return;
1187
+ }
1188
+
960
1189
  if (e.key === 'm' || e.key === 'M') {
961
1190
  if (!measureMode || !lastHovered) return;
962
1191
  if (pinEl) {