browsertrack 0.1.2 → 0.2.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/AGENTS.md +6 -2
- package/dist/{chunk-ILRYKMME.js → chunk-3HOXPTM2.js} +97 -3
- package/dist/chunk-3HOXPTM2.js.map +1 -0
- package/dist/{chunk-G2Y3CXCY.js → chunk-464D4U2U.js} +86 -3
- package/dist/chunk-464D4U2U.js.map +1 -0
- package/dist/{chunk-SPCIROIU.js → chunk-7OCOQGDN.js} +24 -5
- package/dist/chunk-7OCOQGDN.js.map +1 -0
- package/dist/{chunk-SKCMT2DE.js → chunk-INXDWPJW.js} +806 -160
- package/dist/chunk-INXDWPJW.js.map +1 -0
- package/dist/cli/index.js +202 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +809 -161
- package/dist/client/index.d.ts +62 -11
- package/dist/client/index.js +7 -3
- package/dist/client.iife.js +206 -27
- package/dist/{notes-CBvN91Wf.d.ts → commands-fjuqKzkm.d.ts} +125 -91
- package/dist/core/index.d.ts +2 -2
- package/dist/daemon/index.d.ts +3 -3
- package/dist/daemon/index.js +2 -2
- package/dist/{engine-CeT9URuN.d.ts → engine-CmchnMDq.d.ts} +13 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/mcp/index.d.ts +4 -4
- package/dist/mcp/index.js +2 -2
- package/dist/{projects-CY8ungMt.d.ts → projects-DB7S312i.d.ts} +1 -1
- package/dist/{server-Dd8NX2Mk.d.ts → server-DiVmTrIR.d.ts} +1 -1
- package/docs/component-resolver.md +108 -0
- package/docs/index.md +3 -1
- package/docs/mcp-reference.md +15 -2
- package/docs/scenarios-flows.md +86 -0
- package/docs/visual-notes.md +36 -0
- package/package.json +1 -1
- package/packages/client/src/client.ts +18 -1
- package/packages/client/src/config.ts +84 -1
- package/packages/client/src/index.ts +4 -1
- package/packages/client/src/interceptors/interaction.ts +3 -0
- package/packages/client/src/notes/inspector.ts +640 -166
- package/packages/client/src/source/resolver.ts +272 -0
- package/packages/core/src/types/events.ts +3 -0
- package/packages/core/src/types/notes.ts +36 -0
- package/packages/daemon/src/notes/engine.ts +6 -0
- package/packages/daemon/src/server/ws.ts +23 -1
- package/packages/daemon/src/storage/db.ts +106 -3
- package/packages/mcp/src/handlers.ts +59 -0
- package/packages/mcp/src/tools.ts +28 -0
- package/test/client/component-resolver.test.ts +141 -0
- package/test/client/interceptors.test.ts +120 -0
- package/test/daemon/scenario-storage.test.ts +158 -0
- package/dist/chunk-G2Y3CXCY.js.map +0 -1
- package/dist/chunk-ILRYKMME.js.map +0 -1
- package/dist/chunk-SKCMT2DE.js.map +0 -1
- package/dist/chunk-SPCIROIU.js.map +0 -1
|
@@ -2,6 +2,8 @@ import type { ElementContext, NoteTarget, RegionContext, VisualNote } from '../.
|
|
|
2
2
|
import { getSemanticSelector, truncate } from '../../../core/src/index.js';
|
|
3
3
|
import type { ScreenshotDriver } from '../screenshot/driver.js';
|
|
4
4
|
import type { WebSocketTransport } from '../transport/websocket.js';
|
|
5
|
+
import { shouldHideUIFromUrl } from '../config.js';
|
|
6
|
+
import { resolveComponentSource } from '../source/resolver.js';
|
|
5
7
|
|
|
6
8
|
export type NoteInspectMode = 'element' | 'region' | 'page' | 'idle';
|
|
7
9
|
|
|
@@ -9,18 +11,29 @@ export interface InspectorOptions {
|
|
|
9
11
|
shortcut?: string; // e.g. "Alt+Click"
|
|
10
12
|
maskSelectors?: string[];
|
|
11
13
|
showToolbar?: boolean;
|
|
14
|
+
showBadges?: boolean;
|
|
15
|
+
hidden?: boolean;
|
|
16
|
+
hideQueryParam?: string | string[];
|
|
12
17
|
onNoteCreated?: (note: Partial<VisualNote>) => void;
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
export interface ActiveScenarioState {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
stepNumber: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
/**
|
|
16
|
-
* Isolated Visual Note Inspector rendering in Shadow DOM.
|
|
27
|
+
* Isolated Visual Note & Multi-Step Scenario Inspector rendering in Shadow DOM.
|
|
17
28
|
* Supports:
|
|
18
29
|
* 1. Element selection (hover highlight & click)
|
|
19
30
|
* 2. Region / Area selection (drag-and-drop rectangle on screen with cancel banner)
|
|
20
31
|
* 3. Whole page note
|
|
21
|
-
* 4.
|
|
22
|
-
* 5.
|
|
23
|
-
* 6.
|
|
32
|
+
* 4. Multi-Step Flow / Scenario Recording (sequential 'Save & Next Step' continuous capture)
|
|
33
|
+
* 5. Real-time Saved Note & Step Markers (pins with step sequencing numbers)
|
|
34
|
+
* 6. Interactive Note & Step Detail Card (with previous/next step walk-through navigation)
|
|
35
|
+
* 7. Floating quick dock / toolbar in bottom-right corner with Notes count and Flow controls
|
|
36
|
+
* 8. Automatic invisibility via query parameter (?bt=0, ?bt=false, ?bt=hidden, ?no_bt, custom query params)
|
|
24
37
|
*/
|
|
25
38
|
export class NoteInspector {
|
|
26
39
|
private transport: WebSocketTransport;
|
|
@@ -37,8 +50,10 @@ export class NoteInspector {
|
|
|
37
50
|
private toolbarElement: HTMLDivElement | null = null;
|
|
38
51
|
private modalOverlay: HTMLDivElement | null = null;
|
|
39
52
|
private cardOverlay: HTMLDivElement | null = null;
|
|
53
|
+
private toastContainer: HTMLDivElement | null = null;
|
|
40
54
|
|
|
41
55
|
private activeMode: NoteInspectMode = 'idle';
|
|
56
|
+
private activeScenario: ActiveScenarioState | null = null;
|
|
42
57
|
private hoveredElement: HTMLElement | null = null;
|
|
43
58
|
private selectedElement: HTMLElement | null = null;
|
|
44
59
|
private selectedRegion: RegionContext | null = null;
|
|
@@ -49,15 +64,20 @@ export class NoteInspector {
|
|
|
49
64
|
|
|
50
65
|
private savedNotes: VisualNote[] = [];
|
|
51
66
|
private showMarkers = true;
|
|
67
|
+
private isHidden = false;
|
|
52
68
|
private cleanups: (() => void)[] = [];
|
|
53
69
|
|
|
54
70
|
constructor(transport: WebSocketTransport, screenshotDriver: ScreenshotDriver, options: InspectorOptions = {}) {
|
|
55
71
|
this.transport = transport;
|
|
56
72
|
this.screenshotDriver = screenshotDriver;
|
|
73
|
+
const hiddenByQuery = shouldHideUIFromUrl(options.hideQueryParam);
|
|
74
|
+
this.isHidden = options.hidden === true || hiddenByQuery;
|
|
75
|
+
this.showMarkers = options.showBadges !== false && !this.isHidden;
|
|
57
76
|
this.options = {
|
|
58
77
|
shortcut: 'Alt+Click',
|
|
59
78
|
maskSelectors: ['input[type="password"]', '[data-sensitive]'],
|
|
60
79
|
showToolbar: true,
|
|
80
|
+
showBadges: true,
|
|
61
81
|
...options,
|
|
62
82
|
};
|
|
63
83
|
}
|
|
@@ -90,6 +110,46 @@ export class NoteInspector {
|
|
|
90
110
|
this.renderMarkers();
|
|
91
111
|
}
|
|
92
112
|
|
|
113
|
+
public showToast(message: string, icon = '✨', durationMs = 2500): void {
|
|
114
|
+
if (typeof document === 'undefined') return;
|
|
115
|
+
const root = this.ensureContainer();
|
|
116
|
+
if (!root || !this.toastContainer) return;
|
|
117
|
+
|
|
118
|
+
const toast = document.createElement('div');
|
|
119
|
+
toast.className = 'bt-toast';
|
|
120
|
+
toast.innerHTML = `<span>${icon}</span> <span>${message}</span>`;
|
|
121
|
+
this.toastContainer.appendChild(toast);
|
|
122
|
+
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
toast.classList.add('bt-toast-fadeout');
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
if (toast.parentElement) {
|
|
127
|
+
toast.parentElement.removeChild(toast);
|
|
128
|
+
}
|
|
129
|
+
}, 250);
|
|
130
|
+
}, durationMs);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public startScenario(title?: string): void {
|
|
134
|
+
const defaultTitle = title || `Scenario ${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
|
135
|
+
this.activeScenario = {
|
|
136
|
+
id: `scen_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
137
|
+
title: defaultTitle,
|
|
138
|
+
stepNumber: 1,
|
|
139
|
+
};
|
|
140
|
+
this.updateToolbarState();
|
|
141
|
+
this.setMode('element');
|
|
142
|
+
this.showToast(`Started flow: "${defaultTitle}"`, '🎬');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public finishScenario(): void {
|
|
146
|
+
const title = this.activeScenario?.title;
|
|
147
|
+
this.activeScenario = null;
|
|
148
|
+
this.updateToolbarState();
|
|
149
|
+
this.setMode('idle');
|
|
150
|
+
this.showToast(title ? `Finished flow: "${title}"` : 'Flow recording finished', '✓');
|
|
151
|
+
}
|
|
152
|
+
|
|
93
153
|
public setMode(mode: NoteInspectMode): void {
|
|
94
154
|
this.activeMode = mode;
|
|
95
155
|
this.updateToolbarState();
|
|
@@ -109,6 +169,44 @@ export class NoteInspector {
|
|
|
109
169
|
}
|
|
110
170
|
}
|
|
111
171
|
|
|
172
|
+
public isVisible(): boolean {
|
|
173
|
+
return !this.isHidden;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public setVisible(visible: boolean): void {
|
|
177
|
+
this.isHidden = !visible;
|
|
178
|
+
this.showMarkers = this.options.showBadges !== false && !this.isHidden;
|
|
179
|
+
|
|
180
|
+
if (this.container) {
|
|
181
|
+
this.container.style.display = this.isHidden ? 'none' : 'block';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.toolbarElement) {
|
|
185
|
+
this.toolbarElement.style.display = this.isHidden ? 'none' : 'flex';
|
|
186
|
+
} else if (!this.isHidden && this.options.showToolbar !== false) {
|
|
187
|
+
this.createToolbar();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.isHidden) {
|
|
191
|
+
this.hideHighlight();
|
|
192
|
+
this.hideRegionOverlay();
|
|
193
|
+
if (this.modalOverlay && this.shadowRoot) {
|
|
194
|
+
this.shadowRoot.removeChild(this.modalOverlay);
|
|
195
|
+
this.modalOverlay = null;
|
|
196
|
+
}
|
|
197
|
+
if (this.cardOverlay && this.shadowRoot) {
|
|
198
|
+
this.shadowRoot.removeChild(this.cardOverlay);
|
|
199
|
+
this.cardOverlay = null;
|
|
200
|
+
}
|
|
201
|
+
if (this.markersContainer) {
|
|
202
|
+
this.markersContainer.innerHTML = '';
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
this.renderMarkers();
|
|
206
|
+
this.updateToolbarCount();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
112
210
|
private ensureContainer(): ShadowRoot | null {
|
|
113
211
|
if (typeof document === 'undefined') return null;
|
|
114
212
|
|
|
@@ -117,6 +215,9 @@ export class NoteInspector {
|
|
|
117
215
|
this.container.id = 'browsertrack-inspector-host';
|
|
118
216
|
this.container.style.cssText =
|
|
119
217
|
'all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none;';
|
|
218
|
+
if (this.isHidden) {
|
|
219
|
+
this.container.style.display = 'none';
|
|
220
|
+
}
|
|
120
221
|
|
|
121
222
|
this.shadowRoot = this.container.attachShadow({ mode: 'open' });
|
|
122
223
|
|
|
@@ -276,6 +377,12 @@ export class NoteInspector {
|
|
|
276
377
|
z-index: 2147483641;
|
|
277
378
|
}
|
|
278
379
|
|
|
380
|
+
.bt-note-marker-step {
|
|
381
|
+
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
382
|
+
border-color: #fef3c7;
|
|
383
|
+
box-shadow: 0 4px 14px rgba(245, 158, 11, 0.4), 0 0 0 1px rgba(217, 119, 6, 0.5);
|
|
384
|
+
}
|
|
385
|
+
|
|
279
386
|
@keyframes bt-pop-in {
|
|
280
387
|
0% { transform: scale(0.6); opacity: 0; }
|
|
281
388
|
100% { transform: scale(1); opacity: 1; }
|
|
@@ -286,6 +393,10 @@ export class NoteInspector {
|
|
|
286
393
|
box-shadow: 0 8px 20px rgba(37,99,235,0.6), 0 0 0 2px #60a5fa;
|
|
287
394
|
}
|
|
288
395
|
|
|
396
|
+
.bt-note-marker-step:hover {
|
|
397
|
+
box-shadow: 0 8px 20px rgba(245, 158, 11, 0.7), 0 0 0 2px #fde68a;
|
|
398
|
+
}
|
|
399
|
+
|
|
289
400
|
.bt-marker-resolved {
|
|
290
401
|
background: linear-gradient(135deg, #475569, #334155);
|
|
291
402
|
border-color: #94a3b8;
|
|
@@ -382,6 +493,19 @@ export class NoteInspector {
|
|
|
382
493
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.35);
|
|
383
494
|
}
|
|
384
495
|
|
|
496
|
+
.bt-toolbar-btn.active-scenario {
|
|
497
|
+
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
498
|
+
color: #ffffff;
|
|
499
|
+
font-weight: 700;
|
|
500
|
+
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.4);
|
|
501
|
+
animation: bt-pulse 2s infinite;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
@keyframes bt-pulse {
|
|
505
|
+
0%, 100% { opacity: 1; }
|
|
506
|
+
50% { opacity: 0.85; }
|
|
507
|
+
}
|
|
508
|
+
|
|
385
509
|
.bt-count-pill {
|
|
386
510
|
background: rgba(255, 255, 255, 0.2);
|
|
387
511
|
color: #ffffff;
|
|
@@ -429,7 +553,7 @@ export class NoteInspector {
|
|
|
429
553
|
border: 1px solid #334155;
|
|
430
554
|
border-radius: 14px;
|
|
431
555
|
padding: 20px;
|
|
432
|
-
width:
|
|
556
|
+
width: 470px;
|
|
433
557
|
max-width: 92vw;
|
|
434
558
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
|
435
559
|
display: flex;
|
|
@@ -452,6 +576,7 @@ export class NoteInspector {
|
|
|
452
576
|
align-items: center;
|
|
453
577
|
gap: 8px;
|
|
454
578
|
color: #f8fafc;
|
|
579
|
+
flex-wrap: wrap;
|
|
455
580
|
}
|
|
456
581
|
|
|
457
582
|
.bt-mode-badge {
|
|
@@ -481,6 +606,12 @@ export class NoteInspector {
|
|
|
481
606
|
border: 1px solid rgba(168, 85, 247, 0.4);
|
|
482
607
|
}
|
|
483
608
|
|
|
609
|
+
.bt-badge-step {
|
|
610
|
+
background: rgba(245, 158, 11, 0.15);
|
|
611
|
+
color: #fbbf24;
|
|
612
|
+
border: 1px solid rgba(245, 158, 11, 0.4);
|
|
613
|
+
}
|
|
614
|
+
|
|
484
615
|
.bt-badge-status-open {
|
|
485
616
|
background: rgba(16, 185, 129, 0.15);
|
|
486
617
|
color: #34d399;
|
|
@@ -532,6 +663,59 @@ export class NoteInspector {
|
|
|
532
663
|
color: #cbd5e1;
|
|
533
664
|
}
|
|
534
665
|
|
|
666
|
+
.bt-component-pill {
|
|
667
|
+
display: flex;
|
|
668
|
+
align-items: center;
|
|
669
|
+
gap: 6px;
|
|
670
|
+
background: rgba(15, 23, 42, 0.95);
|
|
671
|
+
border: 1px solid rgba(99, 102, 241, 0.35);
|
|
672
|
+
border-radius: 6px;
|
|
673
|
+
padding: 6px 10px;
|
|
674
|
+
font-size: 11.5px;
|
|
675
|
+
color: #c7d2fe;
|
|
676
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
677
|
+
overflow: hidden;
|
|
678
|
+
text-overflow: ellipsis;
|
|
679
|
+
white-space: nowrap;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.bt-scenario-stepper {
|
|
683
|
+
display: flex;
|
|
684
|
+
align-items: center;
|
|
685
|
+
justify-content: space-between;
|
|
686
|
+
background: #090d16;
|
|
687
|
+
border: 1px solid #1e293b;
|
|
688
|
+
border-radius: 8px;
|
|
689
|
+
padding: 6px 10px;
|
|
690
|
+
font-size: 12px;
|
|
691
|
+
font-weight: 600;
|
|
692
|
+
color: #f59e0b;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.bt-step-nav-btn {
|
|
696
|
+
background: #1e293b;
|
|
697
|
+
color: #f8fafc;
|
|
698
|
+
border: 1px solid #334155;
|
|
699
|
+
padding: 4px 10px;
|
|
700
|
+
border-radius: 6px;
|
|
701
|
+
cursor: pointer;
|
|
702
|
+
font-size: 11px;
|
|
703
|
+
font-weight: 600;
|
|
704
|
+
transition: all 0.12s ease;
|
|
705
|
+
font-family: inherit;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.bt-step-nav-btn:hover:not(:disabled) {
|
|
709
|
+
background: #2563eb;
|
|
710
|
+
border-color: #3b82f6;
|
|
711
|
+
color: #ffffff;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.bt-step-nav-btn:disabled {
|
|
715
|
+
opacity: 0.3;
|
|
716
|
+
cursor: not-allowed;
|
|
717
|
+
}
|
|
718
|
+
|
|
535
719
|
.bt-note-message-box {
|
|
536
720
|
background: #090d16;
|
|
537
721
|
border: 1px solid #1e293b;
|
|
@@ -576,8 +760,9 @@ export class NoteInspector {
|
|
|
576
760
|
display: flex;
|
|
577
761
|
justify-content: space-between;
|
|
578
762
|
align-items: center;
|
|
579
|
-
gap:
|
|
763
|
+
gap: 10px;
|
|
580
764
|
margin-top: 2px;
|
|
765
|
+
flex-wrap: wrap;
|
|
581
766
|
}
|
|
582
767
|
|
|
583
768
|
.bt-kbd-hint {
|
|
@@ -605,12 +790,13 @@ export class NoteInspector {
|
|
|
605
790
|
justify-content: flex-end;
|
|
606
791
|
align-items: center;
|
|
607
792
|
gap: 8px;
|
|
793
|
+
flex-wrap: wrap;
|
|
608
794
|
}
|
|
609
795
|
|
|
610
796
|
.bt-btn {
|
|
611
|
-
padding:
|
|
797
|
+
padding: 7px 14px;
|
|
612
798
|
border-radius: 7px;
|
|
613
|
-
font-size:
|
|
799
|
+
font-size: 12px;
|
|
614
800
|
font-weight: 600;
|
|
615
801
|
cursor: pointer;
|
|
616
802
|
border: 1px solid transparent;
|
|
@@ -619,7 +805,7 @@ export class NoteInspector {
|
|
|
619
805
|
display: inline-flex;
|
|
620
806
|
align-items: center;
|
|
621
807
|
justify-content: center;
|
|
622
|
-
gap:
|
|
808
|
+
gap: 5px;
|
|
623
809
|
}
|
|
624
810
|
|
|
625
811
|
.bt-btn-cancel {
|
|
@@ -641,6 +827,25 @@ export class NoteInspector {
|
|
|
641
827
|
box-shadow: 0 4px 10px rgba(37, 99, 235, 0.45);
|
|
642
828
|
}
|
|
643
829
|
|
|
830
|
+
.bt-btn-next-step {
|
|
831
|
+
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
|
832
|
+
color: #ffffff;
|
|
833
|
+
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
|
|
834
|
+
}
|
|
835
|
+
.bt-btn-next-step:hover {
|
|
836
|
+
background: linear-gradient(135deg, #4f46e5, #4338ca);
|
|
837
|
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.55);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
.bt-btn-finish-flow {
|
|
841
|
+
background: linear-gradient(135deg, #10b981, #059669);
|
|
842
|
+
color: #ffffff;
|
|
843
|
+
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.35);
|
|
844
|
+
}
|
|
845
|
+
.bt-btn-finish-flow:hover {
|
|
846
|
+
background: linear-gradient(135deg, #059669, #047857);
|
|
847
|
+
}
|
|
848
|
+
|
|
644
849
|
.bt-btn-resolve {
|
|
645
850
|
background: rgba(16, 185, 129, 0.15);
|
|
646
851
|
color: #6ee7b7;
|
|
@@ -660,6 +865,47 @@ export class NoteInspector {
|
|
|
660
865
|
background: rgba(239, 68, 68, 0.3);
|
|
661
866
|
color: #ffffff;
|
|
662
867
|
}
|
|
868
|
+
|
|
869
|
+
/* 6. Toast Notifications */
|
|
870
|
+
.bt-toast-container {
|
|
871
|
+
position: fixed;
|
|
872
|
+
bottom: 74px;
|
|
873
|
+
right: 24px;
|
|
874
|
+
display: flex;
|
|
875
|
+
flex-direction: column;
|
|
876
|
+
gap: 8px;
|
|
877
|
+
pointer-events: none;
|
|
878
|
+
z-index: 2147483647;
|
|
879
|
+
align-items: flex-end;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.bt-toast {
|
|
883
|
+
background: #0f172a;
|
|
884
|
+
color: #f8fafc;
|
|
885
|
+
border: 1px solid #334155;
|
|
886
|
+
border-radius: 30px;
|
|
887
|
+
padding: 8px 16px;
|
|
888
|
+
font-size: 12.5px;
|
|
889
|
+
font-weight: 500;
|
|
890
|
+
display: inline-flex;
|
|
891
|
+
align-items: center;
|
|
892
|
+
gap: 8px;
|
|
893
|
+
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.6), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
|
|
894
|
+
pointer-events: auto;
|
|
895
|
+
animation: bt-toast-in 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
896
|
+
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
897
|
+
user-select: none;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.bt-toast-fadeout {
|
|
901
|
+
opacity: 0;
|
|
902
|
+
transform: translateY(6px);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
@keyframes bt-toast-in {
|
|
906
|
+
from { opacity: 0; transform: translateY(8px) scale(0.95); }
|
|
907
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
908
|
+
}
|
|
663
909
|
`;
|
|
664
910
|
|
|
665
911
|
this.shadowRoot.appendChild(style);
|
|
@@ -675,8 +921,13 @@ export class NoteInspector {
|
|
|
675
921
|
this.markersContainer.className = 'bt-markers-layer';
|
|
676
922
|
this.shadowRoot.appendChild(this.markersContainer);
|
|
677
923
|
|
|
924
|
+
// Toast container layer
|
|
925
|
+
this.toastContainer = document.createElement('div');
|
|
926
|
+
this.toastContainer.className = 'bt-toast-container';
|
|
927
|
+
this.shadowRoot.appendChild(this.toastContainer);
|
|
928
|
+
|
|
678
929
|
// Floating Toolbar
|
|
679
|
-
if (this.options.showToolbar) {
|
|
930
|
+
if (this.options.showToolbar && !this.isHidden) {
|
|
680
931
|
this.createToolbar();
|
|
681
932
|
}
|
|
682
933
|
}
|
|
@@ -707,6 +958,10 @@ export class NoteInspector {
|
|
|
707
958
|
<span>📄</span> Page
|
|
708
959
|
</button>
|
|
709
960
|
<div class="bt-toolbar-divider"></div>
|
|
961
|
+
<button class="bt-toolbar-btn" id="bt-mode-flow" title="Record multi-step reproduction flow">
|
|
962
|
+
<span>🎬</span> Flow
|
|
963
|
+
</button>
|
|
964
|
+
<div class="bt-toolbar-divider"></div>
|
|
710
965
|
<button class="bt-toolbar-btn active" id="bt-toggle-notes" title="Toggle visible note markers on screen">
|
|
711
966
|
<span>📌</span> Notes <span class="bt-count-pill" id="bt-notes-count">0</span>
|
|
712
967
|
</button>
|
|
@@ -715,6 +970,7 @@ export class NoteInspector {
|
|
|
715
970
|
const btnElement = this.toolbarElement.querySelector('#bt-mode-element') as HTMLButtonElement;
|
|
716
971
|
const btnRegion = this.toolbarElement.querySelector('#bt-mode-region') as HTMLButtonElement;
|
|
717
972
|
const btnPage = this.toolbarElement.querySelector('#bt-mode-page') as HTMLButtonElement;
|
|
973
|
+
const btnFlow = this.toolbarElement.querySelector('#bt-mode-flow') as HTMLButtonElement;
|
|
718
974
|
const btnToggleNotes = this.toolbarElement.querySelector('#bt-toggle-notes') as HTMLButtonElement;
|
|
719
975
|
|
|
720
976
|
btnElement.onclick = (e: MouseEvent) => {
|
|
@@ -732,6 +988,15 @@ export class NoteInspector {
|
|
|
732
988
|
this.setMode('page');
|
|
733
989
|
};
|
|
734
990
|
|
|
991
|
+
btnFlow.onclick = (e: MouseEvent) => {
|
|
992
|
+
e.stopPropagation();
|
|
993
|
+
if (this.activeScenario) {
|
|
994
|
+
this.finishScenario();
|
|
995
|
+
} else {
|
|
996
|
+
this.startScenario();
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
735
1000
|
btnToggleNotes.onclick = (e: MouseEvent) => {
|
|
736
1001
|
e.stopPropagation();
|
|
737
1002
|
this.showMarkers = !this.showMarkers;
|
|
@@ -748,10 +1013,23 @@ export class NoteInspector {
|
|
|
748
1013
|
const btnElement = this.toolbarElement.querySelector('#bt-mode-element');
|
|
749
1014
|
const btnRegion = this.toolbarElement.querySelector('#bt-mode-region');
|
|
750
1015
|
const btnPage = this.toolbarElement.querySelector('#bt-mode-page');
|
|
1016
|
+
const btnFlow = this.toolbarElement.querySelector('#bt-mode-flow');
|
|
751
1017
|
|
|
752
1018
|
btnElement?.classList.toggle('active', this.activeMode === 'element');
|
|
753
1019
|
btnRegion?.classList.toggle('active', this.activeMode === 'region');
|
|
754
1020
|
btnPage?.classList.toggle('active', this.activeMode === 'page');
|
|
1021
|
+
|
|
1022
|
+
if (btnFlow) {
|
|
1023
|
+
if (this.activeScenario) {
|
|
1024
|
+
btnFlow.className = 'bt-toolbar-btn active-scenario';
|
|
1025
|
+
btnFlow.innerHTML = `<span>🎬</span> Step ${this.activeScenario.stepNumber} (Finish)`;
|
|
1026
|
+
btnFlow.setAttribute('title', `Click to finish recording "${this.activeScenario.title}"`);
|
|
1027
|
+
} else {
|
|
1028
|
+
btnFlow.className = 'bt-toolbar-btn';
|
|
1029
|
+
btnFlow.innerHTML = `<span>🎬</span> Flow`;
|
|
1030
|
+
btnFlow.setAttribute('title', 'Record multi-step reproduction flow');
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
755
1033
|
}
|
|
756
1034
|
|
|
757
1035
|
private updateToolbarCount(): void {
|
|
@@ -768,7 +1046,7 @@ export class NoteInspector {
|
|
|
768
1046
|
if (!root || !this.markersContainer) return;
|
|
769
1047
|
|
|
770
1048
|
this.markersContainer.innerHTML = '';
|
|
771
|
-
if (!this.showMarkers) return;
|
|
1049
|
+
if (!this.showMarkers || this.isHidden) return;
|
|
772
1050
|
|
|
773
1051
|
const currentPath = window.location.pathname;
|
|
774
1052
|
// Filter open notes on this route or global
|
|
@@ -784,6 +1062,10 @@ export class NoteInspector {
|
|
|
784
1062
|
return;
|
|
785
1063
|
}
|
|
786
1064
|
|
|
1065
|
+
const isStep = !!note.scenarioId && note.stepNumber != null;
|
|
1066
|
+
const stepLabel = isStep ? `🎬 Step ${note.stepNumber}` : `#${index + 1}`;
|
|
1067
|
+
const markerClass = `bt-note-marker ${isStep ? 'bt-note-marker-step' : ''}`;
|
|
1068
|
+
|
|
787
1069
|
if (note.type === 'region' && note.region) {
|
|
788
1070
|
// Region box
|
|
789
1071
|
const regBox = document.createElement('div');
|
|
@@ -796,11 +1078,11 @@ export class NoteInspector {
|
|
|
796
1078
|
|
|
797
1079
|
// Pin on top-left of region
|
|
798
1080
|
const pin = document.createElement('div');
|
|
799
|
-
pin.className =
|
|
1081
|
+
pin.className = markerClass;
|
|
800
1082
|
pin.style.left = `${Math.max(4, note.region.x - 12)}px`;
|
|
801
1083
|
pin.style.top = `${Math.max(4, note.region.y - 12)}px`;
|
|
802
|
-
pin.title = note.message;
|
|
803
|
-
pin.innerHTML = `<span
|
|
1084
|
+
pin.title = isStep ? `[${note.scenarioTitle || 'Scenario'}] Step ${note.stepNumber}: ${note.message}` : note.message;
|
|
1085
|
+
pin.innerHTML = `<span>${isStep ? '🎬' : '📐'}</span> <span>${stepLabel}</span>`;
|
|
804
1086
|
pin.onclick = (e) => {
|
|
805
1087
|
e.stopPropagation();
|
|
806
1088
|
this.openNoteCard(note);
|
|
@@ -820,12 +1102,12 @@ export class NoteInspector {
|
|
|
820
1102
|
const rect = targetEl ? targetEl.getBoundingClientRect() : note.target?.boundingRect;
|
|
821
1103
|
if (rect) {
|
|
822
1104
|
const pin = document.createElement('div');
|
|
823
|
-
pin.className =
|
|
1105
|
+
pin.className = markerClass;
|
|
824
1106
|
pin.setAttribute('data-note-id', note.id);
|
|
825
|
-
pin.title = note.message;
|
|
1107
|
+
pin.title = isStep ? `[${note.scenarioTitle || 'Scenario'}] Step ${note.stepNumber}: ${note.message}` : note.message;
|
|
826
1108
|
pin.style.left = `${Math.max(4, rect.left - 10)}px`;
|
|
827
1109
|
pin.style.top = `${Math.max(4, rect.top - 12)}px`;
|
|
828
|
-
pin.innerHTML = `<span
|
|
1110
|
+
pin.innerHTML = `<span>${isStep ? '🎬' : '📝'}</span> <span>${stepLabel}</span>`;
|
|
829
1111
|
|
|
830
1112
|
pin.onclick = (e) => {
|
|
831
1113
|
e.stopPropagation();
|
|
@@ -845,10 +1127,11 @@ export class NoteInspector {
|
|
|
845
1127
|
if (pageNotes.length > 0) {
|
|
846
1128
|
const pageDock = document.createElement('div');
|
|
847
1129
|
pageDock.className = 'bt-page-notes-dock';
|
|
848
|
-
pageNotes.forEach((pNote
|
|
1130
|
+
pageNotes.forEach((pNote) => {
|
|
849
1131
|
const pill = document.createElement('div');
|
|
850
1132
|
pill.className = 'bt-page-note-pill';
|
|
851
|
-
|
|
1133
|
+
const label = pNote.scenarioId && pNote.stepNumber ? `Step ${pNote.stepNumber}: ` : '';
|
|
1134
|
+
pill.innerHTML = `<span>📄</span> <span>${label}${truncate(pNote.message, 25)}</span>`;
|
|
852
1135
|
pill.onclick = (e) => {
|
|
853
1136
|
e.stopPropagation();
|
|
854
1137
|
this.openNoteCard(pNote);
|
|
@@ -889,17 +1172,71 @@ export class NoteInspector {
|
|
|
889
1172
|
|
|
890
1173
|
const formattedDate = new Date(note.createdAt).toLocaleString();
|
|
891
1174
|
|
|
1175
|
+
// Scenario flow steps navigation
|
|
1176
|
+
let scenarioStepsHtml = '';
|
|
1177
|
+
let scenarioSteps: VisualNote[] = [];
|
|
1178
|
+
if (note.scenarioId) {
|
|
1179
|
+
scenarioSteps = this.savedNotes
|
|
1180
|
+
.filter((n) => n.scenarioId === note.scenarioId)
|
|
1181
|
+
.sort((a, b) => (a.stepNumber || 0) - (b.stepNumber || 0));
|
|
1182
|
+
|
|
1183
|
+
const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
|
|
1184
|
+
const prevStep = currentIndex > 0 ? scenarioSteps[currentIndex - 1] : null;
|
|
1185
|
+
const nextStep = currentIndex >= 0 && currentIndex < scenarioSteps.length - 1 ? scenarioSteps[currentIndex + 1] : null;
|
|
1186
|
+
|
|
1187
|
+
scenarioStepsHtml = `
|
|
1188
|
+
<div class="bt-scenario-stepper">
|
|
1189
|
+
<button class="bt-step-nav-btn" id="btn-prev-step" ${!prevStep ? 'disabled' : ''}>
|
|
1190
|
+
◀ Step ${prevStep ? prevStep.stepNumber : ''}
|
|
1191
|
+
</button>
|
|
1192
|
+
<span>🎬 Step ${note.stepNumber || 1} of ${scenarioSteps.length}</span>
|
|
1193
|
+
<button class="bt-step-nav-btn" id="btn-next-step-card" ${!nextStep ? 'disabled' : ''}>
|
|
1194
|
+
Step ${nextStep ? nextStep.stepNumber : ''} ▶
|
|
1195
|
+
</button>
|
|
1196
|
+
</div>
|
|
1197
|
+
`;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
892
1200
|
backdrop.innerHTML = `
|
|
893
1201
|
<div class="bt-modal">
|
|
894
1202
|
<div class="bt-modal-header">
|
|
895
1203
|
<div class="bt-modal-title">
|
|
896
|
-
<span>${icon
|
|
1204
|
+
<span>${note.scenarioId ? '🎬 ' + (note.scenarioTitle || 'Scenario Flow') : icon + ' Visual Note Details'}</span>
|
|
897
1205
|
<span class="bt-mode-badge ${badgeClass}">${note.type.toUpperCase()}</span>
|
|
1206
|
+
${note.scenarioId && note.stepNumber ? `<span class="bt-mode-badge bt-badge-step">STEP ${note.stepNumber}</span>` : ''}
|
|
898
1207
|
<span class="bt-mode-badge ${statusClass}">${note.status}</span>
|
|
899
1208
|
</div>
|
|
900
1209
|
<button class="bt-close-btn" id="btn-card-close" title="Close (Esc)">✕</button>
|
|
901
1210
|
</div>
|
|
902
1211
|
|
|
1212
|
+
${scenarioStepsHtml}
|
|
1213
|
+
|
|
1214
|
+
${
|
|
1215
|
+
note.elementContext?.componentSource?.componentName
|
|
1216
|
+
? `<div class="bt-component-pill" title="${
|
|
1217
|
+
note.elementContext.componentSource.sourceFile
|
|
1218
|
+
? note.elementContext.componentSource.sourceFile +
|
|
1219
|
+
(note.elementContext.componentSource.sourceLine ? ':' + note.elementContext.componentSource.sourceLine : '')
|
|
1220
|
+
: note.elementContext.componentSource.componentName
|
|
1221
|
+
}">
|
|
1222
|
+
<span>🧬</span>
|
|
1223
|
+
<span style="font-weight: 600; color: #a5b4fc;"><${note.elementContext.componentSource.componentName}></span>
|
|
1224
|
+
${
|
|
1225
|
+
note.elementContext.componentSource.sourceFile
|
|
1226
|
+
? `<span style="color: #64748b; font-size: 11px; margin-left: 4px;">${note.elementContext.componentSource.sourceFile}${
|
|
1227
|
+
note.elementContext.componentSource.sourceLine ? ':' + note.elementContext.componentSource.sourceLine : ''
|
|
1228
|
+
}</span>`
|
|
1229
|
+
: ''
|
|
1230
|
+
}
|
|
1231
|
+
${
|
|
1232
|
+
note.elementContext.componentSource.framework
|
|
1233
|
+
? `<span class="bt-mode-badge" style="background: rgba(99, 102, 241, 0.15); color: #818cf8; margin-left: auto; font-size: 10px;">${note.elementContext.componentSource.framework.toUpperCase()}</span>`
|
|
1234
|
+
: ''
|
|
1235
|
+
}
|
|
1236
|
+
</div>`
|
|
1237
|
+
: ''
|
|
1238
|
+
}
|
|
1239
|
+
|
|
903
1240
|
<div class="bt-target-pill" title="${contextText}">
|
|
904
1241
|
<span>🏷️</span>
|
|
905
1242
|
<span class="bt-pill-content">${contextText}</span>
|
|
@@ -913,9 +1250,18 @@ export class NoteInspector {
|
|
|
913
1250
|
</div>
|
|
914
1251
|
|
|
915
1252
|
<div class="bt-modal-footer">
|
|
916
|
-
<
|
|
917
|
-
<
|
|
918
|
-
|
|
1253
|
+
<div style="display: flex; gap: 6px;">
|
|
1254
|
+
<button class="bt-btn bt-btn-delete" id="btn-card-delete" title="Delete this note">
|
|
1255
|
+
<span>🗑️</span> Delete
|
|
1256
|
+
</button>
|
|
1257
|
+
${
|
|
1258
|
+
note.scenarioId
|
|
1259
|
+
? `<button class="bt-btn bt-btn-delete" id="btn-card-delete-flow" title="Delete entire scenario flow">
|
|
1260
|
+
<span>🗑️</span> Delete Flow
|
|
1261
|
+
</button>`
|
|
1262
|
+
: ''
|
|
1263
|
+
}
|
|
1264
|
+
</div>
|
|
919
1265
|
<div class="bt-modal-actions">
|
|
920
1266
|
<button class="bt-btn bt-btn-cancel" id="btn-card-dismiss">Close</button>
|
|
921
1267
|
<button class="bt-btn ${note.status === 'OPEN' ? 'bt-btn-resolve' : 'bt-btn-save'}" id="btn-card-resolve">
|
|
@@ -930,6 +1276,9 @@ export class NoteInspector {
|
|
|
930
1276
|
const btnDismiss = backdrop.querySelector('#btn-card-dismiss') as HTMLButtonElement;
|
|
931
1277
|
const btnResolve = backdrop.querySelector('#btn-card-resolve') as HTMLButtonElement;
|
|
932
1278
|
const btnDelete = backdrop.querySelector('#btn-card-delete') as HTMLButtonElement;
|
|
1279
|
+
const btnDeleteFlow = backdrop.querySelector('#btn-card-delete-flow') as HTMLButtonElement | null;
|
|
1280
|
+
const btnPrevStep = backdrop.querySelector('#btn-prev-step') as HTMLButtonElement | null;
|
|
1281
|
+
const btnNextStepCard = backdrop.querySelector('#btn-next-step-card') as HTMLButtonElement | null;
|
|
933
1282
|
|
|
934
1283
|
const closeCard = () => {
|
|
935
1284
|
if (this.cardOverlay) {
|
|
@@ -945,27 +1294,56 @@ export class NoteInspector {
|
|
|
945
1294
|
if (e.target === backdrop) closeCard();
|
|
946
1295
|
};
|
|
947
1296
|
|
|
1297
|
+
if (btnPrevStep && scenarioSteps.length > 0) {
|
|
1298
|
+
const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
|
|
1299
|
+
if (currentIndex > 0) {
|
|
1300
|
+
btnPrevStep.onclick = () => {
|
|
1301
|
+
this.openNoteCard(scenarioSteps[currentIndex - 1]);
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (btnNextStepCard && scenarioSteps.length > 0) {
|
|
1307
|
+
const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
|
|
1308
|
+
if (currentIndex >= 0 && currentIndex < scenarioSteps.length - 1) {
|
|
1309
|
+
btnNextStepCard.onclick = () => {
|
|
1310
|
+
this.openNoteCard(scenarioSteps[currentIndex + 1]);
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
948
1315
|
btnResolve.onclick = () => {
|
|
949
1316
|
if (note.status === 'OPEN') {
|
|
950
1317
|
this.transport.send({ type: 'resolve_note', noteId: note.id });
|
|
1318
|
+
this.showToast('Note marked as resolved', '✅');
|
|
951
1319
|
} else {
|
|
952
1320
|
this.transport.send({ type: 'reopen_note', noteId: note.id });
|
|
1321
|
+
this.showToast('Note reopened', '↺');
|
|
953
1322
|
}
|
|
954
1323
|
closeCard();
|
|
955
1324
|
};
|
|
956
1325
|
|
|
957
1326
|
btnDelete.onclick = () => {
|
|
958
1327
|
this.transport.send({ type: 'delete_note', noteId: note.id });
|
|
1328
|
+
this.showToast('Note deleted', '🗑️');
|
|
959
1329
|
closeCard();
|
|
960
1330
|
};
|
|
961
1331
|
|
|
1332
|
+
if (btnDeleteFlow && note.scenarioId) {
|
|
1333
|
+
btnDeleteFlow.onclick = () => {
|
|
1334
|
+
this.transport.send({ type: 'delete_scenario', scenarioId: note.scenarioId });
|
|
1335
|
+
this.showToast('Scenario flow deleted', '🗑️');
|
|
1336
|
+
closeCard();
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
|
|
962
1340
|
root.appendChild(backdrop);
|
|
963
1341
|
}
|
|
964
1342
|
|
|
965
1343
|
private setupListeners(): void {
|
|
966
1344
|
// 1. Mouse move: hover highlight in element mode or with Alt key
|
|
967
1345
|
const onMouseMove = (e: MouseEvent) => {
|
|
968
|
-
if (this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === 'region') return;
|
|
1346
|
+
if (this.isHidden || this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === 'region') return;
|
|
969
1347
|
|
|
970
1348
|
// Don't highlight elements if mouse is over our own inspector UI
|
|
971
1349
|
const path = e.composedPath ? e.composedPath() : [];
|
|
@@ -990,7 +1368,7 @@ export class NoteInspector {
|
|
|
990
1368
|
|
|
991
1369
|
// 2. Click: Alt+Click or Element mode click selects element and opens editor
|
|
992
1370
|
const onClick = (e: MouseEvent) => {
|
|
993
|
-
if (this.modalOverlay || this.cardOverlay) return;
|
|
1371
|
+
if (this.isHidden || this.modalOverlay || this.cardOverlay) return;
|
|
994
1372
|
|
|
995
1373
|
// If clicking inside inspector toolbar or container, do NOT intercept
|
|
996
1374
|
const path = e.composedPath ? e.composedPath() : [];
|
|
@@ -1008,7 +1386,7 @@ export class NoteInspector {
|
|
|
1008
1386
|
if (target && target !== this.container && !this.container?.contains(target)) {
|
|
1009
1387
|
this.selectedElement = target;
|
|
1010
1388
|
this.openNoteEditor(target, 'element');
|
|
1011
|
-
if (this.activeMode === 'element') {
|
|
1389
|
+
if (this.activeMode === 'element' && !this.activeScenario) {
|
|
1012
1390
|
this.setMode('idle');
|
|
1013
1391
|
}
|
|
1014
1392
|
}
|
|
@@ -1022,7 +1400,9 @@ export class NoteInspector {
|
|
|
1022
1400
|
this.shadowRoot.removeChild(this.cardOverlay);
|
|
1023
1401
|
this.cardOverlay = null;
|
|
1024
1402
|
} else if (this.activeMode === 'region' || this.activeMode === 'element') {
|
|
1025
|
-
this.
|
|
1403
|
+
if (!this.activeScenario) {
|
|
1404
|
+
this.setMode('idle');
|
|
1405
|
+
}
|
|
1026
1406
|
}
|
|
1027
1407
|
}
|
|
1028
1408
|
};
|
|
@@ -1153,31 +1533,30 @@ export class NoteInspector {
|
|
|
1153
1533
|
width: Math.round(width),
|
|
1154
1534
|
height: Math.round(height),
|
|
1155
1535
|
};
|
|
1156
|
-
this.hideRegionOverlay();
|
|
1157
|
-
this.setMode('idle');
|
|
1158
1536
|
this.openRegionNoteEditor(this.selectedRegion);
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (this.regionBox) {
|
|
1540
|
+
this.regionBox.style.display = 'none';
|
|
1541
|
+
}
|
|
1542
|
+
if (!this.activeScenario) {
|
|
1161
1543
|
this.setMode('idle');
|
|
1162
1544
|
}
|
|
1163
1545
|
};
|
|
1164
|
-
}
|
|
1165
1546
|
|
|
1166
|
-
// Always ensure regionOverlay is mounted before toolbar so toolbar is interactable
|
|
1167
|
-
if (this.toolbarElement && this.toolbarElement.parentNode === root) {
|
|
1168
|
-
root.insertBefore(this.regionOverlay, this.toolbarElement);
|
|
1169
|
-
} else {
|
|
1170
1547
|
root.appendChild(this.regionOverlay);
|
|
1548
|
+
} else {
|
|
1549
|
+
this.regionOverlay.style.display = 'block';
|
|
1171
1550
|
}
|
|
1172
1551
|
}
|
|
1173
1552
|
|
|
1174
1553
|
private hideRegionOverlay(): void {
|
|
1175
|
-
this.
|
|
1176
|
-
|
|
1177
|
-
this.
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1554
|
+
if (this.regionOverlay) {
|
|
1555
|
+
this.regionOverlay.style.display = 'none';
|
|
1556
|
+
this.isDraggingRegion = false;
|
|
1557
|
+
if (this.regionBox) {
|
|
1558
|
+
this.regionBox.style.display = 'none';
|
|
1559
|
+
}
|
|
1181
1560
|
}
|
|
1182
1561
|
}
|
|
1183
1562
|
|
|
@@ -1187,21 +1566,21 @@ export class NoteInspector {
|
|
|
1187
1566
|
|
|
1188
1567
|
const rect = el.getBoundingClientRect();
|
|
1189
1568
|
this.highlightOverlay.style.display = 'block';
|
|
1190
|
-
this.highlightOverlay.style.top = `${rect.top}px`;
|
|
1191
1569
|
this.highlightOverlay.style.left = `${rect.left}px`;
|
|
1570
|
+
this.highlightOverlay.style.top = `${rect.top}px`;
|
|
1192
1571
|
this.highlightOverlay.style.width = `${rect.width}px`;
|
|
1193
1572
|
this.highlightOverlay.style.height = `${rect.height}px`;
|
|
1194
1573
|
|
|
1195
|
-
const selector = getSemanticSelector(el);
|
|
1196
|
-
const badgeText = `${selector} · ${Math.round(rect.width)}×${Math.round(rect.height)}`;
|
|
1197
|
-
|
|
1198
1574
|
let badge = this.highlightOverlay.querySelector('.bt-badge') as HTMLDivElement | null;
|
|
1199
1575
|
if (!badge) {
|
|
1200
1576
|
badge = document.createElement('div');
|
|
1201
1577
|
badge.className = 'bt-badge';
|
|
1202
1578
|
this.highlightOverlay.appendChild(badge);
|
|
1203
1579
|
}
|
|
1204
|
-
|
|
1580
|
+
|
|
1581
|
+
const selector = getSemanticSelector(el);
|
|
1582
|
+
const label = this.activeScenario ? `🎬 Step ${this.activeScenario.stepNumber} · ${selector}` : selector;
|
|
1583
|
+
badge.textContent = `${label} (${Math.round(rect.width)} × ${Math.round(rect.height)} px)`;
|
|
1205
1584
|
}
|
|
1206
1585
|
|
|
1207
1586
|
private hideHighlight(): void {
|
|
@@ -1220,26 +1599,79 @@ export class NoteInspector {
|
|
|
1220
1599
|
this.updateHighlight(targetEl);
|
|
1221
1600
|
}
|
|
1222
1601
|
|
|
1602
|
+
const comp = noteType === 'element' ? resolveComponentSource(targetEl) : undefined;
|
|
1603
|
+
const compPrefix = comp?.componentName
|
|
1604
|
+
? `🧬 <${comp.componentName}>${comp.sourceFile ? ' (' + comp.sourceFile + (comp.sourceLine ? ':' + comp.sourceLine : '') + ')' : ''} · `
|
|
1605
|
+
: '';
|
|
1606
|
+
|
|
1223
1607
|
this.renderNoteModal({
|
|
1224
|
-
title:
|
|
1225
|
-
|
|
1608
|
+
title: this.activeScenario
|
|
1609
|
+
? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
|
|
1610
|
+
: 'Add Visual Note',
|
|
1611
|
+
modeBadge: this.activeScenario ? `STEP ${this.activeScenario.stepNumber}` : noteType.toUpperCase(),
|
|
1226
1612
|
pillText:
|
|
1227
1613
|
noteType === 'page'
|
|
1228
1614
|
? `Page Viewport: ${window.innerWidth} × ${window.innerHeight} px`
|
|
1229
|
-
: `${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}×${Math.round(targetEl.getBoundingClientRect().height)}) · Viewport: ${window.innerWidth}×${window.innerHeight}`,
|
|
1230
|
-
onSave: async (message) => {
|
|
1231
|
-
|
|
1615
|
+
: `${compPrefix}${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}×${Math.round(targetEl.getBoundingClientRect().height)}) · Viewport: ${window.innerWidth}×${window.innerHeight}`,
|
|
1616
|
+
onSave: async (message, action) => {
|
|
1617
|
+
let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
|
|
1618
|
+
|
|
1619
|
+
if (action === 'next_step' && !this.activeScenario) {
|
|
1620
|
+
this.startScenario();
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
if (this.activeScenario) {
|
|
1624
|
+
scenarioParam = {
|
|
1625
|
+
scenarioId: this.activeScenario.id,
|
|
1626
|
+
stepNumber: this.activeScenario.stepNumber,
|
|
1627
|
+
scenarioTitle: this.activeScenario.title,
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
await this.saveVisualNote(targetEl, message, noteType, scenarioParam);
|
|
1632
|
+
|
|
1633
|
+
if (action === 'next_step' && this.activeScenario) {
|
|
1634
|
+
this.activeScenario.stepNumber++;
|
|
1635
|
+
this.updateToolbarState();
|
|
1636
|
+
this.setMode('element');
|
|
1637
|
+
} else if (action === 'finish_flow' && this.activeScenario) {
|
|
1638
|
+
this.finishScenario();
|
|
1639
|
+
}
|
|
1232
1640
|
},
|
|
1233
1641
|
});
|
|
1234
1642
|
}
|
|
1235
1643
|
|
|
1236
1644
|
public openRegionNoteEditor(region: RegionContext): void {
|
|
1237
1645
|
this.renderNoteModal({
|
|
1238
|
-
title:
|
|
1239
|
-
|
|
1646
|
+
title: this.activeScenario
|
|
1647
|
+
? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
|
|
1648
|
+
: 'Add Region Note',
|
|
1649
|
+
modeBadge: this.activeScenario ? `STEP ${this.activeScenario.stepNumber}` : 'REGION',
|
|
1240
1650
|
pillText: `Selected Area: x:${region.x}, y:${region.y} (${region.width} × ${region.height} px)`,
|
|
1241
|
-
onSave: async (message) => {
|
|
1242
|
-
|
|
1651
|
+
onSave: async (message, action) => {
|
|
1652
|
+
let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
|
|
1653
|
+
|
|
1654
|
+
if (action === 'next_step' && !this.activeScenario) {
|
|
1655
|
+
this.startScenario();
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (this.activeScenario) {
|
|
1659
|
+
scenarioParam = {
|
|
1660
|
+
scenarioId: this.activeScenario.id,
|
|
1661
|
+
stepNumber: this.activeScenario.stepNumber,
|
|
1662
|
+
scenarioTitle: this.activeScenario.title,
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
await this.saveRegionVisualNote(region, message, scenarioParam);
|
|
1667
|
+
|
|
1668
|
+
if (action === 'next_step' && this.activeScenario) {
|
|
1669
|
+
this.activeScenario.stepNumber++;
|
|
1670
|
+
this.updateToolbarState();
|
|
1671
|
+
this.setMode('element');
|
|
1672
|
+
} else if (action === 'finish_flow' && this.activeScenario) {
|
|
1673
|
+
this.finishScenario();
|
|
1674
|
+
}
|
|
1243
1675
|
},
|
|
1244
1676
|
});
|
|
1245
1677
|
}
|
|
@@ -1248,7 +1680,7 @@ export class NoteInspector {
|
|
|
1248
1680
|
title: string;
|
|
1249
1681
|
modeBadge: string;
|
|
1250
1682
|
pillText: string;
|
|
1251
|
-
onSave: (message: string) => Promise<void>;
|
|
1683
|
+
onSave: (message: string, action: 'save' | 'next_step' | 'finish_flow') => Promise<void>;
|
|
1252
1684
|
}): void {
|
|
1253
1685
|
const root = this.ensureContainer();
|
|
1254
1686
|
if (!root) return;
|
|
@@ -1263,15 +1695,16 @@ export class NoteInspector {
|
|
|
1263
1695
|
backdrop.className = 'bt-modal-backdrop';
|
|
1264
1696
|
this.modalOverlay = backdrop;
|
|
1265
1697
|
|
|
1266
|
-
const
|
|
1267
|
-
const
|
|
1698
|
+
const isStep = options.modeBadge.startsWith('STEP');
|
|
1699
|
+
const badgeClass = isStep ? 'bt-badge-step' : `bt-badge-${options.modeBadge.toLowerCase()}`;
|
|
1700
|
+
const icon = isStep ? '🎬' : options.modeBadge === 'REGION' ? '📐' : options.modeBadge === 'PAGE' ? '📄' : '🎯';
|
|
1268
1701
|
const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
1269
1702
|
|
|
1270
1703
|
backdrop.innerHTML = `
|
|
1271
1704
|
<div class="bt-modal">
|
|
1272
1705
|
<div class="bt-modal-header">
|
|
1273
1706
|
<div class="bt-modal-title">
|
|
1274
|
-
<span
|
|
1707
|
+
<span>${icon} ${options.title}</span>
|
|
1275
1708
|
<span class="bt-mode-badge ${badgeClass}">${options.modeBadge}</span>
|
|
1276
1709
|
</div>
|
|
1277
1710
|
<button class="bt-close-btn" id="btn-close" title="Close (Esc)">✕</button>
|
|
@@ -1280,14 +1713,23 @@ export class NoteInspector {
|
|
|
1280
1713
|
<span>${icon}</span>
|
|
1281
1714
|
<span class="bt-pill-content">${options.pillText}</span>
|
|
1282
1715
|
</div>
|
|
1283
|
-
<textarea class="bt-textarea" placeholder="Describe the layout issue,
|
|
1716
|
+
<textarea class="bt-textarea" placeholder="Describe the layout issue, user action, or note for AI agent..." autofocus></textarea>
|
|
1284
1717
|
<div class="bt-modal-footer">
|
|
1285
1718
|
<div class="bt-kbd-hint">
|
|
1286
1719
|
<kbd>${isMac ? '⌘' : 'Ctrl'}+Enter</kbd> save · <kbd>Esc</kbd> cancel
|
|
1287
1720
|
</div>
|
|
1288
1721
|
<div class="bt-modal-actions">
|
|
1289
1722
|
<button class="bt-btn bt-btn-cancel" id="btn-cancel">Cancel</button>
|
|
1290
|
-
<button class="bt-btn bt-btn-
|
|
1723
|
+
<button class="bt-btn bt-btn-next-step" id="btn-next-step" title="Save this step and immediately select the next element">
|
|
1724
|
+
<span>➡️</span> ${this.activeScenario ? 'Save & Next Step' : 'Save as Step 1 (Flow)'}
|
|
1725
|
+
</button>
|
|
1726
|
+
${
|
|
1727
|
+
this.activeScenario
|
|
1728
|
+
? `<button class="bt-btn bt-btn-finish-flow" id="btn-finish-flow" title="Save final step and complete scenario">
|
|
1729
|
+
<span>✓</span> Save & Finish Flow
|
|
1730
|
+
</button>`
|
|
1731
|
+
: `<button class="bt-btn bt-btn-save" id="btn-save">Save Note</button>`
|
|
1732
|
+
}
|
|
1291
1733
|
</div>
|
|
1292
1734
|
</div>
|
|
1293
1735
|
</div>
|
|
@@ -1296,7 +1738,9 @@ export class NoteInspector {
|
|
|
1296
1738
|
const textarea = backdrop.querySelector('textarea') as HTMLTextAreaElement;
|
|
1297
1739
|
const btnClose = backdrop.querySelector('#btn-close') as HTMLButtonElement;
|
|
1298
1740
|
const btnCancel = backdrop.querySelector('#btn-cancel') as HTMLButtonElement;
|
|
1299
|
-
const btnSave = backdrop.querySelector('#btn-save') as HTMLButtonElement;
|
|
1741
|
+
const btnSave = backdrop.querySelector('#btn-save') as HTMLButtonElement | null;
|
|
1742
|
+
const btnNextStep = backdrop.querySelector('#btn-next-step') as HTMLButtonElement;
|
|
1743
|
+
const btnFinishFlow = backdrop.querySelector('#btn-finish-flow') as HTMLButtonElement | null;
|
|
1300
1744
|
|
|
1301
1745
|
const closeModal = () => {
|
|
1302
1746
|
if (this.modalOverlay) {
|
|
@@ -1316,26 +1760,33 @@ export class NoteInspector {
|
|
|
1316
1760
|
}
|
|
1317
1761
|
};
|
|
1318
1762
|
|
|
1319
|
-
const
|
|
1763
|
+
const handleAction = async (action: 'save' | 'next_step' | 'finish_flow') => {
|
|
1320
1764
|
const message = textarea.value.trim();
|
|
1321
1765
|
if (!message) return;
|
|
1322
1766
|
|
|
1323
|
-
|
|
1324
|
-
btnSave.disabled = true;
|
|
1767
|
+
btnNextStep.disabled = true;
|
|
1768
|
+
if (btnSave) btnSave.disabled = true;
|
|
1769
|
+
if (btnFinishFlow) btnFinishFlow.disabled = true;
|
|
1325
1770
|
|
|
1326
1771
|
try {
|
|
1327
|
-
await options.onSave(message);
|
|
1772
|
+
await options.onSave(message, action);
|
|
1328
1773
|
} finally {
|
|
1329
1774
|
closeModal();
|
|
1330
1775
|
}
|
|
1331
1776
|
};
|
|
1332
1777
|
|
|
1333
|
-
btnSave
|
|
1778
|
+
if (btnSave) {
|
|
1779
|
+
btnSave.onclick = () => handleAction('save');
|
|
1780
|
+
}
|
|
1781
|
+
btnNextStep.onclick = () => handleAction('next_step');
|
|
1782
|
+
if (btnFinishFlow) {
|
|
1783
|
+
btnFinishFlow.onclick = () => handleAction('finish_flow');
|
|
1784
|
+
}
|
|
1334
1785
|
|
|
1335
1786
|
textarea.onkeydown = (e: KeyboardEvent) => {
|
|
1336
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey
|
|
1787
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
1337
1788
|
e.preventDefault();
|
|
1338
|
-
|
|
1789
|
+
handleAction(this.activeScenario ? 'next_step' : 'save');
|
|
1339
1790
|
}
|
|
1340
1791
|
if (e.key === 'Escape') {
|
|
1341
1792
|
closeModal();
|
|
@@ -1346,7 +1797,12 @@ export class NoteInspector {
|
|
|
1346
1797
|
setTimeout(() => textarea?.focus(), 50);
|
|
1347
1798
|
}
|
|
1348
1799
|
|
|
1349
|
-
public async saveVisualNote(
|
|
1800
|
+
public async saveVisualNote(
|
|
1801
|
+
targetEl: HTMLElement,
|
|
1802
|
+
message: string,
|
|
1803
|
+
noteType: 'element' | 'page' = 'element',
|
|
1804
|
+
scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
|
|
1805
|
+
): Promise<void> {
|
|
1350
1806
|
const rect = targetEl.getBoundingClientRect();
|
|
1351
1807
|
const selector = noteType === 'page' ? 'body' : getSemanticSelector(targetEl);
|
|
1352
1808
|
|
|
@@ -1381,25 +1837,30 @@ export class NoteInspector {
|
|
|
1381
1837
|
confidence: selector.startsWith('[data-test') ? 'high' : selector.startsWith('#') ? 'medium' : 'low',
|
|
1382
1838
|
};
|
|
1383
1839
|
|
|
1840
|
+
const route = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
|
|
1841
|
+
const url = typeof window !== 'undefined' ? window.location.href : 'http://localhost/';
|
|
1842
|
+
const viewport = typeof window !== 'undefined'
|
|
1843
|
+
? { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio || 1 }
|
|
1844
|
+
: { width: 1280, height: 800, devicePixelRatio: 1 };
|
|
1845
|
+
const scroll = typeof window !== 'undefined'
|
|
1846
|
+
? { scrollX: window.scrollX, scrollY: window.scrollY }
|
|
1847
|
+
: { scrollX: 0, scrollY: 0 };
|
|
1848
|
+
|
|
1384
1849
|
const notePayload = {
|
|
1385
1850
|
type: 'create_note',
|
|
1386
1851
|
sessionId: this.transport.getSessionId() || '',
|
|
1387
1852
|
noteType,
|
|
1388
1853
|
message,
|
|
1389
|
-
route
|
|
1390
|
-
url
|
|
1391
|
-
viewport
|
|
1392
|
-
|
|
1393
|
-
height: window.innerHeight,
|
|
1394
|
-
devicePixelRatio: window.devicePixelRatio || 1,
|
|
1395
|
-
},
|
|
1396
|
-
scroll: {
|
|
1397
|
-
scrollX: window.scrollX,
|
|
1398
|
-
scrollY: window.scrollY,
|
|
1399
|
-
},
|
|
1854
|
+
route,
|
|
1855
|
+
url,
|
|
1856
|
+
viewport,
|
|
1857
|
+
scroll,
|
|
1400
1858
|
target,
|
|
1401
1859
|
elementContext,
|
|
1402
1860
|
screenshot: screenshotDataUrl,
|
|
1861
|
+
scenarioId: scenario?.scenarioId,
|
|
1862
|
+
stepNumber: scenario?.stepNumber,
|
|
1863
|
+
scenarioTitle: scenario?.scenarioTitle,
|
|
1403
1864
|
timestamp: Date.now(),
|
|
1404
1865
|
};
|
|
1405
1866
|
|
|
@@ -1407,9 +1868,19 @@ export class NoteInspector {
|
|
|
1407
1868
|
if (this.options.onNoteCreated) {
|
|
1408
1869
|
this.options.onNoteCreated(notePayload as any);
|
|
1409
1870
|
}
|
|
1871
|
+
|
|
1872
|
+
if (scenario?.scenarioId) {
|
|
1873
|
+
this.showToast(`Step ${scenario.stepNumber || 1} recorded`, '🎬');
|
|
1874
|
+
} else {
|
|
1875
|
+
this.showToast('Visual note saved', '✨');
|
|
1876
|
+
}
|
|
1410
1877
|
}
|
|
1411
1878
|
|
|
1412
|
-
public async saveRegionVisualNote(
|
|
1879
|
+
public async saveRegionVisualNote(
|
|
1880
|
+
region: RegionContext,
|
|
1881
|
+
message: string,
|
|
1882
|
+
scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
|
|
1883
|
+
): Promise<void> {
|
|
1413
1884
|
// 1. Capture full page screenshot and crop to region
|
|
1414
1885
|
let screenshotDataUrl: string | undefined;
|
|
1415
1886
|
try {
|
|
@@ -1421,24 +1892,29 @@ export class NoteInspector {
|
|
|
1421
1892
|
// Defensive
|
|
1422
1893
|
}
|
|
1423
1894
|
|
|
1895
|
+
const route = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
|
|
1896
|
+
const url = typeof window !== 'undefined' ? window.location.href : 'http://localhost/';
|
|
1897
|
+
const viewport = typeof window !== 'undefined'
|
|
1898
|
+
? { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio || 1 }
|
|
1899
|
+
: { width: 1280, height: 800, devicePixelRatio: 1 };
|
|
1900
|
+
const scroll = typeof window !== 'undefined'
|
|
1901
|
+
? { scrollX: window.scrollX, scrollY: window.scrollY }
|
|
1902
|
+
: { scrollX: 0, scrollY: 0 };
|
|
1903
|
+
|
|
1424
1904
|
const notePayload = {
|
|
1425
1905
|
type: 'create_note',
|
|
1426
1906
|
sessionId: this.transport.getSessionId() || '',
|
|
1427
1907
|
noteType: 'region',
|
|
1428
1908
|
message,
|
|
1429
|
-
route
|
|
1430
|
-
url
|
|
1431
|
-
viewport
|
|
1432
|
-
|
|
1433
|
-
height: window.innerHeight,
|
|
1434
|
-
devicePixelRatio: window.devicePixelRatio || 1,
|
|
1435
|
-
},
|
|
1436
|
-
scroll: {
|
|
1437
|
-
scrollX: window.scrollX,
|
|
1438
|
-
scrollY: window.scrollY,
|
|
1439
|
-
},
|
|
1909
|
+
route,
|
|
1910
|
+
url,
|
|
1911
|
+
viewport,
|
|
1912
|
+
scroll,
|
|
1440
1913
|
region,
|
|
1441
1914
|
screenshot: screenshotDataUrl,
|
|
1915
|
+
scenarioId: scenario?.scenarioId,
|
|
1916
|
+
stepNumber: scenario?.stepNumber,
|
|
1917
|
+
scenarioTitle: scenario?.scenarioTitle,
|
|
1442
1918
|
timestamp: Date.now(),
|
|
1443
1919
|
};
|
|
1444
1920
|
|
|
@@ -1446,108 +1922,106 @@ export class NoteInspector {
|
|
|
1446
1922
|
if (this.options.onNoteCreated) {
|
|
1447
1923
|
this.options.onNoteCreated(notePayload as any);
|
|
1448
1924
|
}
|
|
1449
|
-
}
|
|
1450
1925
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
const canvas = document.createElement('canvas');
|
|
1457
|
-
canvas.width = region.width;
|
|
1458
|
-
canvas.height = region.height;
|
|
1459
|
-
const ctx = canvas.getContext('2d');
|
|
1460
|
-
if (!ctx) {
|
|
1461
|
-
resolve(dataUrl);
|
|
1462
|
-
return;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
const dpr = window.devicePixelRatio || 1;
|
|
1466
|
-
ctx.drawImage(
|
|
1467
|
-
img,
|
|
1468
|
-
region.x * dpr,
|
|
1469
|
-
region.y * dpr,
|
|
1470
|
-
region.width * dpr,
|
|
1471
|
-
region.height * dpr,
|
|
1472
|
-
0,
|
|
1473
|
-
0,
|
|
1474
|
-
region.width,
|
|
1475
|
-
region.height
|
|
1476
|
-
);
|
|
1477
|
-
resolve(canvas.toDataURL('image/webp', 0.9));
|
|
1478
|
-
} catch {
|
|
1479
|
-
resolve(dataUrl);
|
|
1480
|
-
}
|
|
1481
|
-
};
|
|
1482
|
-
img.onerror = () => resolve(dataUrl);
|
|
1483
|
-
img.src = dataUrl;
|
|
1484
|
-
});
|
|
1926
|
+
if (scenario?.scenarioId) {
|
|
1927
|
+
this.showToast(`Step ${scenario.stepNumber || 1} region recorded`, '🎬');
|
|
1928
|
+
} else {
|
|
1929
|
+
this.showToast('Region note saved', '📐');
|
|
1930
|
+
}
|
|
1485
1931
|
}
|
|
1486
1932
|
|
|
1487
1933
|
private extractElementContext(el: HTMLElement): ElementContext {
|
|
1488
1934
|
const selector = getSemanticSelector(el);
|
|
1489
|
-
const tag = el.tagName.toLowerCase();
|
|
1490
|
-
|
|
1491
|
-
// Attributes (sanitize passwords/tokens)
|
|
1492
1935
|
const attributes: Record<string, string> = {};
|
|
1936
|
+
|
|
1493
1937
|
for (let i = 0; i < el.attributes.length; i++) {
|
|
1494
1938
|
const attr = el.attributes[i];
|
|
1495
|
-
if (
|
|
1939
|
+
if (
|
|
1940
|
+
this.options.maskSelectors?.some((mask) => {
|
|
1941
|
+
try {
|
|
1942
|
+
return el.matches(mask);
|
|
1943
|
+
} catch {
|
|
1944
|
+
return false;
|
|
1945
|
+
}
|
|
1946
|
+
}) &&
|
|
1947
|
+
(attr.name === 'value' || attr.name === 'data-secret')
|
|
1948
|
+
) {
|
|
1496
1949
|
attributes[attr.name] = '[REDACTED]';
|
|
1497
1950
|
} else {
|
|
1498
1951
|
attributes[attr.name] = attr.value;
|
|
1499
1952
|
}
|
|
1500
1953
|
}
|
|
1501
1954
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const clone = el.cloneNode(true) as HTMLElement;
|
|
1506
|
-
for (const passInput of Array.from(clone.querySelectorAll('input[type="password"]'))) {
|
|
1507
|
-
passInput.setAttribute('value', '[REDACTED]');
|
|
1508
|
-
}
|
|
1509
|
-
outerHTML = truncate(clone.outerHTML, 10240); // 10 KB max
|
|
1510
|
-
} catch {
|
|
1511
|
-
outerHTML = truncate(el.outerHTML, 10240);
|
|
1955
|
+
let outerHTML = el.outerHTML;
|
|
1956
|
+
if (outerHTML && outerHTML.length > 1000) {
|
|
1957
|
+
outerHTML = truncate(outerHTML, 1000);
|
|
1512
1958
|
}
|
|
1513
1959
|
|
|
1514
|
-
let
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1517
|
-
selector: getSemanticSelector(el.parentElement),
|
|
1518
|
-
tag: el.parentElement.tagName.toLowerCase(),
|
|
1519
|
-
};
|
|
1960
|
+
let innerText = el.innerText || el.textContent || '';
|
|
1961
|
+
if (innerText && innerText.length > 200) {
|
|
1962
|
+
innerText = truncate(innerText, 200);
|
|
1520
1963
|
}
|
|
1521
1964
|
|
|
1965
|
+
const componentSource = resolveComponentSource(el);
|
|
1966
|
+
|
|
1522
1967
|
return {
|
|
1523
1968
|
selector,
|
|
1524
|
-
tag,
|
|
1969
|
+
tag: el.tagName.toLowerCase(),
|
|
1525
1970
|
attributes,
|
|
1526
1971
|
outerHTML,
|
|
1527
|
-
innerText
|
|
1528
|
-
|
|
1972
|
+
innerText,
|
|
1973
|
+
componentSource,
|
|
1974
|
+
parent: el.parentElement
|
|
1975
|
+
? {
|
|
1976
|
+
selector: getSemanticSelector(el.parentElement),
|
|
1977
|
+
tag: el.parentElement.tagName.toLowerCase(),
|
|
1978
|
+
}
|
|
1979
|
+
: undefined,
|
|
1529
1980
|
};
|
|
1530
1981
|
}
|
|
1531
1982
|
|
|
1983
|
+
private async cropDataUrl(dataUrl: string, region: RegionContext): Promise<string> {
|
|
1984
|
+
return new Promise((resolve) => {
|
|
1985
|
+
const img = new Image();
|
|
1986
|
+
img.onload = () => {
|
|
1987
|
+
const canvas = document.createElement('canvas');
|
|
1988
|
+
canvas.width = region.width;
|
|
1989
|
+
canvas.height = region.height;
|
|
1990
|
+
const ctx = canvas.getContext('2d');
|
|
1991
|
+
if (!ctx) {
|
|
1992
|
+
resolve(dataUrl);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1997
|
+
ctx.drawImage(
|
|
1998
|
+
img,
|
|
1999
|
+
region.x * dpr,
|
|
2000
|
+
region.y * dpr,
|
|
2001
|
+
region.width * dpr,
|
|
2002
|
+
region.height * dpr,
|
|
2003
|
+
0,
|
|
2004
|
+
0,
|
|
2005
|
+
region.width,
|
|
2006
|
+
region.height
|
|
2007
|
+
);
|
|
2008
|
+
resolve(canvas.toDataURL('image/png'));
|
|
2009
|
+
};
|
|
2010
|
+
img.onerror = () => resolve(dataUrl);
|
|
2011
|
+
img.src = dataUrl;
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1532
2015
|
public destroy(): void {
|
|
1533
2016
|
for (const cleanup of this.cleanups) {
|
|
1534
|
-
|
|
1535
|
-
cleanup();
|
|
1536
|
-
} catch {}
|
|
2017
|
+
cleanup();
|
|
1537
2018
|
}
|
|
1538
2019
|
this.cleanups = [];
|
|
1539
2020
|
|
|
1540
|
-
if (this.container && this.container.
|
|
1541
|
-
this.container.
|
|
2021
|
+
if (this.container && this.container.parentNode) {
|
|
2022
|
+
this.container.parentNode.removeChild(this.container);
|
|
2023
|
+
this.container = null;
|
|
2024
|
+
this.shadowRoot = null;
|
|
1542
2025
|
}
|
|
1543
|
-
this.container = null;
|
|
1544
|
-
this.shadowRoot = null;
|
|
1545
|
-
this.toolbarElement = null;
|
|
1546
|
-
this.regionOverlay = null;
|
|
1547
|
-
this.regionBox = null;
|
|
1548
|
-
this.regionBanner = null;
|
|
1549
|
-
this.markersContainer = null;
|
|
1550
|
-
this.modalOverlay = null;
|
|
1551
|
-
this.cardOverlay = null;
|
|
1552
2026
|
}
|
|
1553
2027
|
}
|