sonance-brand-mcp 1.3.111 → 1.3.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/api/sonance-save-image/route.ts +625 -0
- package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +988 -57
- package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
- package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
- package/dist/assets/brand-system.ts +13 -12
- package/dist/assets/components/accordion.tsx +15 -7
- package/dist/assets/components/alert-dialog.tsx +35 -10
- package/dist/assets/components/alert.tsx +11 -10
- package/dist/assets/components/avatar.tsx +4 -4
- package/dist/assets/components/badge.tsx +16 -12
- package/dist/assets/components/button.stories.tsx +3 -3
- package/dist/assets/components/button.tsx +50 -31
- package/dist/assets/components/calendar.tsx +12 -8
- package/dist/assets/components/card.tsx +35 -29
- package/dist/assets/components/checkbox.tsx +9 -8
- package/dist/assets/components/code.tsx +19 -11
- package/dist/assets/components/command.tsx +32 -13
- package/dist/assets/components/context-menu.tsx +37 -16
- package/dist/assets/components/dialog.tsx +8 -5
- package/dist/assets/components/divider.tsx +15 -5
- package/dist/assets/components/drawer.tsx +4 -3
- package/dist/assets/components/dropdown-menu.tsx +15 -13
- package/dist/assets/components/hover-card.tsx +4 -1
- package/dist/assets/components/image.tsx +1 -1
- package/dist/assets/components/input.tsx +29 -14
- package/dist/assets/components/kbd.stories.tsx +3 -3
- package/dist/assets/components/kbd.tsx +29 -13
- package/dist/assets/components/listbox.tsx +8 -8
- package/dist/assets/components/menubar.tsx +50 -23
- package/dist/assets/components/navbar.stories.tsx +140 -13
- package/dist/assets/components/navbar.tsx +22 -5
- package/dist/assets/components/navigation-menu.tsx +28 -6
- package/dist/assets/components/pagination.tsx +10 -10
- package/dist/assets/components/popover.tsx +10 -8
- package/dist/assets/components/progress.tsx +6 -4
- package/dist/assets/components/radio-group.tsx +5 -5
- package/dist/assets/components/select.tsx +49 -29
- package/dist/assets/components/separator.tsx +3 -3
- package/dist/assets/components/sheet.tsx +4 -4
- package/dist/assets/components/sidebar.tsx +10 -10
- package/dist/assets/components/skeleton.tsx +13 -5
- package/dist/assets/components/slider.tsx +12 -10
- package/dist/assets/components/switch.tsx +4 -4
- package/dist/assets/components/table.tsx +5 -5
- package/dist/assets/components/tabs.tsx +8 -8
- package/dist/assets/components/textarea.tsx +11 -9
- package/dist/assets/components/toast.tsx +7 -7
- package/dist/assets/components/toggle.tsx +27 -7
- package/dist/assets/components/tooltip.tsx +10 -8
- package/dist/assets/components/user.tsx +8 -6
- package/dist/assets/dev-tools/SonanceDevTools.tsx +429 -362
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
- package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
- package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
- package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +4 -64
- package/dist/assets/dev-tools/hooks/index.ts +69 -0
- package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
- package/dist/assets/dev-tools/hooks/useComputedStyles.ts +171 -65
- package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
- package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
- package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
- package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +160 -57
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +42 -0
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- package/package.json +1 -1
|
@@ -51,6 +51,9 @@ import {
|
|
|
51
51
|
ParentSectionInfo,
|
|
52
52
|
ElementFilters,
|
|
53
53
|
SelectedElementType,
|
|
54
|
+
ImageOverride,
|
|
55
|
+
OriginalImageState,
|
|
56
|
+
PublicImageAsset,
|
|
54
57
|
} from "./types";
|
|
55
58
|
import {
|
|
56
59
|
QUICK_ACTIONS,
|
|
@@ -81,6 +84,7 @@ import { TextPanel } from "./panels/TextPanel";
|
|
|
81
84
|
import { LogoToolsPanel } from "./panels/LogoToolsPanel";
|
|
82
85
|
import { LogosPanel } from "./panels/LogosPanel";
|
|
83
86
|
import { ComponentsPanel } from "./panels/ComponentsPanel";
|
|
87
|
+
import { useElementScanner, extractLogoName } from "./hooks";
|
|
84
88
|
|
|
85
89
|
// ============================================
|
|
86
90
|
// SONANCE DEVTOOLS
|
|
@@ -185,7 +189,7 @@ function findParentSection(element: Element): ParentSectionInfo | undefined {
|
|
|
185
189
|
|
|
186
190
|
// Frame layout dimensions
|
|
187
191
|
const BANNER_HEIGHT = 44;
|
|
188
|
-
const SIDEBAR_WIDTH =
|
|
192
|
+
const SIDEBAR_WIDTH = 300;
|
|
189
193
|
const SIDEBAR_COLLAPSED_WIDTH = 48;
|
|
190
194
|
|
|
191
195
|
export function SonanceDevTools() {
|
|
@@ -211,9 +215,20 @@ export function SonanceDevTools() {
|
|
|
211
215
|
const [componentScope, setComponentScope] = useState<"all" | "variant" | "page" | "selected">("all");
|
|
212
216
|
const [selectedComponentId, setSelectedComponentId] = useState<string | null>(null);
|
|
213
217
|
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
|
214
|
-
const [taggedElements, setTaggedElements] = useState<DetectedElement[]>([]);
|
|
215
218
|
const [viewMode, setViewMode] = useState<ComponentsViewMode>("visual");
|
|
216
|
-
|
|
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
|
+
});
|
|
217
232
|
|
|
218
233
|
// AI Preview mode - when active, shows preview highlights instead of inspector
|
|
219
234
|
const [isPreviewActive, setIsPreviewActive] = useState(false);
|
|
@@ -261,6 +276,41 @@ export function SonanceDevTools() {
|
|
|
261
276
|
return () => observer.disconnect();
|
|
262
277
|
}, []);
|
|
263
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
|
+
|
|
264
314
|
// Component-specific style overrides (for scalable, project-agnostic styling)
|
|
265
315
|
// Key: component type (e.g., "card", "button-primary", "card:variant123"), Value: style overrides
|
|
266
316
|
const [componentOverrides, setComponentOverrides] = useState<Record<string, ComponentStyle>>({});
|
|
@@ -335,13 +385,12 @@ export function SonanceDevTools() {
|
|
|
335
385
|
const [selectedLogoId, setSelectedLogoId] = useState<string | null>(null);
|
|
336
386
|
const [globalLogoConfig, setGlobalLogoConfig] = useState<LogoOverride>({});
|
|
337
387
|
const [individualLogoConfigs, setIndividualLogoConfigs] = useState<Record<string, LogoOverride>>({});
|
|
388
|
+
// originalLogoStates now synced from useElementScanner hook (scannedLogoStates)
|
|
338
389
|
const [originalLogoStates, setOriginalLogoStates] = useState<Record<string, OriginalLogoState>>({});
|
|
339
390
|
const [logoSaveStatus, setLogoSaveStatus] = useState<LogoSaveStatus>("idle");
|
|
340
391
|
const [logoSaveMessage, setLogoSaveMessage] = useState<string>("");
|
|
341
392
|
const [autoFixStatus, setAutoFixStatus] = useState<AutoFixStatus>("idle");
|
|
342
393
|
const [autoFixMessage, setAutoFixMessage] = useState<string>("");
|
|
343
|
-
const logoIdCounter = useRef(0);
|
|
344
|
-
const textIdCounter = useRef(0);
|
|
345
394
|
|
|
346
395
|
// Text Tool State
|
|
347
396
|
const [selectedTextId, setSelectedTextId] = useState<string | null>(null);
|
|
@@ -351,6 +400,13 @@ export function SonanceDevTools() {
|
|
|
351
400
|
const [savedTextOverrides, setSavedTextOverrides] = useState<Record<string, TextOverride>>({});
|
|
352
401
|
const SAVED_TEXT_STORAGE_KEY = "sonance-devtools-saved-text-overrides";
|
|
353
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
|
+
|
|
354
410
|
// Project Analysis state
|
|
355
411
|
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
|
|
356
412
|
const [analysisStatus, setAnalysisStatus] = useState<AnalysisStatus>("idle");
|
|
@@ -366,7 +422,7 @@ export function SonanceDevTools() {
|
|
|
366
422
|
const { theme, setTheme, resolvedTheme } = useTheme();
|
|
367
423
|
|
|
368
424
|
// Global brand context for logo switching
|
|
369
|
-
const { setBrand } = useBrand();
|
|
425
|
+
const { currentBrand, setBrand } = useBrand();
|
|
370
426
|
|
|
371
427
|
// Toggle between light and dark mode
|
|
372
428
|
const toggleThemeMode = useCallback(() => {
|
|
@@ -379,6 +435,24 @@ export function SonanceDevTools() {
|
|
|
379
435
|
setMounted(true);
|
|
380
436
|
}, []);
|
|
381
437
|
|
|
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
|
+
|
|
382
456
|
// Body layout injection for "squeeze" effect - app content respects dev tools frame
|
|
383
457
|
useEffect(() => {
|
|
384
458
|
if (!mounted) return;
|
|
@@ -470,7 +544,7 @@ export function SonanceDevTools() {
|
|
|
470
544
|
setApplyFirstStatus("reviewing");
|
|
471
545
|
// Auto-open the DevTools panel so user sees the Accept/Revert UI
|
|
472
546
|
setIsOpen(true);
|
|
473
|
-
setActiveTab("
|
|
547
|
+
setActiveTab("elements");
|
|
474
548
|
} else {
|
|
475
549
|
// Session expired, clear it
|
|
476
550
|
console.log("[Apply-First] Session expired, clearing localStorage");
|
|
@@ -656,29 +730,27 @@ export function SonanceDevTools() {
|
|
|
656
730
|
}
|
|
657
731
|
}, [mounted, elementFilters.images, inspectorEnabled, logoAssets.length]);
|
|
658
732
|
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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);
|
|
667
745
|
}
|
|
668
746
|
}
|
|
669
747
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return null;
|
|
748
|
+
if (mounted && elementFilters.images && publicImages.length === 0) {
|
|
749
|
+
fetchPublicImages();
|
|
673
750
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const filename = parts[parts.length - 1];
|
|
678
|
-
// Remove file extension
|
|
679
|
-
const nameWithoutExt = filename.replace(/\.(png|jpg|jpeg|svg|webp|gif)$/i, "");
|
|
680
|
-
return nameWithoutExt || null;
|
|
681
|
-
}, []);
|
|
751
|
+
}, [mounted, elementFilters.images, publicImages.length]);
|
|
752
|
+
|
|
753
|
+
// extractLogoName is now imported from "./hooks"
|
|
682
754
|
|
|
683
755
|
// Helper to find complementary light/dark logo variant
|
|
684
756
|
// Returns { light: path, dark: path } or null if no match found
|
|
@@ -820,319 +892,9 @@ export function SonanceDevTools() {
|
|
|
820
892
|
});
|
|
821
893
|
}, [taggedElements, selectedComponentType, inspectorEnabled, viewMode, elementFilters]);
|
|
822
894
|
|
|
823
|
-
//
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (str.length === 0) return hash.toString();
|
|
827
|
-
for (let i = 0; i < str.length; i++) {
|
|
828
|
-
const char = str.charCodeAt(i);
|
|
829
|
-
hash = ((hash << 5) - hash) + char;
|
|
830
|
-
hash = hash & hash; // Convert to 32bit integer
|
|
831
|
-
}
|
|
832
|
-
return Math.abs(hash).toString(16);
|
|
833
|
-
};
|
|
834
|
-
|
|
835
|
-
// Visual Inspector: Scan for tagged elements, logos, and text, update positions
|
|
836
|
-
useEffect(() => {
|
|
837
|
-
// Scan when inspector is enabled or on elements tab - now unified with filters
|
|
838
|
-
const shouldScan = inspectorEnabled || activeTab === "elements";
|
|
839
|
-
|
|
840
|
-
// console.log("Scan effect running. shouldScan:", shouldScan, "activeTab:", activeTab);
|
|
841
|
-
|
|
842
|
-
if (!shouldScan || !mounted) {
|
|
843
|
-
setTaggedElements([]);
|
|
844
|
-
if (inspectorRef.current) {
|
|
845
|
-
cancelAnimationFrame(inspectorRef.current);
|
|
846
|
-
inspectorRef.current = null;
|
|
847
|
-
}
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const scanElements = () => {
|
|
852
|
-
const newTagged: DetectedElement[] = [];
|
|
853
|
-
const newOriginalStates: Record<string, OriginalLogoState> = { ...originalLogoStates };
|
|
854
|
-
|
|
855
|
-
// Helper: Check if element is inside a scroll container and clip rect to visible bounds
|
|
856
|
-
const getVisibleRect = (el: Element, rect: DOMRect): DOMRect | null => {
|
|
857
|
-
// Find the nearest scroll container (aside, nav, or element with overflow)
|
|
858
|
-
const scrollParent = el.closest("aside, nav, [data-sidebar]");
|
|
859
|
-
if (scrollParent) {
|
|
860
|
-
const parentRect = scrollParent.getBoundingClientRect();
|
|
861
|
-
// Check if element is fully outside the scroll container's visible area
|
|
862
|
-
if (rect.bottom < parentRect.top || rect.top > parentRect.bottom ||
|
|
863
|
-
rect.right < parentRect.left || rect.left > parentRect.right) {
|
|
864
|
-
return null; // Element is not visible
|
|
865
|
-
}
|
|
866
|
-
// Clip the rect to the parent's visible bounds
|
|
867
|
-
const clippedTop = Math.max(rect.top, parentRect.top);
|
|
868
|
-
const clippedBottom = Math.min(rect.bottom, parentRect.bottom);
|
|
869
|
-
const clippedLeft = Math.max(rect.left, parentRect.left);
|
|
870
|
-
const clippedRight = Math.min(rect.right, parentRect.right);
|
|
871
|
-
// If clipped area is too small, skip it
|
|
872
|
-
if (clippedBottom - clippedTop < 10 || clippedRight - clippedLeft < 10) {
|
|
873
|
-
return null;
|
|
874
|
-
}
|
|
875
|
-
return new DOMRect(clippedLeft, clippedTop, clippedRight - clippedLeft, clippedBottom - clippedTop);
|
|
876
|
-
}
|
|
877
|
-
return rect; // No scroll parent, use original rect
|
|
878
|
-
};
|
|
879
|
-
|
|
880
|
-
// Helper: Detect active modal and filter elements to only those in the topmost layer
|
|
881
|
-
const activeModalContent = getActiveModalContent();
|
|
882
|
-
const isInActiveLayer = (el: Element): boolean => {
|
|
883
|
-
// Always exclude DevTools panel
|
|
884
|
-
if (el.closest("[data-sonance-devtools]")) return false;
|
|
885
|
-
|
|
886
|
-
// If a modal is active, only include elements inside the modal content
|
|
887
|
-
if (activeModalContent) {
|
|
888
|
-
return activeModalContent.contains(el) || el === activeModalContent;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
return true;
|
|
892
|
-
};
|
|
893
|
-
|
|
894
|
-
// Scan for tagged components (always scan when filters allow)
|
|
895
|
-
if ((inspectorEnabled || activeTab === "elements") && elementFilters.components) {
|
|
896
|
-
// 1. Scan for explicitly tagged components
|
|
897
|
-
const taggedComponents = document.querySelectorAll("[data-sonance-name]");
|
|
898
|
-
taggedComponents.forEach((el) => {
|
|
899
|
-
// Skip elements outside the active layer (DevTools, behind modals)
|
|
900
|
-
if (!isInActiveLayer(el)) return;
|
|
901
|
-
|
|
902
|
-
const name = el.getAttribute("data-sonance-name");
|
|
903
|
-
if (name) {
|
|
904
|
-
const rawRect = el.getBoundingClientRect();
|
|
905
|
-
const rect = getVisibleRect(el, rawRect);
|
|
906
|
-
if (rect && rect.width > 0 && rect.height > 0) {
|
|
907
|
-
// Generate variant ID based on class names and computed styles
|
|
908
|
-
// This groups visually identical components together
|
|
909
|
-
// Use stable variant ID: compute once, reuse from attribute
|
|
910
|
-
// This prevents hover/focus states from changing the hash
|
|
911
|
-
let variantId = el.getAttribute("data-sonance-variant");
|
|
912
|
-
if (!variantId) {
|
|
913
|
-
const className = el.className;
|
|
914
|
-
const computed = window.getComputedStyle(el);
|
|
915
|
-
const styleSignature = `${className}|${computed.backgroundColor}|${computed.borderColor}|${computed.borderRadius}|${computed.color}`;
|
|
916
|
-
variantId = generateHash(styleSignature);
|
|
917
|
-
el.setAttribute("data-sonance-variant", variantId);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// Capture textContent and className for dynamic element matching
|
|
921
|
-
const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
|
|
922
|
-
const className = el.className?.toString() || "";
|
|
923
|
-
|
|
924
|
-
// Capture element ID and child IDs for precise code targeting
|
|
925
|
-
const elementId = el.id || undefined;
|
|
926
|
-
const childIds = Array.from(el.querySelectorAll('[id]'))
|
|
927
|
-
.map(child => child.id)
|
|
928
|
-
.filter(id => id) as string[];
|
|
929
|
-
|
|
930
|
-
newTagged.push({ name, rect, type: "component", variantId, textContent, className, elementId, childIds: childIds.length > 0 ? childIds : undefined });
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
// 2. Scan for untagged primitive elements (Buttons, Inputs) to ensure list isn't empty
|
|
936
|
-
// Only add if not already tagged (though querySelectorAll excludes them implicitly if we check attribute)
|
|
937
|
-
const genericSelectors = {
|
|
938
|
-
"button": "button:not([data-sonance-name])",
|
|
939
|
-
"input": "input:not([data-sonance-name])",
|
|
940
|
-
"select": "select:not([data-sonance-name])",
|
|
941
|
-
"textarea": "textarea:not([data-sonance-name])"
|
|
942
|
-
};
|
|
943
|
-
|
|
944
|
-
Object.entries(genericSelectors).forEach(([genericName, selector]) => {
|
|
945
|
-
const elements = document.querySelectorAll(selector);
|
|
946
|
-
elements.forEach((el) => {
|
|
947
|
-
// Skip elements outside the active layer (DevTools, behind modals)
|
|
948
|
-
if (!isInActiveLayer(el)) return;
|
|
949
|
-
|
|
950
|
-
const rawRect = el.getBoundingClientRect();
|
|
951
|
-
const rect = getVisibleRect(el, rawRect);
|
|
952
|
-
if (rect && rect.width > 0 && rect.height > 0) {
|
|
953
|
-
// Generate stable variant ID for generic elements too
|
|
954
|
-
// Compute once, reuse from attribute to prevent hover state changes
|
|
955
|
-
let variantId = el.getAttribute("data-sonance-variant");
|
|
956
|
-
if (!variantId) {
|
|
957
|
-
const className = el.className;
|
|
958
|
-
const computed = window.getComputedStyle(el);
|
|
959
|
-
const styleSignature = `${className}|${computed.backgroundColor}|${computed.borderColor}|${computed.borderRadius}|${computed.color}`;
|
|
960
|
-
variantId = generateHash(styleSignature);
|
|
961
|
-
el.setAttribute("data-sonance-variant", variantId);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// Ensure generic elements have a data-sonance-name for targeting
|
|
965
|
-
if (!el.getAttribute("data-sonance-name")) {
|
|
966
|
-
el.setAttribute("data-sonance-name", genericName);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// Capture textContent and className for dynamic element matching
|
|
970
|
-
const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
|
|
971
|
-
const elClassName = el.className?.toString() || "";
|
|
972
|
-
|
|
973
|
-
// Capture element ID and child IDs for precise code targeting
|
|
974
|
-
const elementId = el.id || undefined;
|
|
975
|
-
const childIds = Array.from(el.querySelectorAll('[id]'))
|
|
976
|
-
.map(child => child.id)
|
|
977
|
-
.filter(id => id) as string[];
|
|
978
|
-
|
|
979
|
-
newTagged.push({ name: genericName, rect, type: "component", variantId, textContent, className: elClassName, elementId, childIds: childIds.length > 0 ? childIds : undefined });
|
|
980
|
-
}
|
|
981
|
-
});
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// Scan for logo/image elements (always scan when filters allow)
|
|
986
|
-
if (elementFilters.images) {
|
|
987
|
-
const images = document.querySelectorAll("img");
|
|
988
|
-
images.forEach((img) => {
|
|
989
|
-
// Skip elements outside the active layer (DevTools, behind modals)
|
|
990
|
-
if (!isInActiveLayer(img)) return;
|
|
991
|
-
|
|
992
|
-
const src = img.src || img.getAttribute("src") || "";
|
|
993
|
-
const alt = img.alt || "";
|
|
994
|
-
|
|
995
|
-
// Check if src or alt contains "logo"
|
|
996
|
-
const logoName = extractLogoName(src);
|
|
997
|
-
const altContainsLogo = alt.toLowerCase().includes("logo");
|
|
998
|
-
|
|
999
|
-
if (logoName || altContainsLogo) {
|
|
1000
|
-
const rect = img.getBoundingClientRect();
|
|
1001
|
-
// Only include visible elements
|
|
1002
|
-
if (rect.width > 0 && rect.height > 0) {
|
|
1003
|
-
// Assign or retrieve a unique ID for this logo element
|
|
1004
|
-
let logoId = img.getAttribute("data-sonance-logo-id");
|
|
1005
|
-
if (!logoId) {
|
|
1006
|
-
logoId = `logo-${logoIdCounter.current++}`;
|
|
1007
|
-
img.setAttribute("data-sonance-logo-id", logoId);
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// Store original state if not already stored
|
|
1011
|
-
if (!newOriginalStates[logoId]) {
|
|
1012
|
-
const originalSrcset = img.getAttribute("data-original-srcset") || img.srcset || "";
|
|
1013
|
-
newOriginalStates[logoId] = {
|
|
1014
|
-
src: img.getAttribute("data-original-src") || src,
|
|
1015
|
-
width: img.naturalWidth || img.width,
|
|
1016
|
-
height: img.naturalHeight || img.height,
|
|
1017
|
-
srcset: originalSrcset,
|
|
1018
|
-
};
|
|
1019
|
-
// Also store original src and srcset as data attributes for reset
|
|
1020
|
-
if (!img.getAttribute("data-original-src")) {
|
|
1021
|
-
img.setAttribute("data-original-src", src);
|
|
1022
|
-
}
|
|
1023
|
-
if (!img.getAttribute("data-original-srcset") && img.srcset) {
|
|
1024
|
-
img.setAttribute("data-original-srcset", img.srcset);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
const displayName = logoName || alt || "Logo";
|
|
1029
|
-
newTagged.push({ name: displayName, rect, type: "logo", logoId });
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Scan for text elements (always scan when filters allow and inspector enabled)
|
|
1036
|
-
if (elementFilters.text && inspectorEnabled) {
|
|
1037
|
-
// Only select meaningful text containers - exclude spans as they're usually nested
|
|
1038
|
-
const textSelectors = "h1, h2, h3, h4, h5, h6, p, a, label, blockquote, figcaption, li";
|
|
1039
|
-
const textElements = document.querySelectorAll(textSelectors);
|
|
1040
|
-
|
|
1041
|
-
// Track which elements we've already added to avoid nested duplicates
|
|
1042
|
-
const addedElements = new Set<Element>();
|
|
1043
|
-
|
|
1044
|
-
textElements.forEach((el) => {
|
|
1045
|
-
// Skip elements outside the active layer (DevTools, behind modals)
|
|
1046
|
-
if (!isInActiveLayer(el)) return;
|
|
1047
|
-
|
|
1048
|
-
// Skip if this element is inside another text element we're already tracking
|
|
1049
|
-
// This prevents duplicate labels for nested structures
|
|
1050
|
-
const parentTextEl = el.parentElement?.closest("h1, h2, h3, h4, h5, h6, p, a, label, blockquote, figcaption, li");
|
|
1051
|
-
if (parentTextEl && !parentTextEl.closest("[data-sonance-devtools]")) {
|
|
1052
|
-
// Allow links inside paragraphs to be selectable, but not other nesting
|
|
1053
|
-
if (!(el.tagName.toLowerCase() === "a" && parentTextEl.tagName.toLowerCase() === "p")) {
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// Skip elements that are already added
|
|
1059
|
-
if (addedElements.has(el)) return;
|
|
1060
|
-
|
|
1061
|
-
const rect = el.getBoundingClientRect();
|
|
1062
|
-
// Only include visible elements with content
|
|
1063
|
-
const textContent = el.textContent?.trim() || "";
|
|
1064
|
-
|
|
1065
|
-
// Skip elements that are too small (likely icons or decorative)
|
|
1066
|
-
if (rect.width < 20 || rect.height < 10) return;
|
|
1067
|
-
|
|
1068
|
-
// Skip elements with very short content (likely icons or single characters)
|
|
1069
|
-
if (textContent.length < 2) return;
|
|
1070
|
-
|
|
1071
|
-
// Skip if the element only contains non-text children (like SVGs/icons)
|
|
1072
|
-
const hasDirectTextContent = Array.from(el.childNodes).some(
|
|
1073
|
-
node => node.nodeType === Node.TEXT_NODE && node.textContent?.trim()
|
|
1074
|
-
);
|
|
1075
|
-
// For links and headings, check if they have meaningful text content
|
|
1076
|
-
const hasMeaningfulContent = textContent.length >= 2 && !/^[\s\u200B-\u200D\uFEFF]+$/.test(textContent);
|
|
1077
|
-
|
|
1078
|
-
if (!hasMeaningfulContent) return;
|
|
1079
|
-
|
|
1080
|
-
if (rect.width > 0 && rect.height > 0 && textContent.length > 0) {
|
|
1081
|
-
// Assign or retrieve a unique ID for this text element
|
|
1082
|
-
let textId = el.getAttribute("data-sonance-text-id");
|
|
1083
|
-
if (!textId) {
|
|
1084
|
-
textId = `text-${textIdCounter.current++}`;
|
|
1085
|
-
el.setAttribute("data-sonance-text-id", textId);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
// Capture original state if not already captured
|
|
1089
|
-
if (!originalTextStatesRef.current[textId]) {
|
|
1090
|
-
const computed = window.getComputedStyle(el);
|
|
1091
|
-
originalTextStatesRef.current[textId] = {
|
|
1092
|
-
textContent: el.textContent,
|
|
1093
|
-
fontSize: computed.fontSize,
|
|
1094
|
-
fontWeight: computed.fontWeight,
|
|
1095
|
-
lineHeight: computed.lineHeight,
|
|
1096
|
-
letterSpacing: computed.letterSpacing,
|
|
1097
|
-
color: computed.color,
|
|
1098
|
-
fontFamily: computed.fontFamily,
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
const tagName = el.tagName.toLowerCase();
|
|
1103
|
-
const displayName = tagName === "a" ? "Link" : tagName === "li" ? "List Item" : tagName.toUpperCase();
|
|
1104
|
-
const truncatedContent = textContent.length > 30 ? textContent.substring(0, 30) + "..." : textContent;
|
|
1105
|
-
|
|
1106
|
-
addedElements.add(el);
|
|
1107
|
-
newTagged.push({
|
|
1108
|
-
name: `${displayName}: ${truncatedContent}`,
|
|
1109
|
-
rect,
|
|
1110
|
-
type: "text",
|
|
1111
|
-
textId,
|
|
1112
|
-
textContent
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// Update original states if new logos were found
|
|
1119
|
-
if (Object.keys(newOriginalStates).length !== Object.keys(originalLogoStates).length) {
|
|
1120
|
-
setOriginalLogoStates(newOriginalStates);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
setTaggedElements(newTagged);
|
|
1124
|
-
inspectorRef.current = requestAnimationFrame(scanElements);
|
|
1125
|
-
};
|
|
1126
|
-
|
|
1127
|
-
scanElements();
|
|
1128
|
-
|
|
1129
|
-
return () => {
|
|
1130
|
-
if (inspectorRef.current) {
|
|
1131
|
-
cancelAnimationFrame(inspectorRef.current);
|
|
1132
|
-
inspectorRef.current = null;
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1135
|
-
}, [inspectorEnabled, mounted, extractLogoName, originalLogoStates, activeTab, elementFilters]);
|
|
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
|
|
1136
898
|
|
|
1137
899
|
// Toggle Visual Inspector
|
|
1138
900
|
const toggleInspector = useCallback((force?: boolean | unknown) => {
|
|
@@ -1202,6 +964,8 @@ export function SonanceDevTools() {
|
|
|
1202
964
|
childIds: element.childIds,
|
|
1203
965
|
// Parent section context for section-level changes
|
|
1204
966
|
parentSection,
|
|
967
|
+
// Capture image src for tracing to source code
|
|
968
|
+
imageSrc: element.imageSrc,
|
|
1205
969
|
};
|
|
1206
970
|
|
|
1207
971
|
setVisionFocusedElements((prev) => {
|
|
@@ -1293,7 +1057,7 @@ export function SonanceDevTools() {
|
|
|
1293
1057
|
|
|
1294
1058
|
// ========== Apply-First Mode Handlers ==========
|
|
1295
1059
|
|
|
1296
|
-
// Handle apply-first edit complete - files are already written,
|
|
1060
|
+
// Handle apply-first edit complete - files are already written, refresh to see changes
|
|
1297
1061
|
const handleApplyFirstComplete = useCallback((session: ApplyFirstSession) => {
|
|
1298
1062
|
console.log("[Apply-First] Changes applied:", {
|
|
1299
1063
|
sessionId: session.sessionId,
|
|
@@ -1307,7 +1071,7 @@ export function SonanceDevTools() {
|
|
|
1307
1071
|
|
|
1308
1072
|
setApplyFirstSession(session);
|
|
1309
1073
|
setApplyFirstStatus("waiting-hmr");
|
|
1310
|
-
|
|
1074
|
+
// Keep Vision mode active so user can continue working with their selection
|
|
1311
1075
|
|
|
1312
1076
|
// Persist session to localStorage so it survives page refreshes
|
|
1313
1077
|
try {
|
|
@@ -1320,9 +1084,12 @@ export function SonanceDevTools() {
|
|
|
1320
1084
|
console.warn("[Apply-First] Failed to persist session:", e);
|
|
1321
1085
|
}
|
|
1322
1086
|
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
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...");
|
|
1325
1091
|
setTimeout(() => {
|
|
1092
|
+
console.log("[Apply-First] Auto-refreshing page to show changes...");
|
|
1326
1093
|
window.location.reload();
|
|
1327
1094
|
}, 500);
|
|
1328
1095
|
}, [visionFocusedElements]);
|
|
@@ -1458,10 +1225,9 @@ export function SonanceDevTools() {
|
|
|
1458
1225
|
if (visionModeActive) {
|
|
1459
1226
|
clearVisionMode();
|
|
1460
1227
|
}
|
|
1461
|
-
//
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
}
|
|
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
|
|
1465
1231
|
}
|
|
1466
1232
|
// Analysis and Vision mode are mutually exclusive
|
|
1467
1233
|
// When switching to Analysis, disable Vision mode
|
|
@@ -1473,7 +1239,7 @@ export function SonanceDevTools() {
|
|
|
1473
1239
|
}
|
|
1474
1240
|
setActiveTab(newTab);
|
|
1475
1241
|
}
|
|
1476
|
-
}, [activeTab, visionModeActive, clearVisionMode
|
|
1242
|
+
}, [activeTab, visionModeActive, clearVisionMode]);
|
|
1477
1243
|
|
|
1478
1244
|
// Load saved text overrides from localStorage on mount
|
|
1479
1245
|
useEffect(() => {
|
|
@@ -1640,7 +1406,9 @@ export function SonanceDevTools() {
|
|
|
1640
1406
|
if (override.letterSpacing) cleanOverride.letterSpacing = override.letterSpacing;
|
|
1641
1407
|
if (override.color) cleanOverride.color = override.color;
|
|
1642
1408
|
if (override.fontFamily) cleanOverride.fontFamily = override.fontFamily;
|
|
1643
|
-
|
|
1409
|
+
if (override.colorLight) cleanOverride.colorLight = override.colorLight;
|
|
1410
|
+
if (override.colorDark) cleanOverride.colorDark = override.colorDark;
|
|
1411
|
+
|
|
1644
1412
|
// Add element identification for persistence
|
|
1645
1413
|
if (element) {
|
|
1646
1414
|
cleanOverride.selector = generateCssSelector(element);
|
|
@@ -1723,7 +1491,7 @@ export function SonanceDevTools() {
|
|
|
1723
1491
|
|
|
1724
1492
|
// Apply text overrides to DOM
|
|
1725
1493
|
useEffect(() => {
|
|
1726
|
-
if (activeTab !== "
|
|
1494
|
+
if (activeTab !== "elements") return;
|
|
1727
1495
|
|
|
1728
1496
|
const textElements = document.querySelectorAll("[data-sonance-text-id]");
|
|
1729
1497
|
textElements.forEach((el) => {
|
|
@@ -1952,6 +1720,295 @@ export function SonanceDevTools() {
|
|
|
1952
1720
|
}
|
|
1953
1721
|
}, [globalLogoConfig]);
|
|
1954
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
|
+
|
|
1955
2012
|
// Auto-fix ID injection into source code
|
|
1956
2013
|
const handleAutoFixId = useCallback(async (logoSrc: string, suggestedId: string): Promise<{ success: boolean; error?: string }> => {
|
|
1957
2014
|
setAutoFixStatus("fixing");
|
|
@@ -2089,8 +2146,8 @@ export function SonanceDevTools() {
|
|
|
2089
2146
|
|
|
2090
2147
|
// Apply logo overrides to DOM in real-time
|
|
2091
2148
|
useEffect(() => {
|
|
2092
|
-
// Apply logo overrides when on
|
|
2093
|
-
if (activeTab !== "
|
|
2149
|
+
// Apply logo overrides when on elements tab
|
|
2150
|
+
if (activeTab !== "elements") return;
|
|
2094
2151
|
|
|
2095
2152
|
const currentTheme = resolvedTheme || theme || "light";
|
|
2096
2153
|
const isDarkMode = currentTheme === "dark";
|
|
@@ -2564,10 +2621,10 @@ export function SonanceDevTools() {
|
|
|
2564
2621
|
<Palette className="h-4 w-4 text-white" />
|
|
2565
2622
|
</div>
|
|
2566
2623
|
<div className="flex flex-col">
|
|
2567
|
-
<span className="text-sm font-semibold text-white tracking-tight">
|
|
2624
|
+
<span id="span-sonance-devtools" className="text-sm font-semibold text-white tracking-tight">
|
|
2568
2625
|
Sonance DevTools
|
|
2569
2626
|
</span>
|
|
2570
|
-
<span className="text-[10px] text-white/50 uppercase tracking-widest">
|
|
2627
|
+
<span id="span-design-system" className="text-[10px] text-white/50 uppercase tracking-widest">
|
|
2571
2628
|
Design System
|
|
2572
2629
|
</span>
|
|
2573
2630
|
</div>
|
|
@@ -2578,7 +2635,7 @@ export function SonanceDevTools() {
|
|
|
2578
2635
|
<div className="flex items-center gap-1 bg-white/5 rounded-full px-2 py-1">
|
|
2579
2636
|
{/* Analysis Toggle */}
|
|
2580
2637
|
<button
|
|
2581
|
-
onClick={() => handleTabChange(activeTab === "analysis" ? "
|
|
2638
|
+
onClick={() => handleTabChange(activeTab === "analysis" ? "elements" : "analysis")}
|
|
2582
2639
|
className={cn(
|
|
2583
2640
|
"flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all",
|
|
2584
2641
|
activeTab === "analysis"
|
|
@@ -2588,7 +2645,7 @@ export function SonanceDevTools() {
|
|
|
2588
2645
|
title="Page Analysis"
|
|
2589
2646
|
>
|
|
2590
2647
|
<Scan className="h-3.5 w-3.5" />
|
|
2591
|
-
<span className="hidden sm:inline">Analysis</span>
|
|
2648
|
+
<span id="span-analysis" className="hidden sm:inline">Analysis</span>
|
|
2592
2649
|
</button>
|
|
2593
2650
|
{/* Vision Mode Toggle */}
|
|
2594
2651
|
<button
|
|
@@ -2602,7 +2659,7 @@ export function SonanceDevTools() {
|
|
|
2602
2659
|
title="AI Vision Mode"
|
|
2603
2660
|
>
|
|
2604
2661
|
<Eye className={cn("h-3.5 w-3.5", visionModeActive && "animate-pulse")} />
|
|
2605
|
-
<span className="hidden sm:inline">Vision</span>
|
|
2662
|
+
<span id="span-vision" className="hidden sm:inline">Vision</span>
|
|
2606
2663
|
</button>
|
|
2607
2664
|
{/* Element Highlight Toggle - only visible when Vision Mode is active */}
|
|
2608
2665
|
{visionModeActive && (
|
|
@@ -2617,7 +2674,7 @@ export function SonanceDevTools() {
|
|
|
2617
2674
|
title={inspectorEnabled ? "Disable element highlighting" : "Enable element highlighting"}
|
|
2618
2675
|
>
|
|
2619
2676
|
<MousePointer className={cn("h-3.5 w-3.5", inspectorEnabled && "animate-pulse")} />
|
|
2620
|
-
<span className="hidden sm:inline">{inspectorEnabled ? "Highlighting" : "Highlight"}</span>
|
|
2677
|
+
<span id="span-inspectorenabled-hig" className="hidden sm:inline">{inspectorEnabled ? "Highlighting" : "Highlight"}</span>
|
|
2621
2678
|
</button>
|
|
2622
2679
|
)}
|
|
2623
2680
|
</div>
|
|
@@ -2706,7 +2763,7 @@ export function SonanceDevTools() {
|
|
|
2706
2763
|
title="Toggle Components"
|
|
2707
2764
|
>
|
|
2708
2765
|
<Box className="h-3 w-3" />
|
|
2709
|
-
<span>Components</span>
|
|
2766
|
+
<span id="span-components">Components</span>
|
|
2710
2767
|
</button>
|
|
2711
2768
|
{/* Images Filter */}
|
|
2712
2769
|
<button
|
|
@@ -2720,7 +2777,7 @@ export function SonanceDevTools() {
|
|
|
2720
2777
|
title="Toggle Images"
|
|
2721
2778
|
>
|
|
2722
2779
|
<ImageIcon className="h-3 w-3" />
|
|
2723
|
-
<span>Images</span>
|
|
2780
|
+
<span id="span-images">Images</span>
|
|
2724
2781
|
</button>
|
|
2725
2782
|
{/* Text Filter */}
|
|
2726
2783
|
<button
|
|
@@ -2734,7 +2791,7 @@ export function SonanceDevTools() {
|
|
|
2734
2791
|
title="Toggle Text"
|
|
2735
2792
|
>
|
|
2736
2793
|
<Type className="h-3 w-3" />
|
|
2737
|
-
<span>Text</span>
|
|
2794
|
+
<span id="span-text">Text</span>
|
|
2738
2795
|
</button>
|
|
2739
2796
|
</div>
|
|
2740
2797
|
)}
|
|
@@ -2854,6 +2911,16 @@ export function SonanceDevTools() {
|
|
|
2854
2911
|
onSelectLogo={handleSelectLogo}
|
|
2855
2912
|
selectedTextId={selectedTextId}
|
|
2856
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}
|
|
2857
2924
|
/>
|
|
2858
2925
|
</div>
|
|
2859
2926
|
)}
|
|
@@ -2879,7 +2946,7 @@ export function SonanceDevTools() {
|
|
|
2879
2946
|
aria-label="Open Sonance DevTools"
|
|
2880
2947
|
>
|
|
2881
2948
|
<Palette className="h-4 w-4 text-[#00D3C8]" />
|
|
2882
|
-
<span>DevTools</span>
|
|
2949
|
+
<span id="span-devtools">DevTools</span>
|
|
2883
2950
|
</button>
|
|
2884
2951
|
);
|
|
2885
2952
|
|