sonance-brand-mcp 1.3.110 → 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-ai-edit/route.ts +30 -7
- 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 +1020 -64
- 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/api/sonance-vision-edit/route.ts +33 -8
- 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 +851 -708
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
- 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 +12 -63
- package/dist/assets/dev-tools/constants.ts +38 -6
- 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 +471 -0
- 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/index.ts +3 -0
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +93 -2
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- package/dist/index.js +22 -3
- package/package.json +2 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
|
-
import { Palette, X, Copy, Check, RotateCcw, ChevronDown, Save, Loader2, AlertCircle, CheckCircle, Sun, Moon, Eye, EyeOff, Zap, Image as ImageIcon, Wand2, Scan, FileCode, Tag, Type, MousePointer, FormInput, Box, Search, Send, Sparkles, RefreshCw, GripHorizontal } from "lucide-react";
|
|
5
|
+
import { Palette, X, Copy, Check, RotateCcw, ChevronDown, Save, Loader2, AlertCircle, CheckCircle, Sun, Moon, Eye, EyeOff, Zap, Image as ImageIcon, Wand2, Scan, FileCode, Tag, Type, MousePointer, FormInput, Box, Search, Send, Sparkles, RefreshCw, GripHorizontal, PanelRightClose, PanelRightOpen, ChevronUp, ChevronDown as ChevronDownIcon } from "lucide-react";
|
|
6
6
|
import { useTheme } from "next-themes";
|
|
7
7
|
import { cn } from "../../lib/utils";
|
|
8
8
|
import {
|
|
@@ -49,9 +49,13 @@ import {
|
|
|
49
49
|
TextOverride,
|
|
50
50
|
OriginalTextState,
|
|
51
51
|
ParentSectionInfo,
|
|
52
|
+
ElementFilters,
|
|
53
|
+
SelectedElementType,
|
|
54
|
+
ImageOverride,
|
|
55
|
+
OriginalImageState,
|
|
56
|
+
PublicImageAsset,
|
|
52
57
|
} from "./types";
|
|
53
58
|
import {
|
|
54
|
-
tabs,
|
|
55
59
|
QUICK_ACTIONS,
|
|
56
60
|
SONANCE_PREVIEW_STYLE_ID,
|
|
57
61
|
TAILWIND_TO_CSS,
|
|
@@ -59,6 +63,8 @@ import {
|
|
|
59
63
|
COMPONENT_CONFIG_MAP,
|
|
60
64
|
getVisibleSections,
|
|
61
65
|
shouldShowScopeOptions,
|
|
66
|
+
DEFAULT_ELEMENT_FILTERS,
|
|
67
|
+
ELEMENT_TYPE_COLORS,
|
|
62
68
|
} from "./constants";
|
|
63
69
|
import {
|
|
64
70
|
generateComponentCSS,
|
|
@@ -78,6 +84,7 @@ import { TextPanel } from "./panels/TextPanel";
|
|
|
78
84
|
import { LogoToolsPanel } from "./panels/LogoToolsPanel";
|
|
79
85
|
import { LogosPanel } from "./panels/LogosPanel";
|
|
80
86
|
import { ComponentsPanel } from "./panels/ComponentsPanel";
|
|
87
|
+
import { useElementScanner, extractLogoName } from "./hooks";
|
|
81
88
|
|
|
82
89
|
// ============================================
|
|
83
90
|
// SONANCE DEVTOOLS
|
|
@@ -180,9 +187,16 @@ function findParentSection(element: Element): ParentSectionInfo | undefined {
|
|
|
180
187
|
|
|
181
188
|
// ---- Main Component ----
|
|
182
189
|
|
|
190
|
+
// Frame layout dimensions
|
|
191
|
+
const BANNER_HEIGHT = 44;
|
|
192
|
+
const SIDEBAR_WIDTH = 300;
|
|
193
|
+
const SIDEBAR_COLLAPSED_WIDTH = 48;
|
|
194
|
+
|
|
183
195
|
export function SonanceDevTools() {
|
|
184
|
-
|
|
185
|
-
const [
|
|
196
|
+
// Frame is always visible in dev mode - start open by default
|
|
197
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
198
|
+
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
|
199
|
+
const [activeTab, setActiveTab] = useState<TabId>("elements");
|
|
186
200
|
const [config, setConfig] = useState<ThemeConfig>(defaultThemeConfig);
|
|
187
201
|
const [mounted, setMounted] = useState(false);
|
|
188
202
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
@@ -190,15 +204,31 @@ export function SonanceDevTools() {
|
|
|
190
204
|
const [saveMessage, setSaveMessage] = useState<string>("");
|
|
191
205
|
const [installedComponents, setInstalledComponents] = useState<string[]>([]);
|
|
192
206
|
|
|
207
|
+
// Unified element filters - show all element types with color coding
|
|
208
|
+
const [elementFilters, setElementFilters] = useState<ElementFilters>(DEFAULT_ELEMENT_FILTERS);
|
|
209
|
+
// Selected element determines what the sidebar shows
|
|
210
|
+
const [selectedElementType, setSelectedElementType] = useState<SelectedElementType>(null);
|
|
211
|
+
|
|
193
212
|
// Visual Inspector state - single toggle, behavior depends on active tab
|
|
194
213
|
const [inspectorEnabled, setInspectorEnabled] = useState(false);
|
|
195
214
|
const [selectedComponentType, setSelectedComponentType] = useState<string>("all");
|
|
196
215
|
const [componentScope, setComponentScope] = useState<"all" | "variant" | "page" | "selected">("all");
|
|
197
216
|
const [selectedComponentId, setSelectedComponentId] = useState<string | null>(null);
|
|
198
217
|
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
199
|
-
const [taggedElements, setTaggedElements] = useState<DetectedElement[]>([]);
|
|
200
218
|
const [viewMode, setViewMode] = useState<ComponentsViewMode>("visual");
|
|
201
|
-
|
|
219
|
+
|
|
220
|
+
// Element Scanner Hook - replaces the old RAF-based detection useEffect
|
|
221
|
+
// Uses MutationObserver for efficient change detection and content-based IDs for stability
|
|
222
|
+
const {
|
|
223
|
+
elements: taggedElements,
|
|
224
|
+
originalLogoStates: scannedLogoStates,
|
|
225
|
+
originalTextStates: scannedTextStates,
|
|
226
|
+
rescan: rescanElements,
|
|
227
|
+
} = useElementScanner({
|
|
228
|
+
enabled: mounted && (inspectorEnabled || activeTab === "elements"),
|
|
229
|
+
filters: elementFilters,
|
|
230
|
+
inspectorEnabled,
|
|
231
|
+
});
|
|
202
232
|
|
|
203
233
|
// AI Preview mode - when active, shows preview highlights instead of inspector
|
|
204
234
|
const [isPreviewActive, setIsPreviewActive] = useState(false);
|
|
@@ -215,12 +245,7 @@ export function SonanceDevTools() {
|
|
|
215
245
|
// Track which elements were changed (for highlighting until accept/revert)
|
|
216
246
|
const [changedElements, setChangedElements] = useState<VisionFocusedElement[]>([]);
|
|
217
247
|
|
|
218
|
-
//
|
|
219
|
-
const DEVTOOLS_POSITION_KEY = "sonance-devtools-pos";
|
|
220
|
-
const DEFAULT_POSITION = { x: 0, y: 0 }; // Offset from default bottom-right position
|
|
221
|
-
const [dragPosition, setDragPosition] = useState(DEFAULT_POSITION);
|
|
222
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
223
|
-
const dragOffsetRef = useRef({ x: 0, y: 0 });
|
|
248
|
+
// Refs for panel
|
|
224
249
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
225
250
|
|
|
226
251
|
// Portal container state - ensures DevTool is always on top of modals
|
|
@@ -251,6 +276,41 @@ export function SonanceDevTools() {
|
|
|
251
276
|
return () => observer.disconnect();
|
|
252
277
|
}, []);
|
|
253
278
|
|
|
279
|
+
// Restore Apply-First session from localStorage on mount
|
|
280
|
+
// This allows users to refresh the page to see structural changes without losing the pending review state
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
try {
|
|
283
|
+
const stored = localStorage.getItem("sonance-apply-first-session");
|
|
284
|
+
if (stored) {
|
|
285
|
+
const parsed = JSON.parse(stored);
|
|
286
|
+
// Check if session is not too old (e.g., less than 1 hour)
|
|
287
|
+
const sessionAge = Date.now() - (parsed.timestamp || parsed.appliedAt || 0);
|
|
288
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
289
|
+
|
|
290
|
+
if (sessionAge < ONE_HOUR && parsed.sessionId) {
|
|
291
|
+
console.log("[Apply-First] Restoring session from localStorage:", parsed.sessionId);
|
|
292
|
+
setApplyFirstSession({
|
|
293
|
+
sessionId: parsed.sessionId,
|
|
294
|
+
modifications: parsed.modifications || [],
|
|
295
|
+
appliedAt: parsed.appliedAt || Date.now(),
|
|
296
|
+
status: 'applied',
|
|
297
|
+
backupPaths: parsed.backupPaths || [],
|
|
298
|
+
isPreview: parsed.isPreview,
|
|
299
|
+
isTextOnlyChange: parsed.isTextOnlyChange,
|
|
300
|
+
});
|
|
301
|
+
// Set status to reviewing since changes are already applied
|
|
302
|
+
setApplyFirstStatus("reviewing");
|
|
303
|
+
} else {
|
|
304
|
+
// Session is too old, clear it
|
|
305
|
+
console.log("[Apply-First] Session too old, clearing from localStorage");
|
|
306
|
+
localStorage.removeItem("sonance-apply-first-session");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch (e) {
|
|
310
|
+
console.warn("[Apply-First] Failed to restore session from localStorage:", e);
|
|
311
|
+
}
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
254
314
|
// Component-specific style overrides (for scalable, project-agnostic styling)
|
|
255
315
|
// Key: component type (e.g., "card", "button-primary", "card:variant123"), Value: style overrides
|
|
256
316
|
const [componentOverrides, setComponentOverrides] = useState<Record<string, ComponentStyle>>({});
|
|
@@ -325,13 +385,12 @@ export function SonanceDevTools() {
|
|
|
325
385
|
const [selectedLogoId, setSelectedLogoId] = useState<string | null>(null);
|
|
326
386
|
const [globalLogoConfig, setGlobalLogoConfig] = useState<LogoOverride>({});
|
|
327
387
|
const [individualLogoConfigs, setIndividualLogoConfigs] = useState<Record<string, LogoOverride>>({});
|
|
388
|
+
// originalLogoStates now synced from useElementScanner hook (scannedLogoStates)
|
|
328
389
|
const [originalLogoStates, setOriginalLogoStates] = useState<Record<string, OriginalLogoState>>({});
|
|
329
390
|
const [logoSaveStatus, setLogoSaveStatus] = useState<LogoSaveStatus>("idle");
|
|
330
391
|
const [logoSaveMessage, setLogoSaveMessage] = useState<string>("");
|
|
331
392
|
const [autoFixStatus, setAutoFixStatus] = useState<AutoFixStatus>("idle");
|
|
332
393
|
const [autoFixMessage, setAutoFixMessage] = useState<string>("");
|
|
333
|
-
const logoIdCounter = useRef(0);
|
|
334
|
-
const textIdCounter = useRef(0);
|
|
335
394
|
|
|
336
395
|
// Text Tool State
|
|
337
396
|
const [selectedTextId, setSelectedTextId] = useState<string | null>(null);
|
|
@@ -341,6 +400,13 @@ export function SonanceDevTools() {
|
|
|
341
400
|
const [savedTextOverrides, setSavedTextOverrides] = useState<Record<string, TextOverride>>({});
|
|
342
401
|
const SAVED_TEXT_STORAGE_KEY = "sonance-devtools-saved-text-overrides";
|
|
343
402
|
|
|
403
|
+
// Image Editing State
|
|
404
|
+
const [publicImages, setPublicImages] = useState<PublicImageAsset[]>([]);
|
|
405
|
+
const [publicImagesByFolder, setPublicImagesByFolder] = useState<Record<string, PublicImageAsset[]>>({});
|
|
406
|
+
const [imageOverrides, setImageOverrides] = useState<Record<string, ImageOverride>>({});
|
|
407
|
+
const [originalImageStates, setOriginalImageStates] = useState<Record<string, OriginalImageState>>({});
|
|
408
|
+
const [isImageSaving, setIsImageSaving] = useState(false);
|
|
409
|
+
|
|
344
410
|
// Project Analysis state
|
|
345
411
|
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
|
|
346
412
|
const [analysisStatus, setAnalysisStatus] = useState<AnalysisStatus>("idle");
|
|
@@ -356,7 +422,7 @@ export function SonanceDevTools() {
|
|
|
356
422
|
const { theme, setTheme, resolvedTheme } = useTheme();
|
|
357
423
|
|
|
358
424
|
// Global brand context for logo switching
|
|
359
|
-
const { setBrand } = useBrand();
|
|
425
|
+
const { currentBrand, setBrand } = useBrand();
|
|
360
426
|
|
|
361
427
|
// Toggle between light and dark mode
|
|
362
428
|
const toggleThemeMode = useCallback(() => {
|
|
@@ -369,21 +435,92 @@ export function SonanceDevTools() {
|
|
|
369
435
|
setMounted(true);
|
|
370
436
|
}, []);
|
|
371
437
|
|
|
372
|
-
//
|
|
438
|
+
// Sync scanned states from useElementScanner hook to local state
|
|
439
|
+
// This allows components to access original states for reset functionality
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (Object.keys(scannedLogoStates).length > 0) {
|
|
442
|
+
setOriginalLogoStates(prev => {
|
|
443
|
+
// Merge scanned states with existing (preserve manual updates)
|
|
444
|
+
return { ...prev, ...scannedLogoStates };
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}, [scannedLogoStates]);
|
|
448
|
+
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (Object.keys(scannedTextStates).length > 0) {
|
|
451
|
+
// Update the ref with scanned text states
|
|
452
|
+
originalTextStatesRef.current = { ...originalTextStatesRef.current, ...scannedTextStates };
|
|
453
|
+
}
|
|
454
|
+
}, [scannedTextStates]);
|
|
455
|
+
|
|
456
|
+
// Body layout injection for "squeeze" effect - app content respects dev tools frame
|
|
373
457
|
useEffect(() => {
|
|
374
458
|
if (!mounted) return;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
459
|
+
|
|
460
|
+
const root = document.documentElement;
|
|
461
|
+
const body = document.body;
|
|
462
|
+
const styleId = 'sonance-devtools-layout-styles';
|
|
463
|
+
|
|
464
|
+
if (isOpen) {
|
|
465
|
+
const sidebarWidth = isSidebarCollapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_WIDTH;
|
|
466
|
+
|
|
467
|
+
// Set CSS variables for components that need to reference the offset
|
|
468
|
+
root.style.setProperty('--devtools-banner-height', `${BANNER_HEIGHT}px`);
|
|
469
|
+
root.style.setProperty('--devtools-sidebar-width', `${sidebarWidth}px`);
|
|
470
|
+
|
|
471
|
+
// Inject styles that adjust fixed elements and body layout
|
|
472
|
+
let styleEl = document.getElementById(styleId) as HTMLStyleElement;
|
|
473
|
+
if (!styleEl) {
|
|
474
|
+
styleEl = document.createElement('style');
|
|
475
|
+
styleEl.id = styleId;
|
|
476
|
+
document.head.appendChild(styleEl);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// CSS that creates space for the DevTools
|
|
480
|
+
// Components that read --devtools-banner-height and --devtools-sidebar-width will adjust automatically
|
|
481
|
+
styleEl.textContent = `
|
|
482
|
+
/* Ensure body/html don't overflow */
|
|
483
|
+
html, body {
|
|
484
|
+
margin: 0 !important;
|
|
485
|
+
padding: 0 !important;
|
|
486
|
+
overflow-x: hidden;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Push the main content wrapper down */
|
|
490
|
+
body > div:first-child:not([data-sonance-devtools="true"]) {
|
|
491
|
+
padding-top: ${BANNER_HEIGHT}px !important;
|
|
492
|
+
margin-right: ${sidebarWidth}px !important;
|
|
493
|
+
min-height: 100vh;
|
|
494
|
+
box-sizing: border-box;
|
|
381
495
|
}
|
|
496
|
+
|
|
497
|
+
/* Ensure main content areas respect the sidebar width */
|
|
498
|
+
main:not([data-sonance-devtools] *) {
|
|
499
|
+
max-width: calc(100vw - 256px - ${sidebarWidth}px) !important;
|
|
500
|
+
}
|
|
501
|
+
`;
|
|
502
|
+
} else {
|
|
503
|
+
// Reset everything when closed
|
|
504
|
+
root.style.removeProperty('--devtools-banner-height');
|
|
505
|
+
root.style.removeProperty('--devtools-sidebar-width');
|
|
506
|
+
|
|
507
|
+
const styleEl = document.getElementById(styleId);
|
|
508
|
+
if (styleEl) {
|
|
509
|
+
styleEl.remove();
|
|
382
510
|
}
|
|
383
|
-
} catch {
|
|
384
|
-
// Ignore parse errors
|
|
385
511
|
}
|
|
386
|
-
|
|
512
|
+
|
|
513
|
+
return () => {
|
|
514
|
+
// Cleanup on unmount
|
|
515
|
+
root.style.removeProperty('--devtools-banner-height');
|
|
516
|
+
root.style.removeProperty('--devtools-sidebar-width');
|
|
517
|
+
|
|
518
|
+
const styleEl = document.getElementById(styleId);
|
|
519
|
+
if (styleEl) {
|
|
520
|
+
styleEl.remove();
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}, [mounted, isOpen, isSidebarCollapsed]);
|
|
387
524
|
|
|
388
525
|
// Restore apply-first session from localStorage on mount
|
|
389
526
|
// This allows the Accept/Revert UI to survive page refreshes
|
|
@@ -407,7 +544,7 @@ export function SonanceDevTools() {
|
|
|
407
544
|
setApplyFirstStatus("reviewing");
|
|
408
545
|
// Auto-open the DevTools panel so user sees the Accept/Revert UI
|
|
409
546
|
setIsOpen(true);
|
|
410
|
-
setActiveTab("
|
|
547
|
+
setActiveTab("elements");
|
|
411
548
|
} else {
|
|
412
549
|
// Session expired, clear it
|
|
413
550
|
console.log("[Apply-First] Session expired, clearing localStorage");
|
|
@@ -420,77 +557,6 @@ export function SonanceDevTools() {
|
|
|
420
557
|
}
|
|
421
558
|
}, [mounted]);
|
|
422
559
|
|
|
423
|
-
// Drag handlers for movable panel
|
|
424
|
-
const headerRef = useRef<HTMLDivElement>(null);
|
|
425
|
-
|
|
426
|
-
const handleDragStart = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
427
|
-
// Don't start drag if clicking on a button or interactive element
|
|
428
|
-
const target = e.target as HTMLElement;
|
|
429
|
-
if (target.closest('button')) return;
|
|
430
|
-
|
|
431
|
-
if (!panelRef.current || !headerRef.current) return;
|
|
432
|
-
|
|
433
|
-
// Prevent text selection during drag
|
|
434
|
-
e.preventDefault();
|
|
435
|
-
|
|
436
|
-
// Store the initial mouse position and current panel offset
|
|
437
|
-
dragOffsetRef.current = {
|
|
438
|
-
x: e.clientX - dragPosition.x,
|
|
439
|
-
y: e.clientY - dragPosition.y,
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
setIsDragging(true);
|
|
443
|
-
headerRef.current.setPointerCapture(e.pointerId);
|
|
444
|
-
}, [dragPosition]);
|
|
445
|
-
|
|
446
|
-
const handleDragMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
447
|
-
if (!isDragging || !panelRef.current) return;
|
|
448
|
-
|
|
449
|
-
const viewportWidth = window.innerWidth;
|
|
450
|
-
const viewportHeight = window.innerHeight;
|
|
451
|
-
const panelWidth = panelRef.current.offsetWidth;
|
|
452
|
-
const panelHeight = panelRef.current.offsetHeight;
|
|
453
|
-
|
|
454
|
-
// Calculate new position directly from mouse movement
|
|
455
|
-
const newX = e.clientX - dragOffsetRef.current.x;
|
|
456
|
-
const newY = e.clientY - dragOffsetRef.current.y;
|
|
457
|
-
|
|
458
|
-
// Clamp to keep panel within viewport (with 24px padding)
|
|
459
|
-
const padding = 24;
|
|
460
|
-
const maxX = viewportWidth - panelWidth - padding;
|
|
461
|
-
const maxY = viewportHeight - panelHeight - padding;
|
|
462
|
-
const minX = -(viewportWidth - panelWidth - padding);
|
|
463
|
-
const minY = -(viewportHeight - panelHeight - padding);
|
|
464
|
-
|
|
465
|
-
setDragPosition({
|
|
466
|
-
x: Math.max(minX, Math.min(maxX, newX)),
|
|
467
|
-
y: Math.max(minY, Math.min(maxY, newY)),
|
|
468
|
-
});
|
|
469
|
-
}, [isDragging]);
|
|
470
|
-
|
|
471
|
-
const handleDragEnd = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
472
|
-
if (!isDragging || !headerRef.current) return;
|
|
473
|
-
|
|
474
|
-
setIsDragging(false);
|
|
475
|
-
headerRef.current.releasePointerCapture(e.pointerId);
|
|
476
|
-
|
|
477
|
-
// Save position to localStorage
|
|
478
|
-
try {
|
|
479
|
-
localStorage.setItem(DEVTOOLS_POSITION_KEY, JSON.stringify(dragPosition));
|
|
480
|
-
} catch {
|
|
481
|
-
// Ignore storage errors
|
|
482
|
-
}
|
|
483
|
-
}, [isDragging, dragPosition, DEVTOOLS_POSITION_KEY]);
|
|
484
|
-
|
|
485
|
-
const handleResetPosition = useCallback(() => {
|
|
486
|
-
setDragPosition(DEFAULT_POSITION);
|
|
487
|
-
try {
|
|
488
|
-
localStorage.removeItem(DEVTOOLS_POSITION_KEY);
|
|
489
|
-
} catch {
|
|
490
|
-
// Ignore storage errors
|
|
491
|
-
}
|
|
492
|
-
}, [DEVTOOLS_POSITION_KEY]);
|
|
493
|
-
|
|
494
560
|
// Inject/update component-specific preview styles whenever overrides change
|
|
495
561
|
useEffect(() => {
|
|
496
562
|
if (!mounted) return;
|
|
@@ -659,34 +725,32 @@ export function SonanceDevTools() {
|
|
|
659
725
|
}
|
|
660
726
|
}
|
|
661
727
|
|
|
662
|
-
if (mounted && (
|
|
728
|
+
if (mounted && (elementFilters.images || inspectorEnabled) && logoAssets.length === 0) {
|
|
663
729
|
fetchLogoAssets();
|
|
664
730
|
}
|
|
665
|
-
}, [mounted,
|
|
731
|
+
}, [mounted, elementFilters.images, inspectorEnabled, logoAssets.length]);
|
|
666
732
|
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
733
|
+
// Fetch public folder images for image editing
|
|
734
|
+
useEffect(() => {
|
|
735
|
+
async function fetchPublicImages() {
|
|
736
|
+
try {
|
|
737
|
+
const response = await fetch("/api/public-images");
|
|
738
|
+
if (response.ok) {
|
|
739
|
+
const data = await response.json();
|
|
740
|
+
setPublicImages(data.images || []);
|
|
741
|
+
setPublicImagesByFolder(data.imagesByFolder || {});
|
|
742
|
+
}
|
|
743
|
+
} catch (error) {
|
|
744
|
+
console.warn("Could not fetch public images:", error);
|
|
675
745
|
}
|
|
676
746
|
}
|
|
677
747
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return null;
|
|
748
|
+
if (mounted && elementFilters.images && publicImages.length === 0) {
|
|
749
|
+
fetchPublicImages();
|
|
681
750
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const filename = parts[parts.length - 1];
|
|
686
|
-
// Remove file extension
|
|
687
|
-
const nameWithoutExt = filename.replace(/\.(png|jpg|jpeg|svg|webp|gif)$/i, "");
|
|
688
|
-
return nameWithoutExt || null;
|
|
689
|
-
}, []);
|
|
751
|
+
}, [mounted, elementFilters.images, publicImages.length]);
|
|
752
|
+
|
|
753
|
+
// extractLogoName is now imported from "./hooks"
|
|
690
754
|
|
|
691
755
|
// Helper to find complementary light/dark logo variant
|
|
692
756
|
// Returns { light: path, dark: path } or null if no match found
|
|
@@ -759,21 +823,15 @@ export function SonanceDevTools() {
|
|
|
759
823
|
// Filter elements for InspectorOverlay based on active tab and selected component type
|
|
760
824
|
// Now supports both specific component IDs (e.g., "button-primary") and categories (e.g., "buttons")
|
|
761
825
|
const filteredOverlayElements = useMemo(() => {
|
|
762
|
-
// Filter based on
|
|
763
|
-
let filtered = taggedElements
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
// On text tab, only show text elements
|
|
770
|
-
filtered = taggedElements.filter(el => el.type === "text");
|
|
771
|
-
} else if (activeTab === "components") {
|
|
772
|
-
// On components tab, only show components
|
|
773
|
-
filtered = taggedElements.filter(el => el.type === "component");
|
|
774
|
-
}
|
|
826
|
+
// Filter based on element type filters (unified color-coded view)
|
|
827
|
+
let filtered = taggedElements.filter(el => {
|
|
828
|
+
if (el.type === "component" && !elementFilters.components) return false;
|
|
829
|
+
if (el.type === "logo" && !elementFilters.images) return false;
|
|
830
|
+
if (el.type === "text" && !elementFilters.text) return false;
|
|
831
|
+
return true;
|
|
832
|
+
});
|
|
775
833
|
|
|
776
|
-
// When inspector is enabled, show
|
|
834
|
+
// When inspector is enabled, show all filtered elements
|
|
777
835
|
if (inspectorEnabled) {
|
|
778
836
|
return filtered;
|
|
779
837
|
}
|
|
@@ -781,19 +839,19 @@ export function SonanceDevTools() {
|
|
|
781
839
|
// When viewing inspector mode for a selected component, show ALL elements
|
|
782
840
|
// (the InspectorOverlay will handle highlighting/dimming based on type match)
|
|
783
841
|
if (viewMode === "inspector" && selectedComponentType !== "all") {
|
|
784
|
-
return
|
|
842
|
+
return filtered;
|
|
785
843
|
}
|
|
786
844
|
|
|
787
845
|
if (selectedComponentType === "all") {
|
|
788
|
-
return
|
|
846
|
+
return filtered;
|
|
789
847
|
}
|
|
790
848
|
|
|
791
849
|
// Check if this is a specific component ID (exists in componentSnippets)
|
|
792
850
|
const isSpecificComponent = componentSnippets.some(s => s.id === selectedComponentType);
|
|
793
851
|
|
|
794
852
|
// Filter components based on selected type
|
|
795
|
-
return
|
|
796
|
-
// Always include logos and text
|
|
853
|
+
return filtered.filter((el) => {
|
|
854
|
+
// Always include logos and text (they're already filtered by elementFilters)
|
|
797
855
|
if (el.type === "logo" || el.type === "text") return true;
|
|
798
856
|
|
|
799
857
|
// For component type filtering
|
|
@@ -832,323 +890,11 @@ export function SonanceDevTools() {
|
|
|
832
890
|
|
|
833
891
|
return true;
|
|
834
892
|
});
|
|
835
|
-
}, [taggedElements, selectedComponentType, inspectorEnabled, viewMode]);
|
|
836
|
-
|
|
837
|
-
// Simple string hash function for variant detection
|
|
838
|
-
const generateHash = (str: string): string => {
|
|
839
|
-
let hash = 0;
|
|
840
|
-
if (str.length === 0) return hash.toString();
|
|
841
|
-
for (let i = 0; i < str.length; i++) {
|
|
842
|
-
const char = str.charCodeAt(i);
|
|
843
|
-
hash = ((hash << 5) - hash) + char;
|
|
844
|
-
hash = hash & hash; // Convert to 32bit integer
|
|
845
|
-
}
|
|
846
|
-
return Math.abs(hash).toString(16);
|
|
847
|
-
};
|
|
893
|
+
}, [taggedElements, selectedComponentType, inspectorEnabled, viewMode, elementFilters]);
|
|
848
894
|
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
// Scan when inspector is enabled - scans based on active tab
|
|
853
|
-
const shouldScan = inspectorEnabled || activeTab === "components" || activeTab === "logos" || activeTab === "text";
|
|
854
|
-
|
|
855
|
-
// console.log("Scan effect running. shouldScan:", shouldScan, "activeTab:", activeTab);
|
|
856
|
-
|
|
857
|
-
if (!shouldScan || !mounted) {
|
|
858
|
-
setTaggedElements([]);
|
|
859
|
-
if (inspectorRef.current) {
|
|
860
|
-
cancelAnimationFrame(inspectorRef.current);
|
|
861
|
-
inspectorRef.current = null;
|
|
862
|
-
}
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const scanElements = () => {
|
|
867
|
-
const newTagged: DetectedElement[] = [];
|
|
868
|
-
const newOriginalStates: Record<string, OriginalLogoState> = { ...originalLogoStates };
|
|
869
|
-
|
|
870
|
-
// Helper: Check if element is inside a scroll container and clip rect to visible bounds
|
|
871
|
-
const getVisibleRect = (el: Element, rect: DOMRect): DOMRect | null => {
|
|
872
|
-
// Find the nearest scroll container (aside, nav, or element with overflow)
|
|
873
|
-
const scrollParent = el.closest("aside, nav, [data-sidebar]");
|
|
874
|
-
if (scrollParent) {
|
|
875
|
-
const parentRect = scrollParent.getBoundingClientRect();
|
|
876
|
-
// Check if element is fully outside the scroll container's visible area
|
|
877
|
-
if (rect.bottom < parentRect.top || rect.top > parentRect.bottom ||
|
|
878
|
-
rect.right < parentRect.left || rect.left > parentRect.right) {
|
|
879
|
-
return null; // Element is not visible
|
|
880
|
-
}
|
|
881
|
-
// Clip the rect to the parent's visible bounds
|
|
882
|
-
const clippedTop = Math.max(rect.top, parentRect.top);
|
|
883
|
-
const clippedBottom = Math.min(rect.bottom, parentRect.bottom);
|
|
884
|
-
const clippedLeft = Math.max(rect.left, parentRect.left);
|
|
885
|
-
const clippedRight = Math.min(rect.right, parentRect.right);
|
|
886
|
-
// If clipped area is too small, skip it
|
|
887
|
-
if (clippedBottom - clippedTop < 10 || clippedRight - clippedLeft < 10) {
|
|
888
|
-
return null;
|
|
889
|
-
}
|
|
890
|
-
return new DOMRect(clippedLeft, clippedTop, clippedRight - clippedLeft, clippedBottom - clippedTop);
|
|
891
|
-
}
|
|
892
|
-
return rect; // No scroll parent, use original rect
|
|
893
|
-
};
|
|
894
|
-
|
|
895
|
-
// Helper: Detect active modal and filter elements to only those in the topmost layer
|
|
896
|
-
const activeModalContent = getActiveModalContent();
|
|
897
|
-
const isInActiveLayer = (el: Element): boolean => {
|
|
898
|
-
// Always exclude DevTools panel
|
|
899
|
-
if (el.closest("[data-sonance-devtools]")) return false;
|
|
900
|
-
|
|
901
|
-
// If a modal is active, only include elements inside the modal content
|
|
902
|
-
if (activeModalContent) {
|
|
903
|
-
return activeModalContent.contains(el) || el === activeModalContent;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
return true;
|
|
907
|
-
};
|
|
908
|
-
|
|
909
|
-
// Scan for tagged components
|
|
910
|
-
if (inspectorEnabled || activeTab === "components") {
|
|
911
|
-
// 1. Scan for explicitly tagged components
|
|
912
|
-
const taggedComponents = document.querySelectorAll("[data-sonance-name]");
|
|
913
|
-
taggedComponents.forEach((el) => {
|
|
914
|
-
// Skip elements outside the active layer (DevTools, behind modals)
|
|
915
|
-
if (!isInActiveLayer(el)) return;
|
|
916
|
-
|
|
917
|
-
const name = el.getAttribute("data-sonance-name");
|
|
918
|
-
if (name) {
|
|
919
|
-
const rawRect = el.getBoundingClientRect();
|
|
920
|
-
const rect = getVisibleRect(el, rawRect);
|
|
921
|
-
if (rect && rect.width > 0 && rect.height > 0) {
|
|
922
|
-
// Generate variant ID based on class names and computed styles
|
|
923
|
-
// This groups visually identical components together
|
|
924
|
-
// Use stable variant ID: compute once, reuse from attribute
|
|
925
|
-
// This prevents hover/focus states from changing the hash
|
|
926
|
-
let variantId = el.getAttribute("data-sonance-variant");
|
|
927
|
-
if (!variantId) {
|
|
928
|
-
const className = el.className;
|
|
929
|
-
const computed = window.getComputedStyle(el);
|
|
930
|
-
const styleSignature = `${className}|${computed.backgroundColor}|${computed.borderColor}|${computed.borderRadius}|${computed.color}`;
|
|
931
|
-
variantId = generateHash(styleSignature);
|
|
932
|
-
el.setAttribute("data-sonance-variant", variantId);
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// Capture textContent and className for dynamic element matching
|
|
936
|
-
const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
|
|
937
|
-
const className = el.className?.toString() || "";
|
|
938
|
-
|
|
939
|
-
// Capture element ID and child IDs for precise code targeting
|
|
940
|
-
const elementId = el.id || undefined;
|
|
941
|
-
const childIds = Array.from(el.querySelectorAll('[id]'))
|
|
942
|
-
.map(child => child.id)
|
|
943
|
-
.filter(id => id) as string[];
|
|
944
|
-
|
|
945
|
-
newTagged.push({ name, rect, type: "component", variantId, textContent, className, elementId, childIds: childIds.length > 0 ? childIds : undefined });
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
// 2. Scan for untagged primitive elements (Buttons, Inputs) to ensure list isn't empty
|
|
951
|
-
// Only add if not already tagged (though querySelectorAll excludes them implicitly if we check attribute)
|
|
952
|
-
const genericSelectors = {
|
|
953
|
-
"button": "button:not([data-sonance-name])",
|
|
954
|
-
"input": "input:not([data-sonance-name])",
|
|
955
|
-
"select": "select:not([data-sonance-name])",
|
|
956
|
-
"textarea": "textarea:not([data-sonance-name])"
|
|
957
|
-
};
|
|
958
|
-
|
|
959
|
-
Object.entries(genericSelectors).forEach(([genericName, selector]) => {
|
|
960
|
-
const elements = document.querySelectorAll(selector);
|
|
961
|
-
elements.forEach((el) => {
|
|
962
|
-
// Skip elements outside the active layer (DevTools, behind modals)
|
|
963
|
-
if (!isInActiveLayer(el)) return;
|
|
964
|
-
|
|
965
|
-
const rawRect = el.getBoundingClientRect();
|
|
966
|
-
const rect = getVisibleRect(el, rawRect);
|
|
967
|
-
if (rect && rect.width > 0 && rect.height > 0) {
|
|
968
|
-
// Generate stable variant ID for generic elements too
|
|
969
|
-
// Compute once, reuse from attribute to prevent hover state changes
|
|
970
|
-
let variantId = el.getAttribute("data-sonance-variant");
|
|
971
|
-
if (!variantId) {
|
|
972
|
-
const className = el.className;
|
|
973
|
-
const computed = window.getComputedStyle(el);
|
|
974
|
-
const styleSignature = `${className}|${computed.backgroundColor}|${computed.borderColor}|${computed.borderRadius}|${computed.color}`;
|
|
975
|
-
variantId = generateHash(styleSignature);
|
|
976
|
-
el.setAttribute("data-sonance-variant", variantId);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
// Ensure generic elements have a data-sonance-name for targeting
|
|
980
|
-
if (!el.getAttribute("data-sonance-name")) {
|
|
981
|
-
el.setAttribute("data-sonance-name", genericName);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// Capture textContent and className for dynamic element matching
|
|
985
|
-
const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
|
|
986
|
-
const elClassName = el.className?.toString() || "";
|
|
987
|
-
|
|
988
|
-
// Capture element ID and child IDs for precise code targeting
|
|
989
|
-
const elementId = el.id || undefined;
|
|
990
|
-
const childIds = Array.from(el.querySelectorAll('[id]'))
|
|
991
|
-
.map(child => child.id)
|
|
992
|
-
.filter(id => id) as string[];
|
|
993
|
-
|
|
994
|
-
newTagged.push({ name: genericName, rect, type: "component", variantId, textContent, className: elClassName, elementId, childIds: childIds.length > 0 ? childIds : undefined });
|
|
995
|
-
}
|
|
996
|
-
});
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// Scan for logo images when on logos tab
|
|
1001
|
-
if (activeTab === "logos") {
|
|
1002
|
-
const images = document.querySelectorAll("img");
|
|
1003
|
-
images.forEach((img) => {
|
|
1004
|
-
// Skip elements outside the active layer (DevTools, behind modals)
|
|
1005
|
-
if (!isInActiveLayer(img)) return;
|
|
1006
|
-
|
|
1007
|
-
const src = img.src || img.getAttribute("src") || "";
|
|
1008
|
-
const alt = img.alt || "";
|
|
1009
|
-
|
|
1010
|
-
// Check if src or alt contains "logo"
|
|
1011
|
-
const logoName = extractLogoName(src);
|
|
1012
|
-
const altContainsLogo = alt.toLowerCase().includes("logo");
|
|
1013
|
-
|
|
1014
|
-
if (logoName || altContainsLogo) {
|
|
1015
|
-
const rect = img.getBoundingClientRect();
|
|
1016
|
-
// Only include visible elements
|
|
1017
|
-
if (rect.width > 0 && rect.height > 0) {
|
|
1018
|
-
// Assign or retrieve a unique ID for this logo element
|
|
1019
|
-
let logoId = img.getAttribute("data-sonance-logo-id");
|
|
1020
|
-
if (!logoId) {
|
|
1021
|
-
logoId = `logo-${logoIdCounter.current++}`;
|
|
1022
|
-
img.setAttribute("data-sonance-logo-id", logoId);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// Store original state if not already stored
|
|
1026
|
-
if (!newOriginalStates[logoId]) {
|
|
1027
|
-
const originalSrcset = img.getAttribute("data-original-srcset") || img.srcset || "";
|
|
1028
|
-
newOriginalStates[logoId] = {
|
|
1029
|
-
src: img.getAttribute("data-original-src") || src,
|
|
1030
|
-
width: img.naturalWidth || img.width,
|
|
1031
|
-
height: img.naturalHeight || img.height,
|
|
1032
|
-
srcset: originalSrcset,
|
|
1033
|
-
};
|
|
1034
|
-
// Also store original src and srcset as data attributes for reset
|
|
1035
|
-
if (!img.getAttribute("data-original-src")) {
|
|
1036
|
-
img.setAttribute("data-original-src", src);
|
|
1037
|
-
}
|
|
1038
|
-
if (!img.getAttribute("data-original-srcset") && img.srcset) {
|
|
1039
|
-
img.setAttribute("data-original-srcset", img.srcset);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
const displayName = logoName || alt || "Logo";
|
|
1044
|
-
newTagged.push({ name: displayName, rect, type: "logo", logoId });
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// Scan for text elements
|
|
1051
|
-
// Scan for text when on text tab and inspector is enabled
|
|
1052
|
-
if (activeTab === "text" && inspectorEnabled) {
|
|
1053
|
-
// Only select meaningful text containers - exclude spans as they're usually nested
|
|
1054
|
-
const textSelectors = "h1, h2, h3, h4, h5, h6, p, a, label, blockquote, figcaption, li";
|
|
1055
|
-
const textElements = document.querySelectorAll(textSelectors);
|
|
1056
|
-
|
|
1057
|
-
// Track which elements we've already added to avoid nested duplicates
|
|
1058
|
-
const addedElements = new Set<Element>();
|
|
1059
|
-
|
|
1060
|
-
textElements.forEach((el) => {
|
|
1061
|
-
// Skip elements outside the active layer (DevTools, behind modals)
|
|
1062
|
-
if (!isInActiveLayer(el)) return;
|
|
1063
|
-
|
|
1064
|
-
// Skip if this element is inside another text element we're already tracking
|
|
1065
|
-
// This prevents duplicate labels for nested structures
|
|
1066
|
-
const parentTextEl = el.parentElement?.closest("h1, h2, h3, h4, h5, h6, p, a, label, blockquote, figcaption, li");
|
|
1067
|
-
if (parentTextEl && !parentTextEl.closest("[data-sonance-devtools]")) {
|
|
1068
|
-
// Allow links inside paragraphs to be selectable, but not other nesting
|
|
1069
|
-
if (!(el.tagName.toLowerCase() === "a" && parentTextEl.tagName.toLowerCase() === "p")) {
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// Skip elements that are already added
|
|
1075
|
-
if (addedElements.has(el)) return;
|
|
1076
|
-
|
|
1077
|
-
const rect = el.getBoundingClientRect();
|
|
1078
|
-
// Only include visible elements with content
|
|
1079
|
-
const textContent = el.textContent?.trim() || "";
|
|
1080
|
-
|
|
1081
|
-
// Skip elements that are too small (likely icons or decorative)
|
|
1082
|
-
if (rect.width < 20 || rect.height < 10) return;
|
|
1083
|
-
|
|
1084
|
-
// Skip elements with very short content (likely icons or single characters)
|
|
1085
|
-
if (textContent.length < 2) return;
|
|
1086
|
-
|
|
1087
|
-
// Skip if the element only contains non-text children (like SVGs/icons)
|
|
1088
|
-
const hasDirectTextContent = Array.from(el.childNodes).some(
|
|
1089
|
-
node => node.nodeType === Node.TEXT_NODE && node.textContent?.trim()
|
|
1090
|
-
);
|
|
1091
|
-
// For links and headings, check if they have meaningful text content
|
|
1092
|
-
const hasMeaningfulContent = textContent.length >= 2 && !/^[\s\u200B-\u200D\uFEFF]+$/.test(textContent);
|
|
1093
|
-
|
|
1094
|
-
if (!hasMeaningfulContent) return;
|
|
1095
|
-
|
|
1096
|
-
if (rect.width > 0 && rect.height > 0 && textContent.length > 0) {
|
|
1097
|
-
// Assign or retrieve a unique ID for this text element
|
|
1098
|
-
let textId = el.getAttribute("data-sonance-text-id");
|
|
1099
|
-
if (!textId) {
|
|
1100
|
-
textId = `text-${textIdCounter.current++}`;
|
|
1101
|
-
el.setAttribute("data-sonance-text-id", textId);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// Capture original state if not already captured
|
|
1105
|
-
if (!originalTextStatesRef.current[textId]) {
|
|
1106
|
-
const computed = window.getComputedStyle(el);
|
|
1107
|
-
originalTextStatesRef.current[textId] = {
|
|
1108
|
-
textContent: el.textContent,
|
|
1109
|
-
fontSize: computed.fontSize,
|
|
1110
|
-
fontWeight: computed.fontWeight,
|
|
1111
|
-
lineHeight: computed.lineHeight,
|
|
1112
|
-
letterSpacing: computed.letterSpacing,
|
|
1113
|
-
color: computed.color,
|
|
1114
|
-
fontFamily: computed.fontFamily,
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
const tagName = el.tagName.toLowerCase();
|
|
1119
|
-
const displayName = tagName === "a" ? "Link" : tagName === "li" ? "List Item" : tagName.toUpperCase();
|
|
1120
|
-
const truncatedContent = textContent.length > 30 ? textContent.substring(0, 30) + "..." : textContent;
|
|
1121
|
-
|
|
1122
|
-
addedElements.add(el);
|
|
1123
|
-
newTagged.push({
|
|
1124
|
-
name: `${displayName}: ${truncatedContent}`,
|
|
1125
|
-
rect,
|
|
1126
|
-
type: "text",
|
|
1127
|
-
textId,
|
|
1128
|
-
textContent
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// Update original states if new logos were found
|
|
1135
|
-
if (Object.keys(newOriginalStates).length !== Object.keys(originalLogoStates).length) {
|
|
1136
|
-
setOriginalLogoStates(newOriginalStates);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
setTaggedElements(newTagged);
|
|
1140
|
-
inspectorRef.current = requestAnimationFrame(scanElements);
|
|
1141
|
-
};
|
|
1142
|
-
|
|
1143
|
-
scanElements();
|
|
1144
|
-
|
|
1145
|
-
return () => {
|
|
1146
|
-
if (inspectorRef.current) {
|
|
1147
|
-
cancelAnimationFrame(inspectorRef.current);
|
|
1148
|
-
inspectorRef.current = null;
|
|
1149
|
-
}
|
|
1150
|
-
};
|
|
1151
|
-
}, [inspectorEnabled, mounted, extractLogoName, originalLogoStates, activeTab]);
|
|
895
|
+
// Element detection is now handled by useElementScanner hook (see above)
|
|
896
|
+
// The hook uses MutationObserver for efficient change detection and
|
|
897
|
+
// content-based hashing for stable element IDs that persist across page loads
|
|
1152
898
|
|
|
1153
899
|
// Toggle Visual Inspector
|
|
1154
900
|
const toggleInspector = useCallback((force?: boolean | unknown) => {
|
|
@@ -1156,20 +902,27 @@ export function SonanceDevTools() {
|
|
|
1156
902
|
}, []);
|
|
1157
903
|
|
|
1158
904
|
// Toggle Vision Mode - AI-powered page-wide editing
|
|
905
|
+
// Inspector functionality is now integrated into Vision Mode
|
|
906
|
+
// Vision mode and Analysis tab are mutually exclusive
|
|
1159
907
|
const toggleVisionMode = useCallback(() => {
|
|
1160
908
|
setVisionModeActive((prev) => {
|
|
1161
909
|
const newValue = !prev;
|
|
1162
910
|
if (newValue) {
|
|
1163
|
-
// When enabling vision mode,
|
|
911
|
+
// When enabling vision mode, auto-enable inspector highlighting
|
|
1164
912
|
setInspectorEnabled(true);
|
|
913
|
+
// If on Analysis tab, switch to Elements tab (mutually exclusive)
|
|
914
|
+
if (activeTab === "analysis") {
|
|
915
|
+
setActiveTab("elements");
|
|
916
|
+
}
|
|
1165
917
|
} else {
|
|
1166
|
-
// When disabling, clear all vision state
|
|
918
|
+
// When disabling, clear all vision state AND disable inspector
|
|
919
|
+
setInspectorEnabled(false);
|
|
1167
920
|
setVisionFocusedElements([]);
|
|
1168
921
|
setVisionScreenshot(null);
|
|
1169
922
|
}
|
|
1170
923
|
return newValue;
|
|
1171
924
|
});
|
|
1172
|
-
}, []);
|
|
925
|
+
}, [activeTab]);
|
|
1173
926
|
|
|
1174
927
|
// Handle vision mode element click - toggle focus on/off
|
|
1175
928
|
const handleVisionElementClick = useCallback((element: DetectedElement) => {
|
|
@@ -1211,6 +964,8 @@ export function SonanceDevTools() {
|
|
|
1211
964
|
childIds: element.childIds,
|
|
1212
965
|
// Parent section context for section-level changes
|
|
1213
966
|
parentSection,
|
|
967
|
+
// Capture image src for tracing to source code
|
|
968
|
+
imageSrc: element.imageSrc,
|
|
1214
969
|
};
|
|
1215
970
|
|
|
1216
971
|
setVisionFocusedElements((prev) => {
|
|
@@ -1219,13 +974,11 @@ export function SonanceDevTools() {
|
|
|
1219
974
|
);
|
|
1220
975
|
|
|
1221
976
|
if (exists) {
|
|
1222
|
-
//
|
|
1223
|
-
return
|
|
1224
|
-
(el) => !(el.name === element.name && el.variantId === element.variantId)
|
|
1225
|
-
);
|
|
977
|
+
// Click on same element - deselect it
|
|
978
|
+
return [];
|
|
1226
979
|
} else {
|
|
1227
|
-
//
|
|
1228
|
-
return [
|
|
980
|
+
// Click on different element - select only this one (single selection)
|
|
981
|
+
return [focusedElement];
|
|
1229
982
|
}
|
|
1230
983
|
});
|
|
1231
984
|
}, [visionModeActive]);
|
|
@@ -1304,7 +1057,7 @@ export function SonanceDevTools() {
|
|
|
1304
1057
|
|
|
1305
1058
|
// ========== Apply-First Mode Handlers ==========
|
|
1306
1059
|
|
|
1307
|
-
// Handle apply-first edit complete - files are already written,
|
|
1060
|
+
// Handle apply-first edit complete - files are already written, refresh to see changes
|
|
1308
1061
|
const handleApplyFirstComplete = useCallback((session: ApplyFirstSession) => {
|
|
1309
1062
|
console.log("[Apply-First] Changes applied:", {
|
|
1310
1063
|
sessionId: session.sessionId,
|
|
@@ -1318,7 +1071,7 @@ export function SonanceDevTools() {
|
|
|
1318
1071
|
|
|
1319
1072
|
setApplyFirstSession(session);
|
|
1320
1073
|
setApplyFirstStatus("waiting-hmr");
|
|
1321
|
-
|
|
1074
|
+
// Keep Vision mode active so user can continue working with their selection
|
|
1322
1075
|
|
|
1323
1076
|
// Persist session to localStorage so it survives page refreshes
|
|
1324
1077
|
try {
|
|
@@ -1331,9 +1084,12 @@ export function SonanceDevTools() {
|
|
|
1331
1084
|
console.warn("[Apply-First] Failed to persist session:", e);
|
|
1332
1085
|
}
|
|
1333
1086
|
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1087
|
+
// Auto-refresh the page after a short delay to show component changes
|
|
1088
|
+
// HMR is unreliable for structural changes, so we force a reload
|
|
1089
|
+
// The session is persisted to localStorage so it survives the refresh
|
|
1090
|
+
console.log("[Apply-First] Scheduling auto-refresh in 500ms...");
|
|
1336
1091
|
setTimeout(() => {
|
|
1092
|
+
console.log("[Apply-First] Auto-refreshing page to show changes...");
|
|
1337
1093
|
window.location.reload();
|
|
1338
1094
|
}, 500);
|
|
1339
1095
|
}, [visionFocusedElements]);
|
|
@@ -1454,33 +1210,36 @@ export function SonanceDevTools() {
|
|
|
1454
1210
|
}, [applyFirstSession]);
|
|
1455
1211
|
|
|
1456
1212
|
// Handle tab change - clear selections when switching tabs
|
|
1213
|
+
// Now simplified: just "analysis" and "elements" modes
|
|
1457
1214
|
const handleTabChange = useCallback((newTab: TabId) => {
|
|
1458
1215
|
if (newTab !== activeTab) {
|
|
1459
|
-
// Clear
|
|
1460
|
-
if (activeTab === "
|
|
1216
|
+
// Clear selections when leaving elements tab
|
|
1217
|
+
if (activeTab === "elements") {
|
|
1461
1218
|
setSelectedComponentType("all");
|
|
1462
1219
|
setSelectedComponentId(null);
|
|
1463
1220
|
setSelectedVariantId(null);
|
|
1464
|
-
|
|
1221
|
+
setSelectedLogoId(null);
|
|
1222
|
+
setSelectedTextId(null);
|
|
1223
|
+
setSelectedElementType(null);
|
|
1224
|
+
// Clear vision mode when leaving elements
|
|
1465
1225
|
if (visionModeActive) {
|
|
1466
1226
|
clearVisionMode();
|
|
1467
1227
|
}
|
|
1468
|
-
//
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
// Clear logo selection when leaving logos tab
|
|
1474
|
-
if (activeTab === "logos") {
|
|
1475
|
-
setSelectedLogoId(null);
|
|
1228
|
+
// Note: We intentionally do NOT auto-revert apply-first sessions when switching tabs
|
|
1229
|
+
// The user should explicitly accept or revert changes from the chat interface
|
|
1230
|
+
// The session persists in localStorage so it survives page refreshes
|
|
1476
1231
|
}
|
|
1477
|
-
//
|
|
1478
|
-
|
|
1479
|
-
|
|
1232
|
+
// Analysis and Vision mode are mutually exclusive
|
|
1233
|
+
// When switching to Analysis, disable Vision mode
|
|
1234
|
+
if (newTab === "analysis" && visionModeActive) {
|
|
1235
|
+
setVisionModeActive(false);
|
|
1236
|
+
setInspectorEnabled(false);
|
|
1237
|
+
setVisionFocusedElements([]);
|
|
1238
|
+
setVisionScreenshot(null);
|
|
1480
1239
|
}
|
|
1481
1240
|
setActiveTab(newTab);
|
|
1482
1241
|
}
|
|
1483
|
-
}, [activeTab, visionModeActive, clearVisionMode
|
|
1242
|
+
}, [activeTab, visionModeActive, clearVisionMode]);
|
|
1484
1243
|
|
|
1485
1244
|
// Load saved text overrides from localStorage on mount
|
|
1486
1245
|
useEffect(() => {
|
|
@@ -1647,7 +1406,9 @@ export function SonanceDevTools() {
|
|
|
1647
1406
|
if (override.letterSpacing) cleanOverride.letterSpacing = override.letterSpacing;
|
|
1648
1407
|
if (override.color) cleanOverride.color = override.color;
|
|
1649
1408
|
if (override.fontFamily) cleanOverride.fontFamily = override.fontFamily;
|
|
1650
|
-
|
|
1409
|
+
if (override.colorLight) cleanOverride.colorLight = override.colorLight;
|
|
1410
|
+
if (override.colorDark) cleanOverride.colorDark = override.colorDark;
|
|
1411
|
+
|
|
1651
1412
|
// Add element identification for persistence
|
|
1652
1413
|
if (element) {
|
|
1653
1414
|
cleanOverride.selector = generateCssSelector(element);
|
|
@@ -1730,7 +1491,7 @@ export function SonanceDevTools() {
|
|
|
1730
1491
|
|
|
1731
1492
|
// Apply text overrides to DOM
|
|
1732
1493
|
useEffect(() => {
|
|
1733
|
-
if (activeTab !== "
|
|
1494
|
+
if (activeTab !== "elements") return;
|
|
1734
1495
|
|
|
1735
1496
|
const textElements = document.querySelectorAll("[data-sonance-text-id]");
|
|
1736
1497
|
textElements.forEach((el) => {
|
|
@@ -1959,6 +1720,295 @@ export function SonanceDevTools() {
|
|
|
1959
1720
|
}
|
|
1960
1721
|
}, [globalLogoConfig]);
|
|
1961
1722
|
|
|
1723
|
+
// Image override handler for the Properties Panel
|
|
1724
|
+
const handleImageOverrideChange = useCallback((imageId: string, override: ImageOverride) => {
|
|
1725
|
+
// Find the target image element using multiple strategies
|
|
1726
|
+
const findImageElement = (): HTMLImageElement | null => {
|
|
1727
|
+
// Strategy 1: Use focused element coordinates
|
|
1728
|
+
const focusedElement = visionFocusedElements.find(el =>
|
|
1729
|
+
el.elementId === imageId || el.variantId === imageId || el.name === imageId
|
|
1730
|
+
);
|
|
1731
|
+
if (focusedElement?.coordinates) {
|
|
1732
|
+
const { x, y, width, height } = focusedElement.coordinates;
|
|
1733
|
+
const centerX = x + width / 2;
|
|
1734
|
+
const centerY = y + height / 2;
|
|
1735
|
+
const el = document.elementFromPoint(centerX, centerY);
|
|
1736
|
+
if (el?.tagName === 'IMG') {
|
|
1737
|
+
return el as HTMLImageElement;
|
|
1738
|
+
}
|
|
1739
|
+
// Check if it's inside the element (for wrapped images)
|
|
1740
|
+
const imgChild = el?.querySelector('img');
|
|
1741
|
+
if (imgChild) return imgChild;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// Strategy 2: Try data-sonance-logo-id (logos are images)
|
|
1745
|
+
const byLogoId = document.querySelector(`[data-sonance-logo-id="${imageId}"]`) as HTMLImageElement | null;
|
|
1746
|
+
if (byLogoId) return byLogoId;
|
|
1747
|
+
|
|
1748
|
+
// Strategy 3: Try element id attribute
|
|
1749
|
+
const byId = document.getElementById(imageId) as HTMLImageElement | null;
|
|
1750
|
+
if (byId?.tagName === 'IMG') return byId;
|
|
1751
|
+
|
|
1752
|
+
// Strategy 4: Try finding by alt text or src containing the imageId
|
|
1753
|
+
const allImages = document.querySelectorAll('img');
|
|
1754
|
+
for (const img of allImages) {
|
|
1755
|
+
if (img.alt?.includes(imageId) || img.src?.includes(imageId)) {
|
|
1756
|
+
return img as HTMLImageElement;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
return null;
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
const img = findImageElement();
|
|
1764
|
+
|
|
1765
|
+
// Handle reset
|
|
1766
|
+
if (override.reset) {
|
|
1767
|
+
if (img && originalImageStates[imageId]) {
|
|
1768
|
+
const original = originalImageStates[imageId];
|
|
1769
|
+
img.src = original.src;
|
|
1770
|
+
if (original.srcset) img.srcset = original.srcset;
|
|
1771
|
+
img.style.width = original.width ? `${original.width}px` : '';
|
|
1772
|
+
img.style.height = original.height ? `${original.height}px` : '';
|
|
1773
|
+
img.style.objectFit = original.objectFit || '';
|
|
1774
|
+
img.style.removeProperty("transform");
|
|
1775
|
+
img.style.removeProperty("transform-origin");
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
setImageOverrides(prev => {
|
|
1779
|
+
const newOverrides = { ...prev };
|
|
1780
|
+
delete newOverrides[imageId];
|
|
1781
|
+
return newOverrides;
|
|
1782
|
+
});
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// Update override state
|
|
1787
|
+
setImageOverrides(prev => ({
|
|
1788
|
+
...prev,
|
|
1789
|
+
[imageId]: { ...prev[imageId], ...override },
|
|
1790
|
+
}));
|
|
1791
|
+
|
|
1792
|
+
// Apply override to DOM element in real-time
|
|
1793
|
+
if (img) {
|
|
1794
|
+
// Store original state if not already stored
|
|
1795
|
+
if (!originalImageStates[imageId]) {
|
|
1796
|
+
setOriginalImageStates(prev => ({
|
|
1797
|
+
...prev,
|
|
1798
|
+
[imageId]: {
|
|
1799
|
+
src: img.getAttribute("data-original-src") || img.src,
|
|
1800
|
+
width: img.naturalWidth || img.offsetWidth,
|
|
1801
|
+
height: img.naturalHeight || img.offsetHeight,
|
|
1802
|
+
objectFit: getComputedStyle(img).objectFit,
|
|
1803
|
+
srcset: img.srcset || undefined,
|
|
1804
|
+
},
|
|
1805
|
+
}));
|
|
1806
|
+
// Also store original src as data attribute for later restoration
|
|
1807
|
+
if (!img.getAttribute("data-original-src")) {
|
|
1808
|
+
img.setAttribute("data-original-src", img.src);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Apply source change
|
|
1813
|
+
if (override.src) {
|
|
1814
|
+
img.srcset = ""; // Clear srcset to force browser to use our src
|
|
1815
|
+
img.src = override.src;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// Apply dimensions
|
|
1819
|
+
if (override.width !== undefined) {
|
|
1820
|
+
img.style.width = override.width ? `${override.width}px` : '';
|
|
1821
|
+
}
|
|
1822
|
+
if (override.height !== undefined) {
|
|
1823
|
+
img.style.height = override.height ? `${override.height}px` : '';
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Apply scale
|
|
1827
|
+
if (override.scale !== undefined) {
|
|
1828
|
+
if (override.scale && override.scale !== 1) {
|
|
1829
|
+
img.style.transform = `scale(${override.scale})`;
|
|
1830
|
+
img.style.transformOrigin = "center";
|
|
1831
|
+
} else {
|
|
1832
|
+
img.style.removeProperty("transform");
|
|
1833
|
+
img.style.removeProperty("transform-origin");
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// Apply object-fit
|
|
1838
|
+
if (override.objectFit) {
|
|
1839
|
+
img.style.objectFit = override.objectFit;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
console.log("[Image Override] Applied to element:", {
|
|
1843
|
+
imageId,
|
|
1844
|
+
override,
|
|
1845
|
+
element: img.src,
|
|
1846
|
+
});
|
|
1847
|
+
} else {
|
|
1848
|
+
console.warn("[Image Override] Could not find target element for imageId:", imageId);
|
|
1849
|
+
}
|
|
1850
|
+
}, [originalImageStates, visionFocusedElements]);
|
|
1851
|
+
|
|
1852
|
+
// Image upload handler
|
|
1853
|
+
const handleImageUpload = useCallback(async (file: File): Promise<string | null> => {
|
|
1854
|
+
try {
|
|
1855
|
+
const formData = new FormData();
|
|
1856
|
+
formData.append("file", file);
|
|
1857
|
+
|
|
1858
|
+
const response = await fetch("/api/upload-image", {
|
|
1859
|
+
method: "POST",
|
|
1860
|
+
body: formData,
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
const data = await response.json();
|
|
1864
|
+
|
|
1865
|
+
if (!response.ok || !data.success) {
|
|
1866
|
+
console.error("Image upload failed:", data.error);
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// Refresh public images list to include the new upload
|
|
1871
|
+
const refreshResponse = await fetch("/api/public-images");
|
|
1872
|
+
if (refreshResponse.ok) {
|
|
1873
|
+
const refreshData = await refreshResponse.json();
|
|
1874
|
+
setPublicImages(refreshData.images || []);
|
|
1875
|
+
setPublicImagesByFolder(refreshData.imagesByFolder || {});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
return data.path;
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
console.error("Image upload error:", error);
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
}, []);
|
|
1884
|
+
|
|
1885
|
+
// Save image changes - uses intelligent detection to determine the best strategy
|
|
1886
|
+
const handleSaveImageChanges = useCallback(async (imageId: string) => {
|
|
1887
|
+
const override = imageOverrides[imageId];
|
|
1888
|
+
if (!override) {
|
|
1889
|
+
console.warn("[Image Save] No overrides found for imageId:", imageId);
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// Find the focused element to get context
|
|
1894
|
+
const focusedElement = visionFocusedElements.find(el =>
|
|
1895
|
+
el.elementId === imageId || el.variantId === imageId || el.name === imageId
|
|
1896
|
+
);
|
|
1897
|
+
|
|
1898
|
+
if (!focusedElement) {
|
|
1899
|
+
console.warn("[Image Save] Could not find focused element for imageId:", imageId);
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
setIsImageSaving(true);
|
|
1904
|
+
|
|
1905
|
+
try {
|
|
1906
|
+
// Extract clean image src
|
|
1907
|
+
let cleanImageSrc = focusedElement.imageSrc || "";
|
|
1908
|
+
if (cleanImageSrc.includes("/_next/image")) {
|
|
1909
|
+
const urlMatch = cleanImageSrc.match(/url=([^&]+)/);
|
|
1910
|
+
if (urlMatch) {
|
|
1911
|
+
cleanImageSrc = decodeURIComponent(urlMatch[1]);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// Get data attributes from the actual DOM element if possible
|
|
1916
|
+
let dataAttributes: Record<string, string> = {};
|
|
1917
|
+
try {
|
|
1918
|
+
// Try to find the actual DOM element using coordinates
|
|
1919
|
+
const centerX = focusedElement.coordinates.x + focusedElement.coordinates.width / 2;
|
|
1920
|
+
const centerY = focusedElement.coordinates.y + focusedElement.coordinates.height / 2;
|
|
1921
|
+
const domElement = document.elementFromPoint(centerX, centerY);
|
|
1922
|
+
|
|
1923
|
+
if (domElement instanceof HTMLImageElement || domElement?.tagName === 'IMG') {
|
|
1924
|
+
// Extract all data attributes
|
|
1925
|
+
Array.from(domElement.attributes).forEach(attr => {
|
|
1926
|
+
if (attr.name.startsWith('data-')) {
|
|
1927
|
+
const key = attr.name.replace('data-', '');
|
|
1928
|
+
dataAttributes[key] = attr.value;
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
// Also get the actual ID from the DOM element
|
|
1932
|
+
if (domElement.id && !focusedElement.elementId) {
|
|
1933
|
+
focusedElement.elementId = domElement.id;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
} catch (e) {
|
|
1937
|
+
console.warn("[Image Save] Could not extract DOM data attributes:", e);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
const pageRoute = window.location.pathname;
|
|
1941
|
+
|
|
1942
|
+
console.log("[Image Save] Using intelligent image detection:", {
|
|
1943
|
+
imageId,
|
|
1944
|
+
imageSrc: cleanImageSrc,
|
|
1945
|
+
override,
|
|
1946
|
+
elementId: focusedElement.elementId,
|
|
1947
|
+
className: focusedElement.className,
|
|
1948
|
+
pageRoute,
|
|
1949
|
+
dataAttributes,
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
// Use the intelligent sonance-save-image endpoint which detects the best strategy
|
|
1953
|
+
const response = await fetch("/api/sonance-save-image", {
|
|
1954
|
+
method: "POST",
|
|
1955
|
+
headers: { "Content-Type": "application/json" },
|
|
1956
|
+
body: JSON.stringify({
|
|
1957
|
+
imageId,
|
|
1958
|
+
imageSrc: cleanImageSrc,
|
|
1959
|
+
altText: focusedElement.name,
|
|
1960
|
+
className: focusedElement.className,
|
|
1961
|
+
elementId: focusedElement.elementId,
|
|
1962
|
+
scale: override.scale,
|
|
1963
|
+
width: override.width,
|
|
1964
|
+
height: override.height,
|
|
1965
|
+
src: override.src,
|
|
1966
|
+
pageRoute,
|
|
1967
|
+
dataAttributes,
|
|
1968
|
+
currentBrand, // Pass the current brand context for correct CSS variable targeting
|
|
1969
|
+
}),
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
const result = await response.json();
|
|
1973
|
+
|
|
1974
|
+
if (result.success) {
|
|
1975
|
+
console.log("[Image Save] Saved successfully:", {
|
|
1976
|
+
strategy: result.strategy,
|
|
1977
|
+
pattern: result.pattern,
|
|
1978
|
+
modifiedFile: result.modifiedFile,
|
|
1979
|
+
message: result.message,
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
// Clear the image override since changes are now in code
|
|
1983
|
+
setImageOverrides(prev => {
|
|
1984
|
+
const newOverrides = { ...prev };
|
|
1985
|
+
delete newOverrides[imageId];
|
|
1986
|
+
return newOverrides;
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
// Reset original state
|
|
1990
|
+
setOriginalImageStates(prev => {
|
|
1991
|
+
const newStates = { ...prev };
|
|
1992
|
+
delete newStates[imageId];
|
|
1993
|
+
return newStates;
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
// Auto-refresh to see the changes
|
|
1997
|
+
setTimeout(() => {
|
|
1998
|
+
window.location.reload();
|
|
1999
|
+
}, 500);
|
|
2000
|
+
} else {
|
|
2001
|
+
console.error("[Image Save] Failed:", result.error, result.pattern);
|
|
2002
|
+
alert(`Failed to save: ${result.error}\n\nDetected pattern: ${result.pattern?.type || 'unknown'}\nDetails: ${result.pattern?.details || 'No details available'}`);
|
|
2003
|
+
}
|
|
2004
|
+
} catch (error) {
|
|
2005
|
+
console.error("[Image Save] Error:", error);
|
|
2006
|
+
alert(`Error saving changes: ${error}`);
|
|
2007
|
+
} finally {
|
|
2008
|
+
setIsImageSaving(false);
|
|
2009
|
+
}
|
|
2010
|
+
}, [imageOverrides, visionFocusedElements, currentBrand]);
|
|
2011
|
+
|
|
1962
2012
|
// Auto-fix ID injection into source code
|
|
1963
2013
|
const handleAutoFixId = useCallback(async (logoSrc: string, suggestedId: string): Promise<{ success: boolean; error?: string }> => {
|
|
1964
2014
|
setAutoFixStatus("fixing");
|
|
@@ -2096,8 +2146,8 @@ export function SonanceDevTools() {
|
|
|
2096
2146
|
|
|
2097
2147
|
// Apply logo overrides to DOM in real-time
|
|
2098
2148
|
useEffect(() => {
|
|
2099
|
-
// Apply logo overrides when on
|
|
2100
|
-
if (activeTab !== "
|
|
2149
|
+
// Apply logo overrides when on elements tab
|
|
2150
|
+
if (activeTab !== "elements") return;
|
|
2101
2151
|
|
|
2102
2152
|
const currentTheme = resolvedTheme || theme || "light";
|
|
2103
2153
|
const isDarkMode = currentTheme === "dark";
|
|
@@ -2543,118 +2593,98 @@ export function SonanceDevTools() {
|
|
|
2543
2593
|
|
|
2544
2594
|
if (!mounted) return null;
|
|
2545
2595
|
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
"fixed bottom-6 right-6 z-[2147483646] pointer-events-auto",
|
|
2551
|
-
"flex h-14 w-14 items-center justify-center",
|
|
2552
|
-
"rounded-full bg-[#333F48] text-white shadow-lg",
|
|
2553
|
-
"hover:scale-105 hover:shadow-xl",
|
|
2554
|
-
"transition-all duration-200",
|
|
2555
|
-
"focus:outline-none focus:ring-2 focus:ring-[#00A3E1] focus:ring-offset-2",
|
|
2556
|
-
isOpen && "hidden"
|
|
2557
|
-
)}
|
|
2558
|
-
aria-label="Open Sonance DevTools"
|
|
2559
|
-
>
|
|
2560
|
-
<Palette className="h-6 w-6" />
|
|
2561
|
-
</button>
|
|
2562
|
-
);
|
|
2596
|
+
// Glassmorphism base classes
|
|
2597
|
+
const glassBase = "backdrop-blur-xl bg-black/75 border-white/10";
|
|
2598
|
+
|
|
2599
|
+
const sidebarWidth = isSidebarCollapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_WIDTH;
|
|
2563
2600
|
|
|
2564
|
-
|
|
2601
|
+
// ======== TOP BANNER ========
|
|
2602
|
+
const topBanner = isOpen && (
|
|
2565
2603
|
<div
|
|
2566
|
-
|
|
2567
|
-
data-sonance-devtools="true"
|
|
2604
|
+
data-sonance-devtools="banner"
|
|
2568
2605
|
className={cn(
|
|
2569
|
-
"fixed
|
|
2570
|
-
"
|
|
2571
|
-
"
|
|
2572
|
-
|
|
2573
|
-
"
|
|
2574
|
-
isDragging && "select-none"
|
|
2606
|
+
"fixed top-0 left-0 right-0 z-[2147483647] pointer-events-auto",
|
|
2607
|
+
"flex items-center justify-between px-4",
|
|
2608
|
+
"border-b font-['Montserrat',sans-serif]",
|
|
2609
|
+
glassBase,
|
|
2610
|
+
"border-b-white/10"
|
|
2575
2611
|
)}
|
|
2576
2612
|
style={{
|
|
2577
|
-
|
|
2578
|
-
|
|
2613
|
+
height: `${BANNER_HEIGHT}px`,
|
|
2614
|
+
colorScheme: "dark",
|
|
2579
2615
|
}}
|
|
2580
|
-
// Event isolation - prevent clicks from closing popups in the main app
|
|
2581
|
-
// onPointerDown fires before onMouseDown in modern browsers - must stop both
|
|
2582
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
2583
|
-
onTouchStart={(e) => e.stopPropagation()}
|
|
2584
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
2585
|
-
onClick={(e) => e.stopPropagation()}
|
|
2586
|
-
onFocus={(e) => e.stopPropagation()}
|
|
2587
|
-
onBlur={(e) => e.stopPropagation()}
|
|
2588
2616
|
>
|
|
2589
|
-
{/*
|
|
2590
|
-
<div
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
onPointerMove={handleDragMove}
|
|
2598
|
-
onPointerUp={handleDragEnd}
|
|
2599
|
-
onPointerCancel={handleDragEnd}
|
|
2600
|
-
onDoubleClick={handleResetPosition}
|
|
2601
|
-
title="Drag to move • Double-click to reset position"
|
|
2602
|
-
>
|
|
2603
|
-
<div className="flex items-center gap-1.5">
|
|
2604
|
-
<GripHorizontal className="h-3.5 w-3.5 text-white/50" />
|
|
2605
|
-
<Palette className="h-4 w-4 text-[#00A3E1]" />
|
|
2606
|
-
<span id="span-sonance-devtools" className="text-xs font-semibold text-white whitespace-nowrap">
|
|
2617
|
+
{/* Left: Branding */}
|
|
2618
|
+
<div className="flex items-center gap-3">
|
|
2619
|
+
<div className="flex items-center gap-2">
|
|
2620
|
+
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-[#00D3C8] to-[#00A3E1]">
|
|
2621
|
+
<Palette className="h-4 w-4 text-white" />
|
|
2622
|
+
</div>
|
|
2623
|
+
<div className="flex flex-col">
|
|
2624
|
+
<span id="span-sonance-devtools" className="text-sm font-semibold text-white tracking-tight">
|
|
2607
2625
|
Sonance DevTools
|
|
2608
2626
|
</span>
|
|
2627
|
+
<span id="span-design-system" className="text-[10px] text-white/50 uppercase tracking-widest">
|
|
2628
|
+
Design System
|
|
2629
|
+
</span>
|
|
2609
2630
|
</div>
|
|
2610
|
-
|
|
2611
|
-
|
|
2631
|
+
</div>
|
|
2632
|
+
</div>
|
|
2633
|
+
|
|
2634
|
+
{/* Center: Mode Toggles */}
|
|
2635
|
+
<div className="flex items-center gap-1 bg-white/5 rounded-full px-2 py-1">
|
|
2636
|
+
{/* Analysis Toggle */}
|
|
2612
2637
|
<button
|
|
2613
|
-
onClick={() => handleTabChange(activeTab === "analysis" ? "
|
|
2638
|
+
onClick={() => handleTabChange(activeTab === "analysis" ? "elements" : "analysis")}
|
|
2614
2639
|
className={cn(
|
|
2615
|
-
|
|
2640
|
+
"flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all",
|
|
2616
2641
|
activeTab === "analysis"
|
|
2617
|
-
|
|
2618
|
-
|
|
2642
|
+
? "bg-[#00D3C8] text-white shadow-lg shadow-[#00D3C8]/25"
|
|
2643
|
+
: "text-white/70 hover:text-white hover:bg-white/10"
|
|
2619
2644
|
)}
|
|
2620
|
-
|
|
2621
|
-
title={activeTab === "analysis" ? "Close analysis" : "Open analysis"}
|
|
2645
|
+
title="Page Analysis"
|
|
2622
2646
|
>
|
|
2623
|
-
|
|
2647
|
+
<Scan className="h-3.5 w-3.5" />
|
|
2648
|
+
<span id="span-analysis" className="hidden sm:inline">Analysis</span>
|
|
2624
2649
|
</button>
|
|
2625
|
-
|
|
2650
|
+
{/* Vision Mode Toggle */}
|
|
2626
2651
|
<button
|
|
2627
|
-
|
|
2652
|
+
onClick={toggleVisionMode}
|
|
2628
2653
|
className={cn(
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2654
|
+
"flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all",
|
|
2655
|
+
visionModeActive
|
|
2656
|
+
? "bg-[#8B5CF6] text-white shadow-lg shadow-[#8B5CF6]/25"
|
|
2657
|
+
: "text-white/70 hover:text-white hover:bg-white/10"
|
|
2658
|
+
)}
|
|
2659
|
+
title="AI Vision Mode"
|
|
2660
|
+
>
|
|
2661
|
+
<Eye className={cn("h-3.5 w-3.5", visionModeActive && "animate-pulse")} />
|
|
2662
|
+
<span id="span-vision" className="hidden sm:inline">Vision</span>
|
|
2638
2663
|
</button>
|
|
2639
|
-
|
|
2664
|
+
{/* Element Highlight Toggle - only visible when Vision Mode is active */}
|
|
2665
|
+
{visionModeActive && (
|
|
2640
2666
|
<button
|
|
2641
|
-
onClick={
|
|
2667
|
+
onClick={toggleInspector}
|
|
2642
2668
|
className={cn(
|
|
2643
|
-
"
|
|
2644
|
-
|
|
2645
|
-
? "bg-[#
|
|
2646
|
-
: "
|
|
2669
|
+
"flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all",
|
|
2670
|
+
inspectorEnabled
|
|
2671
|
+
? "bg-[#00A3E1] text-white shadow-lg shadow-[#00A3E1]/25"
|
|
2672
|
+
: "text-white/70 hover:text-white hover:bg-white/10"
|
|
2647
2673
|
)}
|
|
2648
|
-
|
|
2649
|
-
title="Vision Mode: AI-powered page editing with screenshot analysis"
|
|
2674
|
+
title={inspectorEnabled ? "Disable element highlighting" : "Enable element highlighting"}
|
|
2650
2675
|
>
|
|
2651
|
-
<
|
|
2676
|
+
<MousePointer className={cn("h-3.5 w-3.5", inspectorEnabled && "animate-pulse")} />
|
|
2677
|
+
<span id="span-inspectorenabled-hig" className="hidden sm:inline">{inspectorEnabled ? "Highlighting" : "Highlight"}</span>
|
|
2652
2678
|
</button>
|
|
2653
|
-
|
|
2679
|
+
)}
|
|
2680
|
+
</div>
|
|
2681
|
+
|
|
2682
|
+
{/* Right: Actions */}
|
|
2683
|
+
<div className="flex items-center gap-2">
|
|
2684
|
+
{/* Theme Toggle */}
|
|
2654
2685
|
<button
|
|
2655
2686
|
onClick={toggleThemeMode}
|
|
2656
|
-
|
|
2657
|
-
aria-label={resolvedTheme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
|
2687
|
+
className="flex items-center justify-center w-8 h-8 rounded-lg bg-white/5 hover:bg-white/10 transition-colors"
|
|
2658
2688
|
title={resolvedTheme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
|
2659
2689
|
>
|
|
2660
2690
|
{resolvedTheme === "dark" ? (
|
|
@@ -2666,182 +2696,295 @@ export function SonanceDevTools() {
|
|
|
2666
2696
|
{/* Close Button */}
|
|
2667
2697
|
<button
|
|
2668
2698
|
onClick={handleClose}
|
|
2669
|
-
|
|
2670
|
-
|
|
2699
|
+
className="flex items-center justify-center w-8 h-8 rounded-lg bg-white/5 hover:bg-red-500/20 hover:text-red-400 transition-colors text-white/70"
|
|
2700
|
+
title="Close DevTools"
|
|
2671
2701
|
>
|
|
2672
|
-
|
|
2702
|
+
<X className="h-4 w-4" />
|
|
2673
2703
|
</button>
|
|
2674
2704
|
</div>
|
|
2675
2705
|
</div>
|
|
2706
|
+
);
|
|
2676
2707
|
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2708
|
+
// ======== RIGHT SIDEBAR ========
|
|
2709
|
+
const sidebar = isOpen && (
|
|
2710
|
+
<div
|
|
2711
|
+
ref={panelRef}
|
|
2712
|
+
data-sonance-devtools="sidebar"
|
|
2713
|
+
className={cn(
|
|
2714
|
+
"fixed top-0 right-0 z-[2147483646] pointer-events-auto",
|
|
2715
|
+
"flex flex-col font-['Montserrat',sans-serif]",
|
|
2716
|
+
"border-l",
|
|
2717
|
+
glassBase,
|
|
2718
|
+
"border-l-white/10",
|
|
2719
|
+
"transition-all duration-200 ease-out"
|
|
2720
|
+
)}
|
|
2721
|
+
style={{
|
|
2722
|
+
width: `${sidebarWidth}px`,
|
|
2723
|
+
height: "100vh",
|
|
2724
|
+
paddingTop: `${BANNER_HEIGHT}px`,
|
|
2725
|
+
colorScheme: "dark",
|
|
2726
|
+
}}
|
|
2727
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
2728
|
+
onTouchStart={(e) => e.stopPropagation()}
|
|
2729
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
2730
|
+
onClick={(e) => e.stopPropagation()}
|
|
2731
|
+
>
|
|
2732
|
+
{/* Collapse/Expand Toggle */}
|
|
2683
2733
|
<button
|
|
2684
|
-
|
|
2685
|
-
onClick={() => handleTabChange(tab.id)}
|
|
2734
|
+
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
|
2686
2735
|
className={cn(
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
aria-label={tab.label}
|
|
2693
|
-
title={tab.label}
|
|
2694
|
-
>
|
|
2695
|
-
<IconComponent className="h-4 w-4" />
|
|
2696
|
-
</button>
|
|
2697
|
-
);
|
|
2698
|
-
})}
|
|
2699
|
-
</div>
|
|
2700
|
-
|
|
2701
|
-
{/* Content */}
|
|
2702
|
-
<div
|
|
2703
|
-
className="flex-1 overflow-y-auto p-4 text-[#333F48]"
|
|
2704
|
-
style={{
|
|
2705
|
-
overscrollBehavior: "contain", // Prevent scroll chaining to parent (modal)
|
|
2706
|
-
WebkitOverflowScrolling: "touch", // Smooth scrolling on iOS
|
|
2707
|
-
}}
|
|
2708
|
-
onWheel={(e) => e.stopPropagation()} // Prevent modal from intercepting wheel events
|
|
2709
|
-
onTouchMove={(e) => e.stopPropagation()} // Prevent modal from intercepting touch scroll
|
|
2710
|
-
>
|
|
2711
|
-
{/* Analysis Tab */}
|
|
2712
|
-
{activeTab === "analysis" && (
|
|
2713
|
-
<AnalysisPanel
|
|
2714
|
-
analysisStatus={analysisStatus}
|
|
2715
|
-
analysisResult={analysisResult}
|
|
2716
|
-
analysisError={analysisError}
|
|
2717
|
-
bulkTagStatus={bulkTagStatus}
|
|
2718
|
-
bulkTagMessage={bulkTagMessage}
|
|
2719
|
-
onRunAnalysis={handleRunAnalysis}
|
|
2720
|
-
onBulkTag={handleBulkTagAll}
|
|
2721
|
-
/>
|
|
2736
|
+
"absolute -left-3 top-1/2 -translate-y-1/2 z-10",
|
|
2737
|
+
"flex items-center justify-center w-6 h-12 rounded-l-lg",
|
|
2738
|
+
"bg-black/80 border border-white/10 border-r-0",
|
|
2739
|
+
"text-white/60 hover:text-white hover:bg-black/90",
|
|
2740
|
+
"transition-all"
|
|
2722
2741
|
)}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
{
|
|
2726
|
-
<
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
installedComponents={installedComponents}
|
|
2730
|
-
inspectorEnabled={inspectorEnabled}
|
|
2731
|
-
onToggleInspector={toggleInspector}
|
|
2732
|
-
config={config}
|
|
2733
|
-
updateConfig={updateConfig}
|
|
2734
|
-
onReset={handleReset}
|
|
2735
|
-
taggedElements={taggedElements}
|
|
2736
|
-
selectedComponentType={selectedComponentType}
|
|
2737
|
-
onSelectComponentType={handleSelectComponentType}
|
|
2738
|
-
componentScope={componentScope}
|
|
2739
|
-
onScopeChange={setComponentScope}
|
|
2740
|
-
selectedComponentId={selectedComponentId}
|
|
2741
|
-
onSelectComponent={setSelectedComponentId}
|
|
2742
|
-
selectedVariantId={selectedVariantId}
|
|
2743
|
-
onSelectVariant={setSelectedVariantId}
|
|
2744
|
-
analysisResult={analysisResult}
|
|
2745
|
-
componentOverrides={componentOverrides}
|
|
2746
|
-
onUpdateComponentOverride={updateComponentOverride}
|
|
2747
|
-
onResetComponentOverrides={resetComponentOverrides}
|
|
2748
|
-
viewMode={viewMode}
|
|
2749
|
-
onViewModeChange={setViewMode}
|
|
2750
|
-
isPreviewActive={isPreviewActive}
|
|
2751
|
-
onPreviewActiveChange={setIsPreviewActive}
|
|
2752
|
-
visionMode={visionModeActive}
|
|
2753
|
-
visionFocusedElements={visionFocusedElements}
|
|
2754
|
-
onVisionEditComplete={handleVisionEditComplete}
|
|
2755
|
-
visionPendingEdit={visionPendingEdit}
|
|
2756
|
-
onSaveVisionEdit={handleSaveVisionEdit}
|
|
2757
|
-
onClearVisionPendingEdit={handleClearVisionPendingEdit}
|
|
2758
|
-
// Apply-First Mode props
|
|
2759
|
-
applyFirstSession={applyFirstSession}
|
|
2760
|
-
applyFirstStatus={applyFirstStatus}
|
|
2761
|
-
onApplyFirstComplete={handleApplyFirstComplete}
|
|
2762
|
-
onApplyFirstAccept={handleApplyFirstAccept}
|
|
2763
|
-
onApplyFirstRevert={handleApplyFirstRevert}
|
|
2764
|
-
onApplyFirstForceClear={handleApplyFirstForceClear}
|
|
2765
|
-
/>
|
|
2742
|
+
title={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
2743
|
+
>
|
|
2744
|
+
{isSidebarCollapsed ? (
|
|
2745
|
+
<PanelRightClose className="h-3.5 w-3.5" />
|
|
2746
|
+
) : (
|
|
2747
|
+
<PanelRightOpen className="h-3.5 w-3.5" />
|
|
2766
2748
|
)}
|
|
2749
|
+
</button>
|
|
2767
2750
|
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2751
|
+
{/* Element Type Filter Toggles - Color coded */}
|
|
2752
|
+
{!isSidebarCollapsed && activeTab === "elements" && (
|
|
2753
|
+
<div className="flex items-center gap-1 px-2 py-2 border-b border-white/10 bg-white/5">
|
|
2754
|
+
{/* Components Filter */}
|
|
2755
|
+
<button
|
|
2756
|
+
onClick={() => setElementFilters(prev => ({ ...prev, components: !prev.components }))}
|
|
2757
|
+
className={cn(
|
|
2758
|
+
"flex items-center gap-1 px-2 py-1.5 rounded text-[10px] font-medium transition-all",
|
|
2759
|
+
elementFilters.components
|
|
2760
|
+
? "bg-purple-500/20 text-purple-300 border border-purple-400/30"
|
|
2761
|
+
: "text-white/40 hover:text-white/60 border border-transparent"
|
|
2762
|
+
)}
|
|
2763
|
+
title="Toggle Components"
|
|
2764
|
+
>
|
|
2765
|
+
<Box className="h-3 w-3" />
|
|
2766
|
+
<span id="span-components">Components</span>
|
|
2767
|
+
</button>
|
|
2768
|
+
{/* Images Filter */}
|
|
2769
|
+
<button
|
|
2770
|
+
onClick={() => setElementFilters(prev => ({ ...prev, images: !prev.images }))}
|
|
2771
|
+
className={cn(
|
|
2772
|
+
"flex items-center gap-1 px-2 py-1.5 rounded text-[10px] font-medium transition-all",
|
|
2773
|
+
elementFilters.images
|
|
2774
|
+
? "bg-blue-500/20 text-blue-300 border border-blue-400/30"
|
|
2775
|
+
: "text-white/40 hover:text-white/60 border border-transparent"
|
|
2776
|
+
)}
|
|
2777
|
+
title="Toggle Images"
|
|
2778
|
+
>
|
|
2779
|
+
<ImageIcon className="h-3 w-3" />
|
|
2780
|
+
<span id="span-images">Images</span>
|
|
2781
|
+
</button>
|
|
2782
|
+
{/* Text Filter */}
|
|
2783
|
+
<button
|
|
2784
|
+
onClick={() => setElementFilters(prev => ({ ...prev, text: !prev.text }))}
|
|
2785
|
+
className={cn(
|
|
2786
|
+
"flex items-center gap-1 px-2 py-1.5 rounded text-[10px] font-medium transition-all",
|
|
2787
|
+
elementFilters.text
|
|
2788
|
+
? "bg-amber-500/20 text-amber-300 border border-amber-400/30"
|
|
2789
|
+
: "text-white/40 hover:text-white/60 border border-transparent"
|
|
2790
|
+
)}
|
|
2791
|
+
title="Toggle Text"
|
|
2792
|
+
>
|
|
2793
|
+
<Type className="h-3 w-3" />
|
|
2794
|
+
<span id="span-text">Text</span>
|
|
2795
|
+
</button>
|
|
2810
2796
|
</div>
|
|
2797
|
+
)}
|
|
2798
|
+
{/* Collapsed sidebar - just show active filters as icons */}
|
|
2799
|
+
{isSidebarCollapsed && activeTab === "elements" && (
|
|
2800
|
+
<div className="flex flex-col items-center gap-0.5 py-1.5 border-b border-white/10 bg-white/5">
|
|
2801
|
+
<button
|
|
2802
|
+
onClick={() => setElementFilters(prev => ({ ...prev, components: !prev.components }))}
|
|
2803
|
+
className={cn(
|
|
2804
|
+
"p-1.5 rounded transition-all",
|
|
2805
|
+
elementFilters.components ? "text-purple-400" : "text-white/30"
|
|
2806
|
+
)}
|
|
2807
|
+
title="Components"
|
|
2808
|
+
>
|
|
2809
|
+
<Box className="h-3.5 w-3.5" />
|
|
2810
|
+
</button>
|
|
2811
|
+
<button
|
|
2812
|
+
onClick={() => setElementFilters(prev => ({ ...prev, images: !prev.images }))}
|
|
2813
|
+
className={cn(
|
|
2814
|
+
"p-1.5 rounded transition-all",
|
|
2815
|
+
elementFilters.images ? "text-blue-400" : "text-white/30"
|
|
2816
|
+
)}
|
|
2817
|
+
title="Images"
|
|
2818
|
+
>
|
|
2819
|
+
<ImageIcon className="h-3.5 w-3.5" />
|
|
2820
|
+
</button>
|
|
2821
|
+
<button
|
|
2822
|
+
onClick={() => setElementFilters(prev => ({ ...prev, text: !prev.text }))}
|
|
2823
|
+
className={cn(
|
|
2824
|
+
"p-1.5 rounded transition-all",
|
|
2825
|
+
elementFilters.text ? "text-amber-400" : "text-white/30"
|
|
2826
|
+
)}
|
|
2827
|
+
title="Text"
|
|
2828
|
+
>
|
|
2829
|
+
<Type className="h-3.5 w-3.5" />
|
|
2830
|
+
</button>
|
|
2831
|
+
</div>
|
|
2832
|
+
)}
|
|
2833
|
+
|
|
2834
|
+
{/* Panel Content - Light themed for contrast with dark frame */}
|
|
2835
|
+
{!isSidebarCollapsed && (
|
|
2836
|
+
<div
|
|
2837
|
+
className={cn(
|
|
2838
|
+
"flex-1 m-1.5 rounded-md bg-white/95 backdrop-blur-sm shadow-inner",
|
|
2839
|
+
// Elements tab needs flex layout for pinned chat
|
|
2840
|
+
activeTab === "elements" ? "flex flex-col overflow-hidden" : "overflow-y-auto"
|
|
2841
|
+
)}
|
|
2842
|
+
style={{
|
|
2843
|
+
overscrollBehavior: "contain",
|
|
2844
|
+
WebkitOverflowScrolling: "touch",
|
|
2845
|
+
colorScheme: "light",
|
|
2846
|
+
}}
|
|
2847
|
+
onWheel={(e) => e.stopPropagation()}
|
|
2848
|
+
onTouchMove={(e) => e.stopPropagation()}
|
|
2849
|
+
>
|
|
2850
|
+
{/* Analysis Tab - with padding wrapper */}
|
|
2851
|
+
{activeTab === "analysis" && (
|
|
2852
|
+
<div className="p-3 text-[#333F48]">
|
|
2853
|
+
<AnalysisPanel
|
|
2854
|
+
analysisStatus={analysisStatus}
|
|
2855
|
+
analysisResult={analysisResult}
|
|
2856
|
+
analysisError={analysisError}
|
|
2857
|
+
bulkTagStatus={bulkTagStatus}
|
|
2858
|
+
bulkTagMessage={bulkTagMessage}
|
|
2859
|
+
onRunAnalysis={handleRunAnalysis}
|
|
2860
|
+
onBulkTag={handleBulkTagAll}
|
|
2861
|
+
/>
|
|
2862
|
+
</div>
|
|
2863
|
+
)}
|
|
2864
|
+
|
|
2865
|
+
{/* Unified Elements Tab - full height flex layout */}
|
|
2866
|
+
{activeTab === "elements" && (
|
|
2867
|
+
<div className="flex-1 flex flex-col min-h-0 text-[#333F48]">
|
|
2868
|
+
{/* Dynamic content based on selected element type or default to ComponentsPanel */}
|
|
2869
|
+
<ComponentsPanel
|
|
2870
|
+
copiedId={copiedId}
|
|
2871
|
+
onCopy={handleCopy}
|
|
2872
|
+
installedComponents={installedComponents}
|
|
2873
|
+
inspectorEnabled={inspectorEnabled}
|
|
2874
|
+
onToggleInspector={toggleInspector}
|
|
2875
|
+
config={config}
|
|
2876
|
+
updateConfig={updateConfig}
|
|
2877
|
+
onReset={handleReset}
|
|
2878
|
+
taggedElements={taggedElements}
|
|
2879
|
+
selectedComponentType={selectedComponentType}
|
|
2880
|
+
onSelectComponentType={handleSelectComponentType}
|
|
2881
|
+
componentScope={componentScope}
|
|
2882
|
+
onScopeChange={setComponentScope}
|
|
2883
|
+
selectedComponentId={selectedComponentId}
|
|
2884
|
+
onSelectComponent={setSelectedComponentId}
|
|
2885
|
+
selectedVariantId={selectedVariantId}
|
|
2886
|
+
onSelectVariant={setSelectedVariantId}
|
|
2887
|
+
analysisResult={analysisResult}
|
|
2888
|
+
componentOverrides={componentOverrides}
|
|
2889
|
+
onUpdateComponentOverride={updateComponentOverride}
|
|
2890
|
+
onResetComponentOverrides={resetComponentOverrides}
|
|
2891
|
+
viewMode={viewMode}
|
|
2892
|
+
onViewModeChange={setViewMode}
|
|
2893
|
+
isPreviewActive={isPreviewActive}
|
|
2894
|
+
onPreviewActiveChange={setIsPreviewActive}
|
|
2895
|
+
visionMode={visionModeActive}
|
|
2896
|
+
visionFocusedElements={visionFocusedElements}
|
|
2897
|
+
onVisionEditComplete={handleVisionEditComplete}
|
|
2898
|
+
visionPendingEdit={visionPendingEdit}
|
|
2899
|
+
onSaveVisionEdit={handleSaveVisionEdit}
|
|
2900
|
+
onClearVisionPendingEdit={handleClearVisionPendingEdit}
|
|
2901
|
+
// Apply-First Mode props
|
|
2902
|
+
applyFirstSession={applyFirstSession}
|
|
2903
|
+
applyFirstStatus={applyFirstStatus}
|
|
2904
|
+
onApplyFirstComplete={handleApplyFirstComplete}
|
|
2905
|
+
onApplyFirstAccept={handleApplyFirstAccept}
|
|
2906
|
+
onApplyFirstRevert={handleApplyFirstRevert}
|
|
2907
|
+
onApplyFirstForceClear={handleApplyFirstForceClear}
|
|
2908
|
+
// Element filter context - for showing filter-aware content
|
|
2909
|
+
elementFilters={elementFilters}
|
|
2910
|
+
selectedLogoId={selectedLogoId}
|
|
2911
|
+
onSelectLogo={handleSelectLogo}
|
|
2912
|
+
selectedTextId={selectedTextId}
|
|
2913
|
+
onSelectText={handleSelectText}
|
|
2914
|
+
// Image editing props
|
|
2915
|
+
imageOverrides={imageOverrides}
|
|
2916
|
+
onImageOverrideChange={handleImageOverrideChange}
|
|
2917
|
+
onSaveImageChanges={handleSaveImageChanges}
|
|
2918
|
+
isImageSaving={isImageSaving}
|
|
2919
|
+
publicImages={publicImages}
|
|
2920
|
+
publicImagesByFolder={publicImagesByFolder}
|
|
2921
|
+
logoAssets={logoAssets}
|
|
2922
|
+
logoAssetsByBrand={logoAssetsByBrand}
|
|
2923
|
+
onImageUpload={handleImageUpload}
|
|
2924
|
+
/>
|
|
2925
|
+
</div>
|
|
2926
|
+
)}
|
|
2927
|
+
</div>
|
|
2928
|
+
)}
|
|
2811
2929
|
</div>
|
|
2812
2930
|
);
|
|
2931
|
+
|
|
2932
|
+
// Floating trigger when closed (for re-opening)
|
|
2933
|
+
const closedTrigger = !isOpen && (
|
|
2934
|
+
<button
|
|
2935
|
+
onClick={() => setIsOpen(true)}
|
|
2936
|
+
className={cn(
|
|
2937
|
+
"fixed bottom-6 right-6 z-[2147483646] pointer-events-auto",
|
|
2938
|
+
"flex items-center gap-2 px-4 py-2.5",
|
|
2939
|
+
"rounded-full shadow-2xl",
|
|
2940
|
+
"backdrop-blur-xl bg-black/80 border border-white/10",
|
|
2941
|
+
"text-white font-medium text-sm",
|
|
2942
|
+
"hover:scale-105 hover:shadow-[0_0_30px_rgba(0,211,200,0.3)]",
|
|
2943
|
+
"transition-all duration-200",
|
|
2944
|
+
"focus:outline-none focus:ring-2 focus:ring-[#00D3C8] focus:ring-offset-2 focus:ring-offset-black"
|
|
2945
|
+
)}
|
|
2946
|
+
aria-label="Open Sonance DevTools"
|
|
2947
|
+
>
|
|
2948
|
+
<Palette className="h-4 w-4 text-[#00D3C8]" />
|
|
2949
|
+
<span id="span-devtools">DevTools</span>
|
|
2950
|
+
</button>
|
|
2951
|
+
);
|
|
2813
2952
|
|
|
2814
2953
|
// Wait for portal container to be ready
|
|
2815
2954
|
if (!portalContainer) return null;
|
|
2816
2955
|
|
|
2817
2956
|
return createPortal(
|
|
2818
2957
|
<>
|
|
2819
|
-
{trigger}
|
|
2820
|
-
{
|
|
2958
|
+
{/* Closed state trigger */}
|
|
2959
|
+
{closedTrigger}
|
|
2960
|
+
{/* Frame layout: Top Banner + Right Sidebar */}
|
|
2961
|
+
{topBanner}
|
|
2962
|
+
{sidebar}
|
|
2821
2963
|
{/* Vision Mode Border Overlay */}
|
|
2822
2964
|
<VisionModeBorder
|
|
2823
2965
|
active={visionModeActive}
|
|
2824
2966
|
focusedCount={visionFocusedElements.length}
|
|
2967
|
+
highlightEnabled={inspectorEnabled}
|
|
2825
2968
|
/>
|
|
2826
2969
|
{/* Section Highlight - shows detected parent section when element is clicked */}
|
|
2827
2970
|
<SectionHighlight
|
|
2828
2971
|
active={visionModeActive}
|
|
2829
2972
|
focusedElements={visionFocusedElements}
|
|
2830
2973
|
/>
|
|
2831
|
-
{/* Visual Inspector Overlay -
|
|
2832
|
-
{(inspectorEnabled || isPreviewActive ||
|
|
2974
|
+
{/* Visual Inspector Overlay - shows when highlighting is enabled in Vision Mode or for previews */}
|
|
2975
|
+
{((visionModeActive && inspectorEnabled) || isPreviewActive || changedElements.length > 0 || (viewMode === "inspector" && selectedComponentType !== "all")) && filteredOverlayElements.length > 0 && (
|
|
2833
2976
|
<InspectorOverlay
|
|
2834
2977
|
elements={filteredOverlayElements}
|
|
2835
2978
|
selectedLogoId={selectedLogoId}
|
|
2836
2979
|
onLogoClick={handleSelectLogo}
|
|
2837
2980
|
selectedTextId={selectedTextId}
|
|
2838
2981
|
onTextClick={handleSelectText}
|
|
2839
|
-
interactive={activeTab === "
|
|
2982
|
+
interactive={activeTab === "elements" && (elementFilters.images || elementFilters.text)}
|
|
2840
2983
|
selectedComponentId={selectedComponentId}
|
|
2841
2984
|
onComponentClick={setSelectedComponentId}
|
|
2842
2985
|
componentSelectionMode={componentScope === "selected" && inspectorEnabled}
|
|
2843
|
-
//
|
|
2844
|
-
inspectorClickMode={
|
|
2986
|
+
// Vision Mode with highlighting: clicking a component selects its type
|
|
2987
|
+
inspectorClickMode={visionModeActive && inspectorEnabled && !isPreviewActive}
|
|
2845
2988
|
onSelectComponentAndClose={handleInspectorSelectAndClose}
|
|
2846
2989
|
// Pass selected type to highlight only matching components
|
|
2847
2990
|
selectedComponentType={selectedComponentType}
|