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.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
 
@@ -315,6 +319,55 @@ function getClientScript(options) {
315
319
  return null;
316
320
  }
317
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
+
318
371
  function getMatchingRules(el) {
319
372
  var results = [];
320
373
  try {
@@ -331,12 +384,7 @@ function getClientScript(options) {
331
384
  if (/^[*,]|^:root|^html|^body$|^::before|^::after/.test(sel.trim())) continue;
332
385
  try {
333
386
  if (el.matches(sel)) {
334
- var props = [];
335
- for (var i = 0; i < rule.style.length; i++) {
336
- var prop = rule.style[i];
337
- var val = rule.style.getPropertyValue(prop);
338
- if (val) props.push(' ' + prop + ': ' + val + ';');
339
- }
387
+ var props = cleanDecls(rule.style);
340
388
  if (props.length) results.push(sel + ' {\\n' + props.join('\\n') + '\\n}');
341
389
  }
342
390
  } catch (e) {}
@@ -456,9 +504,13 @@ function getClientScript(options) {
456
504
  }
457
505
 
458
506
  function buildMultiSelectCopyText() {
507
+ var n = selectedEls.length;
459
508
  return selectedEls.map(function (el, i) {
460
509
  var body = buildLLMClipboard(buildInfo(el));
461
- 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);
462
514
  }).join('\\n\\n' + Array(41).join('\u2500') + '\\n\\n');
463
515
  }
464
516
 
@@ -509,16 +561,25 @@ function getClientScript(options) {
509
561
  }
510
562
 
511
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
512
- function makeBadge(el, index) {
564
+ function makeBadge(el, index, note) {
513
565
  var rect = el.getBoundingClientRect();
514
- var badge = document.createElement('div');
515
- badge.textContent = String(index + 1);
516
- Object.assign(badge.style, {
566
+ var wrap = document.createElement('div');
567
+ Object.assign(wrap.style, {
517
568
  position: 'fixed',
518
569
  left: (rect.left - 4) + 'px',
519
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, {
520
580
  width: '20px',
521
581
  height: '20px',
582
+ flexShrink: '0',
522
583
  background: PURPLE,
523
584
  color: '#fff',
524
585
  fontSize: '11px',
@@ -528,19 +589,38 @@ function getClientScript(options) {
528
589
  display: 'flex',
529
590
  alignItems: 'center',
530
591
  justifyContent: 'center',
531
- zIndex: '2147483644',
532
- pointerEvents: 'none',
533
592
  boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
534
593
  });
535
- document.body.appendChild(badge);
536
- 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;
537
617
  }
538
618
 
539
619
  function rebuildBadges() {
540
620
  selectedBadges.forEach(function (b) { b.remove(); });
541
621
  selectedBadges.length = 0;
542
622
  selectedEls.forEach(function (el, i) {
543
- selectedBadges.push(makeBadge(el, i));
623
+ selectedBadges.push(makeBadge(el, i, noteMap.get(el)));
544
624
  });
545
625
  }
546
626
 
@@ -549,6 +629,7 @@ function getClientScript(options) {
549
629
  if (idx >= 0) {
550
630
  selectedEls.splice(idx, 1);
551
631
  el.style.outline = '';
632
+ noteMap.delete(el);
552
633
  } else {
553
634
  selectedEls.push(el);
554
635
  el.style.outline = '2px solid ' + PURPLE;
@@ -559,10 +640,12 @@ function getClientScript(options) {
559
640
  }
560
641
 
561
642
  function clearSelection() {
643
+ closeNoteInput();
562
644
  selectedEls.forEach(function (el) { el.style.outline = ''; el.style.outlineOffset = ''; });
563
645
  selectedEls.length = 0;
564
646
  selectedBadges.forEach(function (b) { b.remove(); });
565
647
  selectedBadges.length = 0;
648
+ noteMap.clear();
566
649
  }
567
650
 
568
651
  function updatePillForSelection() {
@@ -570,6 +653,142 @@ function getClientScript(options) {
570
653
  else if (!pillWrap.matches(':hover')) collapsePill();
571
654
  }
572
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
+
573
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
574
793
  function setPin(el) {
575
794
  pinEl = el;
@@ -866,6 +1085,7 @@ function getClientScript(options) {
866
1085
  var target = e.target;
867
1086
  if (!target || target === pillWrap || pillWrap.contains(target)) return;
868
1087
  if (target === tooltip || tooltip.contains(target)) return;
1088
+ if (noteInputEl && (target === noteInputEl || noteInputEl.contains(target))) return;
869
1089
 
870
1090
  lastHovered = target;
871
1091
 
@@ -900,6 +1120,9 @@ function getClientScript(options) {
900
1120
 
901
1121
  if (!fiActive) return;
902
1122
 
1123
+ // Note editor open: let the field handle its own keys (Enter/Esc) and native copy/paste.
1124
+ if (noteInputEl) return;
1125
+
903
1126
  if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
904
1127
  optionHeld = true;
905
1128
  return;
@@ -930,6 +1153,12 @@ function getClientScript(options) {
930
1153
  return;
931
1154
  }
932
1155
 
1156
+ if (e.key === 'n' || e.key === 'N') {
1157
+ if (!lastHovered) return;
1158
+ openNoteInput(lastHovered);
1159
+ return;
1160
+ }
1161
+
933
1162
  if (e.key === 'm' || e.key === 'M') {
934
1163
  if (!measureMode || !lastHovered) return;
935
1164
  if (pinEl) {