sonance-brand-mcp 1.3.111 → 1.3.113

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.
Files changed (79) hide show
  1. package/dist/assets/api/sonance-save-image/route.ts +625 -0
  2. package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
  3. package/dist/assets/api/sonance-vision-apply/route.ts +988 -57
  4. package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
  5. package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
  6. package/dist/assets/brand-system.ts +13 -12
  7. package/dist/assets/components/accordion.tsx +15 -7
  8. package/dist/assets/components/alert-dialog.tsx +35 -10
  9. package/dist/assets/components/alert.tsx +11 -10
  10. package/dist/assets/components/avatar.tsx +4 -4
  11. package/dist/assets/components/badge.tsx +16 -12
  12. package/dist/assets/components/button.stories.tsx +3 -3
  13. package/dist/assets/components/button.tsx +50 -31
  14. package/dist/assets/components/calendar.tsx +12 -8
  15. package/dist/assets/components/card.tsx +35 -29
  16. package/dist/assets/components/checkbox.tsx +9 -8
  17. package/dist/assets/components/code.tsx +19 -11
  18. package/dist/assets/components/command.tsx +32 -13
  19. package/dist/assets/components/context-menu.tsx +37 -16
  20. package/dist/assets/components/dialog.tsx +8 -5
  21. package/dist/assets/components/divider.tsx +15 -5
  22. package/dist/assets/components/drawer.tsx +4 -3
  23. package/dist/assets/components/dropdown-menu.tsx +15 -13
  24. package/dist/assets/components/hover-card.tsx +4 -1
  25. package/dist/assets/components/image.tsx +1 -1
  26. package/dist/assets/components/input.tsx +29 -14
  27. package/dist/assets/components/kbd.stories.tsx +3 -3
  28. package/dist/assets/components/kbd.tsx +29 -13
  29. package/dist/assets/components/listbox.tsx +8 -8
  30. package/dist/assets/components/menubar.tsx +50 -23
  31. package/dist/assets/components/navbar.stories.tsx +140 -13
  32. package/dist/assets/components/navbar.tsx +22 -5
  33. package/dist/assets/components/navigation-menu.tsx +28 -6
  34. package/dist/assets/components/pagination.tsx +10 -10
  35. package/dist/assets/components/popover.tsx +10 -8
  36. package/dist/assets/components/progress.tsx +6 -4
  37. package/dist/assets/components/radio-group.tsx +5 -5
  38. package/dist/assets/components/select.tsx +49 -29
  39. package/dist/assets/components/separator.tsx +3 -3
  40. package/dist/assets/components/sheet.tsx +4 -4
  41. package/dist/assets/components/sidebar.tsx +10 -10
  42. package/dist/assets/components/skeleton.tsx +13 -5
  43. package/dist/assets/components/slider.tsx +12 -10
  44. package/dist/assets/components/switch.tsx +4 -4
  45. package/dist/assets/components/table.tsx +5 -5
  46. package/dist/assets/components/tabs.tsx +8 -8
  47. package/dist/assets/components/textarea.tsx +11 -9
  48. package/dist/assets/components/toast.tsx +7 -7
  49. package/dist/assets/components/toggle.tsx +27 -7
  50. package/dist/assets/components/tooltip.tsx +10 -8
  51. package/dist/assets/components/user.tsx +8 -6
  52. package/dist/assets/dev-tools/SonanceDevTools.tsx +429 -362
  53. package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
  54. package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
  55. package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
  56. package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
  57. package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
  58. package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
  59. package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
  60. package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
  61. package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
  62. package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
  63. package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
  64. package/dist/assets/dev-tools/components/VisionModeBorder.tsx +4 -64
  65. package/dist/assets/dev-tools/hooks/index.ts +69 -0
  66. package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
  67. package/dist/assets/dev-tools/hooks/useComputedStyles.ts +171 -65
  68. package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
  69. package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
  70. package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
  71. package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
  72. package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +160 -57
  73. package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
  74. package/dist/assets/dev-tools/types.ts +42 -0
  75. package/dist/assets/globals.css +225 -9
  76. package/dist/assets/styles/brand-overrides.css +3 -2
  77. package/dist/assets/utils.ts +2 -1
  78. package/dist/index.js +32 -1
  79. 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 = 260;
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
- const inspectorRef = useRef<number | null>(null);
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("components");
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
- // Helper to extract logo filename from src (handles next/image optimized URLs)
660
- const extractLogoName = useCallback((src: string): string | null => {
661
- // Handle next/image optimized URLs like /_next/image?url=%2Flogos%2F...
662
- let decodedSrc = src;
663
- if (src.includes("/_next/image")) {
664
- const urlParam = new URL(src, window.location.origin).searchParams.get("url");
665
- if (urlParam) {
666
- decodedSrc = decodeURIComponent(urlParam);
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
- // Check if the path contains "logo" anywhere
671
- if (!decodedSrc.toLowerCase().includes("logo")) {
672
- return null;
748
+ if (mounted && elementFilters.images && publicImages.length === 0) {
749
+ fetchPublicImages();
673
750
  }
674
-
675
- // Extract filename from path
676
- const parts = decodedSrc.split("/");
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
- // Simple string hash function for variant detection
824
- const generateHash = (str: string): string => {
825
- let hash = 0;
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, waiting for HMR
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
- setVisionModeActive(false);
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
- // Force page refresh to ensure changes are visible
1324
- // Session is already persisted to localStorage, so it survives refresh
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
- // Clear apply-first session (auto-revert would be triggered)
1462
- if (applyFirstSession) {
1463
- handleApplyFirstRevert();
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, applyFirstSession, handleApplyFirstRevert]);
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 !== "text") return;
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 logos tab
2093
- if (activeTab !== "logos") return;
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" ? "components" : "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