sonance-brand-mcp 1.3.110 → 1.3.112

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