sonance-brand-mcp 1.3.14 → 1.3.16
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-analyze/route.ts +1 -1
- package/dist/assets/api/sonance-save-logo/route.ts +2 -2
- package/dist/assets/brand-system.ts +4 -1
- package/dist/assets/components/image.tsx +3 -1
- package/dist/assets/components/select.tsx +3 -0
- package/dist/assets/dev-tools/SonanceDevTools.tsx +1837 -3579
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +230 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +455 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +190 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +353 -0
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +199 -0
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +116 -0
- package/dist/assets/dev-tools/components/common.tsx +94 -0
- package/dist/assets/dev-tools/constants.ts +616 -0
- package/dist/assets/dev-tools/index.ts +29 -8
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +329 -0
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +623 -0
- package/dist/assets/dev-tools/panels/LogoToolsPanel.tsx +621 -0
- package/dist/assets/dev-tools/panels/LogosPanel.tsx +16 -0
- package/dist/assets/dev-tools/panels/TextPanel.tsx +332 -0
- package/dist/assets/dev-tools/types.ts +295 -0
- package/dist/assets/dev-tools/utils.ts +360 -0
- package/dist/index.js +278 -3
- package/package.json +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef } from "react";
|
|
4
|
+
import { X, Sparkles, Eye, Loader2, Save, RefreshCw } from "lucide-react";
|
|
5
|
+
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { PendingEdit } from "../types";
|
|
7
|
+
import { removePreviewStyles, injectPreviewStyles, extractPreviewCSSFromCode } from "../utils";
|
|
8
|
+
|
|
9
|
+
export interface DiffPreviewProps {
|
|
10
|
+
pendingEdit: PendingEdit;
|
|
11
|
+
componentType: string;
|
|
12
|
+
onSave: () => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
onPreviewToggle: (isActive: boolean) => void;
|
|
15
|
+
isPreviewActive: boolean;
|
|
16
|
+
saveStatus: "idle" | "saving" | "success" | "error";
|
|
17
|
+
saveMessage: string;
|
|
18
|
+
// Variant ID for targeted preview (applies styles to specific variant instances)
|
|
19
|
+
variantId?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function DiffPreview({
|
|
23
|
+
pendingEdit,
|
|
24
|
+
componentType,
|
|
25
|
+
onSave,
|
|
26
|
+
onCancel,
|
|
27
|
+
onPreviewToggle,
|
|
28
|
+
isPreviewActive,
|
|
29
|
+
saveStatus,
|
|
30
|
+
saveMessage,
|
|
31
|
+
variantId,
|
|
32
|
+
}: DiffPreviewProps) {
|
|
33
|
+
// Track if we've auto-activated preview for this edit
|
|
34
|
+
const hasAutoActivated = useRef(false);
|
|
35
|
+
|
|
36
|
+
// Auto-activate preview when component mounts or pendingEdit changes
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!hasAutoActivated.current && pendingEdit) {
|
|
39
|
+
// First try AI-provided previewCSS (most reliable - AI knows exactly what it changed)
|
|
40
|
+
// Fall back to extraction if AI didn't provide previewCSS
|
|
41
|
+
let previewCSS = pendingEdit.previewCSS;
|
|
42
|
+
|
|
43
|
+
if (!previewCSS) {
|
|
44
|
+
// Fallback: try to extract CSS from code changes
|
|
45
|
+
previewCSS = extractPreviewCSSFromCode(
|
|
46
|
+
pendingEdit.originalCode,
|
|
47
|
+
pendingEdit.modifiedCode,
|
|
48
|
+
componentType,
|
|
49
|
+
variantId
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (previewCSS) {
|
|
54
|
+
injectPreviewStyles(previewCSS);
|
|
55
|
+
onPreviewToggle(true);
|
|
56
|
+
}
|
|
57
|
+
hasAutoActivated.current = true;
|
|
58
|
+
}
|
|
59
|
+
}, [pendingEdit, componentType, variantId, onPreviewToggle]);
|
|
60
|
+
|
|
61
|
+
// Reset auto-activation flag when pendingEdit changes (new edit)
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
hasAutoActivated.current = false;
|
|
64
|
+
}, [pendingEdit.modifiedCode]);
|
|
65
|
+
|
|
66
|
+
// Cleanup preview on unmount or cancel
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
return () => {
|
|
69
|
+
removePreviewStyles();
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="space-y-3 p-3 rounded border border-[#00A3E1]/30 bg-[#00A3E1]/5">
|
|
75
|
+
<div className="flex items-center justify-between">
|
|
76
|
+
<div className="flex items-center gap-2">
|
|
77
|
+
<Sparkles className="h-4 w-4 text-[#00A3E1]" />
|
|
78
|
+
<span id="diff-preview-span-ai-changes-ready" className="text-xs font-semibold text-gray-900">AI Changes Ready</span>
|
|
79
|
+
</div>
|
|
80
|
+
<button
|
|
81
|
+
onClick={onCancel}
|
|
82
|
+
className="text-gray-400 hover:text-gray-600"
|
|
83
|
+
>
|
|
84
|
+
<X className="h-4 w-4" />
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Explanation */}
|
|
89
|
+
<p id="diff-preview-p-pendingeditexplanati" className="text-xs text-gray-600">{pendingEdit.explanation}</p>
|
|
90
|
+
|
|
91
|
+
{/* Diff Display */}
|
|
92
|
+
<div className="max-h-40 overflow-auto rounded border border-gray-200 bg-white">
|
|
93
|
+
<pre className="p-2 text-[10px] font-mono whitespace-pre-wrap">
|
|
94
|
+
{pendingEdit.diff.split("\n").map((line, i) => (
|
|
95
|
+
<div
|
|
96
|
+
key={i}
|
|
97
|
+
className={cn(
|
|
98
|
+
line.startsWith("+") && !line.startsWith("@@")
|
|
99
|
+
? "bg-green-50 text-green-700"
|
|
100
|
+
: line.startsWith("-") && !line.startsWith("@@")
|
|
101
|
+
? "bg-red-50 text-red-700"
|
|
102
|
+
: line.startsWith("@@")
|
|
103
|
+
? "bg-blue-50 text-blue-600 font-semibold"
|
|
104
|
+
: "text-gray-600"
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
{line}
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
</pre>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* File Path */}
|
|
114
|
+
<div className="text-[10px] text-gray-400">
|
|
115
|
+
File: {pendingEdit.filePath}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Live Preview Indicator */}
|
|
119
|
+
<div className="flex items-center gap-2 p-2 rounded bg-[#00A3E1]/10 border border-[#00A3E1]/30">
|
|
120
|
+
<Eye className="h-3.5 w-3.5 text-[#00A3E1]" />
|
|
121
|
+
<span className="text-xs text-[#00A3E1]">
|
|
122
|
+
Live preview active - scroll to see changes on the page
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Action Buttons */}
|
|
127
|
+
<div className="flex gap-2">
|
|
128
|
+
{/* Save Button */}
|
|
129
|
+
<button
|
|
130
|
+
onClick={onSave}
|
|
131
|
+
disabled={saveStatus === "saving"}
|
|
132
|
+
className={cn(
|
|
133
|
+
"flex-1 flex items-center justify-center gap-2 py-2",
|
|
134
|
+
"text-xs font-medium rounded transition-colors",
|
|
135
|
+
"bg-[#333F48] text-white hover:bg-[#2a343c]",
|
|
136
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{saveStatus === "saving" ? (
|
|
140
|
+
<>
|
|
141
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
142
|
+
Saving...
|
|
143
|
+
</>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<Save className="h-3.5 w-3.5" />
|
|
147
|
+
Save to File
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</button>
|
|
151
|
+
{/* Cancel Button */}
|
|
152
|
+
<button
|
|
153
|
+
onClick={onCancel}
|
|
154
|
+
disabled={saveStatus === "saving"}
|
|
155
|
+
className={cn(
|
|
156
|
+
"px-4 py-2 text-xs font-medium rounded transition-colors",
|
|
157
|
+
"border border-gray-200 text-gray-600 hover:bg-gray-50",
|
|
158
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
Cancel
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Save Status Message */}
|
|
166
|
+
{saveMessage && (
|
|
167
|
+
<div
|
|
168
|
+
className={cn(
|
|
169
|
+
"p-2 rounded text-xs",
|
|
170
|
+
saveStatus === "success" && "bg-green-50 text-green-700 border border-green-200",
|
|
171
|
+
saveStatus === "error" && "bg-red-50 text-red-700 border border-red-200"
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
{saveMessage}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Success: Refresh Hint */}
|
|
179
|
+
{saveStatus === "success" && (
|
|
180
|
+
<div className="flex items-center gap-2 p-2 rounded bg-amber-50 border border-amber-200">
|
|
181
|
+
<RefreshCw className="h-3.5 w-3.5 text-amber-600" />
|
|
182
|
+
<span id="diff-preview-span-refresh-the-page-to-" className="text-xs text-amber-700">
|
|
183
|
+
Refresh the page to see your changes
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect } from "react";
|
|
4
|
+
import { Box, Image as ImageIcon, Type, MousePointer, Sparkles, Eye, Check } from "lucide-react";
|
|
5
|
+
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { DetectedElement, DetectedElementType, VisionFocusedElement } from "../types";
|
|
7
|
+
|
|
8
|
+
export interface InspectorOverlayProps {
|
|
9
|
+
elements: DetectedElement[];
|
|
10
|
+
selectedLogoId?: string | null;
|
|
11
|
+
onLogoClick?: (logoId: string) => void;
|
|
12
|
+
selectedTextId?: string | null;
|
|
13
|
+
onTextClick?: (textId: string) => void;
|
|
14
|
+
interactive?: boolean;
|
|
15
|
+
selectedComponentId?: string | null;
|
|
16
|
+
onComponentClick?: (componentName: string) => void;
|
|
17
|
+
componentSelectionMode?: boolean;
|
|
18
|
+
// New: For inspector-first workflow - called when any component is clicked
|
|
19
|
+
onSelectComponentAndClose?: (componentType: string, variantId?: string) => void;
|
|
20
|
+
inspectorClickMode?: boolean; // When true, all components are clickable
|
|
21
|
+
// The currently selected component type (for visual highlighting)
|
|
22
|
+
selectedComponentType?: string;
|
|
23
|
+
// The currently selected component variant ID (for scoped highlighting)
|
|
24
|
+
selectedVariantId?: string | null;
|
|
25
|
+
// Current edit scope
|
|
26
|
+
componentScope?: "all" | "variant" | "page" | "selected";
|
|
27
|
+
// Preview mode - when AI changes are pending, shows green highlights
|
|
28
|
+
previewMode?: boolean;
|
|
29
|
+
// Vision mode props
|
|
30
|
+
visionMode?: boolean;
|
|
31
|
+
visionFocusedElements?: VisionFocusedElement[];
|
|
32
|
+
onVisionElementClick?: (element: DetectedElement) => void;
|
|
33
|
+
// Changed elements - elements that were modified, shown with green highlight until accept/revert
|
|
34
|
+
changedElements?: VisionFocusedElement[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Color config for different element types
|
|
38
|
+
const inspectorColors: Record<DetectedElementType, { border: string; bg: string; selectedBorder: string; selectedBg: string }> = {
|
|
39
|
+
component: { border: "#00A3E1", bg: "#00A3E1", selectedBorder: "#00A3E1", selectedBg: "#00A3E1" }, // Sonance blue
|
|
40
|
+
logo: { border: "#FC4C02", bg: "#FC4C02", selectedBorder: "#C02B0A", selectedBg: "#C02B0A" }, // IPORT orange / Blaze red for selected
|
|
41
|
+
text: { border: "#8B5CF6", bg: "#8B5CF6", selectedBorder: "#7C3AED", selectedBg: "#7C3AED" }, // Purple for text
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Preview mode colors - green to indicate pending changes
|
|
45
|
+
const previewColors = {
|
|
46
|
+
border: "#22C55E", // Green-500
|
|
47
|
+
bg: "#22C55E",
|
|
48
|
+
selectedBorder: "#16A34A", // Green-600
|
|
49
|
+
selectedBg: "#16A34A",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Vision mode colors - purple for AI vision analysis
|
|
53
|
+
const visionColors = {
|
|
54
|
+
border: "#8B5CF6", // Purple-600
|
|
55
|
+
bg: "#8B5CF6",
|
|
56
|
+
selectedBorder: "#7C3AED", // Purple-700 (focused elements)
|
|
57
|
+
selectedBg: "#7C3AED",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function InspectorOverlay({
|
|
61
|
+
elements,
|
|
62
|
+
selectedLogoId,
|
|
63
|
+
onLogoClick,
|
|
64
|
+
selectedTextId,
|
|
65
|
+
onTextClick,
|
|
66
|
+
interactive = false,
|
|
67
|
+
selectedComponentId,
|
|
68
|
+
onComponentClick,
|
|
69
|
+
componentSelectionMode = false,
|
|
70
|
+
onSelectComponentAndClose,
|
|
71
|
+
inspectorClickMode = false,
|
|
72
|
+
selectedComponentType = "all",
|
|
73
|
+
selectedVariantId,
|
|
74
|
+
componentScope,
|
|
75
|
+
previewMode = false,
|
|
76
|
+
visionMode = false,
|
|
77
|
+
visionFocusedElements = [],
|
|
78
|
+
onVisionElementClick,
|
|
79
|
+
changedElements = [],
|
|
80
|
+
}: InspectorOverlayProps) {
|
|
81
|
+
// Use document-level click listener for selection (doesn't block scroll)
|
|
82
|
+
// Disabled in preview mode - user should save or cancel, not select new elements
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (previewMode) return; // No interaction in preview mode
|
|
85
|
+
if (!inspectorClickMode && !componentSelectionMode && !interactive && !visionMode) return;
|
|
86
|
+
|
|
87
|
+
const handleDocumentClick = (e: MouseEvent) => {
|
|
88
|
+
const clickX = e.clientX;
|
|
89
|
+
const clickY = e.clientY;
|
|
90
|
+
|
|
91
|
+
// Don't capture clicks on the DevTools panel itself
|
|
92
|
+
const devToolsPanel = document.querySelector('[data-sonance-devtools="true"]');
|
|
93
|
+
if (devToolsPanel?.contains(e.target as Node)) return;
|
|
94
|
+
|
|
95
|
+
// Find ALL elements that contain the click point
|
|
96
|
+
const matchingElements = elements.filter(el =>
|
|
97
|
+
clickX >= el.rect.left &&
|
|
98
|
+
clickX <= el.rect.left + el.rect.width &&
|
|
99
|
+
clickY >= el.rect.top &&
|
|
100
|
+
clickY <= el.rect.top + el.rect.height
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Sort by area (smallest first) - smaller elements are more specific
|
|
104
|
+
// This ensures clicking on a nested element picks that element, not its parent
|
|
105
|
+
matchingElements.sort((a, b) => {
|
|
106
|
+
const areaA = a.rect.width * a.rect.height;
|
|
107
|
+
const areaB = b.rect.width * b.rect.height;
|
|
108
|
+
return areaA - areaB;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Process the most specific (smallest) matching element
|
|
112
|
+
for (const el of matchingElements) {
|
|
113
|
+
// Vision mode takes priority - clicking elements adds/removes from focused list
|
|
114
|
+
if (visionMode && onVisionElementClick) {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
e.stopPropagation();
|
|
117
|
+
onVisionElementClick(el);
|
|
118
|
+
return;
|
|
119
|
+
} else if (interactive && el.type === "logo" && el.logoId && onLogoClick) {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
onLogoClick(el.logoId);
|
|
123
|
+
return;
|
|
124
|
+
} else if (interactive && el.type === "text" && el.textId && onTextClick) {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
onTextClick(el.textId);
|
|
128
|
+
return;
|
|
129
|
+
} else if (inspectorClickMode && el.type === "component" && onSelectComponentAndClose) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
e.stopPropagation();
|
|
132
|
+
let componentType = el.name.toLowerCase();
|
|
133
|
+
if (componentType === "button") {
|
|
134
|
+
componentType = "button-primary";
|
|
135
|
+
}
|
|
136
|
+
onSelectComponentAndClose(componentType, el.variantId);
|
|
137
|
+
return;
|
|
138
|
+
} else if (componentSelectionMode && el.type === "component" && onComponentClick) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
e.stopPropagation();
|
|
141
|
+
onComponentClick(el.name);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Use capture phase to intercept before other handlers
|
|
148
|
+
document.addEventListener("click", handleDocumentClick, true);
|
|
149
|
+
return () => document.removeEventListener("click", handleDocumentClick, true);
|
|
150
|
+
}, [elements, inspectorClickMode, componentSelectionMode, interactive, onLogoClick, onTextClick, onSelectComponentAndClose, onComponentClick, previewMode, visionMode, onVisionElementClick]);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
style={{
|
|
155
|
+
position: "fixed",
|
|
156
|
+
top: 0,
|
|
157
|
+
left: 0,
|
|
158
|
+
width: 0,
|
|
159
|
+
height: 0,
|
|
160
|
+
zIndex: 9997,
|
|
161
|
+
overflow: "visible",
|
|
162
|
+
pointerEvents: "none",
|
|
163
|
+
touchAction: "auto", // Ensure touch scrolling works
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{/* Visual overlays only - no pointer events, scroll works naturally */}
|
|
167
|
+
{elements.map((el, index) => {
|
|
168
|
+
const isLogoSelected = el.type === "logo" && el.logoId === selectedLogoId;
|
|
169
|
+
const isTextSelected = el.type === "text" && el.textId === selectedTextId;
|
|
170
|
+
const isComponentSelected = el.type === "component" && el.name === selectedComponentId;
|
|
171
|
+
const isSelected = isLogoSelected || isComponentSelected || isTextSelected;
|
|
172
|
+
|
|
173
|
+
// Check if this element is focused in vision mode
|
|
174
|
+
const isVisionFocused = visionMode && visionFocusedElements.some(
|
|
175
|
+
(ve) => ve.name === el.name && ve.variantId === el.variantId
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Check if this element was changed (Apply-First mode - highlight until accept/revert)
|
|
179
|
+
const isChangedElement = changedElements.length > 0 && changedElements.some(
|
|
180
|
+
(ce) => ce.name === el.name && ce.variantId === el.variantId
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// In vision mode, only show focused elements (hide non-focused to reduce clutter)
|
|
184
|
+
if (visionMode && !isVisionFocused && visionFocusedElements.length > 0) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// When showing changed elements, only show those specific elements
|
|
189
|
+
if (changedElements.length > 0 && !isChangedElement && !visionMode && !inspectorClickMode) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Use appropriate colors based on mode:
|
|
194
|
+
// - Changed elements (green) - highest priority (Apply-First mode)
|
|
195
|
+
// - Preview mode (green) takes priority
|
|
196
|
+
// - Vision mode (purple) for vision-focused elements
|
|
197
|
+
// - Otherwise use type-based colors
|
|
198
|
+
const colors = isChangedElement
|
|
199
|
+
? previewColors // Green for changed elements
|
|
200
|
+
: previewMode
|
|
201
|
+
? previewColors
|
|
202
|
+
: visionMode
|
|
203
|
+
? (isVisionFocused ? visionColors : { ...visionColors, border: "#8B5CF680", bg: "#8B5CF680" })
|
|
204
|
+
: inspectorColors[el.type];
|
|
205
|
+
|
|
206
|
+
// Check if this element matches the currently selected component type
|
|
207
|
+
const elementType = el.name?.toLowerCase() || "";
|
|
208
|
+
const selectedType = selectedComponentType?.toLowerCase() || "all";
|
|
209
|
+
|
|
210
|
+
// Check for type match (used for label visibility)
|
|
211
|
+
const typeMatches = selectedType === "all" ||
|
|
212
|
+
elementType === selectedType ||
|
|
213
|
+
elementType.startsWith(selectedType.replace("-primary", "").replace("-secondary", "").replace("-outline", "").replace("-ghost", ""));
|
|
214
|
+
|
|
215
|
+
// Check for full match (used for border highlighting - considers variant scope)
|
|
216
|
+
let isMatchingType = false;
|
|
217
|
+
if (selectedType === "all") {
|
|
218
|
+
isMatchingType = true;
|
|
219
|
+
} else if (componentScope === "variant" && selectedVariantId) {
|
|
220
|
+
// If we have a selected variant, we must match BOTH type AND variant for full highlight
|
|
221
|
+
isMatchingType = typeMatches && el.variantId === selectedVariantId;
|
|
222
|
+
} else {
|
|
223
|
+
isMatchingType = typeMatches;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// In vision mode, don't dim elements - show all with purple, focused ones brighter
|
|
227
|
+
// In other modes, dim non-matching elements when a specific type is selected
|
|
228
|
+
// Never dim changed elements
|
|
229
|
+
const isDimmed = !visionMode && !isChangedElement && selectedType !== "all" && !isMatchingType;
|
|
230
|
+
|
|
231
|
+
const borderColor = isDimmed ? "#6b728080" : (isChangedElement ? colors.selectedBorder : (isVisionFocused ? colors.selectedBorder : (isSelected ? colors.selectedBorder : colors.border)));
|
|
232
|
+
const bgColor = isDimmed ? "#6b728080" : (isChangedElement ? colors.selectedBg : (isVisionFocused ? colors.selectedBg : (isSelected ? colors.selectedBg : colors.bg)));
|
|
233
|
+
const isLogoClickable = interactive && el.type === "logo" && el.logoId && onLogoClick;
|
|
234
|
+
const isTextClickable = interactive && el.type === "text" && el.textId && onTextClick;
|
|
235
|
+
const isComponentClickable = componentSelectionMode && el.type === "component" && onComponentClick;
|
|
236
|
+
const isInspectorClickable = inspectorClickMode && el.type === "component" && onSelectComponentAndClose;
|
|
237
|
+
const isVisionClickable = visionMode && onVisionElementClick;
|
|
238
|
+
const isClickable = isLogoClickable || isComponentClickable || isInspectorClickable || isVisionClickable || isTextClickable;
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div
|
|
242
|
+
key={`${el.type}-${el.logoId || el.name}-${index}`}
|
|
243
|
+
className={cn(
|
|
244
|
+
"absolute",
|
|
245
|
+
isClickable && "cursor-pointer"
|
|
246
|
+
)}
|
|
247
|
+
style={{
|
|
248
|
+
position: "fixed", // Use fixed instead of absolute relative to 0x0 parent
|
|
249
|
+
top: el.rect.top,
|
|
250
|
+
left: el.rect.left,
|
|
251
|
+
width: el.rect.width,
|
|
252
|
+
height: el.rect.height,
|
|
253
|
+
pointerEvents: "none",
|
|
254
|
+
touchAction: "auto", // Allow touch scrolling
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
{/* Border highlight - in preview/changed mode, use only glow (no border) so actual styling is visible */}
|
|
258
|
+
<div
|
|
259
|
+
className={cn(
|
|
260
|
+
"absolute transition-all",
|
|
261
|
+
// In preview/changed mode: no border, just glow - so component's actual styling is visible
|
|
262
|
+
(previewMode && isMatchingType) || isChangedElement ? "inset-[-4px]" : "inset-0 rounded-sm",
|
|
263
|
+
!previewMode && !visionMode && !isChangedElement && (isSelected ? "border-3" : isDimmed ? "border" : "border-2"),
|
|
264
|
+
visionMode && (isVisionFocused ? "border-3" : "border-2"),
|
|
265
|
+
(previewMode && isMatchingType) && "animate-pulse",
|
|
266
|
+
(visionMode && isVisionFocused) && "animate-pulse",
|
|
267
|
+
isChangedElement && "animate-pulse"
|
|
268
|
+
)}
|
|
269
|
+
style={{
|
|
270
|
+
pointerEvents: "none",
|
|
271
|
+
borderColor: (previewMode && isMatchingType) || isChangedElement ? "transparent" : borderColor,
|
|
272
|
+
borderWidth: (previewMode && isMatchingType) || isChangedElement ? 0 : undefined,
|
|
273
|
+
// Preview mode / Changed elements: outer glow only, no border overlay
|
|
274
|
+
// Vision mode: show glow for focused elements
|
|
275
|
+
boxShadow: isChangedElement
|
|
276
|
+
? `0 0 0 3px ${previewColors.border}, 0 0 20px 8px ${previewColors.border}80`
|
|
277
|
+
: previewMode && isMatchingType
|
|
278
|
+
? `0 0 0 3px ${previewColors.border}, 0 0 20px 8px ${previewColors.border}80`
|
|
279
|
+
: visionMode && isVisionFocused
|
|
280
|
+
? `0 0 0 3px ${visionColors.border}, 0 0 15px 5px ${visionColors.border}60`
|
|
281
|
+
: isSelected
|
|
282
|
+
? `0 0 0 2px ${borderColor}40`
|
|
283
|
+
: undefined,
|
|
284
|
+
opacity: isDimmed ? 0.4 : (visionMode && !isVisionFocused ? 0.6 : 1),
|
|
285
|
+
borderRadius: previewMode && isMatchingType ? "inherit" : undefined,
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
{/* Label - show for matching elements, changed elements, or vision mode */}
|
|
289
|
+
{(typeMatches || isChangedElement || (visionMode && isVisionFocused)) && (
|
|
290
|
+
<div
|
|
291
|
+
className={cn(
|
|
292
|
+
"absolute -top-6 left-0",
|
|
293
|
+
"px-1.5 py-0.5 text-[10px] font-medium",
|
|
294
|
+
"text-white rounded-t-sm",
|
|
295
|
+
"whitespace-nowrap shadow-sm",
|
|
296
|
+
"flex items-center gap-1",
|
|
297
|
+
"transition-all",
|
|
298
|
+
(previewMode && isMatchingType) && "animate-pulse",
|
|
299
|
+
(visionMode && isVisionFocused) && "animate-pulse",
|
|
300
|
+
isChangedElement && "animate-pulse"
|
|
301
|
+
)}
|
|
302
|
+
style={{ backgroundColor: bgColor }}
|
|
303
|
+
>
|
|
304
|
+
{isChangedElement ? (
|
|
305
|
+
<>
|
|
306
|
+
<Check className="h-3 w-3" />
|
|
307
|
+
<span>Changed: {el.name}</span>
|
|
308
|
+
{el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
|
|
309
|
+
</>
|
|
310
|
+
) : previewMode && isMatchingType ? (
|
|
311
|
+
<>
|
|
312
|
+
<Sparkles className="h-3 w-3" />
|
|
313
|
+
<span>Preview: {el.name}</span>
|
|
314
|
+
{el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
|
|
315
|
+
</>
|
|
316
|
+
) : visionMode && isVisionFocused ? (
|
|
317
|
+
<>
|
|
318
|
+
<Eye className="h-3 w-3" />
|
|
319
|
+
<span>Focused: {el.name}</span>
|
|
320
|
+
{el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px]">#{el.variantId.substring(0, 4)}</span>}
|
|
321
|
+
</>
|
|
322
|
+
) : visionMode ? (
|
|
323
|
+
<>
|
|
324
|
+
<Eye className="h-3 w-3 opacity-60" />
|
|
325
|
+
{el.name}
|
|
326
|
+
{el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px] border-l border-white/30 pl-1">#{el.variantId.substring(0, 4)}</span>}
|
|
327
|
+
</>
|
|
328
|
+
) : (
|
|
329
|
+
<>
|
|
330
|
+
{el.type === "logo" && <ImageIcon className="h-3 w-3" />}
|
|
331
|
+
{el.type === "component" && <Box className="h-3 w-3" />}
|
|
332
|
+
{el.type === "text" && <Type className="h-3 w-3" />}
|
|
333
|
+
{el.name}
|
|
334
|
+
{el.variantId && <span className="ml-1 opacity-80 font-mono text-[9px] border-l border-white/30 pl-1">#{el.variantId.substring(0, 4)}</span>}
|
|
335
|
+
{isSelected && <span className="ml-1">✓</span>}
|
|
336
|
+
</>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
{/* Click hint for logos */}
|
|
341
|
+
{isClickable && !isSelected && (
|
|
342
|
+
<div className="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
|
|
343
|
+
<span id="span-click-to-select" className="px-2 py-1 text-[10px] font-medium text-white bg-black/70 rounded">
|
|
344
|
+
Click to select
|
|
345
|
+
</span>
|
|
346
|
+
</div>
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
})}
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
}
|