sonance-brand-mcp 1.3.111 → 1.3.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/api/sonance-save-image/route.ts +625 -0
- package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +988 -57
- package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
- package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
- package/dist/assets/brand-system.ts +13 -12
- package/dist/assets/components/accordion.tsx +15 -7
- package/dist/assets/components/alert-dialog.tsx +35 -10
- package/dist/assets/components/alert.tsx +11 -10
- package/dist/assets/components/avatar.tsx +4 -4
- package/dist/assets/components/badge.tsx +16 -12
- package/dist/assets/components/button.stories.tsx +3 -3
- package/dist/assets/components/button.tsx +50 -31
- package/dist/assets/components/calendar.tsx +12 -8
- package/dist/assets/components/card.tsx +35 -29
- package/dist/assets/components/checkbox.tsx +9 -8
- package/dist/assets/components/code.tsx +19 -11
- package/dist/assets/components/command.tsx +32 -13
- package/dist/assets/components/context-menu.tsx +37 -16
- package/dist/assets/components/dialog.tsx +8 -5
- package/dist/assets/components/divider.tsx +15 -5
- package/dist/assets/components/drawer.tsx +4 -3
- package/dist/assets/components/dropdown-menu.tsx +15 -13
- package/dist/assets/components/hover-card.tsx +4 -1
- package/dist/assets/components/image.tsx +1 -1
- package/dist/assets/components/input.tsx +29 -14
- package/dist/assets/components/kbd.stories.tsx +3 -3
- package/dist/assets/components/kbd.tsx +29 -13
- package/dist/assets/components/listbox.tsx +8 -8
- package/dist/assets/components/menubar.tsx +50 -23
- package/dist/assets/components/navbar.stories.tsx +140 -13
- package/dist/assets/components/navbar.tsx +22 -5
- package/dist/assets/components/navigation-menu.tsx +28 -6
- package/dist/assets/components/pagination.tsx +10 -10
- package/dist/assets/components/popover.tsx +10 -8
- package/dist/assets/components/progress.tsx +6 -4
- package/dist/assets/components/radio-group.tsx +5 -5
- package/dist/assets/components/select.tsx +49 -29
- package/dist/assets/components/separator.tsx +3 -3
- package/dist/assets/components/sheet.tsx +4 -4
- package/dist/assets/components/sidebar.tsx +10 -10
- package/dist/assets/components/skeleton.tsx +13 -5
- package/dist/assets/components/slider.tsx +12 -10
- package/dist/assets/components/switch.tsx +4 -4
- package/dist/assets/components/table.tsx +5 -5
- package/dist/assets/components/tabs.tsx +8 -8
- package/dist/assets/components/textarea.tsx +11 -9
- package/dist/assets/components/toast.tsx +7 -7
- package/dist/assets/components/toggle.tsx +27 -7
- package/dist/assets/components/tooltip.tsx +10 -8
- package/dist/assets/components/user.tsx +8 -6
- package/dist/assets/dev-tools/SonanceDevTools.tsx +429 -362
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
- package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
- package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
- package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +4 -64
- package/dist/assets/dev-tools/hooks/index.ts +69 -0
- package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
- package/dist/assets/dev-tools/hooks/useComputedStyles.ts +171 -65
- package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
- package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
- package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
- package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +160 -57
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +42 -0
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- 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: "
|
|
59
|
-
boxShadow: "inset 0 0
|
|
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()
|
|
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
|
-
|
|
284
|
-
|
|
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 (!
|
|
366
|
+
if (!focusedElement) {
|
|
289
367
|
setStyles(null);
|
|
290
368
|
return;
|
|
291
369
|
}
|
|
292
370
|
|
|
293
|
-
//
|
|
294
|
-
const
|
|
295
|
-
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
318
|
-
|
|
393
|
+
const hasText = isTextElement(element);
|
|
394
|
+
const textContent = element.textContent?.trim() || '';
|
|
319
395
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
}
|