sonance-brand-mcp 1.3.111 → 1.3.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) 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/package.json +1 -1
@@ -2,8 +2,9 @@
2
2
 
3
3
  import React, { useState, useMemo, useCallback, useEffect } from "react";
4
4
  import { Box, X, RotateCcw, MousePointer, Sparkles, Info, MessageCircle } from "lucide-react";
5
+ import { useTheme } from "next-themes";
5
6
  import { cn } from "../../../lib/utils";
6
- import { DetectedElement, AnalysisResult, ComponentStyle, PendingEdit, AIEditResult, ComponentStyleOverride, VisionFocusedElement, VisionPendingEdit, ApplyFirstSession, ApplyFirstStatus, ElementFilters } from "../types";
7
+ import { DetectedElement, AnalysisResult, ComponentStyle, PendingEdit, AIEditResult, ComponentStyleOverride, VisionFocusedElement, VisionPendingEdit, ApplyFirstSession, ApplyFirstStatus, ElementFilters, ImageOverride, PublicImageAsset, LogoAsset } from "../types";
7
8
  import { COMPONENT_CONFIG_MAP, getVisibleSections } from "../constants";
8
9
  import { componentSnippets, ThemeConfig } from "../../../lib/brand-system";
9
10
  import { ChatInterface } from "../components/ChatInterface";
@@ -56,8 +57,8 @@ export interface ComponentsPanelProps {
56
57
  applyFirstSession?: ApplyFirstSession | null;
57
58
  applyFirstStatus?: ApplyFirstStatus;
58
59
  onApplyFirstComplete?: (session: ApplyFirstSession) => void;
59
- onApplyFirstAccept?: () => void;
60
- onApplyFirstRevert?: () => void;
60
+ onApplyFirstAccept?: () => Promise<void>;
61
+ onApplyFirstRevert?: () => Promise<void>;
61
62
  onApplyFirstForceClear?: () => void;
62
63
  onApplyPreview?: () => void; // Apply previewed modifications to files
63
64
  // Unified element filters (new)
@@ -66,6 +67,16 @@ export interface ComponentsPanelProps {
66
67
  onSelectLogo?: (logoId: string | null) => void;
67
68
  selectedTextId?: string | null;
68
69
  onSelectText?: (textId: string | null) => void;
70
+ // Image editing props
71
+ imageOverrides?: Record<string, ImageOverride>;
72
+ onImageOverrideChange?: (imageId: string, override: ImageOverride) => void;
73
+ onSaveImageChanges?: (imageId: string) => void;
74
+ isImageSaving?: boolean;
75
+ publicImages?: PublicImageAsset[];
76
+ publicImagesByFolder?: Record<string, PublicImageAsset[]>;
77
+ logoAssets?: LogoAsset[];
78
+ logoAssetsByBrand?: Record<string, LogoAsset[]>;
79
+ onImageUpload?: (file: File) => Promise<string | null>;
69
80
  }
70
81
 
71
82
  export function ComponentsPanel({
@@ -113,7 +124,21 @@ export function ComponentsPanel({
113
124
  onSelectLogo,
114
125
  selectedTextId,
115
126
  onSelectText,
127
+ // Image editing
128
+ imageOverrides = {},
129
+ onImageOverrideChange,
130
+ onSaveImageChanges,
131
+ isImageSaving = false,
132
+ publicImages = [],
133
+ publicImagesByFolder = {},
134
+ logoAssets = [],
135
+ logoAssetsByBrand = {},
136
+ onImageUpload,
116
137
  }: ComponentsPanelProps) {
138
+ // Theme detection for mode-specific color changes
139
+ const { resolvedTheme } = useTheme();
140
+ const currentTheme = resolvedTheme || "light";
141
+
117
142
  // Auto-activate inspector when entering this tab
118
143
  useEffect(() => {
119
144
  if (!inspectorEnabled && selectedComponentType === "all") {
@@ -423,6 +448,13 @@ export function ComponentsPanel({
423
448
  // Build a natural language description of the changes
424
449
  const changeDescriptions: string[] = [];
425
450
 
451
+ // Text content change (put first as it's the most significant change)
452
+ if (edits.textContent) {
453
+ const truncated = edits.textContent.length > 50
454
+ ? `${edits.textContent.substring(0, 50)}...`
455
+ : edits.textContent;
456
+ changeDescriptions.push(`text content to "${truncated}"`);
457
+ }
426
458
  if (edits.width) changeDescriptions.push(`width to ${edits.width}`);
427
459
  if (edits.height) changeDescriptions.push(`height to ${edits.height}`);
428
460
  if (edits.opacity) changeDescriptions.push(`opacity to ${edits.opacity}%`);
@@ -432,6 +464,9 @@ export function ComponentsPanel({
432
464
  if (edits.lineHeight) changeDescriptions.push(`line-height to ${edits.lineHeight}`);
433
465
  if (edits.letterSpacing) changeDescriptions.push(`letter-spacing to ${edits.letterSpacing}`);
434
466
  if (edits.color) changeDescriptions.push(`text color to ${edits.color}`);
467
+ // Theme-specific color changes
468
+ if (edits.colorLight) changeDescriptions.push(`text color (light mode) to ${edits.colorLight}`);
469
+ if (edits.colorDark) changeDescriptions.push(`text color (dark mode) to ${edits.colorDark}`);
435
470
  if (edits.backgroundColor) changeDescriptions.push(`background color to ${edits.backgroundColor}`);
436
471
  if (edits.padding) changeDescriptions.push(`padding to ${edits.padding}`);
437
472
  if (edits.margin) changeDescriptions.push(`margin to ${edits.margin}`);
@@ -439,89 +474,116 @@ export function ComponentsPanel({
439
474
 
440
475
  if (changeDescriptions.length === 0) return;
441
476
 
477
+ // Detect if this is a text-only change (only textContent is set, no style properties)
478
+ const editKeys = Object.keys(edits).filter(k => edits[k as keyof PropertyEdits] !== undefined);
479
+ const isTextOnlyChange = editKeys.length === 1 && editKeys[0] === 'textContent';
480
+
442
481
  const prompt = `Change the ${changeDescriptions.join(", ")} for this ${element.name} element.`;
443
482
 
444
483
  try {
484
+ // Use "apply" action so changes are written immediately with backups
485
+ // This allows HMR to show the changes, then user can accept/revert
445
486
  const response = await fetch("/api/sonance-vision-apply", {
446
487
  method: "POST",
447
488
  headers: { "Content-Type": "application/json" },
448
489
  body: JSON.stringify({
449
- action: "preview",
490
+ action: "apply",
450
491
  focusedElements: [element],
451
492
  userPrompt: prompt,
452
493
  propertyEdits: edits,
494
+ pageRoute: window.location.pathname,
495
+ currentTheme: currentTheme, // Include theme for mode-specific color changes
453
496
  }),
454
497
  });
455
498
 
456
499
  const result = await response.json();
457
500
 
458
- if (result.success && onApplyFirstComplete) {
459
- // Trigger the apply-first preview flow
460
- onApplyFirstComplete({
461
- id: `property-edit-${Date.now()}`,
462
- userPrompt: prompt,
463
- focusedElements: [element],
464
- originalCode: result.originalCode || "",
465
- modifiedCode: result.modifiedCode || "",
466
- filePath: result.filePath || "",
467
- diff: result.diff || "",
468
- backupId: result.backupId,
469
- createdAt: new Date(),
470
- });
501
+ if (result.success && result.modifications && result.modifications.length > 0) {
502
+ if (isTextOnlyChange) {
503
+ // For text-only changes, auto-accept immediately (user already clicked "Save Changes")
504
+ // This deletes the backup - no need to ask again
505
+ try {
506
+ await fetch("/api/sonance-vision-apply", {
507
+ method: "POST",
508
+ headers: { "Content-Type": "application/json" },
509
+ body: JSON.stringify({
510
+ action: "accept",
511
+ sessionId: result.sessionId,
512
+ }),
513
+ });
514
+ console.log("[Text Edit] Auto-accepted text change, backup deleted");
515
+ } catch (acceptError) {
516
+ console.warn("[Text Edit] Failed to auto-accept:", acceptError);
517
+ }
518
+ // Don't trigger applyFirstSession - change is complete
519
+ } else if (onApplyFirstComplete) {
520
+ // For component/style changes, trigger the apply-first flow for review
521
+ onApplyFirstComplete({
522
+ sessionId: result.sessionId,
523
+ modifications: result.modifications,
524
+ appliedAt: Date.now(),
525
+ status: "applied",
526
+ backupPaths: result.backupPaths || [],
527
+ isPreview: false,
528
+ isTextOnlyChange: false,
529
+ });
530
+ }
531
+ } else if (!result.success) {
532
+ console.error("Failed to save property changes:", result.error);
533
+ } else if (result.modifications?.length === 0) {
534
+ console.log("No changes needed or AI could not locate the element");
471
535
  }
472
536
  } catch (error) {
473
537
  console.error("Failed to save property changes:", error);
474
538
  }
475
- }, [onApplyFirstComplete]);
539
+ }, [onApplyFirstComplete, currentTheme]);
476
540
 
477
541
  // Get the first focused element for properties panel
478
542
  const primaryFocusedElement = visionFocusedElements.length > 0 ? visionFocusedElements[0] : null;
479
- const focusedCoords = primaryFocusedElement?.coordinates || null;
480
- const computedStyles = useElementFromCoordinates(focusedCoords);
543
+ // Pass full element to hook for multi-strategy element finding (robust after HMR)
544
+ const computedStyles = useElementFromCoordinates(primaryFocusedElement);
481
545
 
482
546
  // Determine if we should show the properties panel (Vision mode with focused elements)
483
547
  const showPropertiesPanel = visionMode && visionFocusedElements.length > 0;
484
548
 
485
549
  // Panel tab state - "details" or "chat"
550
+ // Default to "chat" for AI-first workflow
486
551
  type PanelTab = "details" | "chat";
487
552
  const [panelTab, setPanelTab] = useState<PanelTab>("details");
488
553
 
489
- // Auto-switch to details when an element is focused
490
- useEffect(() => {
491
- if (showPropertiesPanel) {
492
- setPanelTab("details");
493
- }
494
- }, [showPropertiesPanel]);
554
+ // NOTE: Removed auto-switch to details when element is focused
555
+ // Users want to stay on the AI chat tab when clicking elements
556
+ // The element info is shown in the chat context anyway
495
557
 
496
558
  return (
497
559
  <div className="flex flex-col h-full">
498
- {/* ===== TAB BAR - Cursor-style underline tabs ===== */}
499
- <div className="flex items-center gap-1 px-3 py-1.5 bg-transparent border-b border-gray-200/50 dark:border-white/10 flex-shrink-0">
560
+ {/* ===== TAB BAR - Dark theme matching the app ===== */}
561
+ <div className="flex items-center gap-1 px-3 py-1.5 bg-[#1a1d21] border-b border-white/10 flex-shrink-0">
500
562
  <button
501
563
  onClick={() => setPanelTab("details")}
502
564
  className={cn(
503
- "relative flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-medium transition-all duration-150 bg-transparent",
565
+ "relative flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-medium transition-all duration-150 rounded",
504
566
  panelTab === "details"
505
- ? "text-gray-900 dark:text-white after:absolute after:bottom-0 after:left-1 after:right-1 after:h-[2px] after:bg-[#00A3E1] after:rounded-full"
506
- : "text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100/30 dark:hover:bg-white/10"
567
+ ? "text-white after:absolute after:bottom-0 after:left-1 after:right-1 after:h-[2px] after:bg-[#00A3E1] after:rounded-full"
568
+ : "text-gray-400 hover:text-white hover:bg-white/5"
507
569
  )}
508
570
  >
509
571
  <Info className="h-3 w-3" />
510
- <span>Details</span>
572
+ <span id="span-details">Details</span>
511
573
  </button>
512
574
  <button
513
575
  onClick={() => setPanelTab("chat")}
514
576
  className={cn(
515
- "relative flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-medium transition-all duration-150 bg-transparent",
577
+ "relative flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-medium transition-all duration-150 rounded",
516
578
  panelTab === "chat"
517
- ? "text-gray-900 dark:text-white after:absolute after:bottom-0 after:left-1 after:right-1 after:h-[2px] after:bg-[#00A3E1] after:rounded-full"
518
- : "text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100/30 dark:hover:bg-white/10"
579
+ ? "text-white after:absolute after:bottom-0 after:left-1 after:right-1 after:h-[2px] after:bg-[#00A3E1] after:rounded-full"
580
+ : "text-gray-400 hover:text-white hover:bg-white/5"
519
581
  )}
520
582
  >
521
583
  <MessageCircle className="h-3 w-3" />
522
- <span>AI Chat</span>
584
+ <span id="span-ai-chat">AI Chat</span>
523
585
  {visionMode && visionFocusedElements.length > 0 && (
524
- <span className="ml-0.5 min-w-[16px] h-4 px-1 flex items-center justify-center text-[9px] font-semibold bg-purple-500/20 text-purple-600 dark:text-purple-400 rounded-full">
586
+ <span id="span-visionfocusedelement" className="ml-0.5 min-w-[16px] h-4 px-1 flex items-center justify-center text-[9px] font-semibold bg-[#00A3E1]/20 text-[#00A3E1] rounded-full">
525
587
  {visionFocusedElements.length}
526
588
  </span>
527
589
  )}
@@ -540,11 +602,44 @@ export function ComponentsPanel({
540
602
  element={primaryFocusedElement}
541
603
  styles={computedStyles}
542
604
  onSaveChanges={handlePropertySave}
605
+ textChangeSession={applyFirstSession?.isTextOnlyChange ? applyFirstSession : null}
606
+ onTextChangeAccept={onApplyFirstAccept}
607
+ onTextChangeRevert={onApplyFirstRevert}
608
+ imageOverride={(() => {
609
+ // Use elementId, variantId, or generate from name for image identification
610
+ const imageId = primaryFocusedElement?.elementId || primaryFocusedElement?.variantId || primaryFocusedElement?.name;
611
+ return imageId ? imageOverrides[imageId] : undefined;
612
+ })()}
613
+ onImageOverrideChange={onImageOverrideChange
614
+ ? (override) => {
615
+ // Use elementId, variantId, or generate from name for image identification
616
+ const imageId = primaryFocusedElement?.elementId || primaryFocusedElement?.variantId || primaryFocusedElement?.name;
617
+ if (imageId) {
618
+ onImageOverrideChange(imageId, override);
619
+ }
620
+ }
621
+ : undefined
622
+ }
623
+ onSaveImageChanges={onSaveImageChanges
624
+ ? () => {
625
+ const imageId = primaryFocusedElement?.elementId || primaryFocusedElement?.variantId || primaryFocusedElement?.name;
626
+ if (imageId) {
627
+ onSaveImageChanges(imageId);
628
+ }
629
+ }
630
+ : undefined
631
+ }
632
+ isImageSaving={isImageSaving}
633
+ publicImages={publicImages}
634
+ publicImagesByFolder={publicImagesByFolder}
635
+ logoAssets={logoAssets}
636
+ logoAssetsByBrand={logoAssetsByBrand}
637
+ onImageUpload={onImageUpload}
543
638
  />
544
639
  {/* Multiple Selections Indicator */}
545
640
  {visionFocusedElements.length > 1 && (
546
641
  <div className="px-2 py-1.5 bg-purple-50 dark:bg-purple-900/30 border-t border-purple-100 dark:border-purple-800">
547
- <p className="text-[10px] text-purple-600 dark:text-purple-400 font-medium">
642
+ <p id="p-visionfocusedelement" className="text-[10px] text-purple-600 dark:text-purple-400 font-medium">
548
643
  +{visionFocusedElements.length - 1} more selected
549
644
  </p>
550
645
  </div>
@@ -558,10 +653,10 @@ export function ComponentsPanel({
558
653
  <div className="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/40 flex items-center justify-center mb-2">
559
654
  <MousePointer className="h-5 w-5 text-purple-500 dark:text-purple-400" />
560
655
  </div>
561
- <p className="text-[11px] font-medium text-foreground mb-1">
656
+ <p id="p-vision-mode-active" className="text-[11px] font-medium text-foreground mb-1">
562
657
  Vision Mode Active
563
658
  </p>
564
- <p className="text-[10px] text-foreground-secondary">
659
+ <p id="p-click-elements-on-th" className="text-[10px] text-foreground-secondary">
565
660
  Click elements on the page to inspect and select them for AI-powered editing.
566
661
  </p>
567
662
  </div>
@@ -621,7 +716,7 @@ export function ComponentsPanel({
621
716
  return (
622
717
  <div className="space-y-1.5">
623
718
  <div className="flex items-center justify-between">
624
- <span className="text-[10px] font-medium text-foreground-secondary">Variants</span>
719
+ <span id="span-variants" className="text-[10px] font-medium text-foreground-secondary">Variants</span>
625
720
  {selectedVariantId && (
626
721
  <button
627
722
  onClick={() => onSelectVariant(null)}
@@ -644,7 +739,7 @@ export function ComponentsPanel({
644
739
  )}
645
740
  >
646
741
  #{variantId.substring(0, 4)}
647
- <span className="opacity-60 ml-0.5">({count})</span>
742
+ <span id="span-count" className="opacity-60 ml-0.5">({count})</span>
648
743
  </button>
649
744
  ))}
650
745
  </div>
@@ -667,7 +762,7 @@ export function ComponentsPanel({
667
762
  {/* Edit Scope Toggle - Compact */}
668
763
  {selectedVariantId && (
669
764
  <div className="flex items-center gap-1 p-1.5 bg-secondary/50 rounded border border-border">
670
- <span className="text-[9px] text-foreground-secondary">Scope:</span>
765
+ <span id="span-scope" className="text-[9px] text-foreground-secondary">Scope:</span>
671
766
  <div className="flex gap-0.5 flex-1">
672
767
  <button
673
768
  onClick={() => setEditScope("component")}
@@ -703,10 +798,10 @@ export function ComponentsPanel({
703
798
  <div className="w-10 h-10 rounded-full bg-secondary flex items-center justify-center mb-2">
704
799
  <Box className="h-5 w-5 text-foreground-muted" />
705
800
  </div>
706
- <p className="text-[11px] font-medium text-foreground mb-1">
801
+ <p id="p-select-a-component" className="text-[11px] font-medium text-foreground mb-1">
707
802
  Select a Component
708
803
  </p>
709
- <p className="text-[10px] text-foreground-secondary">
804
+ <p id="p-click-a-component-on" className="text-[10px] text-foreground-secondary">
710
805
  Click a component on the page or enable Vision Mode for AI-powered editing.
711
806
  </p>
712
807
  </div>
@@ -717,6 +812,21 @@ export function ComponentsPanel({
717
812
  {/* AI CHAT TAB - Full Height Cursor-Style Interface */}
718
813
  {panelTab === "chat" && (
719
814
  <div className="flex flex-col h-full bg-background">
815
+ {/* Selected Element Indicator - Shows what's selected without leaving chat */}
816
+ {visionMode && visionFocusedElements.length > 0 && (
817
+ <div className="flex items-center gap-1.5 px-3 py-1.5 bg-background-secondary border-b border-border">
818
+ <MousePointer className="h-3 w-3 text-foreground-muted flex-shrink-0" />
819
+ <span className="text-[11px] font-medium text-foreground truncate">
820
+ {primaryFocusedElement?.name || visionFocusedElements[0]?.type}
821
+ </span>
822
+ {visionFocusedElements.length > 1 && (
823
+ <span className="text-[10px] text-foreground-muted flex-shrink-0">
824
+ +{visionFocusedElements.length - 1}
825
+ </span>
826
+ )}
827
+ </div>
828
+ )}
829
+
720
830
  {/* Chat Interface - Always visible with inline diffs */}
721
831
  <ChatInterface
722
832
  componentType={selectedComponentType}
@@ -732,6 +842,10 @@ export function ComponentsPanel({
732
842
  visionFocusedElements={visionFocusedElements}
733
843
  onVisionEditComplete={onVisionEditComplete}
734
844
  onApplyFirstComplete={onApplyFirstComplete}
845
+ applyFirstSession={applyFirstSession}
846
+ applyFirstStatus={applyFirstStatus}
847
+ onApplyFirstAccept={onApplyFirstAccept}
848
+ onApplyFirstRevert={onApplyFirstRevert}
735
849
  />
736
850
 
737
851
  {/* Legacy Diff Previews (fallback for non-inline mode) */}
@@ -761,18 +875,7 @@ export function ComponentsPanel({
761
875
  </div>
762
876
  )}
763
877
 
764
- {applyFirstSession && onApplyFirstAccept && onApplyFirstRevert && (
765
- <div className="border-t border-border p-2">
766
- <ApplyFirstPreview
767
- session={applyFirstSession}
768
- status={applyFirstStatus}
769
- onAccept={onApplyFirstAccept}
770
- onRevert={onApplyFirstRevert}
771
- onForceClear={onApplyFirstForceClear}
772
- onApplyPreview={onApplyPreview}
773
- />
774
- </div>
775
- )}
878
+ {/* ApplyFirstPreview is now displayed inline in the chat via InlineDiffPreview */}
776
879
  </div>
777
880
  )}
778
881
  </div>
@@ -65,8 +65,8 @@ export function TextPanel({
65
65
  return (
66
66
  <div className="flex flex-col items-center justify-center h-full text-center p-6 text-gray-500">
67
67
  <Type className="h-12 w-12 mb-4 opacity-20" />
68
- <p className="text-sm font-medium mb-2">Text Inspector Disabled</p>
69
- <p className="text-xs max-w-[200px]">
68
+ <p id="text-panel-p-text-inspector-disab" className="text-sm font-medium mb-2">Text Inspector Disabled</p>
69
+ <p id="text-panel-p-enable-the-inspector" className="text-xs max-w-[200px]">
70
70
  Enable the inspector using the mouse icon in the top bar to select and edit text.
71
71
  </p>
72
72
  </div>
@@ -82,12 +82,12 @@ export function TextPanel({
82
82
  <div className="flex items-center justify-between mb-2">
83
83
  <div className="flex items-center gap-2">
84
84
  <div className="w-2 h-2 bg-amber-500 rounded-full animate-pulse" />
85
- <span className="text-xs font-medium text-amber-800">
85
+ <span id="text-panel-span-modifiedcount-text-e" className="text-xs font-medium text-amber-800">
86
86
  {modifiedCount} text element{modifiedCount !== 1 ? 's' : ''} modified
87
87
  </span>
88
88
  </div>
89
89
  </div>
90
- <p className="text-[10px] text-amber-600 mb-3">
90
+ <p id="text-panel-p-changes-are-previewe" className="text-[10px] text-amber-600 mb-3">
91
91
  Changes are previewed on the page. Save to keep them or revert to discard.
92
92
  </p>
93
93
  <div className="flex gap-2">
@@ -131,8 +131,8 @@ export function TextPanel({
131
131
  <Type className="h-3 w-3 text-white" />
132
132
  </div>
133
133
  </div>
134
- <p className="text-sm font-medium mb-2">Select Text to Edit</p>
135
- <p className="text-xs max-w-[200px]">
134
+ <p id="text-panel-p-select-text-to-edit" className="text-sm font-medium mb-2">Select Text to Edit</p>
135
+ <p id="text-panel-p-click-on-any-text-el" className="text-xs max-w-[200px]">
136
136
  Click on any text element on the page to adjust its typography and content.
137
137
  </p>
138
138
  </div>
@@ -153,16 +153,16 @@ export function TextPanel({
153
153
  <div className="flex items-center justify-between pb-2 border-b border-gray-100">
154
154
  <div className="flex flex-col">
155
155
  <div className="flex items-center gap-2">
156
- <span className="text-xs font-medium text-gray-900 truncate max-w-[180px]">
156
+ <span id="text-panel-span-selectedelementnames" className="text-xs font-medium text-gray-900 truncate max-w-[180px]">
157
157
  {selectedElement.name.split(':')[0]}
158
158
  </span>
159
159
  {currentHasChanges && (
160
- <span className="px-1.5 py-0.5 text-[9px] font-medium bg-amber-100 text-amber-700 rounded">
160
+ <span id="text-panel-span-modified" className="px-1.5 py-0.5 text-[9px] font-medium bg-amber-100 text-amber-700 rounded">
161
161
  Modified
162
162
  </span>
163
163
  )}
164
164
  </div>
165
- <span className="text-[10px] text-gray-400 font-mono">
165
+ <span id="text-panel-span-selectedtextid" className="text-[10px] text-gray-400 font-mono">
166
166
  {selectedTextId}
167
167
  </span>
168
168
  </div>
@@ -320,7 +320,7 @@ export function TextPanel({
320
320
  className="flex-1 h-7 px-2 text-xs border border-gray-200 rounded focus:border-[#00A3E1] focus:outline-none font-mono"
321
321
  />
322
322
  </div>
323
- <p className="text-[10px] text-gray-400">
323
+ <p id="text-panel-p-switch-theme-to-set-" className="text-[10px] text-gray-400">
324
324
  Switch theme to set color for {currentTheme === "dark" ? "light" : "dark"} mode
325
325
  </p>
326
326
  </div>
@@ -42,6 +42,8 @@ export interface DetectedElement {
42
42
  elementId?: string;
43
43
  /** IDs of child elements for more precise targeting */
44
44
  childIds?: string[];
45
+ /** The src attribute for image elements (for tracing to source code) */
46
+ imageSrc?: string;
45
47
  }
46
48
 
47
49
  // Logo asset from the API
@@ -93,6 +95,43 @@ export interface OriginalLogoState {
93
95
  srcset?: string;
94
96
  }
95
97
 
98
+ // ---- Image Editing Types ----
99
+
100
+ // Image override configuration (for general images, not just logos)
101
+ export interface ImageOverride {
102
+ src?: string; // New image source URL
103
+ width?: number; // Override width in pixels
104
+ height?: number; // Override height in pixels
105
+ scale?: number; // Scale factor (1 = 100%)
106
+ objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
107
+ reset?: boolean; // If true, indicates this config is resetting to defaults
108
+ }
109
+
110
+ // Original image state for reset functionality
111
+ export interface OriginalImageState {
112
+ src: string;
113
+ width: number;
114
+ height: number;
115
+ objectFit: string;
116
+ srcset?: string;
117
+ }
118
+
119
+ // Public folder image asset
120
+ export interface PublicImageAsset {
121
+ id: string;
122
+ name: string;
123
+ path: string; // Relative path from /public (e.g., "/banners/hero.jpg")
124
+ folder: string; // Parent folder name for grouping
125
+ extension: string;
126
+ dimensions?: {
127
+ width: number;
128
+ height: number;
129
+ };
130
+ }
131
+
132
+ // Image save status
133
+ export type ImageSaveStatus = "idle" | "saving" | "success" | "error";
134
+
96
135
  // Original text state for reset
97
136
  export interface OriginalTextState {
98
137
  textContent: string | null;
@@ -299,6 +338,8 @@ export interface VisionFocusedElement {
299
338
  childIds?: string[];
300
339
  /** Parent section context for section-level targeting */
301
340
  parentSection?: ParentSectionInfo;
341
+ /** The src attribute for image elements (for tracing to source code) */
342
+ imageSrc?: string;
302
343
  }
303
344
 
304
345
  export interface VisionEditRequest {
@@ -344,6 +385,7 @@ export interface ApplyFirstSession {
344
385
  status: 'preview' | 'applied' | 'accepted' | 'reverted';
345
386
  backupPaths: string[];
346
387
  isPreview?: boolean; // True if changes are not yet written to disk
388
+ isTextOnlyChange?: boolean; // True if only text content was changed (show in Details tab instead of AI Chat)
347
389
  }
348
390
 
349
391
  export type ApplyFirstStatus =