sonance-brand-mcp 1.3.111 → 1.3.113

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 (79) hide show
  1. package/dist/assets/api/sonance-save-image/route.ts +625 -0
  2. package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
  3. package/dist/assets/api/sonance-vision-apply/route.ts +988 -57
  4. package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
  5. package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
  6. package/dist/assets/brand-system.ts +13 -12
  7. package/dist/assets/components/accordion.tsx +15 -7
  8. package/dist/assets/components/alert-dialog.tsx +35 -10
  9. package/dist/assets/components/alert.tsx +11 -10
  10. package/dist/assets/components/avatar.tsx +4 -4
  11. package/dist/assets/components/badge.tsx +16 -12
  12. package/dist/assets/components/button.stories.tsx +3 -3
  13. package/dist/assets/components/button.tsx +50 -31
  14. package/dist/assets/components/calendar.tsx +12 -8
  15. package/dist/assets/components/card.tsx +35 -29
  16. package/dist/assets/components/checkbox.tsx +9 -8
  17. package/dist/assets/components/code.tsx +19 -11
  18. package/dist/assets/components/command.tsx +32 -13
  19. package/dist/assets/components/context-menu.tsx +37 -16
  20. package/dist/assets/components/dialog.tsx +8 -5
  21. package/dist/assets/components/divider.tsx +15 -5
  22. package/dist/assets/components/drawer.tsx +4 -3
  23. package/dist/assets/components/dropdown-menu.tsx +15 -13
  24. package/dist/assets/components/hover-card.tsx +4 -1
  25. package/dist/assets/components/image.tsx +1 -1
  26. package/dist/assets/components/input.tsx +29 -14
  27. package/dist/assets/components/kbd.stories.tsx +3 -3
  28. package/dist/assets/components/kbd.tsx +29 -13
  29. package/dist/assets/components/listbox.tsx +8 -8
  30. package/dist/assets/components/menubar.tsx +50 -23
  31. package/dist/assets/components/navbar.stories.tsx +140 -13
  32. package/dist/assets/components/navbar.tsx +22 -5
  33. package/dist/assets/components/navigation-menu.tsx +28 -6
  34. package/dist/assets/components/pagination.tsx +10 -10
  35. package/dist/assets/components/popover.tsx +10 -8
  36. package/dist/assets/components/progress.tsx +6 -4
  37. package/dist/assets/components/radio-group.tsx +5 -5
  38. package/dist/assets/components/select.tsx +49 -29
  39. package/dist/assets/components/separator.tsx +3 -3
  40. package/dist/assets/components/sheet.tsx +4 -4
  41. package/dist/assets/components/sidebar.tsx +10 -10
  42. package/dist/assets/components/skeleton.tsx +13 -5
  43. package/dist/assets/components/slider.tsx +12 -10
  44. package/dist/assets/components/switch.tsx +4 -4
  45. package/dist/assets/components/table.tsx +5 -5
  46. package/dist/assets/components/tabs.tsx +8 -8
  47. package/dist/assets/components/textarea.tsx +11 -9
  48. package/dist/assets/components/toast.tsx +7 -7
  49. package/dist/assets/components/toggle.tsx +27 -7
  50. package/dist/assets/components/tooltip.tsx +10 -8
  51. package/dist/assets/components/user.tsx +8 -6
  52. package/dist/assets/dev-tools/SonanceDevTools.tsx +429 -362
  53. package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
  54. package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
  55. package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
  56. package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
  57. package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
  58. package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
  59. package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
  60. package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
  61. package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
  62. package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
  63. package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
  64. package/dist/assets/dev-tools/components/VisionModeBorder.tsx +4 -64
  65. package/dist/assets/dev-tools/hooks/index.ts +69 -0
  66. package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
  67. package/dist/assets/dev-tools/hooks/useComputedStyles.ts +171 -65
  68. package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
  69. package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
  70. package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
  71. package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
  72. package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +160 -57
  73. package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
  74. package/dist/assets/dev-tools/types.ts +42 -0
  75. package/dist/assets/globals.css +225 -9
  76. package/dist/assets/styles/brand-overrides.css +3 -2
  77. package/dist/assets/utils.ts +2 -1
  78. package/dist/index.js +32 -1
  79. package/package.json +1 -1
@@ -368,7 +368,7 @@ export function ScreenshotAnnotator({
368
368
  }}
369
369
  >
370
370
  <Crop size={18} />
371
- <span>Click and drag to select the area you want to focus on</span>
371
+ <span id="span-click-and-drag-to-se">Click and drag to select the area you want to focus on</span>
372
372
  </div>
373
373
 
374
374
  {/* Current rectangle selection */}
@@ -96,7 +96,7 @@ export function SectionHighlight({ active, focusedElements }: SectionHighlightPr
96
96
  }}
97
97
  >
98
98
  <Box size={12} />
99
- <span>Section: {labelText}</span>
99
+ <span id="span-section-labeltext">Section: {labelText}</span>
100
100
  </div>
101
101
 
102
102
  {/* Corner markers for emphasis */}
@@ -34,14 +34,14 @@ function FileModificationCard({
34
34
  <ChevronRight className="h-3.5 w-3.5 text-gray-400 flex-shrink-0" />
35
35
  )}
36
36
  <FileCode className="h-3.5 w-3.5 text-purple-500 flex-shrink-0" />
37
- <span className="text-xs font-mono text-gray-700 truncate flex-1">
37
+ <span id="file-modification-card-span-modificationfilepath" className="text-xs font-mono text-gray-700 truncate flex-1">
38
38
  {modification.filePath}
39
39
  </span>
40
40
  </button>
41
41
 
42
42
  {/* Explanation */}
43
43
  <div className="px-2 pb-2">
44
- <p className="text-[10px] text-gray-500">{modification.explanation}</p>
44
+ <p id="file-modification-card-p-modificationexplanat" className="text-[10px] text-gray-500">{modification.explanation}</p>
45
45
  </div>
46
46
 
47
47
  {/* Expanded Diff */}
@@ -108,10 +108,10 @@ export function VisionDiffPreview({
108
108
  <div className="flex items-center justify-between">
109
109
  <div className="flex items-center gap-2">
110
110
  <Eye className="h-4 w-4 text-purple-600" />
111
- <span className="text-xs font-semibold text-gray-900">
111
+ <span id="vision-diff-preview-span-vision-mode-changes-" className="text-xs font-semibold text-gray-900">
112
112
  Vision Mode Changes Ready
113
113
  </span>
114
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-purple-200 text-purple-700 font-medium">
114
+ <span id="vision-diff-preview-span-filecount-filefileco" className="text-[10px] px-1.5 py-0.5 rounded bg-purple-200 text-purple-700 font-medium">
115
115
  {fileCount} file{fileCount !== 1 ? "s" : ""}
116
116
  </span>
117
117
  </div>
@@ -124,7 +124,7 @@ export function VisionDiffPreview({
124
124
  </div>
125
125
 
126
126
  {/* Overall Explanation */}
127
- <p className="text-xs text-gray-600">{pendingEdit.explanation}</p>
127
+ <p id="vision-diff-preview-p-pendingeditexplanati" className="text-xs text-gray-600">{pendingEdit.explanation}</p>
128
128
 
129
129
  {/* File Modifications List */}
130
130
  <div className="space-y-2 max-h-80 overflow-y-auto">
@@ -141,7 +141,7 @@ export function VisionDiffPreview({
141
141
  {/* Live Preview Indicator */}
142
142
  <div className="flex items-center gap-2 p-2 rounded bg-purple-100 border border-purple-200">
143
143
  <Eye className="h-3.5 w-3.5 text-purple-600" />
144
- <span className="text-xs text-purple-700">
144
+ <span id="vision-diff-preview-span-live-preview-active-" className="text-xs text-purple-700">
145
145
  Live preview active - scroll to see changes on the page
146
146
  </span>
147
147
  </div>
@@ -150,7 +150,7 @@ export function VisionDiffPreview({
150
150
  {fileCount > 1 && (
151
151
  <div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
152
152
  <AlertCircle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
153
- <span className="text-xs text-amber-700">
153
+ <span id="vision-diff-preview-span-multiple-files-will-" className="text-xs text-amber-700">
154
154
  Multiple files will be modified. Review each file before saving.
155
155
  </span>
156
156
  </div>
@@ -2,7 +2,6 @@
2
2
 
3
3
  import React, { useEffect, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
- import { Eye } from "lucide-react";
6
5
 
7
6
  interface VisionModeBorderProps {
8
7
  active: boolean;
@@ -12,8 +11,6 @@ interface VisionModeBorderProps {
12
11
 
13
12
  export function VisionModeBorder({
14
13
  active,
15
- focusedCount = 0,
16
- highlightEnabled = false,
17
14
  }: VisionModeBorderProps) {
18
15
  const [mounted, setMounted] = useState(false);
19
16
 
@@ -45,6 +42,7 @@ export function VisionModeBorder({
45
42
  `}</style>
46
43
 
47
44
  {/* Main border overlay - respects devtools frame if present */}
45
+ {/* Removed corner pill and bottom instruction to not cover UI elements */}
48
46
  <div
49
47
  data-vision-mode-border="true"
50
48
  style={{
@@ -55,70 +53,12 @@ export function VisionModeBorder({
55
53
  bottom: 0,
56
54
  pointerEvents: "none",
57
55
  zIndex: 9996, // Below DevTools (9999) but above content
58
- border: "4px solid #8B5CF6",
59
- boxShadow: "inset 0 0 30px rgba(139, 92, 246, 0.3), 0 0 30px rgba(139, 92, 246, 0.3)",
56
+ border: "3px solid #8B5CF6",
57
+ boxShadow: "inset 0 0 20px rgba(139, 92, 246, 0.2), 0 0 20px rgba(139, 92, 246, 0.2)",
60
58
  animation: "vision-pulse 2s ease-in-out infinite, vision-glow 2s ease-in-out infinite",
61
59
  transition: "all 0.2s ease-out",
62
60
  }}
63
- >
64
- {/* Corner indicator */}
65
- <div
66
- style={{
67
- position: "absolute",
68
- top: "12px",
69
- left: "12px",
70
- display: "flex",
71
- alignItems: "center",
72
- gap: "6px",
73
- backgroundColor: "#8B5CF6",
74
- color: "white",
75
- padding: "6px 12px",
76
- borderRadius: "6px",
77
- fontSize: "12px",
78
- fontWeight: 600,
79
- fontFamily: "Montserrat, system-ui, -apple-system, sans-serif",
80
- boxShadow: "0 2px 8px rgba(139, 92, 246, 0.4)",
81
- }}
82
- >
83
- <Eye size={14} />
84
- <span>Vision Mode</span>
85
- {focusedCount > 0 && (
86
- <span
87
- style={{
88
- backgroundColor: "rgba(255, 255, 255, 0.2)",
89
- padding: "2px 6px",
90
- borderRadius: "4px",
91
- marginLeft: "4px",
92
- }}
93
- >
94
- {focusedCount} focused
95
- </span>
96
- )}
97
- </div>
98
-
99
- {/* Instruction hint at bottom */}
100
- <div
101
- style={{
102
- position: "absolute",
103
- bottom: "12px",
104
- left: "50%",
105
- transform: "translateX(-50%)",
106
- backgroundColor: "rgba(139, 92, 246, 0.9)",
107
- color: "white",
108
- padding: "8px 16px",
109
- borderRadius: "8px",
110
- fontSize: "13px",
111
- fontFamily: "Montserrat, system-ui, -apple-system, sans-serif",
112
- boxShadow: "0 2px 12px rgba(139, 92, 246, 0.5)",
113
- whiteSpace: "nowrap",
114
- }}
115
- >
116
- {highlightEnabled
117
- ? "Click elements to focus AI attention, then describe your changes"
118
- : "Toggle highlighting in the banner to select elements"
119
- }
120
- </div>
121
- </div>
61
+ />
122
62
  </>,
123
63
  document.body
124
64
  );
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Dev Tools Hooks
3
+ *
4
+ * Modular hooks for element detection and styling in the Sonance DevTools.
5
+ *
6
+ * Architecture:
7
+ * - useElementScanner: Main orchestrator that combines all detection types
8
+ * - useComponentDetection: Component-specific detection
9
+ * - useImageDetection: Image/logo detection with original state tracking
10
+ * - useTextDetection: Text element detection with filtering
11
+ * - useContentHash: Content-based ID generation for stability
12
+ * - useComputedStyles: Get computed styles for selected elements
13
+ */
14
+
15
+ // Main scanner hook (use this in most cases)
16
+ export {
17
+ useElementScanner,
18
+ extractLogoName,
19
+ type ElementScannerOptions,
20
+ type ElementScannerResult,
21
+ } from "./useElementScanner";
22
+
23
+ // Individual detection hooks (for custom implementations)
24
+ export {
25
+ useComponentDetection,
26
+ detectComponents,
27
+ type ComponentDetectionConfig,
28
+ } from "./useComponentDetection";
29
+
30
+ export {
31
+ useImageDetection,
32
+ detectImages,
33
+ type ImageDetectionConfig,
34
+ type ImageDetectionResult,
35
+ } from "./useImageDetection";
36
+
37
+ export {
38
+ useTextDetection,
39
+ detectTextElements,
40
+ type TextDetectionConfig,
41
+ type TextDetectionResult,
42
+ } from "./useTextDetection";
43
+
44
+ // Content-based hashing utilities
45
+ export {
46
+ hashString,
47
+ generateTextElementId,
48
+ generateImageElementId,
49
+ generateComponentId,
50
+ generateVariantId,
51
+ migrateLegacyIds,
52
+ isLegacyId,
53
+ isMigrationComplete,
54
+ markMigrationComplete,
55
+ MIGRATION_STORAGE_KEY,
56
+ MIGRATION_VERSION,
57
+ } from "./useContentHash";
58
+
59
+ // Computed styles hook (existing)
60
+ export {
61
+ useComputedStyles,
62
+ useElementFromCoordinates,
63
+ type ComputedStyles,
64
+ type ComputedGeometry,
65
+ type ComputedTypography,
66
+ type ComputedFill,
67
+ type ComputedStroke,
68
+ type ComputedEffect,
69
+ } from "./useComputedStyles";
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { useCallback } from "react";
4
+ import { DetectedElement } from "../types";
5
+ import { generateVariantId, generateComponentId } from "./useContentHash";
6
+
7
+ /**
8
+ * Component detection configuration
9
+ */
10
+ export interface ComponentDetectionConfig {
11
+ /** Whether to include elements in modals/dialogs */
12
+ includeModals: boolean;
13
+ /** Callback to check if element is in active layer */
14
+ isInActiveLayer: (el: Element) => boolean;
15
+ /** Callback to get visible rect (clipped to scroll container) */
16
+ getVisibleRect: (el: Element, rect: DOMRect) => DOMRect | null;
17
+ }
18
+
19
+ /**
20
+ * Generic element selectors for untagged primitives
21
+ */
22
+ const GENERIC_SELECTORS: Record<string, string> = {
23
+ button: "button:not([data-sonance-name])",
24
+ input: "input:not([data-sonance-name])",
25
+ select: "select:not([data-sonance-name])",
26
+ textarea: "textarea:not([data-sonance-name])",
27
+ };
28
+
29
+ /**
30
+ * Detect components on the page
31
+ * Handles both explicitly tagged components and generic primitives
32
+ */
33
+ export function detectComponents(config: ComponentDetectionConfig): DetectedElement[] {
34
+ const { isInActiveLayer, getVisibleRect } = config;
35
+ const detected: DetectedElement[] = [];
36
+
37
+ // 1. Scan for explicitly tagged components
38
+ const taggedComponents = document.querySelectorAll("[data-sonance-name]");
39
+ taggedComponents.forEach((el) => {
40
+ if (!isInActiveLayer(el)) return;
41
+
42
+ const name = el.getAttribute("data-sonance-name");
43
+ if (!name) return;
44
+
45
+ const rawRect = el.getBoundingClientRect();
46
+ const rect = getVisibleRect(el, rawRect);
47
+ if (!rect || rect.width <= 0 || rect.height <= 0) return;
48
+
49
+ // Get or generate stable variant ID
50
+ let variantId = el.getAttribute("data-sonance-variant");
51
+ if (!variantId) {
52
+ variantId = generateVariantId(el);
53
+ el.setAttribute("data-sonance-variant", variantId);
54
+ }
55
+
56
+ // Capture metadata for AI context
57
+ const textContent = (el.textContent?.trim() || "").substring(0, 100);
58
+ const className = el.className?.toString() || "";
59
+ const elementId = el.id || undefined;
60
+ const childIds = Array.from(el.querySelectorAll("[id]"))
61
+ .map((child) => child.id)
62
+ .filter((id) => id) as string[];
63
+
64
+ detected.push({
65
+ name,
66
+ rect,
67
+ type: "component",
68
+ variantId,
69
+ textContent,
70
+ className,
71
+ elementId,
72
+ childIds: childIds.length > 0 ? childIds : undefined,
73
+ });
74
+ });
75
+
76
+ // 2. Scan for untagged primitive elements
77
+ for (const [genericName, selector] of Object.entries(GENERIC_SELECTORS)) {
78
+ const elements = document.querySelectorAll(selector);
79
+ elements.forEach((el) => {
80
+ if (!isInActiveLayer(el)) return;
81
+
82
+ const rawRect = el.getBoundingClientRect();
83
+ const rect = getVisibleRect(el, rawRect);
84
+ if (!rect || rect.width <= 0 || rect.height <= 0) return;
85
+
86
+ // Get or generate stable variant ID
87
+ let variantId = el.getAttribute("data-sonance-variant");
88
+ if (!variantId) {
89
+ variantId = generateVariantId(el);
90
+ el.setAttribute("data-sonance-variant", variantId);
91
+ }
92
+
93
+ // Auto-tag generic elements for future detection
94
+ if (!el.getAttribute("data-sonance-name")) {
95
+ el.setAttribute("data-sonance-name", genericName);
96
+ }
97
+
98
+ // Capture metadata
99
+ const textContent = (el.textContent?.trim() || "").substring(0, 100);
100
+ const elClassName = el.className?.toString() || "";
101
+ const elementId = el.id || undefined;
102
+ const childIds = Array.from(el.querySelectorAll("[id]"))
103
+ .map((child) => child.id)
104
+ .filter((id) => id) as string[];
105
+
106
+ detected.push({
107
+ name: genericName,
108
+ rect,
109
+ type: "component",
110
+ variantId,
111
+ textContent,
112
+ className: elClassName,
113
+ elementId,
114
+ childIds: childIds.length > 0 ? childIds : undefined,
115
+ });
116
+ });
117
+ }
118
+
119
+ return detected;
120
+ }
121
+
122
+ /**
123
+ * Hook for component detection
124
+ * Returns a memoized detection function
125
+ */
126
+ export function useComponentDetection() {
127
+ const detect = useCallback((config: ComponentDetectionConfig): DetectedElement[] => {
128
+ return detectComponents(config);
129
+ }, []);
130
+
131
+ return { detectComponents: detect };
132
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect, useCallback } from "react";
4
+ import type { VisionFocusedElement } from "../types";
4
5
 
5
6
  export interface ComputedGeometry {
6
7
  x: number;
@@ -216,7 +217,7 @@ export function useComputedStyles(elementId: string | null, variantId?: string |
216
217
  }
217
218
 
218
219
  const hasText = isTextElement(element);
219
- const textContent = element.textContent?.trim().substring(0, 50) || '';
220
+ const textContent = element.textContent?.trim() || '';
220
221
 
221
222
  const computedStyles: ComputedStyles = {
222
223
  tagName: element.tagName.toLowerCase(),
@@ -280,85 +281,190 @@ export function useComputedStyles(elementId: string | null, variantId?: string |
280
281
  return styles;
281
282
  }
282
283
 
283
- // Simpler version that works with VisionFocusedElement coordinates
284
- export function useElementFromCoordinates(coordinates: { x: number; y: number; width: number; height: number } | null): ComputedStyles | null {
284
+ /**
285
+ * Multi-strategy element finding for robustness after HMR
286
+ * Tries multiple methods to find the element, falling back through them
287
+ *
288
+ * Priority order:
289
+ * 1. Element ID with styled child detection (most reliable for nested styled elements)
290
+ * 2. Text content with span priority (finds styled inline elements)
291
+ * 3. Coordinates (fallback for when other strategies fail)
292
+ *
293
+ * Note: Coordinates are stored as screen coordinates at click time and become
294
+ * invalid after scroll/layout changes, so they're used as a fallback, not primary.
295
+ */
296
+ function findElementByMultipleStrategies(focusedElement: VisionFocusedElement): HTMLElement | null {
297
+ // Strategy 1: Find by element ID with styled child detection
298
+ // This handles cases where the detected element is a container (like <p>)
299
+ // but the actual styled element is a child (like <span style="color:...">)
300
+ if (focusedElement.elementId) {
301
+ const byId = document.getElementById(focusedElement.elementId);
302
+ if (byId && !byId.closest('[data-sonance-devtools="true"]')) {
303
+ // First, check if there's a styled child element with inline color
304
+ const styledChild = byId.querySelector('[style*="color"]') as HTMLElement;
305
+ if (styledChild) {
306
+ return styledChild;
307
+ }
308
+ // No styled child, return the element itself
309
+ return byId;
310
+ }
311
+ }
312
+
313
+ // Strategy 2: Find by text content, prioritizing span elements (which often have styling)
314
+ if (focusedElement.textContent) {
315
+ const textToFind = focusedElement.textContent.trim();
316
+ if (textToFind.length > 0) {
317
+ // Search specifically for span first (inline styled elements), then other text elements
318
+ const spanCandidates = document.querySelectorAll('span');
319
+ for (const el of spanCandidates) {
320
+ if (el.closest('[data-sonance-devtools="true"]')) continue;
321
+ const elText = el.textContent?.trim() || '';
322
+ const compareLength = Math.min(30, textToFind.length);
323
+ if (elText.startsWith(textToFind.substring(0, compareLength))) {
324
+ return el as HTMLElement;
325
+ }
326
+ }
327
+
328
+ // Fallback to other text elements
329
+ const candidates = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, label, button, a, td, th, li, div');
330
+ for (const el of candidates) {
331
+ if (el.closest('[data-sonance-devtools="true"]')) continue;
332
+ const elText = el.textContent?.trim() || '';
333
+ const compareLength = Math.min(30, textToFind.length);
334
+ if (elText.startsWith(textToFind.substring(0, compareLength))) {
335
+ // Check if this element has a styled span child
336
+ const styledSpan = el.querySelector('span[style*="color"]') as HTMLElement;
337
+ if (styledSpan) {
338
+ return styledSpan;
339
+ }
340
+ return el as HTMLElement;
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ // Strategy 3: Find by coordinates (fallback - coordinates may be stale after scroll)
347
+ const { coordinates } = focusedElement;
348
+ if (coordinates) {
349
+ const centerX = coordinates.x + coordinates.width / 2;
350
+ const centerY = coordinates.y + coordinates.height / 2;
351
+ const byCoords = document.elementFromPoint(centerX, centerY) as HTMLElement;
352
+ if (byCoords && !byCoords.closest('[data-sonance-devtools="true"]')) {
353
+ return byCoords;
354
+ }
355
+ }
356
+
357
+ return null;
358
+ }
359
+
360
+ // Enhanced version that works with full VisionFocusedElement for robust element finding
361
+ export function useElementFromCoordinates(focusedElement: VisionFocusedElement | null): ComputedStyles | null {
285
362
  const [styles, setStyles] = useState<ComputedStyles | null>(null);
363
+ const [refreshCounter, setRefreshCounter] = useState(0);
286
364
 
287
365
  useEffect(() => {
288
- if (!coordinates) {
366
+ if (!focusedElement) {
289
367
  setStyles(null);
290
368
  return;
291
369
  }
292
370
 
293
- // Find element at center of coordinates
294
- const centerX = coordinates.x + coordinates.width / 2;
295
- const centerY = coordinates.y + coordinates.height / 2;
296
-
297
- const element = document.elementFromPoint(centerX, centerY) as HTMLElement;
298
-
299
- if (!element || element.closest('[data-sonance-devtools="true"]')) {
371
+ // Use multi-strategy element finding for robustness after HMR
372
+ const element = findElementByMultipleStrategies(focusedElement);
373
+
374
+ if (!element) {
300
375
  setStyles(null);
301
376
  return;
302
377
  }
303
378
 
304
- const computed = window.getComputedStyle(element);
305
- const rect = element.getBoundingClientRect();
306
-
307
- let rotation = 0;
308
- const transform = computed.transform;
309
- if (transform && transform !== 'none') {
310
- const matrixMatch = transform.match(/matrix\(([^)]+)\)/);
311
- if (matrixMatch) {
312
- const values = matrixMatch[1].split(',').map(v => parseFloat(v.trim()));
313
- rotation = Math.round(Math.atan2(values[1], values[0]) * (180 / Math.PI));
379
+ const extractAndSetStyles = () => {
380
+ const computed = window.getComputedStyle(element);
381
+ const rect = element.getBoundingClientRect();
382
+
383
+ let rotation = 0;
384
+ const transform = computed.transform;
385
+ if (transform && transform !== 'none') {
386
+ const matrixMatch = transform.match(/matrix\(([^)]+)\)/);
387
+ if (matrixMatch) {
388
+ const values = matrixMatch[1].split(',').map(v => parseFloat(v.trim()));
389
+ rotation = Math.round(Math.atan2(values[1], values[0]) * (180 / Math.PI));
390
+ }
314
391
  }
315
- }
316
392
 
317
- const hasText = isTextElement(element);
318
- const textContent = element.textContent?.trim().substring(0, 50) || '';
393
+ const hasText = isTextElement(element);
394
+ const textContent = element.textContent?.trim() || '';
319
395
 
320
- setStyles({
321
- tagName: element.tagName.toLowerCase(),
322
- className: element.className?.toString() || '',
323
- id: element.id || '',
324
-
325
- geometry: {
326
- x: Math.round(rect.left),
327
- y: Math.round(rect.top),
328
- width: Math.round(rect.width),
329
- height: Math.round(rect.height),
330
- rotation,
331
- },
332
-
333
- opacity: parseFloat(computed.opacity) * 100,
334
- borderRadius: computed.borderRadius,
335
- overflow: computed.overflow,
336
-
337
- typography: hasText ? {
338
- fontFamily: computed.fontFamily.split(',')[0].replace(/['"]/g, ''),
339
- fontSize: computed.fontSize,
340
- fontWeight: computed.fontWeight,
341
- lineHeight: computed.lineHeight,
342
- letterSpacing: computed.letterSpacing,
343
- textAlign: computed.textAlign,
344
- color: parseColor(computed.color).color,
345
- } : null,
346
- hasText,
347
- textContent,
348
-
349
- fills: extractFills(computed),
350
- strokes: extractStrokes(computed),
351
- effects: extractEffects(computed),
352
-
353
- display: computed.display,
354
- flexDirection: computed.flexDirection,
355
- alignItems: computed.alignItems,
356
- justifyContent: computed.justifyContent,
357
- gap: computed.gap,
358
- padding: computed.padding,
359
- margin: computed.margin,
396
+ setStyles({
397
+ tagName: element.tagName.toLowerCase(),
398
+ className: element.className?.toString() || '',
399
+ id: element.id || '',
400
+
401
+ geometry: {
402
+ x: Math.round(rect.left),
403
+ y: Math.round(rect.top),
404
+ width: Math.round(rect.width),
405
+ height: Math.round(rect.height),
406
+ rotation,
407
+ },
408
+
409
+ opacity: parseFloat(computed.opacity) * 100,
410
+ borderRadius: computed.borderRadius,
411
+ overflow: computed.overflow,
412
+
413
+ typography: hasText ? {
414
+ fontFamily: computed.fontFamily.split(',')[0].replace(/['"]/g, ''),
415
+ fontSize: computed.fontSize,
416
+ fontWeight: computed.fontWeight,
417
+ lineHeight: computed.lineHeight,
418
+ letterSpacing: computed.letterSpacing,
419
+ textAlign: computed.textAlign,
420
+ color: parseColor(computed.color).color,
421
+ } : null,
422
+ hasText,
423
+ textContent,
424
+
425
+ fills: extractFills(computed),
426
+ strokes: extractStrokes(computed),
427
+ effects: extractEffects(computed),
428
+
429
+ display: computed.display,
430
+ flexDirection: computed.flexDirection,
431
+ alignItems: computed.alignItems,
432
+ justifyContent: computed.justifyContent,
433
+ gap: computed.gap,
434
+ padding: computed.padding,
435
+ margin: computed.margin,
436
+ });
437
+ };
438
+
439
+ // Initial extraction
440
+ extractAndSetStyles();
441
+
442
+ // Watch for DOM changes (HMR updates) using MutationObserver
443
+ const observer = new MutationObserver((mutations) => {
444
+ // Check if any mutation affects our element's content
445
+ for (const mutation of mutations) {
446
+ if (mutation.type === 'characterData' ||
447
+ mutation.type === 'childList' ||
448
+ (mutation.type === 'attributes' && mutation.target === element)) {
449
+ // Trigger a refresh
450
+ setRefreshCounter(c => c + 1);
451
+ break;
452
+ }
453
+ }
360
454
  });
361
- }, [coordinates]);
455
+
456
+ // Observe the element and its subtree for changes
457
+ observer.observe(element, {
458
+ characterData: true,
459
+ childList: true,
460
+ subtree: true,
461
+ attributes: true,
462
+ });
463
+
464
+ return () => {
465
+ observer.disconnect();
466
+ };
467
+ }, [focusedElement, refreshCounter]);
362
468
 
363
469
  return styles;
364
470
  }