sonance-brand-mcp 1.3.93 → 1.3.95

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.
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import React, { useState, useEffect, useCallback, useRef } from "react";
4
- import { Loader2, Send, Sparkles, Eye, AlertCircle, X } from "lucide-react";
4
+ import { Loader2, Send, Sparkles, Eye, AlertCircle, X, Crop } from "lucide-react";
5
5
  import { cn } from "../../../lib/utils";
6
6
  import { ChatMessage, AIEditResult, PendingEdit, VisionFocusedElement, VisionPendingEdit, ApplyFirstSession } from "../types";
7
7
  import html2canvas from "html2canvas-pro";
8
+ import { ScreenshotAnnotator, Rectangle } from "./ScreenshotAnnotator";
8
9
 
9
10
  // Helper to detect location failure in explanation
10
11
  function isLocationFailure(explanation: string | undefined): boolean {
@@ -108,6 +109,13 @@ export function ChatInterface({
108
109
  const messagesEndRef = useRef<HTMLDivElement>(null);
109
110
  const inputRef = useRef<HTMLInputElement>(null);
110
111
 
112
+ // Screenshot annotation state
113
+ const [isAnnotating, setIsAnnotating] = useState(false);
114
+ const [annotatedScreenshot, setAnnotatedScreenshot] = useState<string | null>(null);
115
+ const [manualFocusBounds, setManualFocusBounds] = useState<Rectangle | null>(null);
116
+ // Discovered elements from annotation tool (for targeting when no element was clicked)
117
+ const [annotationDiscoveredElements, setAnnotationDiscoveredElements] = useState<VisionFocusedElement[]>([]);
118
+
111
119
  // Auto-dismiss toast after 5 seconds
112
120
  useEffect(() => {
113
121
  if (toastMessage) {
@@ -164,14 +172,61 @@ export function ChatInterface({
164
172
  }
165
173
  }, []);
166
174
 
175
+ // Start screenshot annotation - show drawing overlay on live app
176
+ const startAnnotation = useCallback(() => {
177
+ console.log("[Vision Mode] Starting screenshot annotation overlay...");
178
+ setIsAnnotating(true);
179
+ }, []);
180
+
181
+ // Handle annotation confirmation - screenshot is already captured and annotated
182
+ // Now also receives discovered elements from within the drawn rectangle
183
+ const handleAnnotationConfirm = useCallback((annotated: string, bounds: Rectangle, discoveredElements: VisionFocusedElement[]) => {
184
+ console.log("[Vision Mode] Annotation confirmed:", {
185
+ bounds,
186
+ discoveredElementsCount: discoveredElements.length,
187
+ discoveredElements: discoveredElements.map(e => ({
188
+ name: e.name,
189
+ text: e.textContent?.substring(0, 30),
190
+ id: e.elementId,
191
+ })),
192
+ });
193
+ setAnnotatedScreenshot(annotated);
194
+ setManualFocusBounds(bounds);
195
+ setAnnotationDiscoveredElements(discoveredElements);
196
+ setIsAnnotating(false);
197
+ // Focus the input so user can type their prompt
198
+ setTimeout(() => inputRef.current?.focus(), 100);
199
+ }, []);
200
+
201
+ // Handle annotation cancel
202
+ const handleAnnotationCancel = useCallback(() => {
203
+ setIsAnnotating(false);
204
+ }, []);
205
+
206
+ // Clear the current annotation and discovered elements
207
+ const clearAnnotation = useCallback(() => {
208
+ setAnnotatedScreenshot(null);
209
+ setManualFocusBounds(null);
210
+ setAnnotationDiscoveredElements([]);
211
+ }, []);
212
+
167
213
  // Handle vision mode edit request
168
214
  const handleVisionEdit = async (prompt: string) => {
169
215
  // Use Apply-First mode if callback is provided (new Cursor-style workflow)
170
216
  const useApplyFirst = !!onApplyFirstComplete;
171
217
 
218
+ // Determine which focused elements to use:
219
+ // - If user clicked an element, use visionFocusedElements (passed from parent)
220
+ // - If user used annotation tool without clicking, use annotationDiscoveredElements
221
+ const effectiveFocusedElements = visionFocusedElements.length > 0
222
+ ? visionFocusedElements
223
+ : annotationDiscoveredElements;
224
+
172
225
  console.log("[Vision Mode] Starting edit request:", {
173
226
  prompt,
174
- focusedElements: visionFocusedElements.length,
227
+ focusedElementsFromClick: visionFocusedElements.length,
228
+ focusedElementsFromAnnotation: annotationDiscoveredElements.length,
229
+ effectiveFocusedElements: effectiveFocusedElements.length,
175
230
  mode: useApplyFirst ? "apply-first" : "preview-first"
176
231
  });
177
232
 
@@ -188,28 +243,49 @@ export function ChatInterface({
188
243
  setIsProcessing(true);
189
244
 
190
245
  try {
191
- // Capture screenshot
192
- console.log("[Vision Mode] Capturing screenshot...");
193
- const rawScreenshot = await captureScreenshot();
194
- console.log("[Vision Mode] Screenshot captured:", rawScreenshot ? `${rawScreenshot.length} bytes` : "null");
195
-
196
- // Annotate screenshot with section highlight if parent section exists
197
- // This helps the LLM visually identify the target area for modifications
198
- let screenshot = rawScreenshot;
199
- if (rawScreenshot && visionFocusedElements.length > 0) {
200
- const parentSection = visionFocusedElements[0].parentSection;
201
- if (parentSection?.coordinates) {
202
- screenshot = await drawSectionHighlight(rawScreenshot, parentSection.coordinates);
203
- console.log("[Vision Mode] Added section highlight to screenshot:", {
204
- sectionType: parentSection.type,
205
- sectionText: parentSection.sectionText?.substring(0, 30),
206
- });
246
+ let screenshot: string | null;
247
+
248
+ // PRIORITY 1: Use manually annotated screenshot if available
249
+ // This is when user drew a focus area using the annotation tool
250
+ if (annotatedScreenshot) {
251
+ console.log("[Vision Mode] Using manually annotated screenshot with discovered elements:", {
252
+ discoveredCount: annotationDiscoveredElements.length,
253
+ elements: annotationDiscoveredElements.slice(0, 3).map(e => ({
254
+ name: e.name,
255
+ text: e.textContent?.substring(0, 20),
256
+ id: e.elementId,
257
+ })),
258
+ });
259
+ screenshot = annotatedScreenshot;
260
+ // Clear the annotation after use (but keep discovered elements for the API call)
261
+ setAnnotatedScreenshot(null);
262
+ setManualFocusBounds(null);
263
+ } else {
264
+ // PRIORITY 2: Capture fresh screenshot and auto-annotate with section highlight
265
+ console.log("[Vision Mode] Capturing screenshot...");
266
+ const rawScreenshot = await captureScreenshot();
267
+ console.log("[Vision Mode] Screenshot captured:", rawScreenshot ? `${rawScreenshot.length} bytes` : "null");
268
+
269
+ // Annotate screenshot with section highlight if parent section exists
270
+ // This helps the LLM visually identify the target area for modifications
271
+ screenshot = rawScreenshot;
272
+ if (rawScreenshot && effectiveFocusedElements.length > 0) {
273
+ const parentSection = effectiveFocusedElements[0].parentSection;
274
+ if (parentSection?.coordinates) {
275
+ screenshot = await drawSectionHighlight(rawScreenshot, parentSection.coordinates);
276
+ console.log("[Vision Mode] Added section highlight to screenshot:", {
277
+ sectionType: parentSection.type,
278
+ sectionText: parentSection.sectionText?.substring(0, 30),
279
+ });
280
+ }
207
281
  }
208
282
  }
209
283
 
210
284
  // Choose API endpoint based on mode
211
285
  const endpoint = useApplyFirst ? "/api/sonance-vision-apply" : "/api/sonance-vision-edit";
212
- console.log("[Vision Mode] Sending to API:", endpoint);
286
+ console.log("[Vision Mode] Sending to API:", endpoint, {
287
+ effectiveFocusedElements: effectiveFocusedElements.length,
288
+ });
213
289
 
214
290
  const response = await fetch(endpoint, {
215
291
  method: "POST",
@@ -220,9 +296,13 @@ export function ChatInterface({
220
296
  screenshot,
221
297
  pageRoute: window.location.pathname,
222
298
  userPrompt: prompt,
223
- focusedElements: visionFocusedElements,
299
+ // Use effective focused elements (from click OR from annotation discovery)
300
+ focusedElements: effectiveFocusedElements,
224
301
  }),
225
302
  });
303
+
304
+ // Clear annotation discovered elements after API call
305
+ setAnnotationDiscoveredElements([]);
226
306
 
227
307
  const data = await response.json();
228
308
  console.log("[Vision Mode] API response:", {
@@ -595,6 +675,26 @@ export function ChatInterface({
595
675
  "disabled:opacity-50 disabled:bg-gray-50"
596
676
  )}
597
677
  />
678
+
679
+ {/* Annotate screenshot button - only in vision mode */}
680
+ {visionMode && (
681
+ <button
682
+ onClick={startAnnotation}
683
+ onPointerDown={(e) => e.stopPropagation()}
684
+ disabled={isProcessing}
685
+ title="Draw on screenshot to focus AI attention"
686
+ className={cn(
687
+ "px-3 py-2 rounded transition-colors",
688
+ annotatedScreenshot
689
+ ? "bg-[#00D3C8] text-[#1a1a1a]" // Teal when annotation is active
690
+ : "bg-gray-100 text-gray-600 hover:bg-gray-200",
691
+ "disabled:opacity-50 disabled:cursor-not-allowed"
692
+ )}
693
+ >
694
+ <Crop className="h-4 w-4" />
695
+ </button>
696
+ )}
697
+
598
698
  <button
599
699
  onClick={() => handleSend(input || inputRef.current?.value || "")}
600
700
  onPointerDown={(e) => e.stopPropagation()}
@@ -614,6 +714,23 @@ export function ChatInterface({
614
714
  )}
615
715
  </button>
616
716
  </div>
717
+
718
+ {/* Annotation indicator */}
719
+ {annotatedScreenshot && visionMode && (
720
+ <div className="flex items-center justify-between text-xs text-[#00D3C8] bg-[#00D3C8]/10 px-2 py-1 rounded">
721
+ <span className="flex items-center gap-1">
722
+ <Crop className="h-3 w-3" />
723
+ Focus area selected - your prompt will target this region
724
+ </span>
725
+ <button
726
+ onClick={clearAnnotation}
727
+ className="text-[#00D3C8] hover:text-[#00b3a8] p-0.5"
728
+ title="Clear annotation"
729
+ >
730
+ <X className="h-3 w-3" />
731
+ </button>
732
+ </div>
733
+ )}
617
734
 
618
735
  {/* Processing Indicator */}
619
736
  {isProcessing && (
@@ -629,6 +746,14 @@ export function ChatInterface({
629
746
  </span>
630
747
  </div>
631
748
  )}
749
+
750
+ {/* Screenshot Annotator Overlay - draws on live app */}
751
+ {isAnnotating && (
752
+ <ScreenshotAnnotator
753
+ onConfirm={handleAnnotationConfirm}
754
+ onCancel={handleAnnotationCancel}
755
+ />
756
+ )}
632
757
  </div>
633
758
  );
634
759
  }
@@ -88,11 +88,14 @@ export function InspectorOverlay({
88
88
  const clickX = e.clientX;
89
89
  const clickY = e.clientY;
90
90
 
91
- // Don't capture clicks on the DevTools panel or its portal container
92
- const devToolsPanel = document.querySelector('[data-sonance-devtools="true"]');
91
+ // Don't capture clicks on ANY DevTools element (panel, annotation overlay, etc.)
92
+ // Check if the click target or any of its ancestors have data-sonance-devtools
93
+ const target = e.target as HTMLElement;
94
+ if (target.closest('[data-sonance-devtools="true"]')) return;
95
+ if (target.closest('[data-annotator-overlay="true"]')) return;
96
+
93
97
  const devToolsRoot = document.getElementById('sonance-devtools-root');
94
- if (devToolsPanel?.contains(e.target as Node)) return;
95
- if (devToolsRoot?.contains(e.target as Node)) return;
98
+ if (devToolsRoot?.contains(target)) return;
96
99
 
97
100
  // Find ALL elements that contain the click point
98
101
  const matchingElements = elements.filter(el =>
@@ -0,0 +1,516 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useState, useCallback } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { Check, X, RotateCcw, Crop } from "lucide-react";
6
+ import html2canvas from "html2canvas-pro";
7
+ import { VisionFocusedElement } from "../types";
8
+
9
+ export interface Rectangle {
10
+ x: number;
11
+ y: number;
12
+ width: number;
13
+ height: number;
14
+ }
15
+
16
+ /** Discovered element info for scoring and ranking */
17
+ interface DiscoveredElement {
18
+ element: Element;
19
+ score: number;
20
+ textContent: string;
21
+ className: string;
22
+ elementId: string;
23
+ tagName: string;
24
+ rect: DOMRect;
25
+ }
26
+
27
+ /**
28
+ * Discover DOM elements within the given rectangle bounds.
29
+ * Uses a grid sampling approach to find all elements in the area,
30
+ * then scores and ranks them for targeting accuracy.
31
+ */
32
+ function discoverElementsInBounds(rect: Rectangle): VisionFocusedElement[] {
33
+ const discoveredMap = new Map<Element, DiscoveredElement>();
34
+
35
+ // Tags to skip - generic containers and non-content elements
36
+ const skipTags = new Set(['html', 'body', 'head', 'script', 'style', 'meta', 'link', 'noscript']);
37
+
38
+ // Semantic elements get bonus points
39
+ const semanticElements = new Set(['section', 'article', 'form', 'header', 'footer', 'main', 'nav', 'aside', 'dialog']);
40
+
41
+ // Sample points in a grid pattern within the rectangle
42
+ const gridSize = 5; // 5x5 grid = 25 sample points
43
+ const stepX = rect.width / (gridSize + 1);
44
+ const stepY = rect.height / (gridSize + 1);
45
+
46
+ for (let i = 1; i <= gridSize; i++) {
47
+ for (let j = 1; j <= gridSize; j++) {
48
+ const x = rect.x + stepX * i;
49
+ const y = rect.y + stepY * j;
50
+
51
+ // Get all elements at this point (from top to bottom)
52
+ const elementsAtPoint = document.elementsFromPoint(x, y);
53
+
54
+ for (const el of elementsAtPoint) {
55
+ // Skip if already processed
56
+ if (discoveredMap.has(el)) continue;
57
+
58
+ // Skip DevTools elements
59
+ if (el.hasAttribute('data-sonance-devtools') ||
60
+ el.hasAttribute('data-annotator-overlay') ||
61
+ el.hasAttribute('data-annotator-toolbar') ||
62
+ el.hasAttribute('data-vision-mode-border')) {
63
+ continue;
64
+ }
65
+
66
+ const tagName = el.tagName.toLowerCase();
67
+
68
+ // Skip generic/non-content elements
69
+ if (skipTags.has(tagName)) continue;
70
+
71
+ // Get element info
72
+ const elRect = el.getBoundingClientRect();
73
+ const id = el.id || '';
74
+ const className = el.className && typeof el.className === 'string' ? el.className : '';
75
+
76
+ // Extract meaningful text content (not from children with their own text)
77
+ let textContent = '';
78
+ for (const node of el.childNodes) {
79
+ if (node.nodeType === Node.TEXT_NODE) {
80
+ const text = node.textContent?.trim();
81
+ if (text) textContent += text + ' ';
82
+ }
83
+ }
84
+ textContent = textContent.trim().substring(0, 100);
85
+
86
+ // If no direct text, try to get visible text from element
87
+ if (!textContent && el instanceof HTMLElement) {
88
+ // For inputs, use placeholder or value
89
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
90
+ textContent = el.placeholder || el.value || '';
91
+ } else if (el instanceof HTMLButtonElement || tagName === 'a') {
92
+ textContent = el.textContent?.trim().substring(0, 100) || '';
93
+ }
94
+ }
95
+
96
+ // Calculate score for ranking
97
+ let score = 0;
98
+
99
+ // ID is most valuable for targeting
100
+ if (id) score += 100;
101
+
102
+ // Text content helps identify the element
103
+ if (textContent) score += 50;
104
+
105
+ // Semantic elements are better targets
106
+ if (semanticElements.has(tagName)) score += 30;
107
+
108
+ // Interactive elements are often targets
109
+ if (['button', 'a', 'input', 'select', 'textarea'].includes(tagName)) score += 25;
110
+
111
+ // Component-like classNames (PascalCase patterns, not Tailwind utilities)
112
+ if (className) {
113
+ const classes = className.split(/\s+/);
114
+ const hasComponentClass = classes.some(c =>
115
+ /^[A-Z][a-zA-Z0-9]+/.test(c) || // PascalCase
116
+ /^[a-z]+-[a-z]+-/.test(c) // kebab-case with multiple segments (likely BEM)
117
+ );
118
+ if (hasComponentClass) score += 20;
119
+ }
120
+
121
+ // Heading elements are important
122
+ if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) score += 20;
123
+
124
+ // Penalize very large elements (probably containers)
125
+ if (elRect.width > window.innerWidth * 0.8 && elRect.height > window.innerHeight * 0.8) {
126
+ score -= 30;
127
+ }
128
+
129
+ discoveredMap.set(el, {
130
+ element: el,
131
+ score,
132
+ textContent,
133
+ className,
134
+ elementId: id,
135
+ tagName,
136
+ rect: elRect,
137
+ });
138
+ }
139
+ }
140
+ }
141
+
142
+ // Convert to array, sort by score descending, take top 10
143
+ const sorted = Array.from(discoveredMap.values())
144
+ .sort((a, b) => b.score - a.score)
145
+ .slice(0, 10);
146
+
147
+ // Convert to VisionFocusedElement format
148
+ return sorted.map((item): VisionFocusedElement => ({
149
+ name: item.elementId || item.tagName,
150
+ type: 'component', // Generic type since we're discovering
151
+ coordinates: {
152
+ x: item.rect.left + window.scrollX,
153
+ y: item.rect.top + window.scrollY,
154
+ width: item.rect.width,
155
+ height: item.rect.height,
156
+ },
157
+ textContent: item.textContent || undefined,
158
+ className: item.className || undefined,
159
+ elementId: item.elementId || undefined,
160
+ description: `${item.tagName}${item.elementId ? '#' + item.elementId : ''}${item.textContent ? ': "' + item.textContent.substring(0, 30) + '"' : ''}`,
161
+ }));
162
+ }
163
+
164
+ interface ScreenshotAnnotatorProps {
165
+ /** Called when user confirms their selection with captured screenshot and discovered elements */
166
+ onConfirm: (annotatedScreenshot: string, bounds: Rectangle, discoveredElements: VisionFocusedElement[]) => void;
167
+ /** Called when user cancels */
168
+ onCancel: () => void;
169
+ }
170
+
171
+ /**
172
+ * ScreenshotAnnotator displays a transparent overlay where users can
173
+ * draw a rectangle on the LIVE app to define the focus area.
174
+ * On confirm, it captures a screenshot and annotates it with the selection.
175
+ */
176
+ export function ScreenshotAnnotator({
177
+ onConfirm,
178
+ onCancel,
179
+ }: ScreenshotAnnotatorProps) {
180
+ const [mounted, setMounted] = useState(false);
181
+ const [isDrawing, setIsDrawing] = useState(false);
182
+ const [startPos, setStartPos] = useState<{ x: number; y: number } | null>(null);
183
+ const [currentRect, setCurrentRect] = useState<Rectangle | null>(null);
184
+ const [isCapturing, setIsCapturing] = useState(false);
185
+
186
+ useEffect(() => {
187
+ setMounted(true);
188
+
189
+ // Handle escape key to cancel
190
+ const handleKeyDown = (e: KeyboardEvent) => {
191
+ if (e.key === "Escape") {
192
+ onCancel();
193
+ }
194
+ };
195
+ document.addEventListener("keydown", handleKeyDown);
196
+
197
+ return () => {
198
+ setMounted(false);
199
+ document.removeEventListener("keydown", handleKeyDown);
200
+ };
201
+ }, [onCancel]);
202
+
203
+ // Mouse handlers for drawing on the live page
204
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
205
+ // Ignore clicks on the toolbar buttons
206
+ if ((e.target as HTMLElement).closest('[data-annotator-toolbar]')) {
207
+ return;
208
+ }
209
+
210
+ // Stop propagation to prevent InspectorOverlay from detecting this as element selection
211
+ e.stopPropagation();
212
+
213
+ setStartPos({ x: e.clientX, y: e.clientY });
214
+ setIsDrawing(true);
215
+ setCurrentRect(null);
216
+ }, []);
217
+
218
+ const handleMouseMove = useCallback((e: React.MouseEvent) => {
219
+ if (!isDrawing || !startPos) return;
220
+
221
+ // Calculate rectangle (handle drawing in any direction)
222
+ const x = Math.min(startPos.x, e.clientX);
223
+ const y = Math.min(startPos.y, e.clientY);
224
+ const width = Math.abs(e.clientX - startPos.x);
225
+ const height = Math.abs(e.clientY - startPos.y);
226
+
227
+ setCurrentRect({ x, y, width, height });
228
+ }, [isDrawing, startPos]);
229
+
230
+ const handleMouseUp = useCallback(() => {
231
+ setIsDrawing(false);
232
+ }, []);
233
+
234
+ // Clear the current selection
235
+ const handleRedraw = useCallback(() => {
236
+ setCurrentRect(null);
237
+ setStartPos(null);
238
+ }, []);
239
+
240
+ // Confirm: discover elements in bounds, capture screenshot and annotate it
241
+ const handleConfirm = useCallback(async () => {
242
+ if (!currentRect || isCapturing) return;
243
+
244
+ setIsCapturing(true);
245
+
246
+ try {
247
+ // FIRST: Discover DOM elements within the drawn rectangle
248
+ // This must happen BEFORE the overlay is removed to get accurate results
249
+ console.log("[ScreenshotAnnotator] Discovering elements in bounds:", currentRect);
250
+ const discoveredElements = discoverElementsInBounds(currentRect);
251
+ console.log("[ScreenshotAnnotator] Discovered elements:", {
252
+ count: discoveredElements.length,
253
+ elements: discoveredElements.map(e => ({
254
+ name: e.name,
255
+ text: e.textContent?.substring(0, 30),
256
+ id: e.elementId,
257
+ })),
258
+ });
259
+
260
+ // Capture the full page screenshot (excluding DevTools elements)
261
+ const canvas = await html2canvas(document.body, {
262
+ ignoreElements: (element) => {
263
+ return (
264
+ element.hasAttribute("data-sonance-devtools") ||
265
+ element.hasAttribute("data-vision-mode-border") ||
266
+ element.hasAttribute("data-annotator-overlay")
267
+ );
268
+ },
269
+ useCORS: true,
270
+ allowTaint: true,
271
+ scale: 1,
272
+ });
273
+
274
+ // Create annotated screenshot with the focus rectangle
275
+ const ctx = canvas.getContext("2d")!;
276
+
277
+ // Draw the focus rectangle (teal dashed border)
278
+ ctx.strokeStyle = "#00D3C8";
279
+ ctx.lineWidth = 4;
280
+ ctx.setLineDash([12, 6]);
281
+ ctx.strokeRect(currentRect.x, currentRect.y, currentRect.width, currentRect.height);
282
+
283
+ // Semi-transparent fill
284
+ ctx.fillStyle = "rgba(0, 211, 200, 0.1)";
285
+ ctx.fillRect(currentRect.x, currentRect.y, currentRect.width, currentRect.height);
286
+
287
+ // Add "FOCUS AREA" label
288
+ ctx.setLineDash([]); // Reset dash
289
+ ctx.fillStyle = "#00D3C8";
290
+ ctx.font = "bold 14px Montserrat, system-ui, sans-serif";
291
+ const labelText = "FOCUS AREA";
292
+ const labelPadding = 8;
293
+ const labelHeight = 22;
294
+ const labelWidth = ctx.measureText(labelText).width + labelPadding * 2;
295
+
296
+ // Position label above the rectangle
297
+ const labelX = currentRect.x;
298
+ const labelY = Math.max(currentRect.y - labelHeight - 4, 0);
299
+
300
+ ctx.fillRect(labelX, labelY, labelWidth, labelHeight);
301
+ ctx.fillStyle = "#1a1a1a";
302
+ ctx.fillText(labelText, labelX + labelPadding, labelY + 16);
303
+
304
+ const annotatedScreenshot = canvas.toDataURL("image/png", 0.9);
305
+
306
+ // Pass screenshot, bounds, AND discovered elements to callback
307
+ onConfirm(annotatedScreenshot, currentRect, discoveredElements);
308
+ } catch (error) {
309
+ console.error("Failed to capture screenshot:", error);
310
+ setIsCapturing(false);
311
+ }
312
+ }, [currentRect, isCapturing, onConfirm]);
313
+
314
+ if (!mounted) return null;
315
+
316
+ return createPortal(
317
+ <div
318
+ data-annotator-overlay="true"
319
+ data-sonance-devtools="true"
320
+ onMouseDown={(e) => {
321
+ e.stopPropagation();
322
+ e.preventDefault();
323
+ handleMouseDown(e);
324
+ }}
325
+ onMouseMove={handleMouseMove}
326
+ onMouseUp={handleMouseUp}
327
+ onMouseLeave={handleMouseUp}
328
+ onClick={(e) => {
329
+ // Prevent clicks from bubbling to InspectorOverlay
330
+ e.stopPropagation();
331
+ }}
332
+ style={{
333
+ position: "fixed",
334
+ top: 0,
335
+ left: 0,
336
+ right: 0,
337
+ bottom: 0,
338
+ zIndex: 10000,
339
+ cursor: isDrawing ? "crosshair" : "crosshair",
340
+ // Transparent overlay - user sees the live app
341
+ backgroundColor: "rgba(0, 0, 0, 0.1)",
342
+ // Ensure we capture all pointer events
343
+ pointerEvents: "all",
344
+ }}
345
+ >
346
+ {/* Instructions banner at top */}
347
+ <div
348
+ data-sonance-devtools="true"
349
+ data-annotator-toolbar="true"
350
+ style={{
351
+ position: "fixed",
352
+ top: 20,
353
+ left: "50%",
354
+ transform: "translateX(-50%)",
355
+ display: "flex",
356
+ alignItems: "center",
357
+ gap: 8,
358
+ backgroundColor: "#00D3C8",
359
+ color: "#1a1a1a",
360
+ padding: "10px 20px",
361
+ borderRadius: 8,
362
+ fontSize: 14,
363
+ fontWeight: 600,
364
+ fontFamily: "'Montserrat', system-ui, -apple-system, sans-serif",
365
+ boxShadow: "0 4px 20px rgba(0, 211, 200, 0.4)",
366
+ pointerEvents: "none",
367
+ userSelect: "none",
368
+ }}
369
+ >
370
+ <Crop size={18} />
371
+ <span>Click and drag to select the area you want to focus on</span>
372
+ </div>
373
+
374
+ {/* Current rectangle selection */}
375
+ {currentRect && (
376
+ <div
377
+ data-sonance-devtools="true"
378
+ style={{
379
+ position: "fixed",
380
+ left: currentRect.x,
381
+ top: currentRect.y,
382
+ width: currentRect.width,
383
+ height: currentRect.height,
384
+ border: "3px dashed #00D3C8",
385
+ backgroundColor: "rgba(0, 211, 200, 0.15)",
386
+ borderRadius: 4,
387
+ boxShadow: "0 0 0 2px rgba(0, 211, 200, 0.3), 0 0 20px rgba(0, 211, 200, 0.2)",
388
+ pointerEvents: "none",
389
+ }}
390
+ />
391
+ )}
392
+
393
+ {/* Toolbar at bottom */}
394
+ <div
395
+ data-sonance-devtools="true"
396
+ data-annotator-toolbar="true"
397
+ style={{
398
+ position: "fixed",
399
+ bottom: 30,
400
+ left: "50%",
401
+ transform: "translateX(-50%)",
402
+ display: "flex",
403
+ gap: 12,
404
+ }}
405
+ >
406
+ {/* Cancel button */}
407
+ <button
408
+ data-sonance-devtools="true"
409
+ onClick={(e) => {
410
+ e.stopPropagation();
411
+ onCancel();
412
+ }}
413
+ onMouseDown={(e) => e.stopPropagation()}
414
+ style={{
415
+ display: "flex",
416
+ alignItems: "center",
417
+ gap: 6,
418
+ padding: "10px 20px",
419
+ backgroundColor: "rgba(30, 30, 30, 0.95)",
420
+ color: "white",
421
+ border: "1px solid rgba(255, 255, 255, 0.2)",
422
+ borderRadius: 6,
423
+ fontSize: 14,
424
+ fontWeight: 500,
425
+ fontFamily: "'Montserrat', system-ui, sans-serif",
426
+ cursor: "pointer",
427
+ transition: "all 0.2s",
428
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
429
+ }}
430
+ >
431
+ <X size={16} />
432
+ Cancel
433
+ </button>
434
+
435
+ {/* Redraw button */}
436
+ <button
437
+ data-sonance-devtools="true"
438
+ onClick={(e) => {
439
+ e.stopPropagation();
440
+ handleRedraw();
441
+ }}
442
+ onMouseDown={(e) => e.stopPropagation()}
443
+ disabled={!currentRect}
444
+ style={{
445
+ display: "flex",
446
+ alignItems: "center",
447
+ gap: 6,
448
+ padding: "10px 20px",
449
+ backgroundColor: currentRect ? "rgba(30, 30, 30, 0.95)" : "rgba(30, 30, 30, 0.6)",
450
+ color: currentRect ? "white" : "rgba(255, 255, 255, 0.4)",
451
+ border: "1px solid rgba(255, 255, 255, 0.2)",
452
+ borderRadius: 6,
453
+ fontSize: 14,
454
+ fontWeight: 500,
455
+ fontFamily: "'Montserrat', system-ui, sans-serif",
456
+ cursor: currentRect ? "pointer" : "not-allowed",
457
+ transition: "all 0.2s",
458
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
459
+ }}
460
+ >
461
+ <RotateCcw size={16} />
462
+ Redraw
463
+ </button>
464
+
465
+ {/* Confirm button */}
466
+ <button
467
+ data-sonance-devtools="true"
468
+ onClick={(e) => {
469
+ e.stopPropagation();
470
+ handleConfirm();
471
+ }}
472
+ onMouseDown={(e) => e.stopPropagation()}
473
+ disabled={!currentRect || isCapturing}
474
+ style={{
475
+ display: "flex",
476
+ alignItems: "center",
477
+ gap: 6,
478
+ padding: "10px 24px",
479
+ backgroundColor: currentRect ? "#00D3C8" : "rgba(0, 211, 200, 0.3)",
480
+ color: currentRect ? "#1a1a1a" : "rgba(26, 26, 26, 0.5)",
481
+ border: "none",
482
+ borderRadius: 6,
483
+ fontSize: 14,
484
+ fontWeight: 600,
485
+ fontFamily: "'Montserrat', system-ui, sans-serif",
486
+ cursor: currentRect && !isCapturing ? "pointer" : "not-allowed",
487
+ transition: "all 0.2s",
488
+ boxShadow: currentRect ? "0 4px 20px rgba(0, 211, 200, 0.4)" : "none",
489
+ }}
490
+ >
491
+ <Check size={16} />
492
+ {isCapturing ? "Capturing..." : "Confirm Selection"}
493
+ </button>
494
+ </div>
495
+
496
+ {/* Keyboard hint */}
497
+ <div
498
+ data-sonance-devtools="true"
499
+ style={{
500
+ position: "fixed",
501
+ bottom: 10,
502
+ left: "50%",
503
+ transform: "translateX(-50%)",
504
+ color: "rgba(255, 255, 255, 0.6)",
505
+ fontSize: 12,
506
+ fontFamily: "'Montserrat', system-ui, sans-serif",
507
+ textShadow: "0 1px 2px rgba(0, 0, 0, 0.5)",
508
+ pointerEvents: "none",
509
+ }}
510
+ >
511
+ Press <kbd style={{ padding: "2px 6px", backgroundColor: "rgba(0,0,0,0.4)", borderRadius: 4 }}>Esc</kbd> to cancel
512
+ </div>
513
+ </div>,
514
+ document.body
515
+ );
516
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.93",
3
+ "version": "1.3.95",
4
4
  "description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",