vite-plugin-specter 0.3.4 → 0.4.1
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 -19
- package/dist/extension-chrome/content.js +254 -19
- package/dist/extension-chrome/manifest.json +2 -2
- package/dist/extension-firefox/content.js +254 -19
- package/dist/extension-firefox/manifest.json +2 -8
- package/dist/index.cjs +254 -19
- package/dist/index.js +254 -19
- package/extension/content.js +254 -19
- package/extension/icons/specter.png +0 -0
- package/extension/manifest.firefox.json +3 -9
- package/extension/manifest.json +1 -1
- 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
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
(function() {
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
3
|
+
// Guard against running twice on one page. Use a DOM marker (not just a
|
|
4
|
+
// window global) so the browser-extension build (isolated world) and the
|
|
5
|
+
// Vite-plugin build (page main world) can see each other and only one runs —
|
|
6
|
+
// they share the DOM but not window globals, so a per-world flag misses.
|
|
7
|
+
if (window.__specter || document.documentElement.hasAttribute('data-specter')) return;
|
|
8
|
+
window.__specter = true;
|
|
9
|
+
document.documentElement.setAttribute('data-specter', '');
|
|
4
10
|
|
|
5
11
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
6
12
|
var PURPLE = '#AD24D3';
|
|
@@ -10,6 +16,7 @@
|
|
|
10
16
|
var LABEL = '#8B8D94';
|
|
11
17
|
var MONO = "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace";
|
|
12
18
|
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>';
|
|
19
|
+
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
20
|
var ACTIVATE = "ctrl+alt+z";
|
|
14
21
|
|
|
15
22
|
// ─── State ────────────────────────────────────────────────────────────────
|
|
@@ -29,6 +36,9 @@
|
|
|
29
36
|
var lastMouse = { x: 0, y: 0 };
|
|
30
37
|
var selectedEls = [];
|
|
31
38
|
var selectedBadges = [];
|
|
39
|
+
var noteMap = new Map();
|
|
40
|
+
var noteInputEl = null;
|
|
41
|
+
var noteTargetEl = null;
|
|
32
42
|
|
|
33
43
|
// ─── Tooltip ──────────────────────────────────────────────────────────────
|
|
34
44
|
var tooltip = document.createElement('div');
|
|
@@ -188,11 +198,11 @@
|
|
|
188
198
|
pillExpanded = true;
|
|
189
199
|
if (text) { pillText.textContent = text; return; }
|
|
190
200
|
if (selectedEls.length > 0) {
|
|
191
|
-
pillText.textContent = selectedEls.length + ' selected · Cmd+C copy all · Esc clear';
|
|
201
|
+
pillText.textContent = selectedEls.length + ' selected · N annotate · Cmd+C copy all · Esc clear';
|
|
192
202
|
} else if (measureMode) {
|
|
193
203
|
pillText.textContent = 'Measure · hover distances · M pin · Cmd+C copy · Option toggle';
|
|
194
204
|
} else {
|
|
195
|
-
pillText.textContent = 'Properties · hover
|
|
205
|
+
pillText.textContent = 'Properties · hover · P pick · N annotate · Cmd+C copy · Option measure';
|
|
196
206
|
}
|
|
197
207
|
}
|
|
198
208
|
|
|
@@ -312,6 +322,55 @@
|
|
|
312
322
|
return null;
|
|
313
323
|
}
|
|
314
324
|
|
|
325
|
+
// Collapse a rule's declarations to their shortest lossless form: drop
|
|
326
|
+
// 'initial' (= default, no signal) and always-'normal' noise, and fold
|
|
327
|
+
// side/corner longhands back into shorthands (margin/padding/radius/gap/transition).
|
|
328
|
+
function cleanDecls(style) {
|
|
329
|
+
var m = {}, order = [];
|
|
330
|
+
for (var i = 0; i < style.length; i++) {
|
|
331
|
+
var p = style[i];
|
|
332
|
+
var v = style.getPropertyValue(p);
|
|
333
|
+
if (!v) continue;
|
|
334
|
+
if (v === 'initial') continue;
|
|
335
|
+
if (p === 'transition-behavior') continue;
|
|
336
|
+
if (!(p in m)) order.push(p);
|
|
337
|
+
m[p] = v;
|
|
338
|
+
}
|
|
339
|
+
var used = {}, collapses = {};
|
|
340
|
+
function four(name, t, r, b, l) {
|
|
341
|
+
if (m[t] === undefined || m[r] === undefined || m[b] === undefined || m[l] === undefined) return;
|
|
342
|
+
used[t] = used[r] = used[b] = used[l] = 1;
|
|
343
|
+
var a = m[t], c = m[r], d = m[b], e = m[l], sh;
|
|
344
|
+
if (a === c && c === d && d === e) sh = a;
|
|
345
|
+
else if (a === d && c === e) sh = a + ' ' + c;
|
|
346
|
+
else if (c === e) sh = a + ' ' + c + ' ' + d;
|
|
347
|
+
else sh = a + ' ' + c + ' ' + d + ' ' + e;
|
|
348
|
+
collapses[t] = name + ': ' + sh;
|
|
349
|
+
}
|
|
350
|
+
four('margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left');
|
|
351
|
+
four('padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left');
|
|
352
|
+
four('border-radius', 'border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius');
|
|
353
|
+
if (m['row-gap'] !== undefined && m['column-gap'] !== undefined) {
|
|
354
|
+
var g = m['row-gap'] === m['column-gap'] ? m['row-gap'] : m['row-gap'] + ' ' + m['column-gap'];
|
|
355
|
+
collapses['row-gap'] = 'gap: ' + g; used['row-gap'] = used['column-gap'] = 1;
|
|
356
|
+
}
|
|
357
|
+
if (m['transition-property'] !== undefined) {
|
|
358
|
+
var tr = 'transition: ' + m['transition-property'];
|
|
359
|
+
if (m['transition-duration'] !== undefined) tr += ' ' + m['transition-duration'];
|
|
360
|
+
if (m['transition-timing-function'] !== undefined) tr += ' ' + m['transition-timing-function'];
|
|
361
|
+
if (m['transition-delay'] !== undefined && m['transition-delay'] !== '0s') tr += ' ' + m['transition-delay'];
|
|
362
|
+
collapses['transition-property'] = tr;
|
|
363
|
+
used['transition-property'] = used['transition-duration'] = used['transition-timing-function'] = used['transition-delay'] = 1;
|
|
364
|
+
}
|
|
365
|
+
var out = [];
|
|
366
|
+
order.forEach(function (p) {
|
|
367
|
+
if (collapses[p]) { out.push(' ' + collapses[p] + ';'); return; }
|
|
368
|
+
if (used[p]) return;
|
|
369
|
+
out.push(' ' + p + ': ' + m[p] + ';');
|
|
370
|
+
});
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
|
|
315
374
|
function getMatchingRules(el) {
|
|
316
375
|
var results = [];
|
|
317
376
|
try {
|
|
@@ -328,12 +387,7 @@
|
|
|
328
387
|
if (/^[*,]|^:root|^html|^body$|^::before|^::after/.test(sel.trim())) continue;
|
|
329
388
|
try {
|
|
330
389
|
if (el.matches(sel)) {
|
|
331
|
-
var props =
|
|
332
|
-
for (var i = 0; i < rule.style.length; i++) {
|
|
333
|
-
var prop = rule.style[i];
|
|
334
|
-
var val = rule.style.getPropertyValue(prop);
|
|
335
|
-
if (val) props.push(' ' + prop + ': ' + val + ';');
|
|
336
|
-
}
|
|
390
|
+
var props = cleanDecls(rule.style);
|
|
337
391
|
if (props.length) results.push(sel + ' {\n' + props.join('\n') + '\n}');
|
|
338
392
|
}
|
|
339
393
|
} catch (e) {}
|
|
@@ -453,9 +507,13 @@
|
|
|
453
507
|
}
|
|
454
508
|
|
|
455
509
|
function buildMultiSelectCopyText() {
|
|
510
|
+
var n = selectedEls.length;
|
|
456
511
|
return selectedEls.map(function (el, i) {
|
|
457
512
|
var body = buildLLMClipboard(buildInfo(el));
|
|
458
|
-
|
|
513
|
+
var note = noteMap.get(el);
|
|
514
|
+
var header = n > 1 ? '[Specter ' + (i + 1) + '/' + n + ']' : '[Specter]';
|
|
515
|
+
if (note) header += '\n✏️ CHANGE: ' + note;
|
|
516
|
+
return body.replace('[Specter]', header);
|
|
459
517
|
}).join('\n\n' + Array(41).join('─') + '\n\n');
|
|
460
518
|
}
|
|
461
519
|
|
|
@@ -506,16 +564,25 @@
|
|
|
506
564
|
}
|
|
507
565
|
|
|
508
566
|
// ─── Multi-select ─────────────────────────────────────────────────────────
|
|
509
|
-
function makeBadge(el, index) {
|
|
567
|
+
function makeBadge(el, index, note) {
|
|
510
568
|
var rect = el.getBoundingClientRect();
|
|
511
|
-
var
|
|
512
|
-
|
|
513
|
-
Object.assign(badge.style, {
|
|
569
|
+
var wrap = document.createElement('div');
|
|
570
|
+
Object.assign(wrap.style, {
|
|
514
571
|
position: 'fixed',
|
|
515
572
|
left: (rect.left - 4) + 'px',
|
|
516
573
|
top: (rect.top - 4) + 'px',
|
|
574
|
+
zIndex: '2147483644',
|
|
575
|
+
pointerEvents: 'none',
|
|
576
|
+
display: 'flex',
|
|
577
|
+
alignItems: 'flex-start',
|
|
578
|
+
gap: '4px',
|
|
579
|
+
});
|
|
580
|
+
var badge = document.createElement('div');
|
|
581
|
+
badge.textContent = String(index + 1);
|
|
582
|
+
Object.assign(badge.style, {
|
|
517
583
|
width: '20px',
|
|
518
584
|
height: '20px',
|
|
585
|
+
flexShrink: '0',
|
|
519
586
|
background: PURPLE,
|
|
520
587
|
color: '#fff',
|
|
521
588
|
fontSize: '11px',
|
|
@@ -525,19 +592,38 @@
|
|
|
525
592
|
display: 'flex',
|
|
526
593
|
alignItems: 'center',
|
|
527
594
|
justifyContent: 'center',
|
|
528
|
-
zIndex: '2147483644',
|
|
529
|
-
pointerEvents: 'none',
|
|
530
595
|
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
|
531
596
|
});
|
|
532
|
-
|
|
533
|
-
|
|
597
|
+
wrap.appendChild(badge);
|
|
598
|
+
if (note) {
|
|
599
|
+
var chip = document.createElement('div');
|
|
600
|
+
chip.textContent = '✏️ ' + (note.length > 32 ? note.slice(0, 32) + '…' : note);
|
|
601
|
+
Object.assign(chip.style, {
|
|
602
|
+
maxWidth: '240px',
|
|
603
|
+
background: TIP_BG,
|
|
604
|
+
color: '#fff',
|
|
605
|
+
fontSize: '10px',
|
|
606
|
+
lineHeight: '16px',
|
|
607
|
+
fontFamily: MONO,
|
|
608
|
+
padding: '2px 6px',
|
|
609
|
+
borderRadius: '4px',
|
|
610
|
+
whiteSpace: 'nowrap',
|
|
611
|
+
overflow: 'hidden',
|
|
612
|
+
textOverflow: 'ellipsis',
|
|
613
|
+
border: '1px solid ' + PURPLE,
|
|
614
|
+
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
|
615
|
+
});
|
|
616
|
+
wrap.appendChild(chip);
|
|
617
|
+
}
|
|
618
|
+
document.body.appendChild(wrap);
|
|
619
|
+
return wrap;
|
|
534
620
|
}
|
|
535
621
|
|
|
536
622
|
function rebuildBadges() {
|
|
537
623
|
selectedBadges.forEach(function (b) { b.remove(); });
|
|
538
624
|
selectedBadges.length = 0;
|
|
539
625
|
selectedEls.forEach(function (el, i) {
|
|
540
|
-
selectedBadges.push(makeBadge(el, i));
|
|
626
|
+
selectedBadges.push(makeBadge(el, i, noteMap.get(el)));
|
|
541
627
|
});
|
|
542
628
|
}
|
|
543
629
|
|
|
@@ -546,6 +632,7 @@
|
|
|
546
632
|
if (idx >= 0) {
|
|
547
633
|
selectedEls.splice(idx, 1);
|
|
548
634
|
el.style.outline = '';
|
|
635
|
+
noteMap.delete(el);
|
|
549
636
|
} else {
|
|
550
637
|
selectedEls.push(el);
|
|
551
638
|
el.style.outline = '2px solid ' + PURPLE;
|
|
@@ -556,10 +643,12 @@
|
|
|
556
643
|
}
|
|
557
644
|
|
|
558
645
|
function clearSelection() {
|
|
646
|
+
closeNoteInput();
|
|
559
647
|
selectedEls.forEach(function (el) { el.style.outline = ''; el.style.outlineOffset = ''; });
|
|
560
648
|
selectedEls.length = 0;
|
|
561
649
|
selectedBadges.forEach(function (b) { b.remove(); });
|
|
562
650
|
selectedBadges.length = 0;
|
|
651
|
+
noteMap.clear();
|
|
563
652
|
}
|
|
564
653
|
|
|
565
654
|
function updatePillForSelection() {
|
|
@@ -567,6 +656,142 @@
|
|
|
567
656
|
else if (!pillWrap.matches(':hover')) collapsePill();
|
|
568
657
|
}
|
|
569
658
|
|
|
659
|
+
// ─── Annotations (inline change notes) ──────────────────────────────────────
|
|
660
|
+
function openNoteInput(el) {
|
|
661
|
+
closeNoteInput();
|
|
662
|
+
if (selectedEls.indexOf(el) < 0) {
|
|
663
|
+
clearHoverOutline();
|
|
664
|
+
hideTooltip();
|
|
665
|
+
selectedEls.push(el);
|
|
666
|
+
el.style.outline = '2px solid ' + PURPLE;
|
|
667
|
+
el.style.outlineOffset = '-1px';
|
|
668
|
+
}
|
|
669
|
+
noteTargetEl = el;
|
|
670
|
+
var rect = el.getBoundingClientRect();
|
|
671
|
+
|
|
672
|
+
var box = document.createElement('div');
|
|
673
|
+
Object.assign(box.style, {
|
|
674
|
+
position: 'fixed',
|
|
675
|
+
zIndex: '2147483647',
|
|
676
|
+
display: 'inline-flex',
|
|
677
|
+
alignItems: 'flex-start',
|
|
678
|
+
gap: '8px',
|
|
679
|
+
boxSizing: 'border-box',
|
|
680
|
+
background: TIP_BG,
|
|
681
|
+
border: '1px solid ' + PURPLE,
|
|
682
|
+
borderRadius: '8px',
|
|
683
|
+
padding: '11px 12px',
|
|
684
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.35)',
|
|
685
|
+
fontFamily: MONO,
|
|
686
|
+
});
|
|
687
|
+
var pen = document.createElement('span');
|
|
688
|
+
pen.innerHTML = PENCIL;
|
|
689
|
+
Object.assign(pen.style, {
|
|
690
|
+
display: 'flex',
|
|
691
|
+
alignItems: 'center',
|
|
692
|
+
flexShrink: '0',
|
|
693
|
+
marginTop: '2px',
|
|
694
|
+
color: '#E0A3F5',
|
|
695
|
+
});
|
|
696
|
+
var input = document.createElement('textarea');
|
|
697
|
+
input.rows = 1;
|
|
698
|
+
input.placeholder = 'Describe the change… (Enter to save)';
|
|
699
|
+
input.value = noteMap.get(el) || '';
|
|
700
|
+
Object.assign(input.style, {
|
|
701
|
+
flexShrink: '0',
|
|
702
|
+
background: 'transparent',
|
|
703
|
+
border: 'none',
|
|
704
|
+
outline: 'none',
|
|
705
|
+
resize: 'none',
|
|
706
|
+
overflow: 'hidden',
|
|
707
|
+
color: '#fff',
|
|
708
|
+
fontFamily: MONO,
|
|
709
|
+
fontSize: '12px',
|
|
710
|
+
lineHeight: '18px',
|
|
711
|
+
padding: '0',
|
|
712
|
+
margin: '0',
|
|
713
|
+
whiteSpace: 'pre-wrap',
|
|
714
|
+
overflowWrap: 'anywhere',
|
|
715
|
+
});
|
|
716
|
+
// Hidden mirror to measure single-line text width so the field grows as you type.
|
|
717
|
+
var meas = document.createElement('span');
|
|
718
|
+
Object.assign(meas.style, {
|
|
719
|
+
position: 'absolute',
|
|
720
|
+
visibility: 'hidden',
|
|
721
|
+
whiteSpace: 'pre',
|
|
722
|
+
pointerEvents: 'none',
|
|
723
|
+
fontFamily: MONO,
|
|
724
|
+
fontSize: '12px',
|
|
725
|
+
left: '-9999px',
|
|
726
|
+
top: '0',
|
|
727
|
+
});
|
|
728
|
+
box.appendChild(pen);
|
|
729
|
+
box.appendChild(input);
|
|
730
|
+
box.appendChild(meas);
|
|
731
|
+
document.body.appendChild(box);
|
|
732
|
+
noteInputEl = box;
|
|
733
|
+
|
|
734
|
+
var margin = 8;
|
|
735
|
+
function textW(t) { meas.textContent = t; return meas.offsetWidth; }
|
|
736
|
+
|
|
737
|
+
// Anchor to the element; the box is repositioned around this as it grows.
|
|
738
|
+
var anchorL = rect.left, anchorT = rect.top, anchorB = rect.bottom;
|
|
739
|
+
|
|
740
|
+
function sizeAndPosition() {
|
|
741
|
+
var vw = window.innerWidth, vh = window.innerHeight;
|
|
742
|
+
var maxBoxW = Math.min(560, vw - margin * 2);
|
|
743
|
+
var maxTextW = Math.max(120, maxBoxW - 55); // room for icon + gaps + padding
|
|
744
|
+
var placeholderW = textW(input.placeholder);
|
|
745
|
+
var minTextW = Math.min(placeholderW, maxTextW);
|
|
746
|
+
var maxTaH = Math.min(14 * 18, Math.max(18, (vh - margin * 2) - 24)); // cap ~14 lines, never past viewport
|
|
747
|
+
|
|
748
|
+
// Width: grow with content up to the max, then wrapping takes over.
|
|
749
|
+
var single = textW(input.value || input.placeholder) + 3;
|
|
750
|
+
input.style.width = Math.min(Math.max(single, minTextW), maxTextW) + 'px';
|
|
751
|
+
// Height: fit wrapped content, scroll only in the extreme case.
|
|
752
|
+
input.style.height = 'auto';
|
|
753
|
+
var h = input.scrollHeight;
|
|
754
|
+
if (h > maxTaH) { input.style.height = maxTaH + 'px'; input.style.overflowY = 'auto'; }
|
|
755
|
+
else { input.style.height = h + 'px'; input.style.overflowY = 'hidden'; }
|
|
756
|
+
|
|
757
|
+
// Keep the whole box on screen.
|
|
758
|
+
var bw = box.offsetWidth, bh = box.offsetHeight;
|
|
759
|
+
var left = Math.min(Math.max(anchorL, margin), Math.max(margin, vw - bw - margin));
|
|
760
|
+
var top = anchorT - bh - 8; // prefer sitting above the element
|
|
761
|
+
if (top < margin) top = anchorB + 8; // otherwise below it
|
|
762
|
+
if (top + bh > vh - margin) top = Math.max(margin, vh - bh - margin);
|
|
763
|
+
box.style.left = left + 'px';
|
|
764
|
+
box.style.top = top + 'px';
|
|
765
|
+
}
|
|
766
|
+
sizeAndPosition();
|
|
767
|
+
|
|
768
|
+
input.addEventListener('input', sizeAndPosition);
|
|
769
|
+
input.addEventListener('keydown', function (ev) {
|
|
770
|
+
ev.stopPropagation();
|
|
771
|
+
if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); commitNote(input.value); }
|
|
772
|
+
else if (ev.key === 'Escape') { ev.preventDefault(); closeNoteInput(); }
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
rebuildBadges();
|
|
776
|
+
updatePillForSelection();
|
|
777
|
+
setTimeout(function () { input.focus(); }, 0);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function commitNote(val) {
|
|
781
|
+
val = (val || '').trim();
|
|
782
|
+
if (noteTargetEl) {
|
|
783
|
+
if (val) noteMap.set(noteTargetEl, val);
|
|
784
|
+
else noteMap.delete(noteTargetEl);
|
|
785
|
+
}
|
|
786
|
+
closeNoteInput();
|
|
787
|
+
rebuildBadges();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function closeNoteInput() {
|
|
791
|
+
if (noteInputEl) { noteInputEl.remove(); noteInputEl = null; }
|
|
792
|
+
noteTargetEl = null;
|
|
793
|
+
}
|
|
794
|
+
|
|
570
795
|
// ─── Pin (measure) ──────────────────────────────────────────────────────────
|
|
571
796
|
function setPin(el) {
|
|
572
797
|
pinEl = el;
|
|
@@ -863,6 +1088,7 @@
|
|
|
863
1088
|
var target = e.target;
|
|
864
1089
|
if (!target || target === pillWrap || pillWrap.contains(target)) return;
|
|
865
1090
|
if (target === tooltip || tooltip.contains(target)) return;
|
|
1091
|
+
if (noteInputEl && (target === noteInputEl || noteInputEl.contains(target))) return;
|
|
866
1092
|
|
|
867
1093
|
lastHovered = target;
|
|
868
1094
|
|
|
@@ -897,6 +1123,9 @@
|
|
|
897
1123
|
|
|
898
1124
|
if (!fiActive) return;
|
|
899
1125
|
|
|
1126
|
+
// Note editor open: let the field handle its own keys (Enter/Esc) and native copy/paste.
|
|
1127
|
+
if (noteInputEl) return;
|
|
1128
|
+
|
|
900
1129
|
if (e.key === 'Alt' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
901
1130
|
optionHeld = true;
|
|
902
1131
|
return;
|
|
@@ -927,6 +1156,12 @@
|
|
|
927
1156
|
return;
|
|
928
1157
|
}
|
|
929
1158
|
|
|
1159
|
+
if (e.key === 'n' || e.key === 'N') {
|
|
1160
|
+
if (!lastHovered) return;
|
|
1161
|
+
openNoteInput(lastHovered);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
930
1165
|
if (e.key === 'm' || e.key === 'M') {
|
|
931
1166
|
if (!measureMode || !lastHovered) return;
|
|
932
1167
|
if (pinEl) {
|