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,623 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useMemo, useCallback, useEffect } from "react";
|
|
4
|
+
import { Box, X, RotateCcw } from "lucide-react";
|
|
5
|
+
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { DetectedElement, AnalysisResult, ComponentStyle, PendingEdit, AIEditResult, ComponentStyleOverride, VisionFocusedElement, VisionPendingEdit, ApplyFirstSession, ApplyFirstStatus } from "../types";
|
|
7
|
+
import { COMPONENT_CONFIG_MAP, getVisibleSections } from "../constants";
|
|
8
|
+
import { componentSnippets, ThemeConfig } from "../../../lib/brand-system";
|
|
9
|
+
import { ChatInterface } from "../components/ChatInterface";
|
|
10
|
+
import { DiffPreview } from "../components/DiffPreview";
|
|
11
|
+
import { VisionDiffPreview } from "../components/VisionDiffPreview";
|
|
12
|
+
import { ApplyFirstPreview } from "../components/ApplyFirstPreview";
|
|
13
|
+
|
|
14
|
+
type ComponentsViewMode = "visual" | "inspector";
|
|
15
|
+
|
|
16
|
+
export interface ComponentsPanelProps {
|
|
17
|
+
copiedId: string | null;
|
|
18
|
+
onCopy: (text: string, id: string) => void;
|
|
19
|
+
installedComponents: string[];
|
|
20
|
+
inspectorEnabled: boolean;
|
|
21
|
+
onToggleInspector: (force?: boolean) => void;
|
|
22
|
+
config: ThemeConfig;
|
|
23
|
+
updateConfig: (updates: Partial<ThemeConfig>) => void;
|
|
24
|
+
onReset: () => void;
|
|
25
|
+
taggedElements: DetectedElement[];
|
|
26
|
+
selectedComponentType: string;
|
|
27
|
+
onSelectComponentType: (type: string) => void;
|
|
28
|
+
componentScope: "all" | "variant" | "page" | "selected";
|
|
29
|
+
onScopeChange: (scope: "all" | "variant" | "page" | "selected") => void;
|
|
30
|
+
selectedComponentId: string | null;
|
|
31
|
+
onSelectComponent: (id: string | null) => void;
|
|
32
|
+
selectedVariantId: string | null;
|
|
33
|
+
onSelectVariant: (id: string | null) => void;
|
|
34
|
+
analysisResult: AnalysisResult | null;
|
|
35
|
+
// Component-specific style overrides
|
|
36
|
+
componentOverrides: Record<string, ComponentStyle>;
|
|
37
|
+
onUpdateComponentOverride: (componentType: string, override: Partial<ComponentStyle>) => void;
|
|
38
|
+
onResetComponentOverrides: () => void;
|
|
39
|
+
// New props for view mode
|
|
40
|
+
viewMode: ComponentsViewMode;
|
|
41
|
+
onViewModeChange: (mode: ComponentsViewMode) => void;
|
|
42
|
+
// Preview mode - lifted to parent for overlay coordination
|
|
43
|
+
isPreviewActive: boolean;
|
|
44
|
+
onPreviewActiveChange: (active: boolean) => void;
|
|
45
|
+
// Vision mode props
|
|
46
|
+
visionMode?: boolean;
|
|
47
|
+
visionFocusedElements?: VisionFocusedElement[];
|
|
48
|
+
onVisionEditComplete?: (result: VisionPendingEdit) => void;
|
|
49
|
+
// Vision pending edit (for VisionDiffPreview)
|
|
50
|
+
visionPendingEdit?: VisionPendingEdit | null;
|
|
51
|
+
onSaveVisionEdit?: () => void;
|
|
52
|
+
onClearVisionPendingEdit?: () => void;
|
|
53
|
+
// Apply-First Mode props
|
|
54
|
+
applyFirstSession?: ApplyFirstSession | null;
|
|
55
|
+
applyFirstStatus?: ApplyFirstStatus;
|
|
56
|
+
onApplyFirstComplete?: (session: ApplyFirstSession) => void;
|
|
57
|
+
onApplyFirstAccept?: () => void;
|
|
58
|
+
onApplyFirstRevert?: () => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ComponentsPanel({
|
|
62
|
+
copiedId,
|
|
63
|
+
onCopy,
|
|
64
|
+
installedComponents,
|
|
65
|
+
inspectorEnabled,
|
|
66
|
+
onToggleInspector,
|
|
67
|
+
config,
|
|
68
|
+
updateConfig,
|
|
69
|
+
onReset,
|
|
70
|
+
taggedElements,
|
|
71
|
+
selectedComponentType,
|
|
72
|
+
onSelectComponentType,
|
|
73
|
+
componentScope,
|
|
74
|
+
onScopeChange,
|
|
75
|
+
selectedComponentId,
|
|
76
|
+
onSelectComponent,
|
|
77
|
+
selectedVariantId,
|
|
78
|
+
onSelectVariant,
|
|
79
|
+
analysisResult,
|
|
80
|
+
componentOverrides,
|
|
81
|
+
onUpdateComponentOverride,
|
|
82
|
+
onResetComponentOverrides,
|
|
83
|
+
viewMode,
|
|
84
|
+
onViewModeChange,
|
|
85
|
+
isPreviewActive,
|
|
86
|
+
onPreviewActiveChange,
|
|
87
|
+
visionMode = false,
|
|
88
|
+
visionFocusedElements = [],
|
|
89
|
+
onVisionEditComplete,
|
|
90
|
+
visionPendingEdit,
|
|
91
|
+
onSaveVisionEdit,
|
|
92
|
+
onClearVisionPendingEdit,
|
|
93
|
+
applyFirstSession,
|
|
94
|
+
applyFirstStatus = "idle",
|
|
95
|
+
onApplyFirstComplete,
|
|
96
|
+
onApplyFirstAccept,
|
|
97
|
+
onApplyFirstRevert,
|
|
98
|
+
}: ComponentsPanelProps) {
|
|
99
|
+
// Auto-activate inspector when entering this tab
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!inspectorEnabled && selectedComponentType === "all") {
|
|
102
|
+
onToggleInspector(true);
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
// AI Chat state
|
|
107
|
+
const [pendingEdit, setPendingEdit] = useState<PendingEdit | null>(null);
|
|
108
|
+
const [aiSaveStatus, setAiSaveStatus] = useState<"idle" | "saving" | "success" | "error">("idle");
|
|
109
|
+
const [aiSaveMessage, setAiSaveMessage] = useState("");
|
|
110
|
+
|
|
111
|
+
// Edit scope: "component" affects all instances, "variant" affects only selected variant
|
|
112
|
+
const [editScope, setEditScope] = useState<"component" | "variant">("component");
|
|
113
|
+
|
|
114
|
+
// Color architecture state
|
|
115
|
+
const [colorArchitecture, setColorArchitecture] = useState<{
|
|
116
|
+
primary: string;
|
|
117
|
+
accent: string;
|
|
118
|
+
sources: { filePath: string; type: string; variables: { name: string; value: string; lineNumber: number }[] }[];
|
|
119
|
+
recommendation: string;
|
|
120
|
+
} | null>(null);
|
|
121
|
+
const [colorSaveStatus, setColorSaveStatus] = useState<"idle" | "saving" | "success" | "error">("idle");
|
|
122
|
+
const [colorSaveMessage, setColorSaveMessage] = useState("");
|
|
123
|
+
|
|
124
|
+
// Get count of detected components on this page
|
|
125
|
+
const pageCount = useMemo(() => {
|
|
126
|
+
if (selectedComponentType === "all") {
|
|
127
|
+
return taggedElements.filter(el => el.type === "component").length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return taggedElements.filter((el) => {
|
|
131
|
+
if (el.type !== "component") return false;
|
|
132
|
+
const name = el.name.toLowerCase();
|
|
133
|
+
|
|
134
|
+
// Match specific component or generic fallback
|
|
135
|
+
if (name === selectedComponentType || name.includes(selectedComponentType)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (selectedComponentType === "button-primary" && name === "button") {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
if (selectedComponentType === "input" && name === "input") return true;
|
|
142
|
+
|
|
143
|
+
return false;
|
|
144
|
+
}).length;
|
|
145
|
+
}, [taggedElements, selectedComponentType]);
|
|
146
|
+
|
|
147
|
+
// Get app-wide count from analysis
|
|
148
|
+
const appCount = useMemo(() => {
|
|
149
|
+
if (!analysisResult?.summary?.totalElements) return 0;
|
|
150
|
+
|
|
151
|
+
// For now, use total elements. Could be refined per component type.
|
|
152
|
+
if (selectedComponentType === "all") {
|
|
153
|
+
return analysisResult.summary.totalElements;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Map selected type to analysis categories
|
|
157
|
+
if (selectedComponentType.includes("button")) {
|
|
158
|
+
return analysisResult.summary.byCategory?.interactive?.total || 0;
|
|
159
|
+
}
|
|
160
|
+
if (selectedComponentType === "input" || selectedComponentType === "select" || selectedComponentType === "textarea") {
|
|
161
|
+
return analysisResult.summary.byCategory?.input?.total || 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return 0;
|
|
165
|
+
}, [analysisResult, selectedComponentType]);
|
|
166
|
+
|
|
167
|
+
// Get the selected component's display name
|
|
168
|
+
const getComponentDisplayName = () => {
|
|
169
|
+
if (selectedComponentType === "all") return "Component";
|
|
170
|
+
|
|
171
|
+
// Check if it's a specific component
|
|
172
|
+
const snippet = componentSnippets.find(s => s.id === selectedComponentType);
|
|
173
|
+
if (snippet) return snippet.name;
|
|
174
|
+
|
|
175
|
+
// Check if it's a category
|
|
176
|
+
const categoryKey = selectedComponentType.toLowerCase();
|
|
177
|
+
if (COMPONENT_CONFIG_MAP[categoryKey]) {
|
|
178
|
+
return categoryKey.charAt(0).toUpperCase() + categoryKey.slice(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Capitalize generic names
|
|
182
|
+
return selectedComponentType
|
|
183
|
+
.split('-')
|
|
184
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
185
|
+
.join(' ');
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const selectedComponentName = useMemo(() => {
|
|
189
|
+
if (selectedComponentType === "all") return null;
|
|
190
|
+
return getComponentDisplayName();
|
|
191
|
+
}, [selectedComponentType]);
|
|
192
|
+
|
|
193
|
+
// Get visible config sections for the selected component
|
|
194
|
+
const visibleSections = useMemo(() => {
|
|
195
|
+
return getVisibleSections(selectedComponentType);
|
|
196
|
+
}, [selectedComponentType]);
|
|
197
|
+
|
|
198
|
+
// Get current component's style overrides (or defaults)
|
|
199
|
+
const currentOverrides = useMemo(() => {
|
|
200
|
+
return componentOverrides[selectedComponentType] || {};
|
|
201
|
+
}, [componentOverrides, selectedComponentType]);
|
|
202
|
+
|
|
203
|
+
// Get computed styles of the selected variant for AI context
|
|
204
|
+
const variantStyles = useMemo(() => {
|
|
205
|
+
if (!selectedVariantId) return null;
|
|
206
|
+
|
|
207
|
+
// Find an element with this variant ID
|
|
208
|
+
const element = document.querySelector(`[data-sonance-variant="${selectedVariantId}"]`);
|
|
209
|
+
if (!element) return null;
|
|
210
|
+
|
|
211
|
+
const computed = window.getComputedStyle(element);
|
|
212
|
+
return {
|
|
213
|
+
backgroundColor: computed.backgroundColor,
|
|
214
|
+
color: computed.color,
|
|
215
|
+
borderColor: computed.borderColor,
|
|
216
|
+
borderRadius: computed.borderRadius,
|
|
217
|
+
borderWidth: computed.borderWidth,
|
|
218
|
+
padding: computed.padding,
|
|
219
|
+
fontSize: computed.fontSize,
|
|
220
|
+
fontWeight: computed.fontWeight,
|
|
221
|
+
boxShadow: computed.boxShadow,
|
|
222
|
+
};
|
|
223
|
+
}, [selectedVariantId]);
|
|
224
|
+
|
|
225
|
+
// Reset edit scope when variant selection changes
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (selectedVariantId) {
|
|
228
|
+
setEditScope("variant"); // Default to variant when one is selected
|
|
229
|
+
} else {
|
|
230
|
+
setEditScope("component");
|
|
231
|
+
}
|
|
232
|
+
}, [selectedVariantId]);
|
|
233
|
+
|
|
234
|
+
// Helper to update a style property for the selected component
|
|
235
|
+
const updateComponentStyle = useCallback((updates: Partial<ComponentStyleOverride>) => {
|
|
236
|
+
if (selectedComponentType !== "all") {
|
|
237
|
+
onUpdateComponentOverride(selectedComponentType, updates);
|
|
238
|
+
}
|
|
239
|
+
}, [selectedComponentType, onUpdateComponentOverride]);
|
|
240
|
+
|
|
241
|
+
// Get list of page instances matching the selected type (for navigation)
|
|
242
|
+
const pageInstances = useMemo<DetectedElement[]>(() => {
|
|
243
|
+
if (selectedComponentType === "all") {
|
|
244
|
+
return taggedElements.filter((el) => el.type === "component");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return taggedElements.filter((el) => {
|
|
248
|
+
if (el.type !== "component") return false;
|
|
249
|
+
const name = el.name.toLowerCase();
|
|
250
|
+
|
|
251
|
+
// Check for specific component match
|
|
252
|
+
if (name === selectedComponentType || name.includes(selectedComponentType)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Handle generic "button" -> "button-primary" mapping
|
|
257
|
+
if (selectedComponentType === "button-primary" && name === "button") {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Handle generic mappings for other types
|
|
262
|
+
if (selectedComponentType === "input" && name === "input") return true;
|
|
263
|
+
if (selectedComponentType === "select" && name === "select") return true;
|
|
264
|
+
if (selectedComponentType === "textarea" && name === "textarea") return true;
|
|
265
|
+
|
|
266
|
+
// Fallback to category matching
|
|
267
|
+
const categoryMappings: Record<string, string[]> = {
|
|
268
|
+
"buttons": ["button"],
|
|
269
|
+
"forms": ["input", "textarea", "select", "checkbox", "radio", "switch"],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const keywords = categoryMappings[selectedComponentType] || [];
|
|
273
|
+
return keywords.some((keyword) => name.includes(keyword));
|
|
274
|
+
});
|
|
275
|
+
}, [taggedElements, selectedComponentType]);
|
|
276
|
+
|
|
277
|
+
// Check color architecture on mount
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
async function fetchAnalysis() {
|
|
280
|
+
try {
|
|
281
|
+
const response = await fetch("/api/sonance-analyze");
|
|
282
|
+
if (response.ok) {
|
|
283
|
+
const data = await response.json();
|
|
284
|
+
|
|
285
|
+
// Update color architecture
|
|
286
|
+
if (data.colorArchitecture) {
|
|
287
|
+
setColorArchitecture(data.colorArchitecture);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
// API might not exist yet
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
fetchAnalysis();
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
// Handle AI edit completion
|
|
298
|
+
const handleAIEditComplete = useCallback((result: AIEditResult) => {
|
|
299
|
+
// Could do additional processing here if needed
|
|
300
|
+
console.log("AI edit complete:", result);
|
|
301
|
+
}, []);
|
|
302
|
+
|
|
303
|
+
// Handle saving AI edits to file
|
|
304
|
+
const handleSaveAIEdit = useCallback(async () => {
|
|
305
|
+
if (!pendingEdit) return;
|
|
306
|
+
|
|
307
|
+
setAiSaveStatus("saving");
|
|
308
|
+
setAiSaveMessage("");
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const response = await fetch("/api/sonance-ai-edit", {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: { "Content-Type": "application/json" },
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
action: "save",
|
|
316
|
+
componentType: selectedComponentType,
|
|
317
|
+
filePath: pendingEdit.filePath,
|
|
318
|
+
modifiedCode: pendingEdit.modifiedCode,
|
|
319
|
+
}),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const data = await response.json();
|
|
323
|
+
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
throw new Error(data.error || "Failed to save changes");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
setAiSaveStatus("success");
|
|
329
|
+
setAiSaveMessage(data.message || "Changes saved successfully!");
|
|
330
|
+
onPreviewActiveChange(false); // Clear preview on save
|
|
331
|
+
|
|
332
|
+
// Keep showing success for a bit, then auto-clear
|
|
333
|
+
setTimeout(() => {
|
|
334
|
+
setAiSaveStatus("idle");
|
|
335
|
+
setAiSaveMessage("");
|
|
336
|
+
}, 10000);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
setAiSaveStatus("error");
|
|
339
|
+
setAiSaveMessage(error instanceof Error ? error.message : "Failed to save changes");
|
|
340
|
+
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
setAiSaveStatus("idle");
|
|
343
|
+
setAiSaveMessage("");
|
|
344
|
+
}, 5000);
|
|
345
|
+
}
|
|
346
|
+
}, [pendingEdit, selectedComponentType]);
|
|
347
|
+
|
|
348
|
+
// Clear pending edit and preview
|
|
349
|
+
const handleClearPendingEdit = useCallback(() => {
|
|
350
|
+
setPendingEdit(null);
|
|
351
|
+
setAiSaveStatus("idle");
|
|
352
|
+
setAiSaveMessage("");
|
|
353
|
+
onPreviewActiveChange(false);
|
|
354
|
+
}, [onPreviewActiveChange]);
|
|
355
|
+
|
|
356
|
+
// Handle preview toggle from DiffPreview
|
|
357
|
+
const handlePreviewToggle = useCallback((active: boolean) => {
|
|
358
|
+
onPreviewActiveChange(active);
|
|
359
|
+
}, [onPreviewActiveChange]);
|
|
360
|
+
|
|
361
|
+
// Handle saving color changes
|
|
362
|
+
const handleSaveColors = async () => {
|
|
363
|
+
if (!colorArchitecture) return;
|
|
364
|
+
|
|
365
|
+
setColorSaveStatus("saving");
|
|
366
|
+
setColorSaveMessage("");
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const response = await fetch("/api/sonance-save-colors", {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: { "Content-Type": "application/json" },
|
|
372
|
+
body: JSON.stringify({
|
|
373
|
+
primaryColor: config.baseColor,
|
|
374
|
+
accentColor: config.accentColor,
|
|
375
|
+
colorArchitecture,
|
|
376
|
+
}),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const data = await response.json();
|
|
380
|
+
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
throw new Error(data.error || "Failed to save colors");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
setColorSaveStatus("success");
|
|
386
|
+
setColorSaveMessage(data.message || "Colors saved successfully!");
|
|
387
|
+
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
setColorSaveStatus("idle");
|
|
390
|
+
setColorSaveMessage("");
|
|
391
|
+
}, 5000);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
setColorSaveStatus("error");
|
|
394
|
+
setColorSaveMessage(error instanceof Error ? error.message : "Failed to save colors");
|
|
395
|
+
|
|
396
|
+
setTimeout(() => {
|
|
397
|
+
setColorSaveStatus("idle");
|
|
398
|
+
setColorSaveMessage("");
|
|
399
|
+
}, 5000);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<div className="space-y-4">
|
|
405
|
+
{/* Selected Component State */}
|
|
406
|
+
{selectedComponentType !== "all" && (
|
|
407
|
+
<>
|
|
408
|
+
{/* Selected Component Header */}
|
|
409
|
+
<div className="p-3 rounded border border-[#00A3E1] bg-[#00A3E1]/5">
|
|
410
|
+
<div className="flex items-center justify-between">
|
|
411
|
+
<div className="flex items-center gap-2">
|
|
412
|
+
<Box className="h-4 w-4 text-[#00A3E1]" />
|
|
413
|
+
<div className="flex flex-col">
|
|
414
|
+
<span id="brand-panel-span-selectedcomponentnam" className="text-sm font-semibold text-gray-900">{selectedComponentName}</span>
|
|
415
|
+
{selectedVariantId && (
|
|
416
|
+
<span id="brand-panel-span-variant-selectedvari" className="text-[10px] text-gray-500 font-mono">
|
|
417
|
+
Variant: {selectedVariantId.substring(0, 6)}
|
|
418
|
+
</span>
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
<button
|
|
423
|
+
onClick={() => onSelectComponentType("all")}
|
|
424
|
+
className="text-xs text-gray-400 hover:text-gray-600"
|
|
425
|
+
>
|
|
426
|
+
<X className="h-4 w-4" />
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{/* Stats */}
|
|
431
|
+
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
|
|
432
|
+
<span id="brand-panel-span"><strong className="text-gray-700">{pageCount}</strong> on this page</span>
|
|
433
|
+
{appCount > 0 && (
|
|
434
|
+
<span id="brand-panel-span"><strong className="text-gray-700">{appCount}</strong> in entire app</span>
|
|
435
|
+
)}
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
{/* Variants List - show all unique variants */}
|
|
440
|
+
{(() => {
|
|
441
|
+
// Get unique variants for this component type
|
|
442
|
+
const variantMap = new Map<string, { count: number; variantId: string }>();
|
|
443
|
+
pageInstances.forEach((el) => {
|
|
444
|
+
if (el.variantId) {
|
|
445
|
+
const existing = variantMap.get(el.variantId);
|
|
446
|
+
if (existing) {
|
|
447
|
+
existing.count++;
|
|
448
|
+
} else {
|
|
449
|
+
variantMap.set(el.variantId, { count: 1, variantId: el.variantId });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const variants = Array.from(variantMap.values());
|
|
455
|
+
|
|
456
|
+
if (variants.length > 1) {
|
|
457
|
+
return (
|
|
458
|
+
<div className="space-y-2">
|
|
459
|
+
<div className="flex items-center justify-between">
|
|
460
|
+
<span className="text-xs font-medium text-gray-600">Variants ({variants.length})</span>
|
|
461
|
+
{selectedVariantId && (
|
|
462
|
+
<button
|
|
463
|
+
onClick={() => onSelectVariant(null)}
|
|
464
|
+
className="text-xs text-[#00A3E1] hover:underline"
|
|
465
|
+
>
|
|
466
|
+
Clear selection
|
|
467
|
+
</button>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
<div className="grid grid-cols-2 gap-2">
|
|
471
|
+
{variants.map(({ variantId, count }) => (
|
|
472
|
+
<button
|
|
473
|
+
key={variantId}
|
|
474
|
+
onClick={() => onSelectVariant(selectedVariantId === variantId ? null : variantId)}
|
|
475
|
+
className={cn(
|
|
476
|
+
"p-2 rounded border text-left transition-colors",
|
|
477
|
+
selectedVariantId === variantId
|
|
478
|
+
? "border-[#00A3E1] bg-[#00A3E1]/10"
|
|
479
|
+
: "border-gray-200 hover:border-gray-300 bg-white"
|
|
480
|
+
)}
|
|
481
|
+
>
|
|
482
|
+
<span className="text-xs font-mono text-gray-700">#{variantId.substring(0, 6)}</span>
|
|
483
|
+
<span className="text-[10px] text-gray-400 ml-1">({count})</span>
|
|
484
|
+
</button>
|
|
485
|
+
))}
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return null;
|
|
492
|
+
})()}
|
|
493
|
+
|
|
494
|
+
{/* No instances on page */}
|
|
495
|
+
{pageInstances.length === 0 && (
|
|
496
|
+
<div className="p-2 rounded border border-amber-200 bg-amber-50 text-center">
|
|
497
|
+
<span id="brand-panel-span-no-instances-on-this" className="text-xs text-amber-700">
|
|
498
|
+
No instances on this page. Navigate to a page with this component.
|
|
499
|
+
</span>
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
|
|
503
|
+
{/* Edit Scope Toggle - only show when a variant is selected */}
|
|
504
|
+
{selectedVariantId && (
|
|
505
|
+
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded border border-gray-200">
|
|
506
|
+
<span className="text-xs text-gray-500">Apply changes to:</span>
|
|
507
|
+
<div className="flex gap-1 flex-1">
|
|
508
|
+
<button
|
|
509
|
+
onClick={() => setEditScope("component")}
|
|
510
|
+
className={cn(
|
|
511
|
+
"flex-1 px-2 py-1.5 text-xs font-medium rounded transition-colors",
|
|
512
|
+
editScope === "component"
|
|
513
|
+
? "bg-[#00A3E1] text-white"
|
|
514
|
+
: "bg-white text-gray-600 border border-gray-200 hover:bg-gray-100"
|
|
515
|
+
)}
|
|
516
|
+
>
|
|
517
|
+
All {selectedComponentName}s
|
|
518
|
+
</button>
|
|
519
|
+
<button
|
|
520
|
+
onClick={() => setEditScope("variant")}
|
|
521
|
+
className={cn(
|
|
522
|
+
"flex-1 px-2 py-1.5 text-xs font-medium rounded transition-colors",
|
|
523
|
+
editScope === "variant"
|
|
524
|
+
? "bg-[#00A3E1] text-white"
|
|
525
|
+
: "bg-white text-gray-600 border border-gray-200 hover:bg-gray-100"
|
|
526
|
+
)}
|
|
527
|
+
>
|
|
528
|
+
This Variant
|
|
529
|
+
</button>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
)}
|
|
533
|
+
|
|
534
|
+
</>
|
|
535
|
+
)}
|
|
536
|
+
|
|
537
|
+
{/* How it works - only when no component selected */}
|
|
538
|
+
{selectedComponentType === "all" && (
|
|
539
|
+
<div className="bg-blue-50/50 rounded-lg p-3 border border-blue-100">
|
|
540
|
+
<h4 id="h4-how-it-works" className="text-xs font-semibold text-blue-900 mb-2 uppercase tracking-wide">How it works</h4>
|
|
541
|
+
<ol className="text-xs text-blue-800 space-y-1 list-decimal list-inside">
|
|
542
|
+
<li>Hover & click any element on the page to select it</li>
|
|
543
|
+
<li>Describe how you want to change it below</li>
|
|
544
|
+
</ol>
|
|
545
|
+
</div>
|
|
546
|
+
)}
|
|
547
|
+
|
|
548
|
+
{/* AI Component Editor */}
|
|
549
|
+
<div className="space-y-4 pt-2">
|
|
550
|
+
{/* Pending Edit Preview (single-file component edit) */}
|
|
551
|
+
{pendingEdit && (
|
|
552
|
+
<DiffPreview
|
|
553
|
+
pendingEdit={pendingEdit}
|
|
554
|
+
componentType={selectedComponentType}
|
|
555
|
+
onSave={handleSaveAIEdit}
|
|
556
|
+
onCancel={handleClearPendingEdit}
|
|
557
|
+
onPreviewToggle={handlePreviewToggle}
|
|
558
|
+
isPreviewActive={isPreviewActive}
|
|
559
|
+
saveStatus={aiSaveStatus}
|
|
560
|
+
saveMessage={aiSaveMessage}
|
|
561
|
+
variantId={selectedVariantId}
|
|
562
|
+
/>
|
|
563
|
+
)}
|
|
564
|
+
|
|
565
|
+
{/* Vision Mode Pending Edit Preview (multi-file) - OLD: CSS preview */}
|
|
566
|
+
{visionPendingEdit && onSaveVisionEdit && onClearVisionPendingEdit && !applyFirstSession && (
|
|
567
|
+
<VisionDiffPreview
|
|
568
|
+
pendingEdit={visionPendingEdit}
|
|
569
|
+
onSave={onSaveVisionEdit}
|
|
570
|
+
onCancel={onClearVisionPendingEdit}
|
|
571
|
+
/>
|
|
572
|
+
)}
|
|
573
|
+
|
|
574
|
+
{/* Apply-First Mode Preview - NEW: Changes already applied */}
|
|
575
|
+
{applyFirstSession && onApplyFirstAccept && onApplyFirstRevert && (
|
|
576
|
+
<ApplyFirstPreview
|
|
577
|
+
session={applyFirstSession}
|
|
578
|
+
status={applyFirstStatus}
|
|
579
|
+
onAccept={onApplyFirstAccept}
|
|
580
|
+
onRevert={onApplyFirstRevert}
|
|
581
|
+
/>
|
|
582
|
+
)}
|
|
583
|
+
|
|
584
|
+
{/* AI Chat Interface - hide when any pending edit is present */}
|
|
585
|
+
{!pendingEdit && !visionPendingEdit && !applyFirstSession && (
|
|
586
|
+
<ChatInterface
|
|
587
|
+
componentType={selectedComponentType}
|
|
588
|
+
componentName={selectedComponentName || selectedComponentType}
|
|
589
|
+
onEditComplete={handleAIEditComplete}
|
|
590
|
+
onSaveRequest={setPendingEdit}
|
|
591
|
+
pendingEdit={pendingEdit}
|
|
592
|
+
onClearPending={handleClearPendingEdit}
|
|
593
|
+
editScope={editScope}
|
|
594
|
+
variantId={selectedVariantId}
|
|
595
|
+
variantStyles={variantStyles}
|
|
596
|
+
visionMode={visionMode}
|
|
597
|
+
visionFocusedElements={visionFocusedElements}
|
|
598
|
+
onVisionEditComplete={onVisionEditComplete}
|
|
599
|
+
onApplyFirstComplete={onApplyFirstComplete}
|
|
600
|
+
/>
|
|
601
|
+
)}
|
|
602
|
+
|
|
603
|
+
{/* Reset Button */}
|
|
604
|
+
<button
|
|
605
|
+
onClick={() => {
|
|
606
|
+
onReset();
|
|
607
|
+
onResetComponentOverrides();
|
|
608
|
+
handleClearPendingEdit();
|
|
609
|
+
}}
|
|
610
|
+
className={cn(
|
|
611
|
+
"w-full flex items-center justify-center gap-2 py-2.5",
|
|
612
|
+
"text-xs font-medium text-gray-500 hover:text-gray-700",
|
|
613
|
+
"border border-gray-200 rounded hover:bg-gray-50 transition-colors"
|
|
614
|
+
)}
|
|
615
|
+
>
|
|
616
|
+
<RotateCcw className="h-3.5 w-3.5" />
|
|
617
|
+
Reset
|
|
618
|
+
</button>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|