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
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "yo-bug",
3
+ "version": "0.1.0",
4
+ "description": "MCP Server for visual test feedback in vibe coding — QA capability as a protocol",
5
+ "type": "module",
6
+ "bin": {
7
+ "yo-bug": "./dist/bin/cli.js"
8
+ },
9
+ "main": "./dist/src/mcp/index.js",
10
+ "files": [
11
+ "dist/",
12
+ "bin/",
13
+ "src/",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build:client": "vite build",
19
+ "build:server": "tsc -p tsconfig.server.json",
20
+ "build": "npm run build:client && npm run build:server",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "vibe-coding",
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "feedback",
28
+ "testing",
29
+ "qa",
30
+ "ai-agent",
31
+ "claude-code",
32
+ "cursor",
33
+ "windsurf",
34
+ "visual-testing",
35
+ "bug-report"
36
+ ],
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": ""
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.12.1",
47
+ "cors": "^2.8.5",
48
+ "express": "^4.21.2",
49
+ "html2canvas": "^1.4.1",
50
+ "http-proxy": "^1.18.1",
51
+ "multer": "^1.4.5-lts.1",
52
+ "open": "^10.1.0",
53
+ "uuid": "^11.1.0",
54
+ "zod": "^3.24.4"
55
+ },
56
+ "devDependencies": {
57
+ "@types/cors": "^2.8.17",
58
+ "@types/express": "^5.0.0",
59
+ "@types/http-proxy": "^1.17.16",
60
+ "@types/multer": "^1.4.12",
61
+ "@types/node": "^22.14.0",
62
+ "@types/uuid": "^10.0.0",
63
+ "tsx": "^4.19.3",
64
+ "typescript": "^5.8.3",
65
+ "vite": "^6.3.1"
66
+ }
67
+ }
@@ -0,0 +1,178 @@
1
+ import { History } from './history.js';
2
+ import { Toolbar, type ToolType } from './toolbar.js';
3
+ import { BaseTool } from './tools/base-tool.js';
4
+ import { ArrowTool } from './tools/arrow.js';
5
+ import { RectTool } from './tools/rect.js';
6
+ import { CircleTool } from './tools/circle.js';
7
+ import { FreehandTool } from './tools/freehand.js';
8
+ import { TextTool } from './tools/text.js';
9
+
10
+ export class CanvasEditor {
11
+ private container: HTMLDivElement;
12
+ private wrapper: HTMLDivElement;
13
+ private bgCanvas: HTMLCanvasElement;
14
+ private drawCanvas: HTMLCanvasElement;
15
+ private drawCtx: CanvasRenderingContext2D;
16
+ private toolbar: Toolbar;
17
+ private history: History;
18
+ private currentTool: BaseTool;
19
+ private tools: Record<ToolType, BaseTool>;
20
+ private color = '#ef4444';
21
+ private lineWidth = 3;
22
+
23
+ constructor(
24
+ /** The cropped region image */
25
+ regionCanvas: HTMLCanvasElement,
26
+ private onDone: (dataUrl: string) => void,
27
+ private onCancel: () => void
28
+ ) {
29
+ const w = regionCanvas.width;
30
+ const h = regionCanvas.height;
31
+
32
+ // Overlay container — dark background, region image centered
33
+ this.container = document.createElement('div');
34
+ this.container.style.cssText = `
35
+ position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
36
+ z-index: 2147483646; background: rgba(0,0,0,0.75);
37
+ display: flex; align-items: center; justify-content: center;
38
+ `;
39
+
40
+ // Canvas wrapper — sized to the region, not the full screen
41
+ this.wrapper = document.createElement('div');
42
+ this.wrapper.style.cssText = `
43
+ position: relative;
44
+ max-width: 90vw;
45
+ max-height: 80vh;
46
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
47
+ border-radius: 8px;
48
+ overflow: hidden;
49
+ `;
50
+
51
+ // Background canvas (cropped region screenshot)
52
+ this.bgCanvas = document.createElement('canvas');
53
+ this.bgCanvas.width = w;
54
+ this.bgCanvas.height = h;
55
+ this.bgCanvas.style.cssText = `display: block; max-width: 90vw; max-height: 80vh; object-fit: contain;`;
56
+ // Scale canvas display size to fit viewport
57
+ const maxW = window.innerWidth * 0.9;
58
+ const maxH = window.innerHeight * 0.8;
59
+ const scale = Math.min(1, maxW / w, maxH / h);
60
+ const displayW = w * scale;
61
+ const displayH = h * scale;
62
+ this.bgCanvas.style.width = `${displayW}px`;
63
+ this.bgCanvas.style.height = `${displayH}px`;
64
+
65
+ const bgCtx = this.bgCanvas.getContext('2d')!;
66
+ bgCtx.drawImage(regionCanvas, 0, 0);
67
+
68
+ // Drawing canvas (on top, same size)
69
+ this.drawCanvas = document.createElement('canvas');
70
+ this.drawCanvas.width = w;
71
+ this.drawCanvas.height = h;
72
+ this.drawCanvas.style.cssText = `
73
+ position: absolute; top: 0; left: 0;
74
+ width: ${displayW}px; height: ${displayH}px;
75
+ cursor: crosshair;
76
+ `;
77
+ this.drawCtx = this.drawCanvas.getContext('2d')!;
78
+
79
+ this.wrapper.style.width = `${displayW}px`;
80
+ this.wrapper.style.height = `${displayH}px`;
81
+ this.wrapper.appendChild(this.bgCanvas);
82
+ this.wrapper.appendChild(this.drawCanvas);
83
+ this.container.appendChild(this.wrapper);
84
+
85
+ // History
86
+ this.history = new History(this.drawCtx);
87
+
88
+ // Tools
89
+ this.tools = {
90
+ arrow: new ArrowTool(this.drawCtx, this.color, this.lineWidth),
91
+ rect: new RectTool(this.drawCtx, this.color, this.lineWidth),
92
+ circle: new CircleTool(this.drawCtx, this.color, this.lineWidth),
93
+ freehand: new FreehandTool(this.drawCtx, this.color, this.lineWidth),
94
+ text: new TextTool(this.drawCtx, this.color, this.lineWidth, this.wrapper),
95
+ };
96
+ this.currentTool = this.tools.arrow;
97
+
98
+ // Toolbar
99
+ this.toolbar = new Toolbar(this.container, {
100
+ onToolSelect: (type) => {
101
+ this.currentTool = this.tools[type];
102
+ },
103
+ onColorChange: (color) => {
104
+ this.color = color;
105
+ Object.values(this.tools).forEach((t) => t.setColor(color));
106
+ },
107
+ onUndo: () => this.history.undo(),
108
+ onDone: () => this.finish(),
109
+ onCancel: () => this.cancel(),
110
+ });
111
+
112
+ // Mouse events
113
+ this.drawCanvas.addEventListener('mousedown', (e) => this.handleMouse(e, 'down'));
114
+ this.drawCanvas.addEventListener('mousemove', (e) => this.handleMouse(e, 'move'));
115
+ this.drawCanvas.addEventListener('mouseup', (e) => this.handleMouse(e, 'up'));
116
+
117
+ // Keyboard
118
+ this.keyHandler = this.keyHandler.bind(this);
119
+ document.addEventListener('keydown', this.keyHandler);
120
+ }
121
+
122
+ show(): void {
123
+ document.body.appendChild(this.container);
124
+ }
125
+
126
+ private handleMouse(e: MouseEvent, phase: 'down' | 'move' | 'up'): void {
127
+ const rect = this.drawCanvas.getBoundingClientRect();
128
+ const scaleX = this.drawCanvas.width / rect.width;
129
+ const scaleY = this.drawCanvas.height / rect.height;
130
+ // Canvas coordinates (for drawing)
131
+ const x = (e.clientX - rect.left) * scaleX;
132
+ const y = (e.clientY - rect.top) * scaleY;
133
+ // CSS coordinates relative to wrapper (for DOM positioning, e.g. text input)
134
+ const cssX = e.clientX - rect.left;
135
+ const cssY = e.clientY - rect.top;
136
+
137
+ if (phase === 'down') {
138
+ this.history.push();
139
+ this.currentTool.onMouseDown(x, y, cssX, cssY);
140
+ } else if (phase === 'move') {
141
+ this.currentTool.onMouseMove(x, y, cssX, cssY);
142
+ } else {
143
+ this.currentTool.onMouseUp(x, y, cssX, cssY);
144
+ }
145
+ }
146
+
147
+ private keyHandler(e: KeyboardEvent): void {
148
+ if (e.key === 'Escape') {
149
+ this.cancel();
150
+ } else if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
151
+ e.preventDefault();
152
+ this.history.undo();
153
+ }
154
+ }
155
+
156
+ private finish(): void {
157
+ const merged = document.createElement('canvas');
158
+ merged.width = this.bgCanvas.width;
159
+ merged.height = this.bgCanvas.height;
160
+ const ctx = merged.getContext('2d')!;
161
+ ctx.drawImage(this.bgCanvas, 0, 0);
162
+ ctx.drawImage(this.drawCanvas, 0, 0);
163
+ const dataUrl = merged.toDataURL('image/png');
164
+ this.destroy();
165
+ this.onDone(dataUrl);
166
+ }
167
+
168
+ private cancel(): void {
169
+ this.destroy();
170
+ this.onCancel();
171
+ }
172
+
173
+ private destroy(): void {
174
+ document.removeEventListener('keydown', this.keyHandler);
175
+ this.toolbar.destroy();
176
+ this.container.remove();
177
+ }
178
+ }
@@ -0,0 +1,32 @@
1
+ export class History {
2
+ private stack: ImageData[] = [];
3
+ private ctx: CanvasRenderingContext2D;
4
+ private maxSize = 30;
5
+
6
+ constructor(ctx: CanvasRenderingContext2D) {
7
+ this.ctx = ctx;
8
+ }
9
+
10
+ push(): void {
11
+ const data = this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
12
+ this.stack.push(data);
13
+ if (this.stack.length > this.maxSize) this.stack.shift();
14
+ }
15
+
16
+ undo(): boolean {
17
+ const prev = this.stack.pop();
18
+ if (prev) {
19
+ this.ctx.putImageData(prev, 0, 0);
20
+ return true;
21
+ }
22
+ return false;
23
+ }
24
+
25
+ get canUndo(): boolean {
26
+ return this.stack.length > 0;
27
+ }
28
+
29
+ clear(): void {
30
+ this.stack = [];
31
+ }
32
+ }
@@ -0,0 +1,123 @@
1
+ import { t } from '../core/i18n.js';
2
+
3
+ /**
4
+ * Region selector — WeChat-style screenshot selection
5
+ */
6
+ export class RegionSelector {
7
+ private overlay: HTMLDivElement;
8
+ private selectionBox: HTMLDivElement;
9
+ private startX = 0;
10
+ private startY = 0;
11
+ private selecting = false;
12
+ private hint: HTMLDivElement;
13
+
14
+ constructor(
15
+ private onSelect: (rect: { x: number; y: number; w: number; h: number }) => void,
16
+ private onCancel: () => void
17
+ ) {
18
+ // Dark overlay covering entire screen
19
+ this.overlay = document.createElement('div');
20
+ this.overlay.style.cssText = `
21
+ position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
22
+ z-index: 2147483646; cursor: crosshair;
23
+ background: rgba(0,0,0,0.35);
24
+ `;
25
+
26
+ // Hint text
27
+ this.hint = document.createElement('div');
28
+ this.hint.style.cssText = `
29
+ position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
30
+ color: white; font-size: 16px; font-weight: 500;
31
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
32
+ text-shadow: 0 2px 8px rgba(0,0,0,0.5);
33
+ pointer-events: none;
34
+ opacity: 0.9;
35
+ `;
36
+ this.hint.textContent = t('region.hint');
37
+ this.overlay.appendChild(this.hint);
38
+
39
+ // Selection rectangle
40
+ this.selectionBox = document.createElement('div');
41
+ this.selectionBox.style.cssText = `
42
+ position: fixed;
43
+ border: 2px solid #3b82f6;
44
+ background: rgba(59,130,246,0.08);
45
+ box-shadow: 0 0 0 9999px rgba(0,0,0,0.35);
46
+ display: none;
47
+ z-index: 2147483647;
48
+ `;
49
+
50
+ this.overlay.addEventListener('mousedown', (e) => this.onMouseDown(e));
51
+ this.overlay.addEventListener('mousemove', (e) => this.onMouseMove(e));
52
+ this.overlay.addEventListener('mouseup', (e) => this.onMouseUp(e));
53
+ this.keyHandler = this.keyHandler.bind(this);
54
+ document.addEventListener('keydown', this.keyHandler);
55
+ }
56
+
57
+ show(): void {
58
+ document.body.appendChild(this.overlay);
59
+ document.body.appendChild(this.selectionBox);
60
+ }
61
+
62
+ private onMouseDown(e: MouseEvent): void {
63
+ this.selecting = true;
64
+ this.startX = e.clientX;
65
+ this.startY = e.clientY;
66
+ this.hint.style.display = 'none';
67
+ this.overlay.style.background = 'transparent'; // Remove overlay bg, use box-shadow instead
68
+ this.selectionBox.style.display = 'block';
69
+ this.updateBox(e.clientX, e.clientY);
70
+ }
71
+
72
+ private onMouseMove(e: MouseEvent): void {
73
+ if (!this.selecting) return;
74
+ this.updateBox(e.clientX, e.clientY);
75
+ }
76
+
77
+ private onMouseUp(e: MouseEvent): void {
78
+ if (!this.selecting) return;
79
+ this.selecting = false;
80
+
81
+ const rect = this.getRect(e.clientX, e.clientY);
82
+
83
+ // Minimum size check — ignore accidental clicks
84
+ if (rect.w < 20 || rect.h < 20) {
85
+ this.selectionBox.style.display = 'none';
86
+ this.overlay.style.background = 'rgba(0,0,0,0.35)';
87
+ this.hint.style.display = '';
88
+ return;
89
+ }
90
+
91
+ this.destroy();
92
+ this.onSelect(rect);
93
+ }
94
+
95
+ private updateBox(currentX: number, currentY: number): void {
96
+ const rect = this.getRect(currentX, currentY);
97
+ this.selectionBox.style.left = `${rect.x}px`;
98
+ this.selectionBox.style.top = `${rect.y}px`;
99
+ this.selectionBox.style.width = `${rect.w}px`;
100
+ this.selectionBox.style.height = `${rect.h}px`;
101
+ }
102
+
103
+ private getRect(currentX: number, currentY: number) {
104
+ const x = Math.min(this.startX, currentX);
105
+ const y = Math.min(this.startY, currentY);
106
+ const w = Math.abs(currentX - this.startX);
107
+ const h = Math.abs(currentY - this.startY);
108
+ return { x, y, w, h };
109
+ }
110
+
111
+ private keyHandler(e: KeyboardEvent): void {
112
+ if (e.key === 'Escape') {
113
+ this.destroy();
114
+ this.onCancel();
115
+ }
116
+ }
117
+
118
+ private destroy(): void {
119
+ document.removeEventListener('keydown', this.keyHandler);
120
+ this.overlay.remove();
121
+ this.selectionBox.remove();
122
+ }
123
+ }
@@ -0,0 +1,17 @@
1
+ import html2canvas from 'html2canvas';
2
+
3
+ export async function captureScreenshot(): Promise<HTMLCanvasElement> {
4
+ const host = document.getElementById('vibe-feedback-host');
5
+ if (host) host.style.display = 'none';
6
+
7
+ try {
8
+ return await html2canvas(document.body, {
9
+ useCORS: true,
10
+ allowTaint: false,
11
+ scale: Math.min(window.devicePixelRatio, 2),
12
+ logging: false,
13
+ });
14
+ } finally {
15
+ if (host) host.style.display = '';
16
+ }
17
+ }
@@ -0,0 +1,139 @@
1
+ export type ToolType = 'arrow' | 'rect' | 'circle' | 'freehand' | 'text';
2
+
3
+ const TOOLS: { type: ToolType; icon: string; label: string }[] = [
4
+ { type: 'arrow', icon: '\u2197', label: 'Arrow' },
5
+ { type: 'rect', icon: '\u25a1', label: 'Rect' },
6
+ { type: 'circle', icon: '\u25cb', label: 'Circle' },
7
+ { type: 'freehand', icon: '\u270e', label: 'Draw' },
8
+ { type: 'text', icon: 'T', label: 'Text' },
9
+ ];
10
+
11
+ const COLORS = ['#ef4444', '#f59e0b', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899', '#000000', '#ffffff'];
12
+
13
+ export class Toolbar {
14
+ private container: HTMLDivElement;
15
+ private toolBtns: HTMLButtonElement[] = [];
16
+
17
+ constructor(
18
+ parent: HTMLElement,
19
+ private callbacks: {
20
+ onToolSelect: (tool: ToolType) => void;
21
+ onColorChange: (color: string) => void;
22
+ onUndo: () => void;
23
+ onDone: () => void;
24
+ onCancel: () => void;
25
+ }
26
+ ) {
27
+ this.container = document.createElement('div');
28
+ this.container.style.cssText = `
29
+ position: absolute;
30
+ top: 12px;
31
+ left: 50%;
32
+ transform: translateX(-50%);
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 6px;
36
+ background: #1e293b;
37
+ padding: 8px 12px;
38
+ border-radius: 10px;
39
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4);
40
+ z-index: 10;
41
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
42
+ `;
43
+
44
+ // Tool buttons
45
+ for (const tool of TOOLS) {
46
+ const btn = this.createBtn(tool.icon, tool.label, () => {
47
+ this.selectTool(tool.type);
48
+ callbacks.onToolSelect(tool.type);
49
+ });
50
+ this.toolBtns.push(btn);
51
+ this.container.appendChild(btn);
52
+ }
53
+
54
+ this.addSeparator();
55
+
56
+ // Color buttons
57
+ for (const color of COLORS) {
58
+ const btn = document.createElement('button');
59
+ btn.style.cssText = `
60
+ width: 22px; height: 22px; border-radius: 50%; border: 2px solid transparent;
61
+ background: ${color}; cursor: pointer; padding: 0;
62
+ ${color === '#ffffff' ? 'border-color: #475569;' : ''}
63
+ `;
64
+ btn.addEventListener('click', () => {
65
+ // Deselect all colors, select this one
66
+ this.container.querySelectorAll<HTMLElement>('[data-color]').forEach(
67
+ (el) => (el.style.borderColor = el.dataset.color === '#ffffff' ? '#475569' : 'transparent')
68
+ );
69
+ btn.style.borderColor = '#60a5fa';
70
+ callbacks.onColorChange(color);
71
+ });
72
+ btn.dataset.color = color;
73
+ this.container.appendChild(btn);
74
+ }
75
+
76
+ this.addSeparator();
77
+
78
+ // Undo
79
+ this.container.appendChild(
80
+ this.createBtn('\u21a9', 'Undo', callbacks.onUndo)
81
+ );
82
+
83
+ this.addSeparator();
84
+
85
+ // Done & Cancel
86
+ const doneBtn = this.createBtn('\u2713', 'Done', callbacks.onDone);
87
+ doneBtn.style.background = '#22c55e';
88
+ doneBtn.style.color = 'white';
89
+ this.container.appendChild(doneBtn);
90
+
91
+ const cancelBtn = this.createBtn('\u2715', 'Cancel', callbacks.onCancel);
92
+ cancelBtn.style.background = '#ef4444';
93
+ cancelBtn.style.color = 'white';
94
+ this.container.appendChild(cancelBtn);
95
+
96
+ parent.appendChild(this.container);
97
+
98
+ // Default selection
99
+ this.selectTool('arrow');
100
+ }
101
+
102
+ private createBtn(icon: string, title: string, onClick: () => void): HTMLButtonElement {
103
+ const btn = document.createElement('button');
104
+ btn.textContent = icon;
105
+ btn.title = title;
106
+ btn.style.cssText = `
107
+ width: 34px; height: 34px; border: none; border-radius: 6px;
108
+ background: transparent; color: #94a3b8; font-size: 16px;
109
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
110
+ `;
111
+ btn.addEventListener('mouseenter', () => {
112
+ if (!btn.classList.contains('tool-active')) btn.style.background = '#334155';
113
+ });
114
+ btn.addEventListener('mouseleave', () => {
115
+ if (!btn.classList.contains('tool-active')) btn.style.background = 'transparent';
116
+ });
117
+ btn.addEventListener('click', onClick);
118
+ return btn;
119
+ }
120
+
121
+ private addSeparator(): void {
122
+ const sep = document.createElement('div');
123
+ sep.style.cssText = 'width:1px; height:24px; background:#334155; margin:0 4px;';
124
+ this.container.appendChild(sep);
125
+ }
126
+
127
+ private selectTool(type: ToolType): void {
128
+ this.toolBtns.forEach((btn, i) => {
129
+ const isActive = TOOLS[i].type === type;
130
+ btn.classList.toggle('tool-active', isActive);
131
+ btn.style.background = isActive ? '#3b82f6' : 'transparent';
132
+ btn.style.color = isActive ? 'white' : '#94a3b8';
133
+ });
134
+ }
135
+
136
+ destroy(): void {
137
+ this.container.remove();
138
+ }
139
+ }
@@ -0,0 +1,57 @@
1
+ import { BaseTool } from './base-tool.js';
2
+
3
+ export class ArrowTool extends BaseTool {
4
+ private startX = 0;
5
+ private startY = 0;
6
+ private snapshot: ImageData | null = null;
7
+
8
+ onMouseDown(x: number, y: number): void {
9
+ this.drawing = true;
10
+ this.startX = x;
11
+ this.startY = y;
12
+ this.snapshot = this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
13
+ }
14
+
15
+ onMouseMove(x: number, y: number): void {
16
+ if (!this.drawing || !this.snapshot) return;
17
+ this.ctx.putImageData(this.snapshot, 0, 0);
18
+ this.drawArrow(this.startX, this.startY, x, y);
19
+ }
20
+
21
+ onMouseUp(x: number, y: number): void {
22
+ if (!this.drawing) return;
23
+ this.onMouseMove(x, y);
24
+ this.drawing = false;
25
+ this.snapshot = null;
26
+ }
27
+
28
+ private drawArrow(x1: number, y1: number, x2: number, y2: number): void {
29
+ const headLen = Math.max(15, this.lineWidth * 5);
30
+ const angle = Math.atan2(y2 - y1, x2 - x1);
31
+
32
+ this.ctx.strokeStyle = this.color;
33
+ this.ctx.fillStyle = this.color;
34
+ this.ctx.lineWidth = this.lineWidth;
35
+ this.ctx.lineCap = 'round';
36
+
37
+ // Line
38
+ this.ctx.beginPath();
39
+ this.ctx.moveTo(x1, y1);
40
+ this.ctx.lineTo(x2, y2);
41
+ this.ctx.stroke();
42
+
43
+ // Arrowhead
44
+ this.ctx.beginPath();
45
+ this.ctx.moveTo(x2, y2);
46
+ this.ctx.lineTo(
47
+ x2 - headLen * Math.cos(angle - Math.PI / 6),
48
+ y2 - headLen * Math.sin(angle - Math.PI / 6)
49
+ );
50
+ this.ctx.lineTo(
51
+ x2 - headLen * Math.cos(angle + Math.PI / 6),
52
+ y2 - headLen * Math.sin(angle + Math.PI / 6)
53
+ );
54
+ this.ctx.closePath();
55
+ this.ctx.fill();
56
+ }
57
+ }
@@ -0,0 +1,25 @@
1
+ export abstract class BaseTool {
2
+ protected ctx: CanvasRenderingContext2D;
3
+ protected color: string;
4
+ protected lineWidth: number;
5
+ protected drawing = false;
6
+
7
+ constructor(ctx: CanvasRenderingContext2D, color: string, lineWidth: number) {
8
+ this.ctx = ctx;
9
+ this.color = color;
10
+ this.lineWidth = lineWidth;
11
+ }
12
+
13
+ /** x,y = canvas coords; cssX,cssY = CSS coords relative to wrapper */
14
+ abstract onMouseDown(x: number, y: number, cssX?: number, cssY?: number): void;
15
+ abstract onMouseMove(x: number, y: number, cssX?: number, cssY?: number): void;
16
+ abstract onMouseUp(x: number, y: number, cssX?: number, cssY?: number): void;
17
+
18
+ setColor(color: string): void {
19
+ this.color = color;
20
+ }
21
+
22
+ setLineWidth(width: number): void {
23
+ this.lineWidth = width;
24
+ }
25
+ }
@@ -0,0 +1,37 @@
1
+ import { BaseTool } from './base-tool.js';
2
+
3
+ export class CircleTool extends BaseTool {
4
+ private startX = 0;
5
+ private startY = 0;
6
+ private snapshot: ImageData | null = null;
7
+
8
+ onMouseDown(x: number, y: number): void {
9
+ this.drawing = true;
10
+ this.startX = x;
11
+ this.startY = y;
12
+ this.snapshot = this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
13
+ }
14
+
15
+ onMouseMove(x: number, y: number): void {
16
+ if (!this.drawing || !this.snapshot) return;
17
+ this.ctx.putImageData(this.snapshot, 0, 0);
18
+
19
+ const rx = Math.abs(x - this.startX) / 2;
20
+ const ry = Math.abs(y - this.startY) / 2;
21
+ const cx = (this.startX + x) / 2;
22
+ const cy = (this.startY + y) / 2;
23
+
24
+ this.ctx.beginPath();
25
+ this.ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
26
+ this.ctx.strokeStyle = this.color;
27
+ this.ctx.lineWidth = this.lineWidth;
28
+ this.ctx.stroke();
29
+ }
30
+
31
+ onMouseUp(x: number, y: number): void {
32
+ if (!this.drawing) return;
33
+ this.onMouseMove(x, y);
34
+ this.drawing = false;
35
+ this.snapshot = null;
36
+ }
37
+ }