yo-bug 0.1.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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/bin/cli.ts +17 -0
  4. package/bin/install.ts +34 -0
  5. package/bin/mcp.ts +2 -0
  6. package/dist/bin/cli.d.ts +3 -0
  7. package/dist/bin/cli.d.ts.map +1 -0
  8. package/dist/bin/cli.js +19 -0
  9. package/dist/bin/cli.js.map +1 -0
  10. package/dist/bin/install.d.ts +3 -0
  11. package/dist/bin/install.d.ts.map +1 -0
  12. package/dist/bin/install.js +28 -0
  13. package/dist/bin/install.js.map +1 -0
  14. package/dist/bin/mcp.d.ts +3 -0
  15. package/dist/bin/mcp.d.ts.map +1 -0
  16. package/dist/bin/mcp.js +3 -0
  17. package/dist/bin/mcp.js.map +1 -0
  18. package/dist/src/detect/dev-server.d.ts +11 -0
  19. package/dist/src/detect/dev-server.d.ts.map +1 -0
  20. package/dist/src/detect/dev-server.js +92 -0
  21. package/dist/src/detect/dev-server.js.map +1 -0
  22. package/dist/src/detect/spawn-dev.d.ts +4 -0
  23. package/dist/src/detect/spawn-dev.d.ts.map +1 -0
  24. package/dist/src/detect/spawn-dev.js +37 -0
  25. package/dist/src/detect/spawn-dev.js.map +1 -0
  26. package/dist/src/install/detect-ai-tool.d.ts +7 -0
  27. package/dist/src/install/detect-ai-tool.d.ts.map +1 -0
  28. package/dist/src/install/detect-ai-tool.js +38 -0
  29. package/dist/src/install/detect-ai-tool.js.map +1 -0
  30. package/dist/src/install/write-mcp-config.d.ts +6 -0
  31. package/dist/src/install/write-mcp-config.d.ts.map +1 -0
  32. package/dist/src/install/write-mcp-config.js +47 -0
  33. package/dist/src/install/write-mcp-config.js.map +1 -0
  34. package/dist/src/mcp/index.d.ts +2 -0
  35. package/dist/src/mcp/index.d.ts.map +1 -0
  36. package/dist/src/mcp/index.js +263 -0
  37. package/dist/src/mcp/index.js.map +1 -0
  38. package/dist/src/mcp/tools/checklist.d.ts +22 -0
  39. package/dist/src/mcp/tools/checklist.d.ts.map +1 -0
  40. package/dist/src/mcp/tools/checklist.js +58 -0
  41. package/dist/src/mcp/tools/checklist.js.map +1 -0
  42. package/dist/src/mcp/tools/get-feedback.d.ts +13 -0
  43. package/dist/src/mcp/tools/get-feedback.d.ts.map +1 -0
  44. package/dist/src/mcp/tools/get-feedback.js +27 -0
  45. package/dist/src/mcp/tools/get-feedback.js.map +1 -0
  46. package/dist/src/mcp/tools/list-feedbacks.d.ts +20 -0
  47. package/dist/src/mcp/tools/list-feedbacks.d.ts.map +1 -0
  48. package/dist/src/mcp/tools/list-feedbacks.js +29 -0
  49. package/dist/src/mcp/tools/list-feedbacks.js.map +1 -0
  50. package/dist/src/mcp/tools/resolve-feedback.d.ts +8 -0
  51. package/dist/src/mcp/tools/resolve-feedback.d.ts.map +1 -0
  52. package/dist/src/mcp/tools/resolve-feedback.js +28 -0
  53. package/dist/src/mcp/tools/resolve-feedback.js.map +1 -0
  54. package/dist/src/mcp/tools/start-session.d.ts +11 -0
  55. package/dist/src/mcp/tools/start-session.d.ts.map +1 -0
  56. package/dist/src/mcp/tools/start-session.js +56 -0
  57. package/dist/src/mcp/tools/start-session.js.map +1 -0
  58. package/dist/src/mcp/tools/stop-session.d.ts +6 -0
  59. package/dist/src/mcp/tools/stop-session.d.ts.map +1 -0
  60. package/dist/src/mcp/tools/stop-session.js +80 -0
  61. package/dist/src/mcp/tools/stop-session.js.map +1 -0
  62. package/dist/src/mcp/tools/test-history.d.ts +33 -0
  63. package/dist/src/mcp/tools/test-history.d.ts.map +1 -0
  64. package/dist/src/mcp/tools/test-history.js +66 -0
  65. package/dist/src/mcp/tools/test-history.js.map +1 -0
  66. package/dist/src/server/feedback-api.d.ts +4 -0
  67. package/dist/src/server/feedback-api.d.ts.map +1 -0
  68. package/dist/src/server/feedback-api.js +152 -0
  69. package/dist/src/server/feedback-api.js.map +1 -0
  70. package/dist/src/server/html-injector.d.ts +10 -0
  71. package/dist/src/server/html-injector.d.ts.map +1 -0
  72. package/dist/src/server/html-injector.js +34 -0
  73. package/dist/src/server/html-injector.js.map +1 -0
  74. package/dist/src/server/proxy-server.d.ts +8 -0
  75. package/dist/src/server/proxy-server.d.ts.map +1 -0
  76. package/dist/src/server/proxy-server.js +87 -0
  77. package/dist/src/server/proxy-server.js.map +1 -0
  78. package/dist/src/server/sdk-serve.d.ts +3 -0
  79. package/dist/src/server/sdk-serve.d.ts.map +1 -0
  80. package/dist/src/server/sdk-serve.js +20 -0
  81. package/dist/src/server/sdk-serve.js.map +1 -0
  82. package/dist/src/storage/store.d.ts +21 -0
  83. package/dist/src/storage/store.d.ts.map +1 -0
  84. package/dist/src/storage/store.js +138 -0
  85. package/dist/src/storage/store.js.map +1 -0
  86. package/dist/src/storage/types.d.ts +74 -0
  87. package/dist/src/storage/types.d.ts.map +1 -0
  88. package/dist/src/storage/types.js +2 -0
  89. package/dist/src/storage/types.js.map +1 -0
  90. package/dist/vibe-feedback.js +399 -0
  91. package/package.json +67 -0
  92. package/src/client/annotation-mode/canvas-editor.ts +178 -0
  93. package/src/client/annotation-mode/history.ts +32 -0
  94. package/src/client/annotation-mode/region-selector.ts +123 -0
  95. package/src/client/annotation-mode/screenshot.ts +17 -0
  96. package/src/client/annotation-mode/toolbar.ts +139 -0
  97. package/src/client/annotation-mode/tools/arrow.ts +57 -0
  98. package/src/client/annotation-mode/tools/base-tool.ts +25 -0
  99. package/src/client/annotation-mode/tools/circle.ts +37 -0
  100. package/src/client/annotation-mode/tools/freehand.ts +23 -0
  101. package/src/client/annotation-mode/tools/rect.ts +32 -0
  102. package/src/client/annotation-mode/tools/text.ts +93 -0
  103. package/src/client/api/client.ts +48 -0
  104. package/src/client/capture/action-recorder.ts +157 -0
  105. package/src/client/capture/console-interceptor.ts +65 -0
  106. package/src/client/capture/context-buffer.ts +23 -0
  107. package/src/client/capture/error-interceptor.ts +52 -0
  108. package/src/client/capture/network-interceptor.ts +143 -0
  109. package/src/client/core/event-bus.ts +20 -0
  110. package/src/client/core/i18n.ts +83 -0
  111. package/src/client/core/sdk.ts +373 -0
  112. package/src/client/core/shadow-host.ts +27 -0
  113. package/src/client/element-mode/highlighter.ts +79 -0
  114. package/src/client/element-mode/inspector.ts +73 -0
  115. package/src/client/element-mode/selector.ts +22 -0
  116. package/src/client/index.ts +10 -0
  117. package/src/client/styles/sdk.css.ts +222 -0
  118. package/src/client/ui/checklist-panel.ts +279 -0
  119. package/src/client/ui/feedback-panel.ts +149 -0
  120. package/src/client/ui/floating-button.ts +103 -0
  121. package/src/client/ui/toast.ts +17 -0
  122. package/src/client/ui/verify-panel.ts +111 -0
  123. package/src/detect/dev-server.ts +110 -0
  124. package/src/detect/spawn-dev.ts +50 -0
  125. package/src/install/detect-ai-tool.ts +49 -0
  126. package/src/install/write-mcp-config.ts +49 -0
  127. package/src/mcp/index.ts +327 -0
  128. package/src/mcp/tools/checklist.ts +61 -0
  129. package/src/mcp/tools/get-feedback.ts +34 -0
  130. package/src/mcp/tools/list-feedbacks.ts +37 -0
  131. package/src/mcp/tools/resolve-feedback.ts +34 -0
  132. package/src/mcp/tools/start-session.ts +65 -0
  133. package/src/mcp/tools/stop-session.ts +93 -0
  134. package/src/mcp/tools/test-history.ts +97 -0
  135. package/src/server/feedback-api.ts +164 -0
  136. package/src/server/html-injector.ts +41 -0
  137. package/src/server/proxy-server.ts +107 -0
  138. package/src/server/sdk-serve.ts +24 -0
  139. package/src/storage/store.ts +172 -0
  140. package/src/storage/types.ts +68 -0
@@ -0,0 +1,83 @@
1
+ const zhCN: Record<string, string> = {
2
+ // Floating button
3
+ 'hint.firstTime': '\u53D1\u73B0\u95EE\u9898\uFF1F\u70B9\u6211\u6216\u6309 Alt+Q \u5FEB\u901F\u6807\u8BB0',
4
+ // Mode bar
5
+ 'mode.quick': '\u5FEB\u901F\u6807\u8BB0',
6
+ 'mode.quick.hint': '\u70B9\u51FB\u95EE\u9898\u5143\u7D20',
7
+ 'mode.detail': '\u8BE6\u7EC6\u63CF\u8FF0',
8
+ 'mode.detail.hint': '\u70B9\u51FB + \u586B\u5199',
9
+ 'mode.screenshot': '\u622A\u56FE\u6807\u6CE8',
10
+ 'mode.screenshot.hint': '\u6846\u9009\u533A\u57DF + \u753B',
11
+ // Feedback panel
12
+ 'panel.element.title': '\u5143\u7D20\u53CD\u9988',
13
+ 'panel.annotation.title': '\u622A\u56FE\u53CD\u9988',
14
+ 'panel.type': '\u95EE\u9898\u7C7B\u578B',
15
+ 'panel.description': '\u95EE\u9898\u63CF\u8FF0',
16
+ 'panel.description.placeholder': '\u63CF\u8FF0\u4F60\u53D1\u73B0\u7684\u95EE\u9898...',
17
+ 'panel.cancel': '\u53D6\u6D88',
18
+ 'panel.submit': '\u63D0\u4EA4',
19
+ // Toast
20
+ 'toast.flagged': '\u5DF2\u6807\u8BB0',
21
+ 'toast.submitted': '\u53CD\u9988\u5DF2\u63D0\u4EA4',
22
+ 'toast.failed': '\u63D0\u4EA4\u5931\u8D25',
23
+ 'toast.screenshotFailed': '\u622A\u56FE\u5931\u8D25',
24
+ 'toast.clickToFlag': '\u70B9\u51FB\u95EE\u9898\u5143\u7D20\u6765\u6807\u8BB0',
25
+ // Region selector
26
+ 'region.hint': '\u62D6\u62FD\u9009\u62E9\u533A\u57DF \xB7 ESC \u53D6\u6D88',
27
+ // Verify
28
+ 'verify.title': '\u9A8C\u8BC1\u4FEE\u590D',
29
+ 'verify.fixed': '\u5DF2\u4FEE\u590D',
30
+ 'verify.broken': '\u672A\u4FEE\u590D',
31
+ // Problem types
32
+ 'type.bug': 'Bug',
33
+ 'type.ui-issue': 'UI',
34
+ 'type.performance': 'Performance',
35
+ 'type.feature-request': 'Feature',
36
+ 'type.other': 'Other',
37
+ };
38
+
39
+ const en: Record<string, string> = {
40
+ 'hint.firstTime': 'Found a problem? Click me or press Alt+Q to flag it.',
41
+ 'mode.quick': 'Quick',
42
+ 'mode.quick.hint': 'Tap problem',
43
+ 'mode.detail': 'Describe',
44
+ 'mode.detail.hint': 'Tap + fill form',
45
+ 'mode.screenshot': 'Screenshot',
46
+ 'mode.screenshot.hint': 'Select area + draw',
47
+ 'panel.element.title': 'Element Feedback',
48
+ 'panel.annotation.title': 'Annotation Feedback',
49
+ 'panel.type': 'Problem Type',
50
+ 'panel.description': 'Description',
51
+ 'panel.description.placeholder': 'Describe the issue...',
52
+ 'panel.cancel': 'Cancel',
53
+ 'panel.submit': 'Submit',
54
+ 'toast.flagged': 'Flagged!',
55
+ 'toast.submitted': 'Feedback submitted',
56
+ 'toast.failed': 'Submit failed',
57
+ 'toast.screenshotFailed': 'Screenshot failed',
58
+ 'toast.clickToFlag': 'Click the problem element to flag it',
59
+ 'region.hint': 'Drag to select area \xB7 ESC to cancel',
60
+ 'verify.title': 'Verify Fix',
61
+ 'verify.fixed': 'Fixed',
62
+ 'verify.broken': 'Still broken',
63
+ 'type.bug': 'Bug',
64
+ 'type.ui-issue': 'UI',
65
+ 'type.performance': 'Performance',
66
+ 'type.feature-request': 'Feature',
67
+ 'type.other': 'Other',
68
+ };
69
+
70
+ let currentLang: Record<string, string> = en;
71
+
72
+ export function initI18n(): void {
73
+ const htmlLang = document.documentElement.lang?.toLowerCase() || '';
74
+ if (htmlLang.startsWith('zh')) {
75
+ currentLang = zhCN;
76
+ } else {
77
+ currentLang = en;
78
+ }
79
+ }
80
+
81
+ export function t(key: string): string {
82
+ return currentLang[key] || en[key] || key;
83
+ }
@@ -0,0 +1,373 @@
1
+ import { ShadowHost } from './shadow-host.js';
2
+ import { EventBus } from './event-bus.js';
3
+ import { FloatingButton } from '../ui/floating-button.js';
4
+ import { FeedbackPanel, type PanelResult } from '../ui/feedback-panel.js';
5
+ import { showToast } from '../ui/toast.js';
6
+ import { ChecklistPanel } from '../ui/checklist-panel.js';
7
+ import { VerifyPanel } from '../ui/verify-panel.js';
8
+ import { Highlighter } from '../element-mode/highlighter.js';
9
+ import { inspectElement, type ElementInfo } from '../element-mode/inspector.js';
10
+ import { captureScreenshot } from '../annotation-mode/screenshot.js';
11
+ import { RegionSelector } from '../annotation-mode/region-selector.js';
12
+ import { CanvasEditor } from '../annotation-mode/canvas-editor.js';
13
+ import { ContextBuffer } from '../capture/context-buffer.js';
14
+ import { ConsoleInterceptor, type ConsoleEntry } from '../capture/console-interceptor.js';
15
+ import { NetworkInterceptor, type NetworkEntry } from '../capture/network-interceptor.js';
16
+ import { ErrorInterceptor, type ErrorEntry } from '../capture/error-interceptor.js';
17
+ import { ApiClient, type FeedbackPayload } from '../api/client.js';
18
+ import { ActionRecorder } from '../capture/action-recorder.js';
19
+ import { t } from './i18n.js';
20
+
21
+ type Mode = 'flag' | 'detail' | 'annotate';
22
+
23
+ export class VibeFeedback {
24
+ private shadowHost: ShadowHost;
25
+ private eventBus: EventBus;
26
+ private fab: FloatingButton;
27
+ private panel: FeedbackPanel;
28
+ private highlighter: Highlighter;
29
+ private apiClient: ApiClient;
30
+
31
+ private actionRecorder = new ActionRecorder();
32
+ private consoleBuffer = new ContextBuffer<ConsoleEntry>(50);
33
+ private networkBuffer = new ContextBuffer<NetworkEntry>(50);
34
+ private errorBuffer = new ContextBuffer<ErrorEntry>(50);
35
+ private consoleInterceptor: ConsoleInterceptor;
36
+ private networkInterceptor: NetworkInterceptor;
37
+ private errorInterceptor: ErrorInterceptor;
38
+
39
+ private checklistPanel: ChecklistPanel | null = null;
40
+ private verifyPanel: VerifyPanel | null = null;
41
+ private active = false;
42
+ private mode: Mode | null = null;
43
+ private modeBar: HTMLDivElement | null = null;
44
+ private selectedElement: ElementInfo | null = null;
45
+ private annotationDataUrl: string | null = null;
46
+ private activeChecklistItemId: number | null = null;
47
+ private keyHandler: (e: KeyboardEvent) => void;
48
+
49
+ constructor() {
50
+ this.shadowHost = new ShadowHost();
51
+ this.eventBus = new EventBus();
52
+ this.apiClient = new ApiClient();
53
+
54
+ // Context capture
55
+ this.consoleInterceptor = new ConsoleInterceptor(this.consoleBuffer);
56
+ this.networkInterceptor = new NetworkInterceptor(this.networkBuffer);
57
+ this.errorInterceptor = new ErrorInterceptor(this.errorBuffer);
58
+ this.consoleInterceptor.install();
59
+ this.networkInterceptor.install();
60
+ this.errorInterceptor.install();
61
+ this.actionRecorder.start();
62
+
63
+ // Checklist + Verify panels
64
+ this.checklistPanel = new ChecklistPanel(
65
+ this.shadowHost.shadow,
66
+ (itemId) => this.onChecklistFail(itemId)
67
+ );
68
+ this.checklistPanel.startPolling();
69
+ this.verifyPanel = new VerifyPanel(this.shadowHost.shadow);
70
+ this.verifyPanel.startPolling();
71
+
72
+ // UI
73
+ this.fab = new FloatingButton(this.shadowHost.shadow, () => this.toggleModeBar());
74
+ this.highlighter = new Highlighter((el) => this.onElementSelected(el));
75
+ this.panel = new FeedbackPanel(
76
+ this.shadowHost.shadow,
77
+ (result) => this.onSubmit(result),
78
+ () => this.cancelFeedback()
79
+ );
80
+
81
+ // Global keyboard shortcuts — always active
82
+ this.keyHandler = (e: KeyboardEvent) => this.onKeyDown(e);
83
+ document.addEventListener('keydown', this.keyHandler);
84
+ }
85
+
86
+ // ──────── Keyboard shortcuts ────────
87
+ private onKeyDown(e: KeyboardEvent): void {
88
+ // Don't intercept when typing in inputs
89
+ const tag = (e.target as HTMLElement)?.tagName;
90
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
91
+ // Don't intercept inside our SDK panels
92
+ if (this.isSDKElement(e.target as Element)) return;
93
+
94
+ // All shortcuts use Alt+key (or Option+key on Mac) to avoid conflicts
95
+ if (e.altKey) {
96
+ switch (e.key) {
97
+ case 'q': case 'Q':
98
+ e.preventDefault();
99
+ this.enterMode('flag');
100
+ return;
101
+ case 'd': case 'D':
102
+ e.preventDefault();
103
+ this.enterMode('detail');
104
+ return;
105
+ case 's': case 'S':
106
+ e.preventDefault();
107
+ this.enterMode('annotate');
108
+ return;
109
+ case 'x': case 'X':
110
+ e.preventDefault();
111
+ if (this.active) this.exitTestMode();
112
+ else this.toggleModeBar();
113
+ return;
114
+ }
115
+ }
116
+ // Esc always exits test mode (universal convention)
117
+ if (e.key === 'Escape' && this.active) {
118
+ e.preventDefault();
119
+ this.exitTestMode();
120
+ }
121
+ }
122
+
123
+ private isSDKElement(el: Element | null): boolean {
124
+ while (el) {
125
+ if (el.id === 'vibe-feedback-host') return true;
126
+ el = el.parentElement;
127
+ }
128
+ return false;
129
+ }
130
+
131
+ // ──────── Mode management ────────
132
+
133
+ /** Enter a specific mode directly — skips mode bar */
134
+ private enterMode(mode: Mode): void {
135
+ if (!this.active) {
136
+ this.active = true;
137
+ this.fab.setActive(true);
138
+ }
139
+ this.mode = mode;
140
+ this.hideModeBar();
141
+ this.showModeBar();
142
+ // Highlight active button
143
+ const idx = mode === 'flag' ? 0 : mode === 'detail' ? 1 : 2;
144
+ this.modeBar?.querySelectorAll('.vf-mode-btn').forEach((btn, i) => {
145
+ btn.classList.toggle('active', i === idx);
146
+ });
147
+ this.activateMode();
148
+ }
149
+
150
+ /** Toggle mode bar visibility (FAB click) */
151
+ private toggleModeBar(): void {
152
+ if (this.active) {
153
+ this.exitTestMode();
154
+ } else {
155
+ this.active = true;
156
+ this.fab.setActive(true);
157
+ this.mode = null;
158
+ this.showModeBar();
159
+ }
160
+ }
161
+
162
+ private exitTestMode(): void {
163
+ this.active = false;
164
+ this.fab.setActive(false);
165
+ this.deactivateAll();
166
+ }
167
+
168
+ private showModeBar(): void {
169
+ if (this.modeBar) return;
170
+
171
+ this.modeBar = document.createElement('div');
172
+ this.modeBar.className = 'vf-mode-bar';
173
+
174
+ const modes: { mode: Mode; label: string; hint: string; key: string }[] = [
175
+ { mode: 'flag', label: t('mode.quick'), hint: t('mode.quick.hint'), key: 'Alt+Q' },
176
+ { mode: 'detail', label: t('mode.detail'), hint: t('mode.detail.hint'), key: 'Alt+D' },
177
+ { mode: 'annotate', label: t('mode.screenshot'), hint: t('mode.screenshot.hint'), key: 'Alt+S' },
178
+ ];
179
+
180
+ for (const m of modes) {
181
+ const btn = document.createElement('button');
182
+ btn.className = `vf-mode-btn${this.mode === m.mode ? ' active' : ''}`;
183
+ btn.innerHTML = `
184
+ <div style="display:flex;align-items:center;gap:6px;font-size:13px">
185
+ ${m.label}
186
+ <span style="font-size:10px;opacity:0.4;background:rgba(255,255,255,0.1);padding:1px 5px;border-radius:3px;">${m.key}</span>
187
+ </div>
188
+ <div style="font-size:10px;opacity:0.5;margin-top:1px">${m.hint}</div>
189
+ `;
190
+ btn.addEventListener('click', () => {
191
+ this.mode = m.mode;
192
+ this.modeBar!.querySelectorAll('.vf-mode-btn').forEach((b) => b.classList.remove('active'));
193
+ btn.classList.add('active');
194
+ this.activateMode();
195
+ });
196
+ this.modeBar.appendChild(btn);
197
+ }
198
+
199
+ this.shadowHost.append(this.modeBar);
200
+ }
201
+
202
+ private hideModeBar(): void {
203
+ this.modeBar?.remove();
204
+ this.modeBar = null;
205
+ }
206
+
207
+ private activateMode(): void {
208
+ this.highlighter.deactivate();
209
+ if (!this.mode) return;
210
+
211
+ if (this.mode === 'flag' || this.mode === 'detail') {
212
+ this.highlighter.activate();
213
+ } else {
214
+ this.startAnnotation();
215
+ }
216
+ }
217
+
218
+ private deactivateAll(): void {
219
+ this.highlighter.deactivate();
220
+ this.panel.hide();
221
+ this.hideModeBar();
222
+ this.selectedElement = null;
223
+ this.annotationDataUrl = null;
224
+ this.activeChecklistItemId = null;
225
+ }
226
+
227
+ // ──────── Element selected ────────
228
+ private onElementSelected(el: Element): void {
229
+ this.selectedElement = inspectElement(el);
230
+
231
+ if (this.mode === 'flag') {
232
+ this.quickFlag();
233
+ } else {
234
+ this.highlighter.deactivate();
235
+ this.hideModeBar();
236
+ this.panel.showForElement(this.selectedElement);
237
+ }
238
+ }
239
+
240
+ // ──────── Quick flag ────────
241
+ private async quickFlag(): Promise<void> {
242
+ const payload = this.buildPayload('bug', '');
243
+ try {
244
+ const { id } = await this.apiClient.submitFeedback(payload);
245
+ showToast(this.shadowHost.shadow, `${t('toast.flagged')} (${id})`);
246
+
247
+ if (this.activeChecklistItemId !== null) {
248
+ await this.linkFeedbackToChecklist(id, this.activeChecklistItemId);
249
+ this.activeChecklistItemId = null;
250
+ }
251
+ } catch {
252
+ showToast(this.shadowHost.shadow, t('toast.failed'), true);
253
+ }
254
+ this.selectedElement = null;
255
+ }
256
+
257
+ // ──────── Checklist integration ────────
258
+ private onChecklistFail(itemId: number): void {
259
+ this.activeChecklistItemId = itemId;
260
+ this.enterMode('flag');
261
+ showToast(this.shadowHost.shadow, t('toast.clickToFlag'));
262
+ }
263
+
264
+ private async linkFeedbackToChecklist(feedbackId: string, checklistItemId: number): Promise<void> {
265
+ try {
266
+ await fetch(`${window.location.origin}/api/checklist/${checklistItemId}`, {
267
+ method: 'PATCH',
268
+ headers: { 'Content-Type': 'application/json' },
269
+ body: JSON.stringify({ status: 'failed', feedback: `[linked:${feedbackId}]` }),
270
+ });
271
+ } catch {}
272
+ }
273
+
274
+ // ──────── Annotation ────────
275
+ private async startAnnotation(): Promise<void> {
276
+ this.hideModeBar();
277
+ this.fab.setActive(false);
278
+
279
+ try {
280
+ const fullScreenshot = await captureScreenshot();
281
+ const selector = new RegionSelector(
282
+ (rect) => {
283
+ const dpr = Math.min(window.devicePixelRatio, 2);
284
+ const cropCanvas = document.createElement('canvas');
285
+ cropCanvas.width = rect.w * dpr;
286
+ cropCanvas.height = rect.h * dpr;
287
+ const cropCtx = cropCanvas.getContext('2d')!;
288
+ cropCtx.drawImage(fullScreenshot,
289
+ rect.x * dpr, rect.y * dpr, rect.w * dpr, rect.h * dpr,
290
+ 0, 0, rect.w * dpr, rect.h * dpr
291
+ );
292
+ const editor = new CanvasEditor(
293
+ cropCanvas,
294
+ (dataUrl) => {
295
+ this.annotationDataUrl = dataUrl;
296
+ this.fab.setActive(true);
297
+ this.panel.showForAnnotation(dataUrl);
298
+ },
299
+ () => { this.fab.setActive(true); this.showModeBar(); }
300
+ );
301
+ editor.show();
302
+ },
303
+ () => { this.fab.setActive(true); this.showModeBar(); }
304
+ );
305
+ selector.show();
306
+ } catch {
307
+ showToast(this.shadowHost.shadow, t('toast.screenshotFailed'), true);
308
+ this.fab.setActive(true);
309
+ this.showModeBar();
310
+ }
311
+ }
312
+
313
+ // ──────── Payload ────────
314
+ private buildPayload(problemType: string, description: string): FeedbackPayload {
315
+ return {
316
+ mode: this.selectedElement ? 'element' : 'annotation',
317
+ problemType, description,
318
+ element: this.selectedElement || undefined,
319
+ pageUrl: window.location.href,
320
+ pageTitle: document.title,
321
+ viewport: { width: window.innerWidth, height: window.innerHeight },
322
+ userAgent: navigator.userAgent,
323
+ timestamp: Date.now(),
324
+ consoleErrors: this.consoleBuffer.getAll(),
325
+ networkErrors: this.networkBuffer.getAll(),
326
+ unhandledErrors: this.errorBuffer.getAll(),
327
+ actionSteps: this.actionRecorder.getSteps(),
328
+ };
329
+ }
330
+
331
+ // ──────── Submit ────────
332
+ private async onSubmit(result: PanelResult): Promise<void> {
333
+ const payload = this.buildPayload(result.problemType, result.description);
334
+ try {
335
+ const { id } = await this.apiClient.submitFeedback(payload, this.annotationDataUrl || undefined);
336
+ showToast(this.shadowHost.shadow, `${t('toast.submitted')} (${id})`);
337
+ if (this.activeChecklistItemId !== null) {
338
+ await this.linkFeedbackToChecklist(id, this.activeChecklistItemId);
339
+ this.activeChecklistItemId = null;
340
+ }
341
+ } catch {
342
+ showToast(this.shadowHost.shadow, t('toast.failed'), true);
343
+ }
344
+ this.panel.hide();
345
+ this.selectedElement = null;
346
+ this.annotationDataUrl = null;
347
+ this.showModeBar();
348
+ this.activateMode();
349
+ }
350
+
351
+ private cancelFeedback(): void {
352
+ this.panel.hide();
353
+ this.selectedElement = null;
354
+ this.annotationDataUrl = null;
355
+ this.activeChecklistItemId = null;
356
+ this.showModeBar();
357
+ this.activateMode();
358
+ }
359
+
360
+ // ──────── Cleanup ────────
361
+ destroy(): void {
362
+ document.removeEventListener('keydown', this.keyHandler);
363
+ this.deactivateAll();
364
+ this.consoleInterceptor.uninstall();
365
+ this.networkInterceptor.uninstall();
366
+ this.errorInterceptor.uninstall();
367
+ this.actionRecorder.stop();
368
+ this.checklistPanel?.destroy();
369
+ this.verifyPanel?.destroy();
370
+ this.fab.destroy();
371
+ this.shadowHost.destroy();
372
+ }
373
+ }
@@ -0,0 +1,27 @@
1
+ import { SDK_CSS } from '../styles/sdk.css.js';
2
+
3
+ export class ShadowHost {
4
+ readonly host: HTMLDivElement;
5
+ readonly shadow: ShadowRoot;
6
+
7
+ constructor() {
8
+ this.host = document.createElement('div');
9
+ this.host.id = 'vibe-feedback-host';
10
+ this.host.style.cssText =
11
+ 'position:fixed;z-index:2147483647;top:0;left:0;width:0;height:0;pointer-events:none;';
12
+ document.body.appendChild(this.host);
13
+ this.shadow = this.host.attachShadow({ mode: 'open' });
14
+
15
+ const style = document.createElement('style');
16
+ style.textContent = SDK_CSS;
17
+ this.shadow.appendChild(style);
18
+ }
19
+
20
+ append(el: HTMLElement): void {
21
+ this.shadow.appendChild(el);
22
+ }
23
+
24
+ destroy(): void {
25
+ this.host.remove();
26
+ }
27
+ }
@@ -0,0 +1,79 @@
1
+ export class Highlighter {
2
+ private overlay: HTMLDivElement;
3
+ private currentTarget: Element | null = null;
4
+ private moveHandler: (e: MouseEvent) => void;
5
+ private clickHandler: (e: MouseEvent) => void;
6
+ private active = false;
7
+
8
+ constructor(private onSelect: (element: Element) => void) {
9
+ this.overlay = document.createElement('div');
10
+ this.overlay.style.cssText = `
11
+ position: fixed;
12
+ pointer-events: none;
13
+ z-index: 2147483646;
14
+ background: rgba(59,130,246,0.12);
15
+ border: 2px solid rgba(59,130,246,0.7);
16
+ border-radius: 3px;
17
+ transition: all 0.06s ease-out;
18
+ display: none;
19
+ `;
20
+
21
+ this.moveHandler = (e: MouseEvent) => this.onMouseMove(e);
22
+ this.clickHandler = (e: MouseEvent) => this.onClick(e);
23
+ }
24
+
25
+ activate(): void {
26
+ if (this.active) return;
27
+ this.active = true;
28
+ document.body.appendChild(this.overlay);
29
+ document.addEventListener('mousemove', this.moveHandler, true);
30
+ document.addEventListener('click', this.clickHandler, true);
31
+ document.body.style.cursor = 'crosshair';
32
+ }
33
+
34
+ deactivate(): void {
35
+ if (!this.active) return;
36
+ this.active = false;
37
+ this.overlay.style.display = 'none';
38
+ this.overlay.remove();
39
+ document.removeEventListener('mousemove', this.moveHandler, true);
40
+ document.removeEventListener('click', this.clickHandler, true);
41
+ document.body.style.cursor = '';
42
+ this.currentTarget = null;
43
+ }
44
+
45
+ private onMouseMove(e: MouseEvent): void {
46
+ const target = document.elementFromPoint(e.clientX, e.clientY);
47
+ if (!target || this.isSDKElement(target) || target === this.currentTarget) return;
48
+
49
+ this.currentTarget = target;
50
+ const rect = target.getBoundingClientRect();
51
+ this.overlay.style.display = 'block';
52
+ this.overlay.style.top = `${rect.top}px`;
53
+ this.overlay.style.left = `${rect.left}px`;
54
+ this.overlay.style.width = `${rect.width}px`;
55
+ this.overlay.style.height = `${rect.height}px`;
56
+ }
57
+
58
+ private onClick(e: MouseEvent): void {
59
+ const target = document.elementFromPoint(e.clientX, e.clientY);
60
+ if (!target || this.isSDKElement(target)) return;
61
+
62
+ e.preventDefault();
63
+ e.stopPropagation();
64
+ e.stopImmediatePropagation();
65
+
66
+ this.onSelect(target);
67
+ }
68
+
69
+ private isSDKElement(el: Element): boolean {
70
+ // Check if element is part of our SDK
71
+ let current: Element | null = el;
72
+ while (current) {
73
+ if (current.id === 'vibe-feedback-host') return true;
74
+ if ((current as HTMLElement).style?.zIndex === '2147483646') return true;
75
+ current = current.parentElement;
76
+ }
77
+ return false;
78
+ }
79
+ }
@@ -0,0 +1,73 @@
1
+ import { generateSelector } from './selector.js';
2
+
3
+ export interface ElementInfo {
4
+ selector: string;
5
+ tagName: string;
6
+ textContent: string;
7
+ rect: { x: number; y: number; width: number; height: number };
8
+ computedStyles: Record<string, string>;
9
+ attributes: Record<string, string>;
10
+ componentName: string | null;
11
+ componentFile: string | null;
12
+ }
13
+
14
+ const STYLE_PROPS = [
15
+ 'display', 'position', 'width', 'height', 'margin', 'padding',
16
+ 'color', 'background-color', 'font-size', 'font-weight',
17
+ 'border', 'opacity', 'cursor', 'visibility', 'overflow',
18
+ 'flex-direction', 'align-items', 'justify-content', 'gap',
19
+ ];
20
+
21
+ export function inspectElement(el: Element): ElementInfo {
22
+ const rect = el.getBoundingClientRect();
23
+ const computed = window.getComputedStyle(el);
24
+
25
+ const styles: Record<string, string> = {};
26
+ for (const prop of STYLE_PROPS) {
27
+ styles[prop] = computed.getPropertyValue(prop);
28
+ }
29
+
30
+ const attrs: Record<string, string> = {};
31
+ for (const attr of el.attributes) {
32
+ if (attr.name.startsWith('data-') || ['class', 'href', 'src', 'type', 'name', 'placeholder'].includes(attr.name)) {
33
+ attrs[attr.name] = attr.value.slice(0, 100);
34
+ }
35
+ }
36
+
37
+ return {
38
+ selector: generateSelector(el),
39
+ tagName: el.tagName.toLowerCase(),
40
+ textContent: (el.textContent || '').trim().slice(0, 200),
41
+ rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
42
+ computedStyles: styles,
43
+ attributes: attrs,
44
+ componentName: getComponentName(el),
45
+ componentFile: null, // Source file detection is unreliable
46
+ };
47
+ }
48
+
49
+ function getComponentName(el: Element): string | null {
50
+ // React: __reactFiber$ or __reactInternalInstance$
51
+ const fiberKey = Object.keys(el).find(
52
+ (k) => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')
53
+ );
54
+ if (fiberKey) {
55
+ let fiber = (el as any)[fiberKey];
56
+ while (fiber) {
57
+ if (typeof fiber.type === 'function') {
58
+ return fiber.type.displayName || fiber.type.name || null;
59
+ }
60
+ fiber = fiber.return;
61
+ }
62
+ }
63
+
64
+ // Vue 2
65
+ const vue2 = (el as any).__vue__;
66
+ if (vue2) return vue2.$options?.name || vue2.$options?._componentTag || null;
67
+
68
+ // Vue 3
69
+ const vue3 = (el as any).__vueParentComponent;
70
+ if (vue3) return vue3.type?.name || vue3.type?.__name || null;
71
+
72
+ return null;
73
+ }
@@ -0,0 +1,22 @@
1
+ export function generateSelector(el: Element): string {
2
+ // Priority: data-testid > id > nth-child path
3
+ const testId = el.getAttribute('data-testid');
4
+ if (testId) return `[data-testid="${testId}"]`;
5
+
6
+ if (el.id) return `#${CSS.escape(el.id)}`;
7
+
8
+ const parts: string[] = [];
9
+ let current: Element | null = el;
10
+ while (current && current !== document.body && current !== document.documentElement) {
11
+ const parent = current.parentElement;
12
+ if (!parent) break;
13
+ const children = Array.from(parent.children);
14
+ const index = children.indexOf(current) + 1;
15
+ const tag = current.tagName.toLowerCase();
16
+ parts.unshift(`${tag}:nth-child(${index})`);
17
+ current = parent;
18
+ // Keep path reasonable
19
+ if (parts.length > 5) break;
20
+ }
21
+ return parts.join(' > ');
22
+ }
@@ -0,0 +1,10 @@
1
+ import { initI18n } from './core/i18n.js';
2
+ import { VibeFeedback } from './core/sdk.js';
3
+
4
+ // Detect language before anything renders
5
+ initI18n();
6
+
7
+ const instance = new VibeFeedback();
8
+
9
+ // Expose globally for potential programmatic use
10
+ (window as any).__vibeFeedback = instance;