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/README.md +22 -4
- package/dist/client.js +254 -29
- package/dist/extension-chrome/background.js +4 -0
- package/dist/extension-chrome/content.js +1211 -0
- package/dist/extension-chrome/icons/icon128.png +0 -0
- package/dist/extension-chrome/icons/icon16.png +0 -0
- package/dist/extension-chrome/icons/icon32.png +0 -0
- package/dist/extension-chrome/icons/icon48.png +0 -0
- package/dist/extension-chrome/manifest.json +38 -0
- package/dist/extension-firefox/content.js +254 -29
- package/dist/extension-firefox/manifest.json +1 -1
- package/dist/index.cjs +254 -29
- package/dist/index.js +254 -29
- package/extension/background.js +1 -5
- package/extension/content.js +254 -29
- package/extension/icons/specter.png +0 -0
- package/extension/manifest.firefox.json +13 -5
- package/extension/manifest.json +8 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,23 @@ In Measure mode, press **M** while hovering to pin that element. Then hover any
|
|
|
60
60
|
|
|
61
61
|
Press **P** while hovering an element to pick it. Numbered badges appear on each picked element. Press **Cmd+C** to copy all selected elements at once. Press **Esc** to clear the selection.
|
|
62
62
|
|
|
63
|
+
### Annotate a change (skip the re-explaining)
|
|
64
|
+
|
|
65
|
+
Press **N** while hovering an element to attach a change note to it. A small field opens on the element — type what you want ("reduce width to 320", "tighten padding to 8px"), press **Enter** to save. The element is added to the selection and its badge shows a chip with your note. Annotate as many issues across the page as you like, then press **Cmd+C** once.
|
|
66
|
+
|
|
67
|
+
Each element's copied block now leads with your instruction:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
[Specter 1/3]
|
|
71
|
+
✏️ CHANGE: reduce width to 320, tighten vertical padding
|
|
72
|
+
<button>.btn-primary Button 200×48
|
|
73
|
+
font: Inter 600 14/20
|
|
74
|
+
padding: 12px 20px
|
|
75
|
+
...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
So when you paste into your AI assistant, it already knows both *what* to change and *which element* — no follow-up typing. Notes are ephemeral (cleared on **Esc** or when you toggle Specter off).
|
|
79
|
+
|
|
63
80
|
## Shortcut reference
|
|
64
81
|
|
|
65
82
|
| Action | Shortcut |
|
|
@@ -70,6 +87,7 @@ Press **P** while hovering an element to pick it. Numbered badges appear on each
|
|
|
70
87
|
| Switch to Measure mode | **Option** (tap) |
|
|
71
88
|
| Pin element for measuring | **M** (in Measure mode) |
|
|
72
89
|
| Pick element (multi-select) | **P** (in Properties mode) |
|
|
90
|
+
| Annotate a change | **N** (in Properties mode) |
|
|
73
91
|
| Clear pin / clear selection | **Esc** |
|
|
74
92
|
| Exit Specter | **Esc** (when nothing selected/pinned) |
|
|
75
93
|
|
|
@@ -77,11 +95,11 @@ Press **P** while hovering an element to pick it. Numbered badges appear on each
|
|
|
77
95
|
|
|
78
96
|
1. Toggle Specter (Ctrl+Option+Z)
|
|
79
97
|
2. Hover the element you want to change
|
|
80
|
-
3.
|
|
81
|
-
4.
|
|
82
|
-
5.
|
|
98
|
+
3. Either press **Cmd+C** to copy its properties, or press **N** to annotate the exact change you want first
|
|
99
|
+
4. Repeat **N** on every element you want to change, then **Cmd+C** once to copy them all
|
|
100
|
+
5. Paste into your AI assistant's chat — the instructions travel with the elements, so there's nothing left to explain
|
|
83
101
|
|
|
84
|
-
No more "which element?", no more "what's the current value?". One paste gives your assistant everything it needs.
|
|
102
|
+
No more "which element?", no more "what's the current value?", and now no more re-typing the change. One paste gives your assistant everything it needs.
|
|
85
103
|
|
|
86
104
|
## Configuration
|
|
87
105
|
|
package/dist/client.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
var LABEL = '#8B8D94';
|
|
11
11
|
var MONO = "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace";
|
|
12
12
|
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>';
|
|
13
|
+
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>';
|
|
13
14
|
var ACTIVATE = "ctrl+alt+z";
|
|
14
15
|
|
|
15
16
|
// ─── State ────────────────────────────────────────────────────────────────
|
|
@@ -29,6 +30,9 @@
|
|
|
29
30
|
var lastMouse = { x: 0, y: 0 };
|
|
30
31
|
var selectedEls = [];
|
|
31
32
|
var selectedBadges = [];
|
|
33
|
+
var noteMap = new Map();
|
|
34
|
+
var noteInputEl = null;
|
|
35
|
+
var noteTargetEl = null;
|
|
32
36
|
|
|
33
37
|
// ─── Tooltip ──────────────────────────────────────────────────────────────
|
|
34
38
|
var tooltip = document.createElement('div');
|
|
@@ -188,11 +192,11 @@
|
|
|
188
192
|
pillExpanded = true;
|
|
189
193
|
if (text) { pillText.textContent = text; return; }
|
|
190
194
|
if (selectedEls.length > 0) {
|
|
191
|
-
pillText.textContent = selectedEls.length + ' selected · Cmd+C copy all · Esc clear';
|
|
195
|
+
pillText.textContent = selectedEls.length + ' selected · N annotate · Cmd+C copy all · Esc clear';
|
|
192
196
|
} else if (measureMode) {
|
|
193
197
|
pillText.textContent = 'Measure · hover distances · M pin · Cmd+C copy · Option toggle';
|
|
194
198
|
} else {
|
|
195
|
-
pillText.textContent = 'Properties · hover
|
|
199
|
+
pillText.textContent = 'Properties · hover · P pick · N annotate · Cmd+C copy · Option measure';
|
|
196
200
|
}
|
|
197
201
|
}
|
|
198
202
|
|
|
@@ -273,17 +277,11 @@
|
|
|
273
277
|
return { hex: toHex(m[1], m[2], m[3]), alpha: m[4] !== undefined ? parseFloat(m[4]) : 1 };
|
|
274
278
|
}
|
|
275
279
|
|
|
276
|
-
function colorLabel(str) {
|
|
277
|
-
var c = parseColor(str);
|
|
278
|
-
if (!c) return null;
|
|
279
|
-
if (c.alpha < 1) return c.hex + ' (' + Math.round(c.alpha * 100) + '% opacity)';
|
|
280
|
-
return c.hex;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
280
|
function colorObj(str) {
|
|
284
281
|
var c = parseColor(str);
|
|
285
282
|
if (!c) return null;
|
|
286
|
-
|
|
283
|
+
var label = c.alpha < 1 ? c.hex + ' (' + Math.round(c.alpha * 100) + '% opacity)' : c.hex;
|
|
284
|
+
return { raw: str, hex: c.hex, alpha: c.alpha, label: label };
|
|
287
285
|
}
|
|
288
286
|
|
|
289
287
|
function edge(t, r, b, l) {
|
|
@@ -318,6 +316,55 @@
|
|
|
318
316
|
return null;
|
|
319
317
|
}
|
|
320
318
|
|
|
319
|
+
// Collapse a rule's declarations to their shortest lossless form: drop
|
|
320
|
+
// 'initial' (= default, no signal) and always-'normal' noise, and fold
|
|
321
|
+
// side/corner longhands back into shorthands (margin/padding/radius/gap/transition).
|
|
322
|
+
function cleanDecls(style) {
|
|
323
|
+
var m = {}, order = [];
|
|
324
|
+
for (var i = 0; i < style.length; i++) {
|
|
325
|
+
var p = style[i];
|
|
326
|
+
var v = style.getPropertyValue(p);
|
|
327
|
+
if (!v) continue;
|
|
328
|
+
if (v === 'initial') continue;
|
|
329
|
+
if (p === 'transition-behavior') continue;
|
|
330
|
+
if (!(p in m)) order.push(p);
|
|
331
|
+
m[p] = v;
|
|
332
|
+
}
|
|
333
|
+
var used = {}, collapses = {};
|
|
334
|
+
function four(name, t, r, b, l) {
|
|
335
|
+
if (m[t] === undefined || m[r] === undefined || m[b] === undefined || m[l] === undefined) return;
|
|
336
|
+
used[t] = used[r] = used[b] = used[l] = 1;
|
|
337
|
+
var a = m[t], c = m[r], d = m[b], e = m[l], sh;
|
|
338
|
+
if (a === c && c === d && d === e) sh = a;
|
|
339
|
+
else if (a === d && c === e) sh = a + ' ' + c;
|
|
340
|
+
else if (c === e) sh = a + ' ' + c + ' ' + d;
|
|
341
|
+
else sh = a + ' ' + c + ' ' + d + ' ' + e;
|
|
342
|
+
collapses[t] = name + ': ' + sh;
|
|
343
|
+
}
|
|
344
|
+
four('margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left');
|
|
345
|
+
four('padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left');
|
|
346
|
+
four('border-radius', 'border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius');
|
|
347
|
+
if (m['row-gap'] !== undefined && m['column-gap'] !== undefined) {
|
|
348
|
+
var g = m['row-gap'] === m['column-gap'] ? m['row-gap'] : m['row-gap'] + ' ' + m['column-gap'];
|
|
349
|
+
collapses['row-gap'] = 'gap: ' + g; used['row-gap'] = used['column-gap'] = 1;
|
|
350
|
+
}
|
|
351
|
+
if (m['transition-property'] !== undefined) {
|
|
352
|
+
var tr = 'transition: ' + m['transition-property'];
|
|
353
|
+
if (m['transition-duration'] !== undefined) tr += ' ' + m['transition-duration'];
|
|
354
|
+
if (m['transition-timing-function'] !== undefined) tr += ' ' + m['transition-timing-function'];
|
|
355
|
+
if (m['transition-delay'] !== undefined && m['transition-delay'] !== '0s') tr += ' ' + m['transition-delay'];
|
|
356
|
+
collapses['transition-property'] = tr;
|
|
357
|
+
used['transition-property'] = used['transition-duration'] = used['transition-timing-function'] = used['transition-delay'] = 1;
|
|
358
|
+
}
|
|
359
|
+
var out = [];
|
|
360
|
+
order.forEach(function (p) {
|
|
361
|
+
if (collapses[p]) { out.push(' ' + collapses[p] + ';'); return; }
|
|
362
|
+
if (used[p]) return;
|
|
363
|
+
out.push(' ' + p + ': ' + m[p] + ';');
|
|
364
|
+
});
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
321
368
|
function getMatchingRules(el) {
|
|
322
369
|
var results = [];
|
|
323
370
|
try {
|
|
@@ -334,12 +381,7 @@
|
|
|
334
381
|
if (/^[*,]|^:root|^html|^body$|^::before|^::after/.test(sel.trim())) continue;
|
|
335
382
|
try {
|
|
336
383
|
if (el.matches(sel)) {
|
|
337
|
-
var props =
|
|
338
|
-
for (var i = 0; i < rule.style.length; i++) {
|
|
339
|
-
var prop = rule.style[i];
|
|
340
|
-
var val = rule.style.getPropertyValue(prop);
|
|
341
|
-
if (val) props.push(' ' + prop + ': ' + val + ';');
|
|
342
|
-
}
|
|
384
|
+
var props = cleanDecls(rule.style);
|
|
343
385
|
if (props.length) results.push(sel + ' {\n' + props.join('\n') + '\n}');
|
|
344
386
|
}
|
|
345
387
|
} catch (e) {}
|
|
@@ -459,9 +501,13 @@
|
|
|
459
501
|
}
|
|
460
502
|
|
|
461
503
|
function buildMultiSelectCopyText() {
|
|
504
|
+
var n = selectedEls.length;
|
|
462
505
|
return selectedEls.map(function (el, i) {
|
|
463
506
|
var body = buildLLMClipboard(buildInfo(el));
|
|
464
|
-
|
|
507
|
+
var note = noteMap.get(el);
|
|
508
|
+
var header = n > 1 ? '[Specter ' + (i + 1) + '/' + n + ']' : '[Specter]';
|
|
509
|
+
if (note) header += '\n✏️ CHANGE: ' + note;
|
|
510
|
+
return body.replace('[Specter]', header);
|
|
465
511
|
}).join('\n\n' + Array(41).join('─') + '\n\n');
|
|
466
512
|
}
|
|
467
513
|
|
|
@@ -485,6 +531,7 @@
|
|
|
485
531
|
function clearHoverOutline() {
|
|
486
532
|
if (outlinedEl) {
|
|
487
533
|
outlinedEl.style.outline = prevOutline;
|
|
534
|
+
outlinedEl.style.outlineOffset = '';
|
|
488
535
|
outlinedEl = null;
|
|
489
536
|
}
|
|
490
537
|
}
|
|
@@ -511,16 +558,25 @@
|
|
|
511
558
|
}
|
|
512
559
|
|
|
513
560
|
// ─── Multi-select ─────────────────────────────────────────────────────────
|
|
514
|
-
function makeBadge(el, index) {
|
|
561
|
+
function makeBadge(el, index, note) {
|
|
515
562
|
var rect = el.getBoundingClientRect();
|
|
516
|
-
var
|
|
517
|
-
|
|
518
|
-
Object.assign(badge.style, {
|
|
563
|
+
var wrap = document.createElement('div');
|
|
564
|
+
Object.assign(wrap.style, {
|
|
519
565
|
position: 'fixed',
|
|
520
566
|
left: (rect.left - 4) + 'px',
|
|
521
567
|
top: (rect.top - 4) + 'px',
|
|
568
|
+
zIndex: '2147483644',
|
|
569
|
+
pointerEvents: 'none',
|
|
570
|
+
display: 'flex',
|
|
571
|
+
alignItems: 'flex-start',
|
|
572
|
+
gap: '4px',
|
|
573
|
+
});
|
|
574
|
+
var badge = document.createElement('div');
|
|
575
|
+
badge.textContent = String(index + 1);
|
|
576
|
+
Object.assign(badge.style, {
|
|
522
577
|
width: '20px',
|
|
523
578
|
height: '20px',
|
|
579
|
+
flexShrink: '0',
|
|
524
580
|
background: PURPLE,
|
|
525
581
|
color: '#fff',
|
|
526
582
|
fontSize: '11px',
|
|
@@ -530,19 +586,38 @@
|
|
|
530
586
|
display: 'flex',
|
|
531
587
|
alignItems: 'center',
|
|
532
588
|
justifyContent: 'center',
|
|
533
|
-
zIndex: '2147483644',
|
|
534
|
-
pointerEvents: 'none',
|
|
535
589
|
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
|
536
590
|
});
|
|
537
|
-
|
|
538
|
-
|
|
591
|
+
wrap.appendChild(badge);
|
|
592
|
+
if (note) {
|
|
593
|
+
var chip = document.createElement('div');
|
|
594
|
+
chip.textContent = '✏️ ' + (note.length > 32 ? note.slice(0, 32) + '…' : note);
|
|
595
|
+
Object.assign(chip.style, {
|
|
596
|
+
maxWidth: '240px',
|
|
597
|
+
background: TIP_BG,
|
|
598
|
+
color: '#fff',
|
|
599
|
+
fontSize: '10px',
|
|
600
|
+
lineHeight: '16px',
|
|
601
|
+
fontFamily: MONO,
|
|
602
|
+
padding: '2px 6px',
|
|
603
|
+
borderRadius: '4px',
|
|
604
|
+
whiteSpace: 'nowrap',
|
|
605
|
+
overflow: 'hidden',
|
|
606
|
+
textOverflow: 'ellipsis',
|
|
607
|
+
border: '1px solid ' + PURPLE,
|
|
608
|
+
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
|
609
|
+
});
|
|
610
|
+
wrap.appendChild(chip);
|
|
611
|
+
}
|
|
612
|
+
document.body.appendChild(wrap);
|
|
613
|
+
return wrap;
|
|
539
614
|
}
|
|
540
615
|
|
|
541
616
|
function rebuildBadges() {
|
|
542
617
|
selectedBadges.forEach(function (b) { b.remove(); });
|
|
543
618
|
selectedBadges.length = 0;
|
|
544
619
|
selectedEls.forEach(function (el, i) {
|
|
545
|
-
selectedBadges.push(makeBadge(el, i));
|
|
620
|
+
selectedBadges.push(makeBadge(el, i, noteMap.get(el)));
|
|
546
621
|
});
|
|
547
622
|
}
|
|
548
623
|
|
|
@@ -551,6 +626,7 @@
|
|
|
551
626
|
if (idx >= 0) {
|
|
552
627
|
selectedEls.splice(idx, 1);
|
|
553
628
|
el.style.outline = '';
|
|
629
|
+
noteMap.delete(el);
|
|
554
630
|
} else {
|
|
555
631
|
selectedEls.push(el);
|
|
556
632
|
el.style.outline = '2px solid ' + PURPLE;
|
|
@@ -561,10 +637,12 @@
|
|
|
561
637
|
}
|
|
562
638
|
|
|
563
639
|
function clearSelection() {
|
|
564
|
-
|
|
640
|
+
closeNoteInput();
|
|
641
|
+
selectedEls.forEach(function (el) { el.style.outline = ''; el.style.outlineOffset = ''; });
|
|
565
642
|
selectedEls.length = 0;
|
|
566
643
|
selectedBadges.forEach(function (b) { b.remove(); });
|
|
567
644
|
selectedBadges.length = 0;
|
|
645
|
+
noteMap.clear();
|
|
568
646
|
}
|
|
569
647
|
|
|
570
648
|
function updatePillForSelection() {
|
|
@@ -572,6 +650,142 @@
|
|
|
572
650
|
else if (!pillWrap.matches(':hover')) collapsePill();
|
|
573
651
|
}
|
|
574
652
|
|
|
653
|
+
// ─── Annotations (inline change notes) ──────────────────────────────────────
|
|
654
|
+
function openNoteInput(el) {
|
|
655
|
+
closeNoteInput();
|
|
656
|
+
if (selectedEls.indexOf(el) < 0) {
|
|
657
|
+
clearHoverOutline();
|
|
658
|
+
hideTooltip();
|
|
659
|
+
selectedEls.push(el);
|
|
660
|
+
el.style.outline = '2px solid ' + PURPLE;
|
|
661
|
+
el.style.outlineOffset = '-1px';
|
|
662
|
+
}
|
|
663
|
+
noteTargetEl = el;
|
|
664
|
+
var rect = el.getBoundingClientRect();
|
|
665
|
+
|
|
666
|
+
var box = document.createElement('div');
|
|
667
|
+
Object.assign(box.style, {
|
|
668
|
+
position: 'fixed',
|
|
669
|
+
zIndex: '2147483647',
|
|
670
|
+
display: 'inline-flex',
|
|
671
|
+
alignItems: 'flex-start',
|
|
672
|
+
gap: '8px',
|
|
673
|
+
boxSizing: 'border-box',
|
|
674
|
+
background: TIP_BG,
|
|
675
|
+
border: '1px solid ' + PURPLE,
|
|
676
|
+
borderRadius: '8px',
|
|
677
|
+
padding: '11px 12px',
|
|
678
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.35)',
|
|
679
|
+
fontFamily: MONO,
|
|
680
|
+
});
|
|
681
|
+
var pen = document.createElement('span');
|
|
682
|
+
pen.innerHTML = PENCIL;
|
|
683
|
+
Object.assign(pen.style, {
|
|
684
|
+
display: 'flex',
|
|
685
|
+
alignItems: 'center',
|
|
686
|
+
flexShrink: '0',
|
|
687
|
+
marginTop: '2px',
|
|
688
|
+
color: '#E0A3F5',
|
|
689
|
+
});
|
|
690
|
+
var input = document.createElement('textarea');
|
|
691
|
+
input.rows = 1;
|
|
692
|
+
input.placeholder = 'Describe the change… (Enter to save)';
|
|
693
|
+
input.value = noteMap.get(el) || '';
|
|
694
|
+
Object.assign(input.style, {
|
|
695
|
+
flexShrink: '0',
|
|
696
|
+
background: 'transparent',
|
|
697
|
+
border: 'none',
|
|
698
|
+
outline: 'none',
|
|
699
|
+
resize: 'none',
|
|
700
|
+
overflow: 'hidden',
|
|
701
|
+
color: '#fff',
|
|
702
|
+
fontFamily: MONO,
|
|
703
|
+
fontSize: '12px',
|
|
704
|
+
lineHeight: '18px',
|
|
705
|
+
padding: '0',
|
|
706
|
+
margin: '0',
|
|
707
|
+
whiteSpace: 'pre-wrap',
|
|
708
|
+
overflowWrap: 'anywhere',
|
|
709
|
+
});
|
|
710
|
+
// Hidden mirror to measure single-line text width so the field grows as you type.
|
|
711
|
+
var meas = document.createElement('span');
|
|
712
|
+
Object.assign(meas.style, {
|
|
713
|
+
position: 'absolute',
|
|
714
|
+
visibility: 'hidden',
|
|
715
|
+
whiteSpace: 'pre',
|
|
716
|
+
pointerEvents: 'none',
|
|
717
|
+
fontFamily: MONO,
|
|
718
|
+
fontSize: '12px',
|
|
719
|
+
left: '-9999px',
|
|
720
|
+
top: '0',
|
|
721
|
+
});
|
|
722
|
+
box.appendChild(pen);
|
|
723
|
+
box.appendChild(input);
|
|
724
|
+
box.appendChild(meas);
|
|
725
|
+
document.body.appendChild(box);
|
|
726
|
+
noteInputEl = box;
|
|
727
|
+
|
|
728
|
+
var margin = 8;
|
|
729
|
+
function textW(t) { meas.textContent = t; return meas.offsetWidth; }
|
|
730
|
+
|
|
731
|
+
// Anchor to the element; the box is repositioned around this as it grows.
|
|
732
|
+
var anchorL = rect.left, anchorT = rect.top, anchorB = rect.bottom;
|
|
733
|
+
|
|
734
|
+
function sizeAndPosition() {
|
|
735
|
+
var vw = window.innerWidth, vh = window.innerHeight;
|
|
736
|
+
var maxBoxW = Math.min(560, vw - margin * 2);
|
|
737
|
+
var maxTextW = Math.max(120, maxBoxW - 55); // room for icon + gaps + padding
|
|
738
|
+
var placeholderW = textW(input.placeholder);
|
|
739
|
+
var minTextW = Math.min(placeholderW, maxTextW);
|
|
740
|
+
var maxTaH = Math.min(14 * 18, Math.max(18, (vh - margin * 2) - 24)); // cap ~14 lines, never past viewport
|
|
741
|
+
|
|
742
|
+
// Width: grow with content up to the max, then wrapping takes over.
|
|
743
|
+
var single = textW(input.value || input.placeholder) + 3;
|
|
744
|
+
input.style.width = Math.min(Math.max(single, minTextW), maxTextW) + 'px';
|
|
745
|
+
// Height: fit wrapped content, scroll only in the extreme case.
|
|
746
|
+
input.style.height = 'auto';
|
|
747
|
+
var h = input.scrollHeight;
|
|
748
|
+
if (h > maxTaH) { input.style.height = maxTaH + 'px'; input.style.overflowY = 'auto'; }
|
|
749
|
+
else { input.style.height = h + 'px'; input.style.overflowY = 'hidden'; }
|
|
750
|
+
|
|
751
|
+
// Keep the whole box on screen.
|
|
752
|
+
var bw = box.offsetWidth, bh = box.offsetHeight;
|
|
753
|
+
var left = Math.min(Math.max(anchorL, margin), Math.max(margin, vw - bw - margin));
|
|
754
|
+
var top = anchorT - bh - 8; // prefer sitting above the element
|
|
755
|
+
if (top < margin) top = anchorB + 8; // otherwise below it
|
|
756
|
+
if (top + bh > vh - margin) top = Math.max(margin, vh - bh - margin);
|
|
757
|
+
box.style.left = left + 'px';
|
|
758
|
+
box.style.top = top + 'px';
|
|
759
|
+
}
|
|
760
|
+
sizeAndPosition();
|
|
761
|
+
|
|
762
|
+
input.addEventListener('input', sizeAndPosition);
|
|
763
|
+
input.addEventListener('keydown', function (ev) {
|
|
764
|
+
ev.stopPropagation();
|
|
765
|
+
if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); commitNote(input.value); }
|
|
766
|
+
else if (ev.key === 'Escape') { ev.preventDefault(); closeNoteInput(); }
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
rebuildBadges();
|
|
770
|
+
updatePillForSelection();
|
|
771
|
+
setTimeout(function () { input.focus(); }, 0);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function commitNote(val) {
|
|
775
|
+
val = (val || '').trim();
|
|
776
|
+
if (noteTargetEl) {
|
|
777
|
+
if (val) noteMap.set(noteTargetEl, val);
|
|
778
|
+
else noteMap.delete(noteTargetEl);
|
|
779
|
+
}
|
|
780
|
+
closeNoteInput();
|
|
781
|
+
rebuildBadges();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function closeNoteInput() {
|
|
785
|
+
if (noteInputEl) { noteInputEl.remove(); noteInputEl = null; }
|
|
786
|
+
noteTargetEl = null;
|
|
787
|
+
}
|
|
788
|
+
|
|
575
789
|
// ─── Pin (measure) ──────────────────────────────────────────────────────────
|
|
576
790
|
function setPin(el) {
|
|
577
791
|
pinEl = el;
|
|
@@ -868,6 +1082,7 @@
|
|
|
868
1082
|
var target = e.target;
|
|
869
1083
|
if (!target || target === pillWrap || pillWrap.contains(target)) return;
|
|
870
1084
|
if (target === tooltip || tooltip.contains(target)) return;
|
|
1085
|
+
if (noteInputEl && (target === noteInputEl || noteInputEl.contains(target))) return;
|
|
871
1086
|
|
|
872
1087
|
lastHovered = target;
|
|
873
1088
|
|
|
@@ -902,6 +1117,9 @@
|
|
|
902
1117
|
|
|
903
1118
|
if (!fiActive) return;
|
|
904
1119
|
|
|
1120
|
+
// Note editor open: let the field handle its own keys (Enter/Esc) and native copy/paste.
|
|
1121
|
+
if (noteInputEl) return;
|
|
1122
|
+
|
|
905
1123
|
if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
906
1124
|
optionHeld = true;
|
|
907
1125
|
return;
|
|
@@ -932,6 +1150,12 @@
|
|
|
932
1150
|
return;
|
|
933
1151
|
}
|
|
934
1152
|
|
|
1153
|
+
if (e.key === 'n' || e.key === 'N') {
|
|
1154
|
+
if (!lastHovered) return;
|
|
1155
|
+
openNoteInput(lastHovered);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
935
1159
|
if (e.key === 'm' || e.key === 'M') {
|
|
936
1160
|
if (!measureMode || !lastHovered) return;
|
|
937
1161
|
if (pinEl) {
|
|
@@ -976,8 +1200,9 @@
|
|
|
976
1200
|
|
|
977
1201
|
window.__specterToggle = function() { if (fiActive) deactivate(); else activate(); };
|
|
978
1202
|
|
|
979
|
-
|
|
980
|
-
|
|
1203
|
+
var _rt = (typeof browser !== 'undefined' && browser.runtime) || (typeof chrome !== 'undefined' && chrome.runtime);
|
|
1204
|
+
if (_rt && _rt.onMessage) {
|
|
1205
|
+
_rt.onMessage.addListener(function(msg) {
|
|
981
1206
|
if (msg && msg.type === 'specter-toggle') window.__specterToggle();
|
|
982
1207
|
});
|
|
983
1208
|
}
|