sonance-brand-mcp 1.2.5 → 1.3.2

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 (189) hide show
  1. package/dist/assets/api/sonance-analyze/route.ts +1116 -0
  2. package/dist/assets/api/sonance-assets/route.ts +113 -0
  3. package/dist/assets/api/sonance-components/route.ts +41 -0
  4. package/dist/assets/api/sonance-inject-id/route.ts +363 -0
  5. package/dist/assets/api/sonance-save-logo/route.ts +426 -0
  6. package/dist/assets/api/sonance-theme/route.ts +106 -0
  7. package/dist/assets/brand-system.ts +1265 -0
  8. package/dist/assets/components/accordion.stories.tsx +26 -26
  9. package/dist/assets/components/accordion.tsx +3 -3
  10. package/dist/assets/components/alert-dialog.stories.tsx +142 -0
  11. package/dist/assets/components/alert-dialog.tsx +143 -0
  12. package/dist/assets/components/alert.stories.tsx +3 -3
  13. package/dist/assets/components/alert.tsx +4 -3
  14. package/dist/assets/components/aspect-ratio.stories.tsx +70 -0
  15. package/dist/assets/components/aspect-ratio.tsx +8 -0
  16. package/dist/assets/components/autocomplete.stories.tsx +9 -9
  17. package/dist/assets/components/autocomplete.tsx +3 -3
  18. package/dist/assets/components/avatar.stories.tsx +5 -5
  19. package/dist/assets/components/avatar.tsx +67 -23
  20. package/dist/assets/components/badge.stories.tsx +10 -10
  21. package/dist/assets/components/badge.tsx +3 -3
  22. package/dist/assets/components/breadcrumbs.stories.tsx +7 -7
  23. package/dist/assets/components/breadcrumbs.tsx +13 -8
  24. package/dist/assets/components/button.stories.tsx +74 -74
  25. package/dist/assets/components/button.tsx +2 -0
  26. package/dist/assets/components/calendar.stories.tsx +11 -11
  27. package/dist/assets/components/calendar.tsx +4 -4
  28. package/dist/assets/components/card.stories.tsx +22 -22
  29. package/dist/assets/components/card.tsx +7 -3
  30. package/dist/assets/components/carousel.stories.tsx +158 -0
  31. package/dist/assets/components/carousel.tsx +264 -0
  32. package/dist/assets/components/chart.stories.tsx +376 -0
  33. package/dist/assets/components/chart.tsx +384 -0
  34. package/dist/assets/components/checkbox-group.stories.tsx +6 -6
  35. package/dist/assets/components/checkbox-group.tsx +3 -3
  36. package/dist/assets/components/checkbox.stories.tsx +23 -20
  37. package/dist/assets/components/checkbox.tsx +13 -6
  38. package/dist/assets/components/code.stories.tsx +24 -24
  39. package/dist/assets/components/code.tsx +22 -27
  40. package/dist/assets/components/collapsible.stories.tsx +128 -0
  41. package/dist/assets/components/collapsible.tsx +10 -0
  42. package/dist/assets/components/command.stories.tsx +183 -0
  43. package/dist/assets/components/command.tsx +171 -0
  44. package/dist/assets/components/context-menu.stories.tsx +159 -0
  45. package/dist/assets/components/context-menu.tsx +214 -0
  46. package/dist/assets/components/date-input.stories.tsx +9 -9
  47. package/dist/assets/components/date-input.tsx +2 -2
  48. package/dist/assets/components/date-picker.stories.tsx +9 -9
  49. package/dist/assets/components/date-picker.tsx +3 -3
  50. package/dist/assets/components/date-range-picker.stories.tsx +12 -12
  51. package/dist/assets/components/date-range-picker.tsx +3 -3
  52. package/dist/assets/components/dialog.stories.tsx +40 -40
  53. package/dist/assets/components/dialog.tsx +8 -12
  54. package/dist/assets/components/divider.stories.tsx +30 -30
  55. package/dist/assets/components/divider.tsx +34 -35
  56. package/dist/assets/components/drawer.stories.tsx +32 -31
  57. package/dist/assets/components/drawer.tsx +7 -6
  58. package/dist/assets/components/dropdown-menu.tsx +213 -0
  59. package/dist/assets/components/dropdown.stories.tsx +12 -12
  60. package/dist/assets/components/dropdown.tsx +5 -5
  61. package/dist/assets/components/form.stories.tsx +30 -29
  62. package/dist/assets/components/form.tsx +5 -5
  63. package/dist/assets/components/hover-card.stories.tsx +115 -0
  64. package/dist/assets/components/hover-card.tsx +35 -0
  65. package/dist/assets/components/image.stories.tsx +48 -25
  66. package/dist/assets/components/image.tsx +8 -5
  67. package/dist/assets/components/input-otp.stories.tsx +15 -15
  68. package/dist/assets/components/input-otp.tsx +5 -5
  69. package/dist/assets/components/input.stories.tsx +30 -25
  70. package/dist/assets/components/input.tsx +7 -4
  71. package/dist/assets/components/kbd.stories.tsx +34 -34
  72. package/dist/assets/components/kbd.tsx +9 -9
  73. package/dist/assets/components/link.stories.tsx +36 -36
  74. package/dist/assets/components/link.tsx +4 -0
  75. package/dist/assets/components/listbox.stories.tsx +5 -5
  76. package/dist/assets/components/listbox.tsx +4 -4
  77. package/dist/assets/components/menubar.stories.tsx +208 -0
  78. package/dist/assets/components/menubar.tsx +247 -0
  79. package/dist/assets/components/navbar.stories.tsx +24 -24
  80. package/dist/assets/components/navbar.tsx +8 -14
  81. package/dist/assets/components/navigation-menu.stories.tsx +239 -0
  82. package/dist/assets/components/navigation-menu.tsx +135 -0
  83. package/dist/assets/components/number-input.stories.tsx +11 -11
  84. package/dist/assets/components/number-input.tsx +3 -3
  85. package/dist/assets/components/pagination.stories.tsx +13 -13
  86. package/dist/assets/components/pagination.tsx +6 -6
  87. package/dist/assets/components/popover.stories.tsx +35 -35
  88. package/dist/assets/components/popover.tsx +98 -15
  89. package/dist/assets/components/progress.stories.tsx +5 -5
  90. package/dist/assets/components/progress.tsx +5 -5
  91. package/dist/assets/components/radio-group.stories.tsx +7 -7
  92. package/dist/assets/components/radio-group.tsx +3 -3
  93. package/dist/assets/components/range-calendar.stories.tsx +18 -18
  94. package/dist/assets/components/range-calendar.tsx +3 -3
  95. package/dist/assets/components/resizable.stories.tsx +197 -0
  96. package/dist/assets/components/resizable.tsx +47 -0
  97. package/dist/assets/components/scroll-area.stories.tsx +123 -0
  98. package/dist/assets/components/scroll-area.tsx +48 -0
  99. package/dist/assets/components/scroll-shadow.stories.tsx +17 -17
  100. package/dist/assets/components/scroll-shadow.tsx +31 -9
  101. package/dist/assets/components/select.stories.tsx +20 -19
  102. package/dist/assets/components/select.tsx +10 -6
  103. package/dist/assets/components/separator.tsx +32 -0
  104. package/dist/assets/components/sheet.tsx +137 -0
  105. package/dist/assets/components/sidebar.stories.tsx +351 -0
  106. package/dist/assets/components/sidebar.tsx +757 -0
  107. package/dist/assets/components/skeleton.stories.tsx +3 -3
  108. package/dist/assets/components/skeleton.tsx +2 -2
  109. package/dist/assets/components/slider.stories.tsx +6 -6
  110. package/dist/assets/components/slider.tsx +3 -3
  111. package/dist/assets/components/spacer.stories.tsx +11 -11
  112. package/dist/assets/components/spacer.tsx +2 -2
  113. package/dist/assets/components/spinner.stories.tsx +8 -8
  114. package/dist/assets/components/spinner.tsx +5 -5
  115. package/dist/assets/components/switch.stories.tsx +24 -20
  116. package/dist/assets/components/switch.tsx +14 -6
  117. package/dist/assets/components/table.stories.tsx +7 -7
  118. package/dist/assets/components/table.tsx +8 -8
  119. package/dist/assets/components/tabs.stories.tsx +37 -37
  120. package/dist/assets/components/tabs.tsx +3 -3
  121. package/dist/assets/components/textarea.stories.tsx +13 -12
  122. package/dist/assets/components/textarea.tsx +3 -3
  123. package/dist/assets/components/theme-toggle.stories.tsx +31 -30
  124. package/dist/assets/components/theme-toggle.tsx +2 -2
  125. package/dist/assets/components/time-input.stories.tsx +16 -16
  126. package/dist/assets/components/time-input.tsx +2 -2
  127. package/dist/assets/components/toast.stories.tsx +8 -5
  128. package/dist/assets/components/toast.tsx +6 -6
  129. package/dist/assets/components/toggle-group.stories.tsx +153 -0
  130. package/dist/assets/components/toggle-group.tsx +61 -0
  131. package/dist/assets/components/toggle.stories.tsx +77 -0
  132. package/dist/assets/components/toggle.tsx +46 -0
  133. package/dist/assets/components/tooltip.stories.tsx +49 -27
  134. package/dist/assets/components/tooltip.tsx +23 -90
  135. package/dist/assets/components/user.stories.tsx +23 -23
  136. package/dist/assets/components/user.tsx +7 -4
  137. package/dist/assets/dev-tools/SonanceDevTools.tsx +4201 -0
  138. package/dist/assets/dev-tools/index.ts +10 -0
  139. package/dist/assets/globals.css +39 -0
  140. package/dist/assets/logos/40th-anniversary/Sonance_40_Logo_CMYK_BEAM_BLUE_40_AND_BEAM_DARK.png +0 -0
  141. package/dist/assets/logos/Sonance logo dark mode.png +0 -0
  142. package/dist/assets/logos/Sonance logo light mode.png +0 -0
  143. package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_2C_Light_RGB_05162025.png +0 -0
  144. package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_3C_Dark_RGB_05162025.png +0 -0
  145. package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_White_RGB_05162025.png +0 -0
  146. package/dist/assets/logos/iport/IPORT_Sonance_LockUp_2C_Dark_RGB.png +0 -0
  147. package/dist/assets/logos/iport/IPORT_Sonance_LockUp_2C_Light_RGB.png +0 -0
  148. package/dist/assets/logos/james/James_Logo_Black_CMYK.png +0 -0
  149. package/dist/assets/logos/james/James_Logo_Black_RGB.png +0 -0
  150. package/dist/assets/logos/james/James_Logo_LtGray_CMYK.png +0 -0
  151. package/dist/assets/logos/james/James_Logo_LtGray_RGB.png +0 -0
  152. package/dist/assets/logos/james/James_Logo_Polished_RGB.png +0 -0
  153. package/dist/assets/logos/james/James_Logo_Reverse_CMYK.png +0 -0
  154. package/dist/assets/logos/james/James_Logo_Reverse_RGB.png +0 -0
  155. package/dist/assets/logos/james/James_Logo_White_CMYK.png +0 -0
  156. package/dist/assets/logos/life-is-better/Sonance_LifeisBetter_Dark_RGB.png +0 -0
  157. package/dist/assets/logos/life-is-better/Sonance_LifeisBetter_Light_RGB.png +0 -0
  158. package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Dark_RGB.png +0 -0
  159. package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Light_RGB.png +0 -0
  160. package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Reverse_RGB.png +0 -0
  161. package/dist/assets/logos/my-sonance/My.Sonance_Logo_Black_RGB.png +0 -0
  162. package/dist/assets/logos/my-sonance/My.Sonance_Logo_Reverse_RGB.png +0 -0
  163. package/dist/assets/logos/sonance/Sonance_Logo_2C_Dark_RGB.png +0 -0
  164. package/dist/assets/logos/sonance/Sonance_Logo_2C_Light_RGB.png +0 -0
  165. package/dist/assets/logos/sonance/Sonance_Logo_2C_Reverse_RGB.png +0 -0
  166. package/dist/assets/logos/sonance/Sonance_Logo_Black_RGB.png +0 -0
  167. package/dist/assets/logos/sonance/Sonance_Logo_Grayscale_RGB.png +0 -0
  168. package/dist/assets/logos/sonance/Sonance_Logo_Reverse_RGB.png +0 -0
  169. package/dist/assets/logos/sonance-academy/SonanceAcademy_Logo_Dark_CMYK.png +0 -0
  170. package/dist/assets/logos/sonance-academy/SonanceAcademy_Logo_Light_CMYK.png +0 -0
  171. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Dark_RGB.png +0 -0
  172. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Light_RGB.png +0 -0
  173. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Reverse_RGB.png +0 -0
  174. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Black_RGB.png +0 -0
  175. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Grayscale_RGB.png +0 -0
  176. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Reverse_RGB.png +0 -0
  177. package/dist/assets/logos/sonance-james/Sonance_James_Lockup_Dark.png +0 -0
  178. package/dist/assets/logos/sonance-james/Sonance_James_Lockup_Light.png +0 -0
  179. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_LockupStacked_Dark.png +0 -0
  180. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_LockupStacked_Light.png +0 -0
  181. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Dark.png +0 -0
  182. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Light.png +0 -0
  183. package/dist/assets/logos/trufig/TrufigLogo_Black.png +0 -0
  184. package/dist/assets/logos/trufig/TrufigLogo_Light.png +0 -0
  185. package/dist/assets/logos/trufig/TrufigWatermark_Black.png +0 -0
  186. package/dist/assets/logos/trufig/TrufigWatermark_Light.png +0 -0
  187. package/dist/assets/styles/brand-overrides.css +37 -0
  188. package/dist/index.js +2055 -15
  189. package/package.json +1 -1
@@ -0,0 +1,4201 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react";
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 } from "lucide-react";
6
+ import { useTheme } from "next-themes";
7
+ import { cn } from "@/lib/utils";
8
+ import {
9
+ ThemeConfig,
10
+ BrandId,
11
+ defaultThemeConfig,
12
+ brandPresets,
13
+ colorPresets,
14
+ radiusValues,
15
+ applyThemeToDOM,
16
+ resetThemeFromDOM,
17
+ generateThemeCSS,
18
+ componentSnippets,
19
+ isLightColor,
20
+ } from "@/lib/brand-system";
21
+ import { useBrand } from "@/lib/brand-context";
22
+
23
+ // ============================================
24
+ // SONANCE DEVTOOLS
25
+ // A floating development overlay for brand theming
26
+ // ============================================
27
+
28
+ type TabId = "analysis" | "brand" | "components" | "logos" | "text";
29
+
30
+ interface TabDefinition {
31
+ id: TabId;
32
+ label: string;
33
+ icon: React.ComponentType<{ className?: string }>;
34
+ }
35
+
36
+ const tabs: TabDefinition[] = [
37
+ { id: "analysis", label: "Analysis", icon: Scan },
38
+ { id: "brand", label: "Brand", icon: Palette },
39
+ { id: "components", label: "Components", icon: Box },
40
+ { id: "logos", label: "Logos", icon: ImageIcon },
41
+ { id: "text", label: "Text", icon: Type },
42
+ ];
43
+
44
+ type SaveStatus = "idle" | "saving" | "success" | "error";
45
+
46
+ // Detected element types for Visual Inspector
47
+ type DetectedElementType = "component" | "logo" | "text";
48
+
49
+ interface DetectedElement {
50
+ name: string;
51
+ rect: DOMRect;
52
+ type: DetectedElementType;
53
+ /** Unique ID for logo elements (for selection/editing) */
54
+ logoId?: string;
55
+ /** Unique ID for text elements (for selection/editing) */
56
+ textId?: string;
57
+ /** The actual text content (for text elements) */
58
+ textContent?: string;
59
+ }
60
+
61
+ // Logo asset from the API
62
+ interface LogoAsset {
63
+ id: string;
64
+ name: string;
65
+ path: string;
66
+ brand: string;
67
+ extension: string;
68
+ }
69
+
70
+ // Logo override configuration
71
+ interface LogoOverride {
72
+ src?: string; // Single source (fallback, or used when no theme-specific)
73
+ srcLight?: string; // Logo for light mode (dark logo on light bg)
74
+ srcDark?: string; // Logo for dark mode (light logo on dark bg)
75
+ width?: number;
76
+ height?: number;
77
+ scale?: number;
78
+ reset?: boolean; // If true, indicates this config is resetting to defaults
79
+ }
80
+
81
+ // Save status for logo persistence
82
+ type LogoSaveStatus = "idle" | "saving" | "success" | "error";
83
+
84
+ // Original logo state for reset
85
+ interface OriginalLogoState {
86
+ src: string;
87
+ width: number;
88
+ height: number;
89
+ srcset?: string;
90
+ }
91
+
92
+ // Analysis result from the project analyzer
93
+ interface AnalysisImageElement {
94
+ id: string;
95
+ filePath: string;
96
+ lineNumber: number;
97
+ elementType: "Image" | "img";
98
+ srcValue: string;
99
+ srcType: "literal" | "variable" | "import";
100
+ hasId: boolean;
101
+ existingId?: string;
102
+ alt?: string;
103
+ suggestedId?: string;
104
+ context: {
105
+ parentComponent?: string;
106
+ semanticContainer?: string;
107
+ };
108
+ }
109
+
110
+ interface CategorySummary {
111
+ total: number;
112
+ withId: number;
113
+ missingId: number;
114
+ }
115
+
116
+ interface AnalysisResult {
117
+ timestamp: string;
118
+ scanDuration: number;
119
+ filesScanned: number;
120
+ elements: AnalysisImageElement[];
121
+ images: AnalysisImageElement[];
122
+ themeFiles: { filePath: string; type: string; hasBrandVariables: boolean }[];
123
+ summary: {
124
+ totalElements: number;
125
+ elementsWithId: number;
126
+ elementsMissingId: number;
127
+ byCategory: {
128
+ image: CategorySummary;
129
+ text: CategorySummary;
130
+ interactive: CategorySummary;
131
+ input: CategorySummary;
132
+ definition: CategorySummary;
133
+ };
134
+ // Legacy fields
135
+ totalImages: number;
136
+ imagesWithId: number;
137
+ imagesMissingId: number;
138
+ brandLogosDetected: number;
139
+ };
140
+ }
141
+
142
+ type AnalysisStatus = "idle" | "scanning" | "complete" | "error";
143
+
144
+ // ---- Main Component ----
145
+
146
+ export function SonanceDevTools() {
147
+ const [isOpen, setIsOpen] = useState(false);
148
+ const [activeTab, setActiveTab] = useState<TabId>("analysis");
149
+ const [config, setConfig] = useState<ThemeConfig>(defaultThemeConfig);
150
+ const [mounted, setMounted] = useState(false);
151
+ const [copiedId, setCopiedId] = useState<string | null>(null);
152
+ const [saveStatus, setSaveStatus] = useState<SaveStatus>("idle");
153
+ const [saveMessage, setSaveMessage] = useState<string>("");
154
+ const [installedComponents, setInstalledComponents] = useState<string[]>([]);
155
+
156
+ // Visual Inspector state
157
+ const [inspectorEnabled, setInspectorEnabled] = useState(false);
158
+ const [logoInspectorEnabled, setLogoInspectorEnabled] = useState(false);
159
+ const [textInspectorEnabled, setTextInspectorEnabled] = useState(false);
160
+ const [taggedElements, setTaggedElements] = useState<DetectedElement[]>([]);
161
+ const inspectorRef = useRef<number | null>(null);
162
+
163
+ // Logo Tools state
164
+ const [logoAssets, setLogoAssets] = useState<LogoAsset[]>([]);
165
+ const [logoAssetsByBrand, setLogoAssetsByBrand] = useState<Record<string, LogoAsset[]>>({});
166
+ const [selectedLogoId, setSelectedLogoId] = useState<string | null>(null);
167
+ const [globalLogoConfig, setGlobalLogoConfig] = useState<LogoOverride>({});
168
+ const [individualLogoConfigs, setIndividualLogoConfigs] = useState<Record<string, LogoOverride>>({});
169
+ const [originalLogoStates, setOriginalLogoStates] = useState<Record<string, OriginalLogoState>>({});
170
+ const [logoSaveStatus, setLogoSaveStatus] = useState<LogoSaveStatus>("idle");
171
+ const [logoSaveMessage, setLogoSaveMessage] = useState<string>("");
172
+ const [autoFixStatus, setAutoFixStatus] = useState<AutoFixStatus>("idle");
173
+ const [autoFixMessage, setAutoFixMessage] = useState<string>("");
174
+ const logoIdCounter = useRef(0);
175
+ const textIdCounter = useRef(0);
176
+
177
+ // Project Analysis state
178
+ const [showAnalysisModal, setShowAnalysisModal] = useState(false);
179
+ const [analysisStatus, setAnalysisStatus] = useState<AnalysisStatus>("idle");
180
+ const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
181
+ const [analysisError, setAnalysisError] = useState<string>("");
182
+ const [bulkTagStatus, setBulkTagStatus] = useState<"idle" | "tagging" | "complete">("idle");
183
+ const [bulkTagMessage, setBulkTagMessage] = useState<string>("");
184
+
185
+ // Track if user has explicitly changed colors (to avoid initial interference)
186
+ const [colorsExplicitlyChanged, setColorsExplicitlyChanged] = useState(false);
187
+
188
+ // Theme mode toggle (light/dark)
189
+ const { theme, setTheme, resolvedTheme } = useTheme();
190
+
191
+ // Global brand context for logo switching
192
+ const { setBrand } = useBrand();
193
+
194
+ // Toggle between light and dark mode
195
+ const toggleThemeMode = useCallback(() => {
196
+ const currentMode = resolvedTheme || theme || "light";
197
+ setTheme(currentMode === "dark" ? "light" : "dark");
198
+ }, [resolvedTheme, theme, setTheme]);
199
+
200
+ // Handle client-side mounting for portal
201
+ useEffect(() => {
202
+ setMounted(true);
203
+ }, []);
204
+
205
+ // Detect current theme colors from DOM on mount
206
+ // This prevents the DevTools from immediately overwriting colors when opened
207
+ useEffect(() => {
208
+ if (!mounted) return;
209
+
210
+ // Helper to convert rgb/rgba to hex
211
+ const rgbToHex = (rgb: string): string | null => {
212
+ const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
213
+ if (!match) return null;
214
+ const r = parseInt(match[1]);
215
+ const g = parseInt(match[2]);
216
+ const b = parseInt(match[3]);
217
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
218
+ };
219
+
220
+ // Helper to check if a color is essentially transparent/white
221
+ const isNeutralColor = (hex: string | null): boolean => {
222
+ if (!hex) return true;
223
+ const lower = hex.toLowerCase();
224
+ // White, near-white, or transparent
225
+ return lower === '#ffffff' || lower === '#fff' ||
226
+ lower === '#000000' || lower === '#000' ||
227
+ lower === '#f9fafb' || lower === '#f3f4f6' || lower === '#fafafa';
228
+ };
229
+
230
+ // Try to detect the current primary color from actual buttons
231
+ const detectPrimaryColor = (): string | null => {
232
+ // Look for definite primary buttons
233
+ const primaryBtn = document.querySelector('.btn-primary, [class*="bg-primary"]:not([class*="bg-primary-foreground"])') as HTMLElement;
234
+ if (primaryBtn) {
235
+ const computed = window.getComputedStyle(primaryBtn);
236
+ const bgHex = rgbToHex(computed.backgroundColor);
237
+ if (bgHex && !isNeutralColor(bgHex)) {
238
+ return bgHex;
239
+ }
240
+ }
241
+
242
+ // Fallback: check CSS variables
243
+ const rootStyles = getComputedStyle(document.documentElement);
244
+ const primaryVar = rootStyles.getPropertyValue('--primary').trim() ||
245
+ rootStyles.getPropertyValue('--sonance-charcoal').trim();
246
+ if (primaryVar && primaryVar.startsWith('#')) {
247
+ return primaryVar;
248
+ }
249
+
250
+ return null;
251
+ };
252
+
253
+ // Try to detect the current accent color from links/accents
254
+ const detectAccentColor = (): string | null => {
255
+ // Look for accent-colored links
256
+ const accentLink = document.querySelector('a[class*="text-sonance-blue"], a[class*="text-accent"], [class*="accent"]') as HTMLElement;
257
+ if (accentLink) {
258
+ const computed = window.getComputedStyle(accentLink);
259
+ const colorHex = rgbToHex(computed.color);
260
+ if (colorHex && !isNeutralColor(colorHex)) {
261
+ return colorHex;
262
+ }
263
+ }
264
+
265
+ // Fallback: check CSS variables
266
+ const rootStyles = getComputedStyle(document.documentElement);
267
+ const accentVar = rootStyles.getPropertyValue('--accent').trim() ||
268
+ rootStyles.getPropertyValue('--sonance-blue').trim();
269
+ if (accentVar && accentVar.startsWith('#')) {
270
+ return accentVar;
271
+ }
272
+
273
+ return null;
274
+ };
275
+
276
+ // Initialize config with detected colors (if found)
277
+ const detectedPrimary = detectPrimaryColor();
278
+ const detectedAccent = detectAccentColor();
279
+
280
+ if (detectedPrimary || detectedAccent) {
281
+ setConfig(prev => ({
282
+ ...prev,
283
+ ...(detectedPrimary && { baseColor: detectedPrimary }),
284
+ ...(detectedAccent && { accentColor: detectedAccent }),
285
+ }));
286
+ }
287
+ }, [mounted]);
288
+
289
+ // Read CSS variables on mount to initialize logo config state
290
+ useEffect(() => {
291
+ if (!mounted) return;
292
+
293
+ // Read the current CSS variable values from the document root
294
+ const rootStyles = getComputedStyle(document.documentElement);
295
+
296
+ // Get scale value for the default brand (sonance)
297
+ const scaleValue = rootStyles.getPropertyValue('--sonance-logo-scale').trim();
298
+ const widthValue = rootStyles.getPropertyValue('--sonance-logo-width').trim();
299
+ const heightValue = rootStyles.getPropertyValue('--sonance-logo-height').trim();
300
+
301
+ const initialConfig: LogoOverride = {};
302
+
303
+ if (scaleValue && scaleValue !== '1') {
304
+ initialConfig.scale = parseFloat(scaleValue);
305
+ }
306
+ if (widthValue && widthValue !== 'auto') {
307
+ initialConfig.width = parseInt(widthValue);
308
+ }
309
+ if (heightValue && heightValue !== 'auto') {
310
+ initialConfig.height = parseInt(heightValue);
311
+ }
312
+
313
+ // Only update if we found saved values
314
+ if (Object.keys(initialConfig).length > 0) {
315
+ setGlobalLogoConfig(initialConfig);
316
+ }
317
+ }, [mounted]);
318
+
319
+ // Fetch installed components from the project
320
+ useEffect(() => {
321
+ async function fetchInstalledComponents() {
322
+ try {
323
+ const response = await fetch("/api/sonance-components");
324
+ if (response.ok) {
325
+ const data = await response.json();
326
+ setInstalledComponents(data.components || []);
327
+ }
328
+ } catch (error) {
329
+ // Silently fail - just means we can't show installed status
330
+ console.warn("Could not fetch installed components:", error);
331
+ }
332
+ }
333
+
334
+ if (mounted) {
335
+ fetchInstalledComponents();
336
+ }
337
+ }, [mounted]);
338
+
339
+ // Fetch logo assets when logo inspector is enabled
340
+ useEffect(() => {
341
+ async function fetchLogoAssets() {
342
+ try {
343
+ const response = await fetch("/api/sonance-assets");
344
+ if (response.ok) {
345
+ const data = await response.json();
346
+ setLogoAssets(data.assets || []);
347
+ setLogoAssetsByBrand(data.byBrand || {});
348
+ }
349
+ } catch (error) {
350
+ console.warn("Could not fetch logo assets:", error);
351
+ }
352
+ }
353
+
354
+ if (mounted && logoInspectorEnabled && logoAssets.length === 0) {
355
+ fetchLogoAssets();
356
+ }
357
+ }, [mounted, logoInspectorEnabled, logoAssets.length]);
358
+
359
+ // Helper to extract logo filename from src (handles next/image optimized URLs)
360
+ const extractLogoName = useCallback((src: string): string | null => {
361
+ // Handle next/image optimized URLs like /_next/image?url=%2Flogos%2F...
362
+ let decodedSrc = src;
363
+ if (src.includes("/_next/image")) {
364
+ const urlParam = new URL(src, window.location.origin).searchParams.get("url");
365
+ if (urlParam) {
366
+ decodedSrc = decodeURIComponent(urlParam);
367
+ }
368
+ }
369
+
370
+ // Check if the path contains "logo" anywhere
371
+ if (!decodedSrc.toLowerCase().includes("logo")) {
372
+ return null;
373
+ }
374
+
375
+ // Extract filename from path
376
+ const parts = decodedSrc.split("/");
377
+ const filename = parts[parts.length - 1];
378
+ // Remove file extension
379
+ const nameWithoutExt = filename.replace(/\.(png|jpg|jpeg|svg|webp|gif)$/i, "");
380
+ return nameWithoutExt || null;
381
+ }, []);
382
+
383
+ // Helper to find complementary light/dark logo variant
384
+ // Returns { light: path, dark: path } or null if no match found
385
+ const findComplementaryLogo = useCallback((selectedPath: string): { light?: string; dark?: string } | null => {
386
+ if (!selectedPath || logoAssets.length === 0) return null;
387
+
388
+ // Patterns that indicate theme variants
389
+ const darkPatterns = [/_Dark/i, /_2C_Dark/i, /_3C_Dark/i, /Dark_RGB/i, /Dark_CMYK/i, /_Reverse/i, /_Light_RGB/i];
390
+ const lightPatterns = [/_Light/i, /_2C_Light/i, /_3C_Light/i, /Light_RGB/i, /Light_CMYK/i, /_Black/i, /_Dark_RGB/i];
391
+
392
+ // Check if selected is a dark variant (for light backgrounds)
393
+ const isDarkVariant = darkPatterns.some(pattern => pattern.test(selectedPath));
394
+ // Check if selected is a light variant (for dark backgrounds)
395
+ const isLightVariant = lightPatterns.some(pattern => pattern.test(selectedPath));
396
+
397
+ // Extract the base name without theme suffix
398
+ const getBaseName = (path: string): string => {
399
+ return path
400
+ .replace(/_2C_Dark_RGB/gi, "")
401
+ .replace(/_2C_Light_RGB/gi, "")
402
+ .replace(/_3C_Dark_RGB/gi, "")
403
+ .replace(/_3C_Light_RGB/gi, "")
404
+ .replace(/_Dark_RGB/gi, "")
405
+ .replace(/_Light_RGB/gi, "")
406
+ .replace(/_Dark_CMYK/gi, "")
407
+ .replace(/_Light_CMYK/gi, "")
408
+ .replace(/_Dark/gi, "")
409
+ .replace(/_Light/gi, "")
410
+ .replace(/_Reverse/gi, "")
411
+ .replace(/_Black/gi, "")
412
+ .replace(/\.(png|jpg|jpeg|svg|webp|gif)$/i, "");
413
+ };
414
+
415
+ const selectedBase = getBaseName(selectedPath);
416
+
417
+ // Find all logos with the same base name
418
+ const matchingLogos = logoAssets.filter(asset => {
419
+ const assetBase = getBaseName(asset.path);
420
+ return assetBase === selectedBase;
421
+ });
422
+
423
+ if (matchingLogos.length < 2) {
424
+ // No complementary variant found
425
+ return { light: selectedPath, dark: selectedPath };
426
+ }
427
+
428
+ // Find the light and dark variants
429
+ let lightLogo: string | undefined;
430
+ let darkLogo: string | undefined;
431
+
432
+ for (const logo of matchingLogos) {
433
+ const path = logo.path;
434
+ // Dark variant = logo with "Dark" in name (appears dark, for light backgrounds)
435
+ if (darkPatterns.some(pattern => pattern.test(path))) {
436
+ lightLogo = path; // Use on light mode (dark logo on light bg)
437
+ }
438
+ // Light variant = logo with "Light" in name (appears light, for dark backgrounds)
439
+ if (lightPatterns.some(pattern => pattern.test(path))) {
440
+ darkLogo = path; // Use on dark mode (light logo on dark bg)
441
+ }
442
+ }
443
+
444
+ // Fallback if patterns aren't matching perfectly
445
+ if (!lightLogo) lightLogo = selectedPath;
446
+ if (!darkLogo) darkLogo = selectedPath;
447
+
448
+ return { light: lightLogo, dark: darkLogo };
449
+ }, [logoAssets]);
450
+
451
+ // Visual Inspector: Scan for tagged elements, logos, and text, update positions
452
+ useEffect(() => {
453
+ const anyInspectorEnabled = inspectorEnabled || logoInspectorEnabled || textInspectorEnabled;
454
+
455
+ if (!anyInspectorEnabled || !mounted) {
456
+ setTaggedElements([]);
457
+ if (inspectorRef.current) {
458
+ cancelAnimationFrame(inspectorRef.current);
459
+ inspectorRef.current = null;
460
+ }
461
+ return;
462
+ }
463
+
464
+ const scanElements = () => {
465
+ const newTagged: DetectedElement[] = [];
466
+ const newOriginalStates: Record<string, OriginalLogoState> = { ...originalLogoStates };
467
+
468
+ // Scan for tagged components
469
+ if (inspectorEnabled) {
470
+ const components = document.querySelectorAll("[data-sonance-name]");
471
+ components.forEach((el) => {
472
+ const name = el.getAttribute("data-sonance-name");
473
+ if (name) {
474
+ const rect = el.getBoundingClientRect();
475
+ // Only include visible elements
476
+ if (rect.width > 0 && rect.height > 0) {
477
+ newTagged.push({ name, rect, type: "component" });
478
+ }
479
+ }
480
+ });
481
+ }
482
+
483
+ // Scan for logo images
484
+ if (logoInspectorEnabled) {
485
+ const images = document.querySelectorAll("img");
486
+ images.forEach((img) => {
487
+ const src = img.src || img.getAttribute("src") || "";
488
+ const alt = img.alt || "";
489
+
490
+ // Check if src or alt contains "logo"
491
+ const logoName = extractLogoName(src);
492
+ const altContainsLogo = alt.toLowerCase().includes("logo");
493
+
494
+ if (logoName || altContainsLogo) {
495
+ const rect = img.getBoundingClientRect();
496
+ // Only include visible elements
497
+ if (rect.width > 0 && rect.height > 0) {
498
+ // Assign or retrieve a unique ID for this logo element
499
+ let logoId = img.getAttribute("data-sonance-logo-id");
500
+ if (!logoId) {
501
+ logoId = `logo-${logoIdCounter.current++}`;
502
+ img.setAttribute("data-sonance-logo-id", logoId);
503
+ }
504
+
505
+ // Store original state if not already stored
506
+ if (!newOriginalStates[logoId]) {
507
+ const originalSrcset = img.getAttribute("data-original-srcset") || img.srcset || "";
508
+ newOriginalStates[logoId] = {
509
+ src: img.getAttribute("data-original-src") || src,
510
+ width: img.naturalWidth || img.width,
511
+ height: img.naturalHeight || img.height,
512
+ srcset: originalSrcset,
513
+ };
514
+ // Also store original src and srcset as data attributes for reset
515
+ if (!img.getAttribute("data-original-src")) {
516
+ img.setAttribute("data-original-src", src);
517
+ }
518
+ if (!img.getAttribute("data-original-srcset") && img.srcset) {
519
+ img.setAttribute("data-original-srcset", img.srcset);
520
+ }
521
+ }
522
+
523
+ const displayName = logoName || alt || "Logo";
524
+ newTagged.push({ name: displayName, rect, type: "logo", logoId });
525
+ }
526
+ }
527
+ });
528
+ }
529
+
530
+ // Scan for text elements
531
+ if (textInspectorEnabled) {
532
+ const textSelectors = "h1, h2, h3, h4, h5, h6, p, span, a";
533
+ const textElements = document.querySelectorAll(textSelectors);
534
+ textElements.forEach((el) => {
535
+ // Skip elements that are part of the DevTools UI
536
+ if (el.closest("[data-sonance-devtools]")) return;
537
+
538
+ const rect = el.getBoundingClientRect();
539
+ // Only include visible elements with content
540
+ const textContent = el.textContent?.trim() || "";
541
+ if (rect.width > 0 && rect.height > 0 && textContent.length > 0) {
542
+ // Assign or retrieve a unique ID for this text element
543
+ let textId = el.getAttribute("data-sonance-text-id");
544
+ if (!textId) {
545
+ textId = `text-${textIdCounter.current++}`;
546
+ el.setAttribute("data-sonance-text-id", textId);
547
+ }
548
+
549
+ const tagName = el.tagName.toLowerCase();
550
+ const displayName = tagName === "a" ? "Link" : tagName.toUpperCase();
551
+ const truncatedContent = textContent.length > 30 ? textContent.substring(0, 30) + "..." : textContent;
552
+
553
+ newTagged.push({
554
+ name: `${displayName}: ${truncatedContent}`,
555
+ rect,
556
+ type: "text",
557
+ textId,
558
+ textContent
559
+ });
560
+ }
561
+ });
562
+ }
563
+
564
+ // Update original states if new logos were found
565
+ if (Object.keys(newOriginalStates).length !== Object.keys(originalLogoStates).length) {
566
+ setOriginalLogoStates(newOriginalStates);
567
+ }
568
+
569
+ setTaggedElements(newTagged);
570
+ inspectorRef.current = requestAnimationFrame(scanElements);
571
+ };
572
+
573
+ scanElements();
574
+
575
+ return () => {
576
+ if (inspectorRef.current) {
577
+ cancelAnimationFrame(inspectorRef.current);
578
+ inspectorRef.current = null;
579
+ }
580
+ };
581
+ }, [inspectorEnabled, logoInspectorEnabled, textInspectorEnabled, mounted, extractLogoName, originalLogoStates]);
582
+
583
+ // Toggle Visual Inspector
584
+ const toggleInspector = useCallback(() => {
585
+ setInspectorEnabled((prev) => !prev);
586
+ }, []);
587
+
588
+ // Toggle Logo Inspector
589
+ const toggleLogoInspector = useCallback(() => {
590
+ setLogoInspectorEnabled((prev) => !prev);
591
+ }, []);
592
+
593
+ // Toggle Text Inspector
594
+ const toggleTextInspector = useCallback(() => {
595
+ setTextInspectorEnabled((prev) => !prev);
596
+ }, []);
597
+
598
+ // Logo Tools handlers
599
+ const handleGlobalLogoConfigChange = useCallback((config: LogoOverride) => {
600
+ setGlobalLogoConfig(config);
601
+ }, []);
602
+
603
+ const handleIndividualLogoConfigChange = useCallback((logoId: string, config: LogoOverride) => {
604
+ setIndividualLogoConfigs((prev) => ({
605
+ ...prev,
606
+ [logoId]: config,
607
+ }));
608
+ }, []);
609
+
610
+ const handleSelectLogo = useCallback((logoId: string | null) => {
611
+ setSelectedLogoId(logoId);
612
+ }, []);
613
+
614
+ const handleResetAllLogos = useCallback(() => {
615
+ // Reset all configs - mark as reset pending save
616
+ setGlobalLogoConfig({ reset: true });
617
+ setIndividualLogoConfigs({});
618
+ setSelectedLogoId(null);
619
+
620
+ // Reset DOM elements to original state
621
+ const logos = document.querySelectorAll("[data-sonance-logo-id]");
622
+ logos.forEach((el) => {
623
+ const img = el as HTMLImageElement;
624
+ const originalSrc = img.getAttribute("data-original-src");
625
+ const originalSrcset = img.getAttribute("data-original-srcset");
626
+
627
+ // Restore srcset first (before src) so browser can use responsive images
628
+ if (originalSrcset) {
629
+ img.srcset = originalSrcset;
630
+ }
631
+ if (originalSrc) {
632
+ img.src = originalSrc;
633
+ }
634
+ img.style.removeProperty("width");
635
+ img.style.removeProperty("height");
636
+ img.style.removeProperty("transform");
637
+ img.style.removeProperty("transform-origin");
638
+ });
639
+ }, []);
640
+
641
+ const handleResetLogo = useCallback((logoId: string) => {
642
+ // Mark individual config as reset instead of deleting it
643
+ setIndividualLogoConfigs((prev) => {
644
+ const newConfigs = { ...prev };
645
+ newConfigs[logoId] = { reset: true };
646
+ return newConfigs;
647
+ });
648
+
649
+ // Reset DOM element
650
+ const img = document.querySelector(`[data-sonance-logo-id="${logoId}"]`) as HTMLImageElement | null;
651
+ if (img) {
652
+ const originalSrc = img.getAttribute("data-original-src");
653
+ const originalSrcset = img.getAttribute("data-original-srcset");
654
+
655
+ // Restore srcset first (before src) so browser can use responsive images
656
+ if (originalSrcset) {
657
+ img.srcset = originalSrcset;
658
+ }
659
+ if (originalSrc) {
660
+ img.src = originalSrc;
661
+ }
662
+ img.style.removeProperty("width");
663
+ img.style.removeProperty("height");
664
+ img.style.removeProperty("transform");
665
+ img.style.removeProperty("transform-origin");
666
+ }
667
+ }, []);
668
+
669
+ // Save logo changes to brand-system.ts and brand-overrides.css
670
+ const handleSaveLogoChanges = useCallback(async (configOverride?: LogoOverride, targetBrandId?: string, selector?: string, logoId?: string) => {
671
+ setLogoSaveStatus("saving");
672
+ setLogoSaveMessage("");
673
+
674
+ try {
675
+ // Get the config to save (override or global)
676
+ const configToSave = configOverride || globalLogoConfig;
677
+
678
+ // Check if there's anything meaningful to save or if it's a reset
679
+ const hasSrcChanges = configToSave.srcLight || configToSave.srcDark || configToSave.src;
680
+ const hasDimensionChanges = configToSave.width || configToSave.height || (configToSave.scale && configToSave.scale !== 1);
681
+ const isReset = configToSave.reset === true;
682
+
683
+ // Validate that we have something to save
684
+ if (!isReset && !hasSrcChanges && !hasDimensionChanges) {
685
+ setLogoSaveStatus("error");
686
+ setLogoSaveMessage("No logo changes to save");
687
+ setTimeout(() => {
688
+ setLogoSaveStatus("idle");
689
+ setLogoSaveMessage("");
690
+ }, 3000);
691
+ return;
692
+ }
693
+
694
+ // Prepare payload
695
+ // Source changes - only include if present
696
+ const srcLight = configToSave.srcLight || configToSave.src || undefined;
697
+ const srcDark = configToSave.srcDark || configToSave.src || undefined;
698
+
699
+ // Dimension changes
700
+ const width = configToSave.width;
701
+ const height = configToSave.height;
702
+ const scale = configToSave.scale !== 1 ? configToSave.scale : undefined;
703
+
704
+ const response = await fetch("/api/sonance-save-logo", {
705
+ method: "POST",
706
+ headers: { "Content-Type": "application/json" },
707
+ body: JSON.stringify({
708
+ srcLight,
709
+ srcDark,
710
+ width,
711
+ height,
712
+ scale,
713
+ brandId: targetBrandId || "sonance",
714
+ selector, // CSS selector for individual element targeting (e.g., "#my-logo")
715
+ reset: isReset,
716
+ }),
717
+ });
718
+
719
+ const data = await response.json();
720
+
721
+ if (!response.ok) {
722
+ throw new Error(data.error || "Failed to save logo changes");
723
+ }
724
+
725
+ setLogoSaveStatus("success");
726
+
727
+ // Clear the config after successful save so the button disappears
728
+ if (logoId) {
729
+ // Individual logo save - clear the config for this specific logo
730
+ setIndividualLogoConfigs((prev) => {
731
+ const newConfigs = { ...prev };
732
+ delete newConfigs[logoId];
733
+ return newConfigs;
734
+ });
735
+ } else if (!configOverride) {
736
+ // Global save - clear global config
737
+ setGlobalLogoConfig({});
738
+ }
739
+
740
+ const targetDescription = selector
741
+ ? `element ${selector}`
742
+ : (targetBrandId ? targetBrandId.charAt(0).toUpperCase() + targetBrandId.slice(1) + " brand" : "Sonance brand");
743
+ setLogoSaveMessage(isReset ? `Reset saved for ${targetDescription}!` : `Saved to ${targetDescription}!`);
744
+
745
+ setTimeout(() => {
746
+ setLogoSaveStatus("idle");
747
+ setLogoSaveMessage("");
748
+ }, 5000);
749
+ } catch (error) {
750
+ setLogoSaveStatus("error");
751
+ setLogoSaveMessage(error instanceof Error ? error.message : "Failed to save logo changes");
752
+
753
+ setTimeout(() => {
754
+ setLogoSaveStatus("idle");
755
+ setLogoSaveMessage("");
756
+ }, 5000);
757
+ }
758
+ }, [globalLogoConfig]);
759
+
760
+ // Auto-fix ID injection into source code
761
+ const handleAutoFixId = useCallback(async (logoSrc: string, suggestedId: string): Promise<{ success: boolean; error?: string }> => {
762
+ setAutoFixStatus("fixing");
763
+ setAutoFixMessage("");
764
+
765
+ try {
766
+ // Extract the clean logo path from the src (may include Next.js image params)
767
+ let cleanSrc = logoSrc;
768
+
769
+ // Handle Next.js optimized images: /_next/image?url=... or encoded URLs
770
+ if (logoSrc.includes("/_next/image")) {
771
+ const urlMatch = logoSrc.match(/url=([^&]+)/);
772
+ if (urlMatch) {
773
+ cleanSrc = decodeURIComponent(urlMatch[1]);
774
+ }
775
+ }
776
+
777
+ // Extract just the path part (remove domain if present)
778
+ try {
779
+ const url = new URL(cleanSrc, window.location.origin);
780
+ cleanSrc = url.pathname;
781
+ } catch {
782
+ // Already a path, use as-is
783
+ }
784
+
785
+ const response = await fetch("/api/sonance-inject-id", {
786
+ method: "POST",
787
+ headers: { "Content-Type": "application/json" },
788
+ body: JSON.stringify({
789
+ logoSrc: cleanSrc,
790
+ suggestedId,
791
+ }),
792
+ });
793
+
794
+ const data = await response.json();
795
+
796
+ if (!response.ok) {
797
+ const errorMessage = data.details || data.error || "Failed to inject ID";
798
+ setAutoFixStatus("error");
799
+ setAutoFixMessage(errorMessage);
800
+
801
+ setTimeout(() => {
802
+ setAutoFixStatus("idle");
803
+ setAutoFixMessage("");
804
+ }, 5000);
805
+
806
+ return { success: false, error: errorMessage };
807
+ }
808
+
809
+ setAutoFixStatus("success");
810
+ setAutoFixMessage(`ID injected in ${data.file}! Reload to apply.`);
811
+
812
+ setTimeout(() => {
813
+ setAutoFixStatus("idle");
814
+ setAutoFixMessage("");
815
+ }, 8000);
816
+
817
+ return { success: true };
818
+ } catch (error) {
819
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
820
+ setAutoFixStatus("error");
821
+ setAutoFixMessage(errorMessage);
822
+
823
+ setTimeout(() => {
824
+ setAutoFixStatus("idle");
825
+ setAutoFixMessage("");
826
+ }, 5000);
827
+
828
+ return { success: false, error: errorMessage };
829
+ }
830
+ }, []);
831
+
832
+ // Run project analysis
833
+ const handleRunAnalysis = useCallback(async () => {
834
+ setAnalysisStatus("scanning");
835
+ setAnalysisError("");
836
+ setAnalysisResult(null);
837
+
838
+ try {
839
+ const response = await fetch("/api/sonance-analyze");
840
+ const data = await response.json();
841
+
842
+ if (!response.ok) {
843
+ throw new Error(data.error || "Failed to analyze project");
844
+ }
845
+
846
+ setAnalysisResult(data);
847
+ setAnalysisStatus("complete");
848
+ } catch (error) {
849
+ setAnalysisError(error instanceof Error ? error.message : "Unknown error");
850
+ setAnalysisStatus("error");
851
+ }
852
+ }, []);
853
+
854
+ // Bulk tag elements missing IDs
855
+ const handleBulkTagAll = useCallback(async (elementIds?: string[], category?: string) => {
856
+ setBulkTagStatus("tagging");
857
+ setBulkTagMessage("");
858
+
859
+ try {
860
+ const response = await fetch("/api/sonance-analyze", {
861
+ method: "POST",
862
+ headers: { "Content-Type": "application/json" },
863
+ body: JSON.stringify({
864
+ action: "auto-tag-all",
865
+ elementIds,
866
+ category,
867
+ }),
868
+ });
869
+
870
+ const data = await response.json();
871
+
872
+ if (!response.ok) {
873
+ throw new Error(data.error || "Failed to auto-tag elements");
874
+ }
875
+
876
+ setBulkTagMessage(data.message);
877
+ setBulkTagStatus("complete");
878
+
879
+ // Re-run analysis to update the counts
880
+ setTimeout(() => {
881
+ handleRunAnalysis();
882
+ }, 1000);
883
+ } catch (error) {
884
+ setBulkTagMessage(error instanceof Error ? error.message : "Unknown error");
885
+ setBulkTagStatus("complete");
886
+ }
887
+ }, [handleRunAnalysis]);
888
+
889
+ // Apply logo overrides to DOM in real-time
890
+ useEffect(() => {
891
+ if (!logoInspectorEnabled) return;
892
+
893
+ const currentTheme = resolvedTheme || theme || "light";
894
+ const isDarkMode = currentTheme === "dark";
895
+
896
+ const logos = document.querySelectorAll("[data-sonance-logo-id]");
897
+ logos.forEach((el) => {
898
+ const img = el as HTMLImageElement;
899
+ const logoId = img.getAttribute("data-sonance-logo-id");
900
+ if (!logoId) return;
901
+
902
+ const originalState = originalLogoStates[logoId];
903
+ const individualConfig = individualLogoConfigs[logoId] || {};
904
+
905
+ // Priority: individual config > global config > original
906
+ // For theme-aware logos, pick the right variant based on current theme
907
+ const getThemeAwareSrc = (config: LogoOverride): string | undefined => {
908
+ if (isDarkMode && config.srcDark) return config.srcDark;
909
+ if (!isDarkMode && config.srcLight) return config.srcLight;
910
+ return config.src;
911
+ };
912
+
913
+ const effectiveSrc = getThemeAwareSrc(individualConfig) || getThemeAwareSrc(globalLogoConfig);
914
+ const effectiveScale = individualConfig.scale ?? globalLogoConfig.scale ?? 1;
915
+ const effectiveWidth = individualConfig.width;
916
+ const effectiveHeight = individualConfig.height;
917
+
918
+ // Apply source change
919
+ if (effectiveSrc) {
920
+ // Remove srcset to force browser to use our new src
921
+ if (img.srcset) {
922
+ img.srcset = "";
923
+ }
924
+ // Only update if different to prevent flashing
925
+ const currentDecodedSrc = img.src.includes("/_next/image")
926
+ ? decodeURIComponent(new URL(img.src).searchParams.get("url") || "")
927
+ : img.src;
928
+ if (!currentDecodedSrc.endsWith(effectiveSrc)) {
929
+ img.src = effectiveSrc;
930
+ }
931
+ } else if (originalState) {
932
+ // Restore original if no override
933
+ const originalSrc = img.getAttribute("data-original-src");
934
+ const originalSrcset = img.getAttribute("data-original-srcset");
935
+
936
+ // Restore srcset if we have it stored
937
+ if (originalSrcset && img.srcset !== originalSrcset) {
938
+ img.srcset = originalSrcset;
939
+ }
940
+
941
+ if (originalSrc && !img.src.includes(originalSrc.split("/").pop() || "")) {
942
+ img.src = originalSrc;
943
+ }
944
+ }
945
+
946
+ // Apply dimensions and scale
947
+ // Use transform: scale() for scale changes to match the persisted CSS behavior
948
+ if (effectiveScale !== 1) {
949
+ img.style.transform = `scale(${effectiveScale})`;
950
+ img.style.transformOrigin = "center";
951
+ } else {
952
+ img.style.removeProperty("transform");
953
+ img.style.removeProperty("transform-origin");
954
+ }
955
+
956
+ // Apply explicit width/height if set (these take precedence over scale for sizing)
957
+ if (effectiveWidth) {
958
+ img.style.width = `${effectiveWidth}px`;
959
+ } else {
960
+ img.style.removeProperty("width");
961
+ }
962
+
963
+ if (effectiveHeight) {
964
+ img.style.height = `${effectiveHeight}px`;
965
+ } else {
966
+ img.style.removeProperty("height");
967
+ }
968
+ });
969
+ }, [logoInspectorEnabled, globalLogoConfig, individualLogoConfigs, originalLogoStates, resolvedTheme, theme]);
970
+
971
+ // Save theme to project files
972
+ const handleSaveTheme = useCallback(async () => {
973
+ setSaveStatus("saving");
974
+ setSaveMessage("");
975
+
976
+ try {
977
+ const css = generateThemeCSS(config);
978
+ const response = await fetch("/api/sonance-theme", {
979
+ method: "POST",
980
+ headers: { "Content-Type": "application/json" },
981
+ body: JSON.stringify({ css, config }),
982
+ });
983
+
984
+ const data = await response.json();
985
+
986
+ if (!response.ok) {
987
+ throw new Error(data.error || "Failed to save theme");
988
+ }
989
+
990
+ setSaveStatus("success");
991
+ setSaveMessage("Theme saved! Add @import \"../theme/sonance-theme.css\"; to your globals.css");
992
+
993
+ // Reset status after 5 seconds
994
+ setTimeout(() => {
995
+ setSaveStatus("idle");
996
+ setSaveMessage("");
997
+ }, 5000);
998
+ } catch (error) {
999
+ setSaveStatus("error");
1000
+ setSaveMessage(error instanceof Error ? error.message : "Failed to save theme");
1001
+
1002
+ // Reset status after 5 seconds
1003
+ setTimeout(() => {
1004
+ setSaveStatus("idle");
1005
+ setSaveMessage("");
1006
+ }, 5000);
1007
+ }
1008
+ }, [config]);
1009
+
1010
+ // Apply theme changes to DOM (CSS variables - works if target app uses them)
1011
+ useEffect(() => {
1012
+ if (isOpen) {
1013
+ applyThemeToDOM(config);
1014
+ }
1015
+ }, [config, isOpen]);
1016
+
1017
+ // Runtime DOM injection for universal color preview
1018
+ // This directly manipulates element styles, working regardless of CSS architecture
1019
+ //
1020
+ // ROLE DEFINITIONS:
1021
+ // - PRIMARY: Solid backgrounds (buttons, filled badges, main action elements)
1022
+ // -> Controls: backgroundColor, plus contrast text color on top
1023
+ // - ACCENT: Text, links, icons, borders (interactive text, highlights)
1024
+ // -> Controls: color (text), borderColor ONLY - never backgroundColor
1025
+ //
1026
+ // IMPORTANT: Only apply color changes when the user has EXPLICITLY changed colors
1027
+ // in the DevTools. This prevents the DevTools from interfering with the
1028
+ // application's natural styling on initial open.
1029
+ //
1030
+ useEffect(() => {
1031
+ if (!isOpen) return;
1032
+ // Only apply color injection when user has explicitly changed colors
1033
+ if (!colorsExplicitlyChanged) return;
1034
+
1035
+ const originalStyles = new Map<HTMLElement, { bg?: string; color?: string; borderColor?: string }>();
1036
+ // Track elements processed by primary to prevent accent from overriding them
1037
+ const primaryProcessedElements = new Set<HTMLElement>();
1038
+
1039
+ // Helper to determine if a color is "light" (for contrast calculation)
1040
+ const isLightColor = (hex: string): boolean => {
1041
+ const c = hex.replace('#', '');
1042
+ if (c.length < 6) return true; // Fallback for short/invalid hex
1043
+ const r = parseInt(c.substring(0, 2), 16);
1044
+ const g = parseInt(c.substring(2, 4), 16);
1045
+ const b = parseInt(c.substring(4, 6), 16);
1046
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
1047
+ return luminance > 0.5;
1048
+ };
1049
+
1050
+ // Helper to convert computed rgb/rgba to hex for comparison
1051
+ const rgbToHex = (rgb: string): string | null => {
1052
+ const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
1053
+ if (!match) return null;
1054
+ const r = parseInt(match[1]);
1055
+ const g = parseInt(match[2]);
1056
+ const b = parseInt(match[3]);
1057
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
1058
+ };
1059
+
1060
+ // Helper to check if computed color matches target colors
1061
+ const colorMatches = (computed: string, targets: string[]): boolean => {
1062
+ const computedHex = rgbToHex(computed);
1063
+ if (!computedHex) return false;
1064
+
1065
+ return targets.some(t => {
1066
+ const target = t.toLowerCase();
1067
+ return computedHex.toLowerCase() === target;
1068
+ });
1069
+ };
1070
+
1071
+ // Helper to check if element is transparent/no-background
1072
+ const isTransparent = (bgColor: string): boolean => {
1073
+ return bgColor === 'rgba(0, 0, 0, 0)' ||
1074
+ bgColor === 'transparent' ||
1075
+ bgColor === '';
1076
+ };
1077
+
1078
+ // Common primary colors to detect (brand defaults + common button colors)
1079
+ // These are colors typically used for SOLID BACKGROUNDS
1080
+ const primaryColorTargets = [
1081
+ '#333f48', // Sonance charcoal
1082
+ '#0f161d', // IPORT dark
1083
+ '#28282b', // Blaze dark
1084
+ '#000000', // Black (common button)
1085
+ '#1f2937', // Tailwind gray-800
1086
+ '#111827', // Tailwind gray-900
1087
+ '#18181b', // Tailwind zinc-900
1088
+ '#171717', // Tailwind neutral-900
1089
+ ];
1090
+
1091
+ // Common accent colors to detect (for TEXT and BORDERS only)
1092
+ // These are colors typically used for links, icons, highlights
1093
+ const accentColorTargets = [
1094
+ '#00a3e1', // Sonance blue / Blaze blue
1095
+ '#00d3c8', // Sonance teal
1096
+ '#fc4c02', // IPORT orange
1097
+ '#c02b0a', // Blaze red
1098
+ '#3b82f6', // Tailwind blue-500
1099
+ '#2563eb', // Tailwind blue-600
1100
+ '#0ea5e9', // Tailwind sky-500
1101
+ '#06b6d4', // Tailwind cyan-500
1102
+ '#14b8a6', // Tailwind teal-500
1103
+ ];
1104
+
1105
+ // PRIMARY COLOR: Apply to solid backgrounds (buttons, filled elements)
1106
+ // This changes backgroundColor and sets appropriate contrast text
1107
+ //
1108
+ // IMPORTANT: Only target elements that are DEFINITELY primary buttons,
1109
+ // not cards or containers that happen to be <button> elements.
1110
+ // We use STRICT class matching to avoid false positives like bg-sonance-blue (accent).
1111
+ //
1112
+ const applyPrimaryColor = () => {
1113
+ // Specific primary class patterns (exact matches, not partial)
1114
+ const primaryClassPatterns = [
1115
+ 'btn-primary',
1116
+ 'bg-primary',
1117
+ 'bg-sonance-charcoal', // Specific Sonance primary
1118
+ 'bg-iport-dark', // Specific IPORT primary
1119
+ 'bg-blaze-dark-gray', // Specific Blaze primary
1120
+ ];
1121
+
1122
+ // Check if element has a definite primary class
1123
+ const hasPrimaryClass = (classList: string): boolean => {
1124
+ return primaryClassPatterns.some(pattern => {
1125
+ // Split classList and check for exact class match
1126
+ const classes = classList.split(/\s+/);
1127
+ return classes.includes(pattern);
1128
+ });
1129
+ };
1130
+
1131
+ // Selectors for elements that are DEFINITELY primary buttons
1132
+ // Avoid generic 'button' selector - too many false positives (cards, etc.)
1133
+ const primarySelectors = [
1134
+ '.btn-primary',
1135
+ '[class*="bg-primary"]',
1136
+ '.btn:not(.btn-outline):not(.btn-ghost):not(.btn-secondary):not(.btn-link)',
1137
+ ].join(', ');
1138
+
1139
+ const elements = document.querySelectorAll(primarySelectors);
1140
+
1141
+ elements.forEach((el) => {
1142
+ const htmlEl = el as HTMLElement;
1143
+ // Skip DevTools elements
1144
+ if (htmlEl.closest('[data-sonance-devtools]')) return;
1145
+ // Skip already processed
1146
+ if (primaryProcessedElements.has(htmlEl)) return;
1147
+ // Skip navigation elements - they have their own active state styling
1148
+ if (htmlEl.closest('nav') || htmlEl.closest('aside') || htmlEl.closest('[role="navigation"]')) return;
1149
+
1150
+ const computed = window.getComputedStyle(htmlEl);
1151
+ const bgColor = computed.backgroundColor;
1152
+
1153
+ // Only apply to elements with solid (non-transparent) backgrounds
1154
+ // that match known primary colors OR have explicit primary classes
1155
+ const hasSolidBg = !isTransparent(bgColor);
1156
+ const matchesPrimaryColor = colorMatches(bgColor, primaryColorTargets);
1157
+ const hasDefinitePrimaryClass = hasPrimaryClass(htmlEl.classList.toString());
1158
+
1159
+ // STRICT: Only apply if element has solid bg AND (color matches OR has definite primary class)
1160
+ if (hasSolidBg && (matchesPrimaryColor || hasDefinitePrimaryClass)) {
1161
+ // Store original if not already stored
1162
+ if (!originalStyles.has(htmlEl)) {
1163
+ originalStyles.set(htmlEl, {
1164
+ bg: htmlEl.style.backgroundColor,
1165
+ color: htmlEl.style.color,
1166
+ });
1167
+ }
1168
+
1169
+ // Apply new primary color as BACKGROUND
1170
+ htmlEl.style.setProperty('background-color', config.baseColor, 'important');
1171
+ // Set contrast text color (white on dark, black on light)
1172
+ const contrastColor = isLightColor(config.baseColor) ? '#000000' : '#FFFFFF';
1173
+ htmlEl.style.setProperty('color', contrastColor, 'important');
1174
+
1175
+ // Mark as processed to prevent accent from touching it
1176
+ primaryProcessedElements.add(htmlEl);
1177
+ }
1178
+ });
1179
+ };
1180
+
1181
+ // ACCENT COLOR: Apply to text, links, icons, borders
1182
+ // This changes color (text) and borderColor ONLY - never backgroundColor
1183
+ //
1184
+ // IMPORTANT: We must NOT interfere with navigation/menu items that have
1185
+ // their own active/inactive state styling managed by the application.
1186
+ //
1187
+ const applyAccentColor = () => {
1188
+ // Selectors for elements that typically use accent for TEXT
1189
+ // EXCLUDE navigation links - they have their own state management
1190
+ const accentSelectors = [
1191
+ // Only target links that are explicitly styled as accent, not nav links
1192
+ 'main a:not(.btn):not([class*="button"]):not(nav a)',
1193
+ '[class*="text-accent"]',
1194
+ '[class*="text-brand"]',
1195
+ // Be more specific - only match explicit accent text utilities
1196
+ '.text-sonance-blue',
1197
+ '.text-iport-orange',
1198
+ '.text-blaze-blue',
1199
+ ].join(', ');
1200
+
1201
+ const elements = document.querySelectorAll(accentSelectors);
1202
+
1203
+ elements.forEach((el) => {
1204
+ const htmlEl = el as HTMLElement;
1205
+ // Skip DevTools elements
1206
+ if (htmlEl.closest('[data-sonance-devtools]')) return;
1207
+ // Skip elements already processed by PRIMARY (conflict prevention)
1208
+ if (primaryProcessedElements.has(htmlEl)) return;
1209
+ // Skip buttons entirely - they belong to primary
1210
+ if (htmlEl.tagName === 'BUTTON' || htmlEl.getAttribute('role') === 'button') return;
1211
+ // Skip navigation elements - they have their own active state styling
1212
+ if (htmlEl.closest('nav') || htmlEl.closest('aside') || htmlEl.closest('[role="navigation"]')) return;
1213
+ // Skip elements with active/selected states
1214
+ if (htmlEl.classList.toString().includes('active') ||
1215
+ htmlEl.classList.toString().includes('selected') ||
1216
+ htmlEl.getAttribute('aria-current')) return;
1217
+
1218
+ const computed = window.getComputedStyle(htmlEl);
1219
+ const textColor = computed.color;
1220
+ const borderColor = computed.borderColor;
1221
+
1222
+ // Check if this element has accent-like text color
1223
+ const hasAccentTextColor = colorMatches(textColor, accentColorTargets);
1224
+ const hasAccentBorderColor = colorMatches(borderColor, accentColorTargets);
1225
+ // Be more strict about class matching - only explicit accent classes
1226
+ const hasAccentClass = htmlEl.classList.contains('text-sonance-blue') ||
1227
+ htmlEl.classList.contains('text-iport-orange') ||
1228
+ htmlEl.classList.contains('text-blaze-blue') ||
1229
+ htmlEl.classList.contains('text-accent');
1230
+
1231
+ // Apply accent color to TEXT and BORDERS only
1232
+ if (hasAccentTextColor || hasAccentClass) {
1233
+ // Store original if not already stored
1234
+ if (!originalStyles.has(htmlEl)) {
1235
+ originalStyles.set(htmlEl, {
1236
+ color: htmlEl.style.color,
1237
+ borderColor: htmlEl.style.borderColor,
1238
+ });
1239
+ }
1240
+
1241
+ // Apply new accent color to TEXT with !important for reliable override
1242
+ htmlEl.style.setProperty('color', config.accentColor, 'important');
1243
+ }
1244
+
1245
+ // Handle border color separately (for outline buttons, input focus rings, etc.)
1246
+ if (hasAccentBorderColor) {
1247
+ if (!originalStyles.has(htmlEl)) {
1248
+ originalStyles.set(htmlEl, {
1249
+ color: htmlEl.style.color,
1250
+ borderColor: htmlEl.style.borderColor,
1251
+ });
1252
+ }
1253
+ htmlEl.style.setProperty('border-color', config.accentColor, 'important');
1254
+ }
1255
+ });
1256
+ };
1257
+
1258
+ // Initial application - PRIMARY first, then ACCENT
1259
+ // Order matters for conflict prevention
1260
+ applyPrimaryColor();
1261
+ applyAccentColor();
1262
+
1263
+ // Set up MutationObserver for dynamically added elements
1264
+ const observer = new MutationObserver((mutations) => {
1265
+ let shouldReapply = false;
1266
+ mutations.forEach((mutation) => {
1267
+ if (mutation.addedNodes.length > 0) {
1268
+ shouldReapply = true;
1269
+ }
1270
+ });
1271
+ if (shouldReapply) {
1272
+ // Clear processed set for fresh re-scan
1273
+ primaryProcessedElements.clear();
1274
+ applyPrimaryColor();
1275
+ applyAccentColor();
1276
+ }
1277
+ });
1278
+
1279
+ observer.observe(document.body, {
1280
+ childList: true,
1281
+ subtree: true,
1282
+ });
1283
+
1284
+ // Cleanup: restore original styles
1285
+ return () => {
1286
+ observer.disconnect();
1287
+ primaryProcessedElements.clear();
1288
+ originalStyles.forEach((original, el) => {
1289
+ if (original.bg !== undefined) el.style.backgroundColor = original.bg;
1290
+ if (original.color !== undefined) el.style.color = original.color;
1291
+ if (original.borderColor !== undefined) el.style.borderColor = original.borderColor;
1292
+ });
1293
+ };
1294
+ }, [isOpen, config.baseColor, config.accentColor, colorsExplicitlyChanged]);
1295
+
1296
+ // Handle brand preset selection
1297
+ const handleBrandSelect = useCallback((brandId: BrandId) => {
1298
+ const preset = brandPresets.find((p) => p.id === brandId);
1299
+ if (preset) {
1300
+ setConfig(preset.config);
1301
+ // Update global brand context for logo switching
1302
+ setBrand(brandId);
1303
+ }
1304
+ }, [setBrand]);
1305
+
1306
+ // Reset theme
1307
+ const handleReset = useCallback(() => {
1308
+ setConfig(defaultThemeConfig);
1309
+ resetThemeFromDOM();
1310
+ // Reset color change tracking
1311
+ setColorsExplicitlyChanged(false);
1312
+ }, []);
1313
+
1314
+ // Copy to clipboard
1315
+ const handleCopy = useCallback(async (text: string, id: string) => {
1316
+ try {
1317
+ await navigator.clipboard.writeText(text);
1318
+ setCopiedId(id);
1319
+ setTimeout(() => setCopiedId(null), 2000);
1320
+ } catch (err) {
1321
+ console.error("Failed to copy:", err);
1322
+ }
1323
+ }, []);
1324
+
1325
+ // Update config property
1326
+ const updateConfig = useCallback((updates: Partial<ThemeConfig>) => {
1327
+ setConfig((prev) => ({ ...prev, ...updates }));
1328
+ // Track if user explicitly changed colors (to trigger color injection)
1329
+ if (updates.baseColor !== undefined || updates.accentColor !== undefined) {
1330
+ setColorsExplicitlyChanged(true);
1331
+ }
1332
+ }, []);
1333
+
1334
+ // Close and reset
1335
+ const handleClose = useCallback(() => {
1336
+ setIsOpen(false);
1337
+ resetThemeFromDOM();
1338
+ // Reset color change tracking for next session
1339
+ setColorsExplicitlyChanged(false);
1340
+ }, []);
1341
+
1342
+ if (!mounted) return null;
1343
+
1344
+ const trigger = (
1345
+ <button
1346
+ onClick={() => setIsOpen(true)}
1347
+ className={cn(
1348
+ "fixed bottom-6 right-6 z-[9998]",
1349
+ "flex h-14 w-14 items-center justify-center",
1350
+ "rounded-full bg-[#333F48] text-white shadow-lg",
1351
+ "hover:scale-105 hover:shadow-xl",
1352
+ "transition-all duration-200",
1353
+ "focus:outline-none focus:ring-2 focus:ring-[#00A3E1] focus:ring-offset-2",
1354
+ isOpen && "hidden"
1355
+ )}
1356
+ aria-label="Open Sonance DevTools"
1357
+ >
1358
+ <Palette className="h-6 w-6" />
1359
+ </button>
1360
+ );
1361
+
1362
+ const panel = isOpen && (
1363
+ <div
1364
+ data-sonance-devtools="true"
1365
+ className={cn(
1366
+ "fixed bottom-6 right-6 z-[9999]",
1367
+ "w-[360px] max-h-[80vh]",
1368
+ "bg-white rounded-lg shadow-2xl border border-gray-200",
1369
+ "flex flex-col overflow-hidden",
1370
+ "font-['Montserrat',sans-serif]"
1371
+ )}
1372
+ style={{ colorScheme: "light" }}
1373
+ >
1374
+ {/* Header - Simplified */}
1375
+ <div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-[#333F48]">
1376
+ <div className="flex items-center gap-2">
1377
+ <Palette className="h-5 w-5 text-[#00A3E1]" />
1378
+ <span id="span-sonance-devtools" className="text-sm font-semibold text-white">
1379
+ Sonance DevTools
1380
+ </span>
1381
+ </div>
1382
+ <div className="flex items-center gap-1">
1383
+ {/* Theme Mode Toggle */}
1384
+ <button
1385
+ onClick={toggleThemeMode}
1386
+ className="p-1.5 rounded hover:bg-white/10 transition-colors"
1387
+ aria-label={resolvedTheme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
1388
+ title={resolvedTheme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
1389
+ >
1390
+ {resolvedTheme === "dark" ? (
1391
+ <Sun className="h-4 w-4 text-yellow-400" />
1392
+ ) : (
1393
+ <Moon className="h-4 w-4 text-white/80" />
1394
+ )}
1395
+ </button>
1396
+ {/* Close Button */}
1397
+ <button
1398
+ onClick={handleClose}
1399
+ className="p-1.5 rounded hover:bg-white/10 transition-colors"
1400
+ aria-label="Close"
1401
+ >
1402
+ <X className="h-4 w-4 text-white" />
1403
+ </button>
1404
+ </div>
1405
+ </div>
1406
+
1407
+ {/* Icon Navigation Bar */}
1408
+ <div className="flex items-center justify-evenly border-b border-gray-200 bg-gray-50 px-2 py-1">
1409
+ {tabs.map((tab) => {
1410
+ const IconComponent = tab.icon;
1411
+ const isActive = activeTab === tab.id;
1412
+ return (
1413
+ <button
1414
+ key={tab.id}
1415
+ onClick={() => setActiveTab(tab.id)}
1416
+ className={cn(
1417
+ "p-2 rounded-md transition-all",
1418
+ isActive
1419
+ ? "bg-[#333F48] text-white"
1420
+ : "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
1421
+ )}
1422
+ aria-label={tab.label}
1423
+ title={tab.label}
1424
+ >
1425
+ <IconComponent className="h-4 w-4" />
1426
+ </button>
1427
+ );
1428
+ })}
1429
+ </div>
1430
+
1431
+ {/* Content */}
1432
+ <div className="flex-1 overflow-y-auto p-4 text-[#333F48]">
1433
+ {/* Analysis Tab */}
1434
+ {activeTab === "analysis" && (
1435
+ <AnalysisPanel
1436
+ analysisStatus={analysisStatus}
1437
+ analysisResult={analysisResult}
1438
+ analysisError={analysisError}
1439
+ bulkTagStatus={bulkTagStatus}
1440
+ bulkTagMessage={bulkTagMessage}
1441
+ onRunAnalysis={handleRunAnalysis}
1442
+ onBulkTag={handleBulkTagAll}
1443
+ />
1444
+ )}
1445
+
1446
+ {/* Brand Tab */}
1447
+ {activeTab === "brand" && (
1448
+ <BrandPanel
1449
+ config={config}
1450
+ onBrandSelect={handleBrandSelect}
1451
+ logoAssetsByBrand={logoAssetsByBrand}
1452
+ currentTheme={resolvedTheme || theme || "light"}
1453
+ />
1454
+ )}
1455
+
1456
+ {/* Components Tab */}
1457
+ {activeTab === "components" && (
1458
+ <ComponentsPanel
1459
+ copiedId={copiedId}
1460
+ onCopy={handleCopy}
1461
+ installedComponents={installedComponents}
1462
+ inspectorEnabled={inspectorEnabled}
1463
+ onToggleInspector={toggleInspector}
1464
+ config={config}
1465
+ updateConfig={updateConfig}
1466
+ onReset={handleReset}
1467
+ />
1468
+ )}
1469
+
1470
+ {/* Logos Tab */}
1471
+ {activeTab === "logos" && (
1472
+ <LogosPanel
1473
+ logoAssets={logoAssets}
1474
+ logoAssetsByBrand={logoAssetsByBrand}
1475
+ selectedLogoId={selectedLogoId}
1476
+ globalLogoConfig={globalLogoConfig}
1477
+ individualLogoConfigs={individualLogoConfigs}
1478
+ originalLogoStates={originalLogoStates}
1479
+ taggedElements={taggedElements}
1480
+ onGlobalConfigChange={handleGlobalLogoConfigChange}
1481
+ onIndividualConfigChange={handleIndividualLogoConfigChange}
1482
+ onSelectLogo={handleSelectLogo}
1483
+ onResetAll={handleResetAllLogos}
1484
+ onResetLogo={handleResetLogo}
1485
+ onSaveChanges={handleSaveLogoChanges}
1486
+ saveStatus={logoSaveStatus}
1487
+ saveMessage={logoSaveMessage}
1488
+ findComplementaryLogo={findComplementaryLogo}
1489
+ currentTheme={resolvedTheme || theme || "light"}
1490
+ onAutoFixId={handleAutoFixId}
1491
+ autoFixStatus={autoFixStatus}
1492
+ autoFixMessage={autoFixMessage}
1493
+ inspectorEnabled={logoInspectorEnabled}
1494
+ onToggleInspector={toggleLogoInspector}
1495
+ />
1496
+ )}
1497
+
1498
+ {/* Text Tab */}
1499
+ {activeTab === "text" && (
1500
+ <TextPanel
1501
+ inspectorEnabled={textInspectorEnabled}
1502
+ onToggleInspector={toggleTextInspector}
1503
+ taggedElements={taggedElements}
1504
+ />
1505
+ )}
1506
+ </div>
1507
+ </div>
1508
+ );
1509
+
1510
+ return createPortal(
1511
+ <>
1512
+ {trigger}
1513
+ {panel}
1514
+ {/* Visual Inspector Overlay */}
1515
+ {(inspectorEnabled || logoInspectorEnabled || textInspectorEnabled) && taggedElements.length > 0 && (
1516
+ <InspectorOverlay
1517
+ elements={taggedElements}
1518
+ selectedLogoId={selectedLogoId}
1519
+ onLogoClick={handleSelectLogo}
1520
+ interactive={logoInspectorEnabled || textInspectorEnabled}
1521
+ />
1522
+ )}
1523
+ </>,
1524
+ document.body
1525
+ );
1526
+ }
1527
+
1528
+ // ---- Visual Inspector Overlay ----
1529
+
1530
+ interface InspectorOverlayProps {
1531
+ elements: DetectedElement[];
1532
+ selectedLogoId?: string | null;
1533
+ onLogoClick?: (logoId: string) => void;
1534
+ interactive?: boolean;
1535
+ }
1536
+
1537
+ // Color config for different element types
1538
+ const inspectorColors: Record<DetectedElementType, { border: string; bg: string; selectedBorder: string; selectedBg: string }> = {
1539
+ component: { border: "#00A3E1", bg: "#00A3E1", selectedBorder: "#00A3E1", selectedBg: "#00A3E1" }, // Sonance blue
1540
+ logo: { border: "#FC4C02", bg: "#FC4C02", selectedBorder: "#C02B0A", selectedBg: "#C02B0A" }, // IPORT orange / Blaze red for selected
1541
+ text: { border: "#8B5CF6", bg: "#8B5CF6", selectedBorder: "#7C3AED", selectedBg: "#7C3AED" }, // Purple for text
1542
+ };
1543
+
1544
+ function InspectorOverlay({ elements, selectedLogoId, onLogoClick, interactive = false }: InspectorOverlayProps) {
1545
+ return (
1546
+ <div
1547
+ className="fixed inset-0 z-[9997] pointer-events-none"
1548
+ style={{ colorScheme: "light" }}
1549
+ >
1550
+ {elements.map((el, index) => {
1551
+ const isSelected = el.type === "logo" && el.logoId === selectedLogoId;
1552
+ const colors = inspectorColors[el.type];
1553
+ const borderColor = isSelected ? colors.selectedBorder : colors.border;
1554
+ const bgColor = isSelected ? colors.selectedBg : colors.bg;
1555
+ const isClickable = interactive && el.type === "logo" && el.logoId && onLogoClick;
1556
+
1557
+ return (
1558
+ <div
1559
+ key={`${el.type}-${el.logoId || el.name}-${index}`}
1560
+ className={cn(
1561
+ "absolute",
1562
+ isClickable && "cursor-pointer"
1563
+ )}
1564
+ style={{
1565
+ top: el.rect.top,
1566
+ left: el.rect.left,
1567
+ width: el.rect.width,
1568
+ height: el.rect.height,
1569
+ pointerEvents: isClickable ? "auto" : "none",
1570
+ }}
1571
+ onClick={(e) => {
1572
+ if (isClickable) {
1573
+ e.stopPropagation();
1574
+ onLogoClick(el.logoId!);
1575
+ }
1576
+ }}
1577
+ >
1578
+ {/* Border highlight */}
1579
+ <div
1580
+ className={cn(
1581
+ "absolute inset-0 rounded-sm transition-all",
1582
+ isSelected ? "border-3" : "border-2"
1583
+ )}
1584
+ style={{
1585
+ pointerEvents: "none",
1586
+ borderColor: borderColor,
1587
+ boxShadow: isSelected ? `0 0 0 2px ${borderColor}40` : undefined,
1588
+ }}
1589
+ />
1590
+ {/* Label */}
1591
+ <div
1592
+ className={cn(
1593
+ "absolute -top-6 left-0",
1594
+ "px-1.5 py-0.5 text-[10px] font-medium",
1595
+ "text-white rounded-t-sm",
1596
+ "whitespace-nowrap shadow-sm",
1597
+ "flex items-center gap-1",
1598
+ "transition-all"
1599
+ )}
1600
+ style={{ backgroundColor: bgColor }}
1601
+ >
1602
+ {el.type === "logo" && <ImageIcon className="h-3 w-3" />}
1603
+ {el.name}
1604
+ {isSelected && <span id="span-title" className="ml-1">✓</span>}
1605
+ </div>
1606
+ {/* Click hint for logos */}
1607
+ {isClickable && !isSelected && (
1608
+ <div className="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
1609
+ <span id="span-click-to-select" className="px-2 py-1 text-[10px] font-medium text-white bg-black/70 rounded">
1610
+ Click to select
1611
+ </span>
1612
+ </div>
1613
+ )}
1614
+ </div>
1615
+ );
1616
+ })}
1617
+ </div>
1618
+ );
1619
+ }
1620
+
1621
+ // ---- Project Analysis Modal ----
1622
+
1623
+ interface AnalysisModalProps {
1624
+ analysisStatus: AnalysisStatus;
1625
+ analysisResult: AnalysisResult | null;
1626
+ analysisError: string;
1627
+ bulkTagStatus: "idle" | "tagging" | "complete";
1628
+ bulkTagMessage: string;
1629
+ onClose: () => void;
1630
+ onRunAnalysis: () => void;
1631
+ onBulkTag: (elementIds?: string[], category?: string) => void;
1632
+ }
1633
+
1634
+ function AnalysisModal({
1635
+ analysisStatus,
1636
+ analysisResult,
1637
+ analysisError,
1638
+ bulkTagStatus,
1639
+ bulkTagMessage,
1640
+ onClose,
1641
+ onRunAnalysis,
1642
+ onBulkTag,
1643
+ }: AnalysisModalProps) {
1644
+ return (
1645
+ <div className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/50 font-['Montserrat',sans-serif]">
1646
+ <div
1647
+ className="w-[500px] max-h-[80vh] bg-white rounded-lg shadow-2xl overflow-hidden flex flex-col"
1648
+ style={{ colorScheme: "light" }}
1649
+ >
1650
+ {/* Header */}
1651
+ <div className="flex items-center justify-between px-5 py-4 border-b border-gray-200 bg-[#333F48]">
1652
+ <div className="flex items-center gap-2">
1653
+ <Scan className="h-5 w-5 text-[#00A3E1]" />
1654
+ <span id="analysis-modal-span-project-analysis" className="text-base font-semibold text-white">Project Analysis</span>
1655
+ </div>
1656
+ <button
1657
+ onClick={onClose}
1658
+ className="p-1.5 rounded hover:bg-white/10 transition-colors"
1659
+ aria-label="Close"
1660
+ >
1661
+ <X className="h-4 w-4 text-white" />
1662
+ </button>
1663
+ </div>
1664
+
1665
+ {/* Content */}
1666
+ <div className="flex-1 overflow-y-auto p-5 space-y-5 text-[#333F48]">
1667
+ {/* Initial State */}
1668
+ {analysisStatus === "idle" && !analysisResult && (
1669
+ <div className="text-center py-8 space-y-4">
1670
+ <div className="mx-auto w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center">
1671
+ <Scan className="h-8 w-8 text-gray-400" />
1672
+ </div>
1673
+ <div>
1674
+ <h3 id="analysis-modal-h3-analyze-your-project" className="text-lg font-medium">Analyze Your Project</h3>
1675
+ <p id="analysis-modal-p-scan-your-codebase-t" className="text-sm text-gray-500 mt-1">
1676
+ Scan your codebase to index all images, logos, and theme files.
1677
+ This enables reliable auto-tagging and one-click ID injection.
1678
+ </p>
1679
+ </div>
1680
+ <button
1681
+ onClick={onRunAnalysis}
1682
+ className="inline-flex items-center gap-2 px-5 py-2.5 bg-[#333F48] text-white text-sm font-medium rounded hover:bg-[#2a343c] transition-colors"
1683
+ >
1684
+ <Scan className="h-4 w-4" />
1685
+ Scan Project
1686
+ </button>
1687
+ </div>
1688
+ )}
1689
+
1690
+ {/* Scanning State */}
1691
+ {analysisStatus === "scanning" && (
1692
+ <div className="text-center py-8 space-y-4">
1693
+ <div className="mx-auto w-16 h-16 rounded-full bg-blue-50 flex items-center justify-center">
1694
+ <Loader2 className="h-8 w-8 text-[#00A3E1] animate-spin" />
1695
+ </div>
1696
+ <div>
1697
+ <h3 id="analysis-modal-h3-scanning" className="text-lg font-medium">Scanning...</h3>
1698
+ <p id="analysis-modal-p-analyzing-your-sourc" className="text-sm text-gray-500 mt-1">
1699
+ Analyzing your source files for images and design assets.
1700
+ </p>
1701
+ </div>
1702
+ </div>
1703
+ )}
1704
+
1705
+ {/* Error State */}
1706
+ {analysisStatus === "error" && (
1707
+ <div className="text-center py-8 space-y-4">
1708
+ <div className="mx-auto w-16 h-16 rounded-full bg-red-50 flex items-center justify-center">
1709
+ <AlertCircle className="h-8 w-8 text-red-500" />
1710
+ </div>
1711
+ <div>
1712
+ <h3 id="analysis-modal-h3-analysis-failed" className="text-lg font-medium text-red-600">Analysis Failed</h3>
1713
+ <p id="analysis-modal-p-analysiserror" className="text-sm text-gray-500 mt-1">{analysisError}</p>
1714
+ </div>
1715
+ <button
1716
+ onClick={onRunAnalysis}
1717
+ className="inline-flex items-center gap-2 px-5 py-2.5 bg-[#333F48] text-white text-sm font-medium rounded hover:bg-[#2a343c] transition-colors"
1718
+ >
1719
+ <RotateCcw className="h-4 w-4" />
1720
+ Try Again
1721
+ </button>
1722
+ </div>
1723
+ )}
1724
+
1725
+ {/* Results */}
1726
+ {analysisStatus === "complete" && analysisResult && (
1727
+ <>
1728
+ {/* Overview Stats */}
1729
+ <div className="grid grid-cols-3 gap-3">
1730
+ <div className="p-3 rounded-lg border border-gray-200 bg-gray-50 text-center">
1731
+ <div className="text-xs text-gray-500 uppercase tracking-wide mb-1">Files</div>
1732
+ <div className="text-xl font-semibold">{analysisResult.filesScanned}</div>
1733
+ </div>
1734
+ <div className="p-3 rounded-lg border border-green-200 bg-green-50 text-center">
1735
+ <div className="text-xs text-green-600 uppercase tracking-wide mb-1">With IDs</div>
1736
+ <div className="text-xl font-semibold text-green-700">{analysisResult.summary.elementsWithId}</div>
1737
+ </div>
1738
+ <div className="p-3 rounded-lg border border-amber-200 bg-amber-50 text-center">
1739
+ <div className="text-xs text-amber-600 uppercase tracking-wide mb-1">Missing IDs</div>
1740
+ <div className="text-xl font-semibold text-amber-700">{analysisResult.summary.elementsMissingId}</div>
1741
+ </div>
1742
+ </div>
1743
+
1744
+ {/* Category Breakdown */}
1745
+ <div className="space-y-2">
1746
+ <h4 id="analysis-modal-h4-elements-by-category" className="text-xs font-medium text-gray-500 uppercase tracking-wide">Elements by Category</h4>
1747
+ <div className="space-y-2">
1748
+ {/* Images */}
1749
+ {(() => {
1750
+ const cat = analysisResult.summary.byCategory?.image;
1751
+ const total = cat?.total || analysisResult.summary.totalImages;
1752
+ const withId = cat?.withId || analysisResult.summary.imagesWithId;
1753
+ const missingId = cat?.missingId || analysisResult.summary.imagesMissingId;
1754
+ const isComplete = withId === total && total > 0;
1755
+ return (
1756
+ <div className={cn(
1757
+ "flex items-center justify-between p-3 rounded border",
1758
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
1759
+ )}>
1760
+ <div className="flex items-center gap-2">
1761
+ <div className={cn("p-1.5 rounded", isComplete ? "bg-green-100" : "bg-blue-100")}>
1762
+ <ImageIcon className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-blue-600")} />
1763
+ </div>
1764
+ <span id="analysis-modal-span-images" className="text-sm font-medium">Images</span>
1765
+ </div>
1766
+ <div className="flex items-center gap-3 text-sm">
1767
+ <div className="flex items-center gap-1">
1768
+ <span id="analysis-modal-span-withid" className={cn("font-medium", isComplete ? "text-green-700" : "text-gray-700")}>
1769
+ {withId}
1770
+ </span>
1771
+ <span id="analysis-modal-span" className="text-gray-400">/</span>
1772
+ <span id="analysis-modal-span-total" className="text-gray-500">{total}</span>
1773
+ </div>
1774
+ {missingId > 0 && (
1775
+ <button
1776
+ onClick={() => onBulkTag(undefined, "image")}
1777
+ disabled={bulkTagStatus === "tagging"}
1778
+ className="px-2 py-1 text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 rounded disabled:opacity-50"
1779
+ >
1780
+ Tag {missingId}
1781
+ </button>
1782
+ )}
1783
+ {isComplete && <CheckCircle className="h-4 w-4 text-green-600" />}
1784
+ </div>
1785
+ </div>
1786
+ );
1787
+ })()}
1788
+
1789
+ {/* Text Elements */}
1790
+ {analysisResult.summary.byCategory?.text && (() => {
1791
+ const cat = analysisResult.summary.byCategory.text;
1792
+ const isComplete = cat.withId === cat.total && cat.total > 0;
1793
+ return (
1794
+ <div className={cn(
1795
+ "flex items-center justify-between p-3 rounded border",
1796
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
1797
+ )}>
1798
+ <div className="flex items-center gap-2">
1799
+ <div className={cn("p-1.5 rounded", isComplete ? "bg-green-100" : "bg-purple-100")}>
1800
+ <Type className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-purple-600")} />
1801
+ </div>
1802
+ <span id="analysis-modal-span-text" className="text-sm font-medium">Text</span>
1803
+ </div>
1804
+ <div className="flex items-center gap-3 text-sm">
1805
+ <div className="flex items-center gap-1">
1806
+ <span id="analysis-modal-span-catwithid" className={cn("font-medium", isComplete ? "text-green-700" : "text-gray-700")}>
1807
+ {cat.withId}
1808
+ </span>
1809
+ <span id="analysis-modal-span" className="text-gray-400">/</span>
1810
+ <span id="analysis-modal-span-cattotal" className="text-gray-500">{cat.total}</span>
1811
+ </div>
1812
+ {cat.missingId > 0 && (
1813
+ <button
1814
+ onClick={() => onBulkTag(undefined, "text")}
1815
+ disabled={bulkTagStatus === "tagging"}
1816
+ className="px-2 py-1 text-xs font-medium text-white bg-purple-500 hover:bg-purple-600 rounded disabled:opacity-50"
1817
+ >
1818
+ Tag {cat.missingId}
1819
+ </button>
1820
+ )}
1821
+ {isComplete && <CheckCircle className="h-4 w-4 text-green-600" />}
1822
+ </div>
1823
+ </div>
1824
+ );
1825
+ })()}
1826
+
1827
+ {/* Interactive (Buttons, Links) */}
1828
+ {analysisResult.summary.byCategory?.interactive && (() => {
1829
+ const cat = analysisResult.summary.byCategory.interactive;
1830
+ const isComplete = cat.withId === cat.total && cat.total > 0;
1831
+ return (
1832
+ <div className={cn(
1833
+ "flex items-center justify-between p-3 rounded border",
1834
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
1835
+ )}>
1836
+ <div className="flex items-center gap-2">
1837
+ <div className={cn("p-1.5 rounded", isComplete ? "bg-green-100" : "bg-cyan-100")}>
1838
+ <MousePointer className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-cyan-600")} />
1839
+ </div>
1840
+ <span id="analysis-modal-span-interactive" className="text-sm font-medium">Interactive</span>
1841
+ </div>
1842
+ <div className="flex items-center gap-3 text-sm">
1843
+ <div className="flex items-center gap-1">
1844
+ <span id="analysis-modal-span-catwithid" className={cn("font-medium", isComplete ? "text-green-700" : "text-gray-700")}>
1845
+ {cat.withId}
1846
+ </span>
1847
+ <span id="analysis-modal-span" className="text-gray-400">/</span>
1848
+ <span id="analysis-modal-span-cattotal" className="text-gray-500">{cat.total}</span>
1849
+ </div>
1850
+ {cat.missingId > 0 && (
1851
+ <button
1852
+ onClick={() => onBulkTag(undefined, "interactive")}
1853
+ disabled={bulkTagStatus === "tagging"}
1854
+ className="px-2 py-1 text-xs font-medium text-white bg-cyan-500 hover:bg-cyan-600 rounded disabled:opacity-50"
1855
+ >
1856
+ Tag {cat.missingId}
1857
+ </button>
1858
+ )}
1859
+ {isComplete && <CheckCircle className="h-4 w-4 text-green-600" />}
1860
+ </div>
1861
+ </div>
1862
+ );
1863
+ })()}
1864
+
1865
+ {/* Component Definitions (UI/Layout components) */}
1866
+ {analysisResult.summary.byCategory?.definition && (() => {
1867
+ const cat = analysisResult.summary.byCategory.definition;
1868
+ const isComplete = cat.withId === cat.total && cat.total > 0;
1869
+ return (
1870
+ <div className={cn(
1871
+ "flex items-center justify-between p-3 rounded border",
1872
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
1873
+ )}>
1874
+ <div className="flex items-center gap-2">
1875
+ <div className={cn("p-1.5 rounded", isComplete ? "bg-green-100" : "bg-violet-100")}>
1876
+ <Box className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-violet-600")} />
1877
+ </div>
1878
+ <span id="analysis-modal-span-definitions" className="text-sm font-medium">Components</span>
1879
+ </div>
1880
+ <div className="flex items-center gap-3 text-sm">
1881
+ <div className="flex items-center gap-1">
1882
+ <span id="analysis-modal-span-defwithid" className={cn("font-medium", isComplete ? "text-green-700" : "text-gray-700")}>
1883
+ {cat.withId}
1884
+ </span>
1885
+ <span className="text-gray-400">/</span>
1886
+ <span id="analysis-modal-span-deftotal" className="text-gray-500">{cat.total}</span>
1887
+ </div>
1888
+ {cat.missingId > 0 && (
1889
+ <button
1890
+ onClick={() => onBulkTag(undefined, "definition")}
1891
+ disabled={bulkTagStatus === "tagging"}
1892
+ className="px-2 py-1 text-xs font-medium text-white bg-violet-500 hover:bg-violet-600 rounded disabled:opacity-50"
1893
+ >
1894
+ Tag {cat.missingId}
1895
+ </button>
1896
+ )}
1897
+ {isComplete && <CheckCircle className="h-4 w-4 text-green-600" />}
1898
+ </div>
1899
+ </div>
1900
+ );
1901
+ })()}
1902
+
1903
+ {/* Inputs */}
1904
+ {analysisResult.summary.byCategory?.input && (() => {
1905
+ const cat = analysisResult.summary.byCategory.input;
1906
+ const isComplete = cat.withId === cat.total && cat.total > 0;
1907
+ return (
1908
+ <div className={cn(
1909
+ "flex items-center justify-between p-3 rounded border",
1910
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
1911
+ )}>
1912
+ <div className="flex items-center gap-2">
1913
+ <div className={cn("p-1.5 rounded", isComplete ? "bg-green-100" : "bg-orange-100")}>
1914
+ <FormInput className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-orange-600")} />
1915
+ </div>
1916
+ <span id="analysis-modal-span-inputs" className="text-sm font-medium">Inputs</span>
1917
+ </div>
1918
+ <div className="flex items-center gap-3 text-sm">
1919
+ <div className="flex items-center gap-1">
1920
+ <span id="analysis-modal-span-catwithid" className={cn("font-medium", isComplete ? "text-green-700" : "text-gray-700")}>
1921
+ {cat.withId}
1922
+ </span>
1923
+ <span id="analysis-modal-span" className="text-gray-400">/</span>
1924
+ <span id="analysis-modal-span-cattotal" className="text-gray-500">{cat.total}</span>
1925
+ </div>
1926
+ {cat.missingId > 0 && (
1927
+ <button
1928
+ onClick={() => onBulkTag(undefined, "input")}
1929
+ disabled={bulkTagStatus === "tagging"}
1930
+ className="px-2 py-1 text-xs font-medium text-white bg-orange-500 hover:bg-orange-600 rounded disabled:opacity-50"
1931
+ >
1932
+ Tag {cat.missingId}
1933
+ </button>
1934
+ )}
1935
+ {isComplete && <CheckCircle className="h-4 w-4 text-green-600" />}
1936
+ </div>
1937
+ </div>
1938
+ );
1939
+ })()}
1940
+ </div>
1941
+ </div>
1942
+
1943
+ {/* Brand Logos Info */}
1944
+ {analysisResult.summary.brandLogosDetected > 0 && (
1945
+ <div className="p-3 rounded border border-blue-200 bg-blue-50">
1946
+ <p id="analysis-modal-p" className="text-sm text-blue-700">
1947
+ <strong>{analysisResult.summary.brandLogosDetected}</strong> brand logo(s) detected (Sonance, IPORT, or Blaze)
1948
+ </p>
1949
+ </div>
1950
+ )}
1951
+
1952
+ {/* Auto-Tag Section */}
1953
+ {/* Global Auto-Tag All Button */}
1954
+ {analysisResult.summary.elementsMissingId > 0 && (
1955
+ <div className="space-y-3 pt-3 border-t border-gray-200">
1956
+ <h4 id="analysis-modal-h4-autotag-all-elements" className="text-sm font-medium text-gray-700">Auto-Tag All Elements</h4>
1957
+ <p id="analysis-modal-p-automatically-add-un" className="text-xs text-gray-500">
1958
+ Automatically add unique IDs to all elements missing them. This enables precise individual editing and persistent saves.
1959
+ </p>
1960
+
1961
+ {bulkTagMessage && (
1962
+ <div className={cn(
1963
+ "p-3 rounded text-sm",
1964
+ bulkTagStatus === "complete" && bulkTagMessage.includes("failed")
1965
+ ? "bg-amber-50 text-amber-700 border border-amber-200"
1966
+ : "bg-green-50 text-green-700 border border-green-200"
1967
+ )}>
1968
+ {bulkTagMessage}
1969
+ </div>
1970
+ )}
1971
+
1972
+ <button
1973
+ onClick={() => onBulkTag()}
1974
+ disabled={bulkTagStatus === "tagging"}
1975
+ className={cn(
1976
+ "w-full flex items-center justify-center gap-2 px-4 py-2.5",
1977
+ "text-sm font-medium text-white rounded transition-colors",
1978
+ "bg-[#333F48] hover:bg-[#2a343c]",
1979
+ "disabled:opacity-50 disabled:cursor-not-allowed"
1980
+ )}
1981
+ >
1982
+ {bulkTagStatus === "tagging" ? (
1983
+ <>
1984
+ <Loader2 className="h-4 w-4 animate-spin" />
1985
+ Tagging...
1986
+ </>
1987
+ ) : (
1988
+ <>
1989
+ <Wand2 className="h-4 w-4" />
1990
+ Auto-Tag All ({analysisResult.summary.elementsMissingId} elements)
1991
+ </>
1992
+ )}
1993
+ </button>
1994
+ </div>
1995
+ )}
1996
+
1997
+ {/* All Images Have IDs */}
1998
+ {analysisResult.summary.imagesMissingId === 0 && analysisResult.summary.totalImages > 0 && (
1999
+ <div className="flex items-center gap-3 p-4 rounded border border-green-200 bg-green-50">
2000
+ <CheckCircle className="h-6 w-6 text-green-600 shrink-0" />
2001
+ <div>
2002
+ <p id="analysis-modal-p-all-images-have-ids" className="text-sm font-medium text-green-700">All images have IDs!</p>
2003
+ <p id="analysis-modal-p-your-project-is-full" className="text-xs text-green-600 mt-0.5">Your project is fully indexed and ready for individual styling.</p>
2004
+ </div>
2005
+ </div>
2006
+ )}
2007
+
2008
+ {/* Theme Files */}
2009
+ {analysisResult.themeFiles.length > 0 && (
2010
+ <div className="space-y-2">
2011
+ <h4 id="analysis-modal-h4-theme-files" className="text-xs font-medium text-gray-500 uppercase tracking-wide">Theme Files</h4>
2012
+ <div className="space-y-1">
2013
+ {analysisResult.themeFiles.map((file) => (
2014
+ <div key={file.filePath} className="flex items-center justify-between p-2 rounded bg-gray-50 text-xs">
2015
+ <span id="analysis-modal-span-filefilepath" className="font-mono text-gray-600 truncate">{file.filePath}</span>
2016
+ {file.hasBrandVariables && (
2017
+ <span id="analysis-modal-span-brand" className="shrink-0 px-1.5 py-0.5 rounded bg-[#00A3E1]/10 text-[#00A3E1] text-[10px] font-medium">
2018
+ Brand
2019
+ </span>
2020
+ )}
2021
+ </div>
2022
+ ))}
2023
+ </div>
2024
+ </div>
2025
+ )}
2026
+
2027
+ {/* Scan Info */}
2028
+ <div className="text-xs text-gray-400 flex items-center justify-between">
2029
+ <span id="analysis-modal-span-scanned-in-analysisr">Scanned in {analysisResult.scanDuration}ms</span>
2030
+ <button
2031
+ onClick={onRunAnalysis}
2032
+ className="text-[#00A3E1] hover:underline"
2033
+ >
2034
+ Re-scan
2035
+ </button>
2036
+ </div>
2037
+ </>
2038
+ )}
2039
+ </div>
2040
+
2041
+ {/* Footer */}
2042
+ <div className="px-5 py-4 border-t border-gray-200 bg-gray-50">
2043
+ <button
2044
+ onClick={onClose}
2045
+ className="w-full py-2 text-sm font-medium text-gray-600 hover:text-gray-800 transition-colors"
2046
+ >
2047
+ Close
2048
+ </button>
2049
+ </div>
2050
+ </div>
2051
+ </div>
2052
+ );
2053
+ }
2054
+
2055
+ // ---- Analysis Panel (Inline version for new nav) ----
2056
+
2057
+ interface AnalysisPanelProps {
2058
+ analysisStatus: AnalysisStatus;
2059
+ analysisResult: AnalysisResult | null;
2060
+ analysisError: string;
2061
+ bulkTagStatus: "idle" | "tagging" | "complete";
2062
+ bulkTagMessage: string;
2063
+ onRunAnalysis: () => void;
2064
+ onBulkTag: (elementIds?: string[], category?: string) => void;
2065
+ }
2066
+
2067
+ function AnalysisPanel({
2068
+ analysisStatus,
2069
+ analysisResult,
2070
+ analysisError,
2071
+ bulkTagStatus,
2072
+ bulkTagMessage,
2073
+ onRunAnalysis,
2074
+ onBulkTag,
2075
+ }: AnalysisPanelProps) {
2076
+ return (
2077
+ <div className="space-y-4">
2078
+ {/* Initial State */}
2079
+ {analysisStatus === "idle" && !analysisResult && (
2080
+ <div className="text-center py-6 space-y-4">
2081
+ <div className="mx-auto w-14 h-14 rounded-full bg-gray-100 flex items-center justify-center">
2082
+ <Scan className="h-7 w-7 text-gray-400" />
2083
+ </div>
2084
+ <div>
2085
+ <h3 id="analysis-panel-h3-analyze-your-project" className="text-base font-medium">Analyze Your Project</h3>
2086
+ <p id="analysis-panel-p-scan-your-codebase-t" className="text-xs text-gray-500 mt-1">
2087
+ Scan your codebase to index all images, logos, and theme files.
2088
+ </p>
2089
+ </div>
2090
+ <button
2091
+ onClick={onRunAnalysis}
2092
+ className="inline-flex items-center gap-2 px-4 py-2 bg-[#333F48] text-white text-sm font-medium rounded hover:bg-[#2a343c] transition-colors"
2093
+ >
2094
+ <Scan className="h-4 w-4" />
2095
+ Scan Project
2096
+ </button>
2097
+ </div>
2098
+ )}
2099
+
2100
+ {/* Scanning State */}
2101
+ {analysisStatus === "scanning" && (
2102
+ <div className="text-center py-6 space-y-4">
2103
+ <div className="mx-auto w-14 h-14 rounded-full bg-blue-50 flex items-center justify-center">
2104
+ <Loader2 className="h-7 w-7 text-[#00A3E1] animate-spin" />
2105
+ </div>
2106
+ <div>
2107
+ <h3 id="analysis-panel-h3-scanning" className="text-base font-medium">Scanning...</h3>
2108
+ <p id="analysis-panel-p-analyzing-your-sourc" className="text-xs text-gray-500 mt-1">
2109
+ Analyzing your source files for design assets.
2110
+ </p>
2111
+ </div>
2112
+ </div>
2113
+ )}
2114
+
2115
+ {/* Error State */}
2116
+ {analysisStatus === "error" && (
2117
+ <div className="text-center py-6 space-y-4">
2118
+ <div className="mx-auto w-14 h-14 rounded-full bg-red-50 flex items-center justify-center">
2119
+ <AlertCircle className="h-7 w-7 text-red-500" />
2120
+ </div>
2121
+ <div>
2122
+ <h3 id="analysis-panel-h3-analysis-failed" className="text-base font-medium text-red-600">Analysis Failed</h3>
2123
+ <p id="analysis-panel-p-analysiserror" className="text-xs text-gray-500 mt-1">{analysisError}</p>
2124
+ </div>
2125
+ <button
2126
+ onClick={onRunAnalysis}
2127
+ className="inline-flex items-center gap-2 px-4 py-2 bg-[#333F48] text-white text-sm font-medium rounded hover:bg-[#2a343c] transition-colors"
2128
+ >
2129
+ <RotateCcw className="h-4 w-4" />
2130
+ Try Again
2131
+ </button>
2132
+ </div>
2133
+ )}
2134
+
2135
+ {/* Results */}
2136
+ {analysisStatus === "complete" && analysisResult && (
2137
+ <>
2138
+ {/* Overview Stats */}
2139
+ <div className="grid grid-cols-3 gap-2">
2140
+ <div className="p-2 rounded border border-gray-200 bg-gray-50 text-center">
2141
+ <div className="text-[10px] text-gray-500 uppercase tracking-wide">Files</div>
2142
+ <div className="text-lg font-semibold">{analysisResult.filesScanned}</div>
2143
+ </div>
2144
+ <div className="p-2 rounded border border-green-200 bg-green-50 text-center">
2145
+ <div className="text-[10px] text-green-600 uppercase tracking-wide">With IDs</div>
2146
+ <div className="text-lg font-semibold text-green-700">{analysisResult.summary.elementsWithId}</div>
2147
+ </div>
2148
+ <div className="p-2 rounded border border-amber-200 bg-amber-50 text-center">
2149
+ <div className="text-[10px] text-amber-600 uppercase tracking-wide">Missing</div>
2150
+ <div className="text-lg font-semibold text-amber-700">{analysisResult.summary.elementsMissingId}</div>
2151
+ </div>
2152
+ </div>
2153
+
2154
+ {/* Category Breakdown */}
2155
+ <div className="space-y-2">
2156
+ <h4 id="analysis-panel-h4-elements-by-category" className="text-xs font-medium text-gray-500 uppercase tracking-wide">Elements by Category</h4>
2157
+ <div className="space-y-1.5">
2158
+ {/* Images */}
2159
+ {(() => {
2160
+ const cat = analysisResult.summary.byCategory?.image;
2161
+ const total = cat?.total || analysisResult.summary.totalImages;
2162
+ const withId = cat?.withId || analysisResult.summary.imagesWithId;
2163
+ const missingId = cat?.missingId || analysisResult.summary.imagesMissingId;
2164
+ const isComplete = withId === total && total > 0;
2165
+ return (
2166
+ <div className={cn(
2167
+ "flex items-center justify-between p-2 rounded border",
2168
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
2169
+ )}>
2170
+ <div className="flex items-center gap-2">
2171
+ <ImageIcon className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-blue-600")} />
2172
+ <span id="analysis-panel-span-images" className="text-xs font-medium">Images</span>
2173
+ </div>
2174
+ <div className="flex items-center gap-2 text-xs">
2175
+ <span id="analysis-panel-span-withidtotal" className={isComplete ? "text-green-700" : "text-gray-600"}>{withId}/{total}</span>
2176
+ {missingId > 0 && (
2177
+ <button
2178
+ onClick={() => onBulkTag(undefined, "image")}
2179
+ disabled={bulkTagStatus === "tagging"}
2180
+ className="px-1.5 py-0.5 text-[10px] font-medium text-white bg-blue-500 hover:bg-blue-600 rounded disabled:opacity-50"
2181
+ >
2182
+ Tag {missingId}
2183
+ </button>
2184
+ )}
2185
+ {isComplete && <CheckCircle className="h-3 w-3 text-green-600" />}
2186
+ </div>
2187
+ </div>
2188
+ );
2189
+ })()}
2190
+
2191
+ {/* Text Elements */}
2192
+ {analysisResult.summary.byCategory?.text && (() => {
2193
+ const cat = analysisResult.summary.byCategory.text;
2194
+ const isComplete = cat.withId === cat.total && cat.total > 0;
2195
+ return (
2196
+ <div className={cn(
2197
+ "flex items-center justify-between p-2 rounded border",
2198
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
2199
+ )}>
2200
+ <div className="flex items-center gap-2">
2201
+ <Type className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-purple-600")} />
2202
+ <span id="analysis-panel-span-text" className="text-xs font-medium">Text</span>
2203
+ </div>
2204
+ <div className="flex items-center gap-2 text-xs">
2205
+ <span id="analysis-panel-span-catwithidcattotal" className={isComplete ? "text-green-700" : "text-gray-600"}>{cat.withId}/{cat.total}</span>
2206
+ {cat.missingId > 0 && (
2207
+ <button
2208
+ onClick={() => onBulkTag(undefined, "text")}
2209
+ disabled={bulkTagStatus === "tagging"}
2210
+ className="px-1.5 py-0.5 text-[10px] font-medium text-white bg-purple-500 hover:bg-purple-600 rounded disabled:opacity-50"
2211
+ >
2212
+ Tag {cat.missingId}
2213
+ </button>
2214
+ )}
2215
+ {isComplete && <CheckCircle className="h-3 w-3 text-green-600" />}
2216
+ </div>
2217
+ </div>
2218
+ );
2219
+ })()}
2220
+
2221
+ {/* Interactive (Buttons, Links) */}
2222
+ {analysisResult.summary.byCategory?.interactive && (() => {
2223
+ const cat = analysisResult.summary.byCategory.interactive;
2224
+ const isComplete = cat.withId === cat.total && cat.total > 0;
2225
+ return (
2226
+ <div className={cn(
2227
+ "flex items-center justify-between p-2 rounded border",
2228
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
2229
+ )}>
2230
+ <div className="flex items-center gap-2">
2231
+ <MousePointer className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-cyan-600")} />
2232
+ <span id="analysis-panel-span-interactive" className="text-xs font-medium">Interactive</span>
2233
+ </div>
2234
+ <div className="flex items-center gap-2 text-xs">
2235
+ <span id="analysis-panel-span-catwithidcattotal" className={isComplete ? "text-green-700" : "text-gray-600"}>{cat.withId}/{cat.total}</span>
2236
+ {cat.missingId > 0 && (
2237
+ <button
2238
+ onClick={() => onBulkTag(undefined, "interactive")}
2239
+ disabled={bulkTagStatus === "tagging"}
2240
+ className="px-1.5 py-0.5 text-[10px] font-medium text-white bg-cyan-500 hover:bg-cyan-600 rounded disabled:opacity-50"
2241
+ >
2242
+ Tag {cat.missingId}
2243
+ </button>
2244
+ )}
2245
+ {isComplete && <CheckCircle className="h-3 w-3 text-green-600" />}
2246
+ </div>
2247
+ </div>
2248
+ );
2249
+ })()}
2250
+
2251
+ {/* Component Definitions */}
2252
+ {analysisResult.summary.byCategory?.definition && (() => {
2253
+ const cat = analysisResult.summary.byCategory.definition;
2254
+ const isComplete = cat.withId === cat.total && cat.total > 0;
2255
+ return (
2256
+ <div className={cn(
2257
+ "flex items-center justify-between p-2 rounded border",
2258
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
2259
+ )}>
2260
+ <div className="flex items-center gap-2">
2261
+ <Box className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-violet-600")} />
2262
+ <span id="analysis-panel-span-definitions" className="text-xs font-medium">Components</span>
2263
+ </div>
2264
+ <div className="flex items-center gap-2 text-xs">
2265
+ <span id="analysis-panel-span-defwithidtotal" className={isComplete ? "text-green-700" : "text-gray-600"}>{cat.withId}/{cat.total}</span>
2266
+ {cat.missingId > 0 && (
2267
+ <button
2268
+ onClick={() => onBulkTag(undefined, "definition")}
2269
+ disabled={bulkTagStatus === "tagging"}
2270
+ className="px-1.5 py-0.5 text-[10px] font-medium text-white bg-violet-500 hover:bg-violet-600 rounded disabled:opacity-50"
2271
+ >
2272
+ Tag {cat.missingId}
2273
+ </button>
2274
+ )}
2275
+ {isComplete && <CheckCircle className="h-3 w-3 text-green-600" />}
2276
+ </div>
2277
+ </div>
2278
+ );
2279
+ })()}
2280
+
2281
+ {/* Inputs */}
2282
+ {analysisResult.summary.byCategory?.input && (() => {
2283
+ const cat = analysisResult.summary.byCategory.input;
2284
+ const isComplete = cat.withId === cat.total && cat.total > 0;
2285
+ return (
2286
+ <div className={cn(
2287
+ "flex items-center justify-between p-2 rounded border",
2288
+ isComplete ? "border-green-200 bg-green-50" : "border-gray-200 bg-gray-50"
2289
+ )}>
2290
+ <div className="flex items-center gap-2">
2291
+ <FormInput className={cn("h-3.5 w-3.5", isComplete ? "text-green-600" : "text-orange-600")} />
2292
+ <span id="analysis-panel-span-inputs" className="text-xs font-medium">Inputs</span>
2293
+ </div>
2294
+ <div className="flex items-center gap-2 text-xs">
2295
+ <span id="analysis-panel-span-catwithidcattotal" className={isComplete ? "text-green-700" : "text-gray-600"}>{cat.withId}/{cat.total}</span>
2296
+ {cat.missingId > 0 && (
2297
+ <button
2298
+ onClick={() => onBulkTag(undefined, "input")}
2299
+ disabled={bulkTagStatus === "tagging"}
2300
+ className="px-1.5 py-0.5 text-[10px] font-medium text-white bg-orange-500 hover:bg-orange-600 rounded disabled:opacity-50"
2301
+ >
2302
+ Tag {cat.missingId}
2303
+ </button>
2304
+ )}
2305
+ {isComplete && <CheckCircle className="h-3 w-3 text-green-600" />}
2306
+ </div>
2307
+ </div>
2308
+ );
2309
+ })()}
2310
+ </div>
2311
+ </div>
2312
+
2313
+ {/* Auto-Tag All Button */}
2314
+ {analysisResult.summary.elementsMissingId > 0 && (
2315
+ <div className="space-y-2 pt-2 border-t border-gray-200">
2316
+ {bulkTagMessage && (
2317
+ <div className={cn(
2318
+ "p-2 rounded text-xs",
2319
+ bulkTagStatus === "complete" && bulkTagMessage.includes("failed")
2320
+ ? "bg-amber-50 text-amber-700 border border-amber-200"
2321
+ : "bg-green-50 text-green-700 border border-green-200"
2322
+ )}>
2323
+ {bulkTagMessage}
2324
+ </div>
2325
+ )}
2326
+
2327
+ <button
2328
+ onClick={() => onBulkTag()}
2329
+ disabled={bulkTagStatus === "tagging"}
2330
+ className={cn(
2331
+ "w-full flex items-center justify-center gap-2 px-3 py-2",
2332
+ "text-sm font-medium text-white rounded transition-colors",
2333
+ "bg-[#333F48] hover:bg-[#2a343c]",
2334
+ "disabled:opacity-50 disabled:cursor-not-allowed"
2335
+ )}
2336
+ >
2337
+ {bulkTagStatus === "tagging" ? (
2338
+ <>
2339
+ <Loader2 className="h-4 w-4 animate-spin" />
2340
+ Tagging...
2341
+ </>
2342
+ ) : (
2343
+ <>
2344
+ <Wand2 className="h-4 w-4" />
2345
+ Auto-Tag All ({analysisResult.summary.elementsMissingId})
2346
+ </>
2347
+ )}
2348
+ </button>
2349
+ </div>
2350
+ )}
2351
+
2352
+ {/* All Elements Have IDs */}
2353
+ {analysisResult.summary.elementsMissingId === 0 && analysisResult.summary.totalElements > 0 && (
2354
+ <div className="flex items-center gap-2 p-3 rounded border border-green-200 bg-green-50">
2355
+ <CheckCircle className="h-5 w-5 text-green-600 shrink-0" />
2356
+ <div>
2357
+ <p id="analysis-panel-p-all-elements-have-id" className="text-xs font-medium text-green-700">All elements have IDs!</p>
2358
+ <p id="analysis-panel-p-ready-for-individual" className="text-[10px] text-green-600">Ready for individual styling.</p>
2359
+ </div>
2360
+ </div>
2361
+ )}
2362
+
2363
+ {/* Re-scan button */}
2364
+ <div className="text-xs text-gray-400 flex items-center justify-between pt-2">
2365
+ <span id="analysis-panel-span-scanned-in-analysisr">Scanned in {analysisResult.scanDuration}ms</span>
2366
+ <button
2367
+ onClick={onRunAnalysis}
2368
+ className="text-[#00A3E1] hover:underline"
2369
+ >
2370
+ Re-scan
2371
+ </button>
2372
+ </div>
2373
+ </>
2374
+ )}
2375
+ </div>
2376
+ );
2377
+ }
2378
+
2379
+ // ---- Brand Panel ----
2380
+
2381
+ interface BrandPanelProps {
2382
+ config: ThemeConfig;
2383
+ onBrandSelect: (brandId: BrandId) => void;
2384
+ logoAssetsByBrand: Record<string, LogoAsset[]>;
2385
+ currentTheme: string;
2386
+ }
2387
+
2388
+ // Default logo paths for each brand (fallback when assets haven't loaded)
2389
+ // Light mode uses "Dark" logos (dark text on light bg), Dark mode uses "Light" logos (light text on dark bg)
2390
+ const defaultBrandLogos: Record<string, { light: string; dark: string }> = {
2391
+ sonance: {
2392
+ light: "/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Dark.png",
2393
+ dark: "/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Light.png",
2394
+ },
2395
+ iport: {
2396
+ light: "/logos/iport/IPORT_Sonance_LockUp_2C_Dark_RGB.png",
2397
+ dark: "/logos/iport/IPORT_Sonance_LockUp_2C_Light_RGB.png",
2398
+ },
2399
+ blaze: {
2400
+ light: "/logos/blaze/BlazeBySonance_Logo_Lockup_3C_Dark_RGB_05162025.png",
2401
+ dark: "/logos/blaze/BlazeBySonance_Logo_Lockup_2C_Light_RGB_05162025.png",
2402
+ },
2403
+ };
2404
+
2405
+ function BrandPanel({ config, onBrandSelect, logoAssetsByBrand, currentTheme }: BrandPanelProps) {
2406
+ // Find current brand
2407
+ const currentBrand = brandPresets.find(
2408
+ (p) =>
2409
+ p.config.baseColor.toLowerCase() === config.baseColor.toLowerCase() &&
2410
+ p.config.accentColor.toLowerCase() === config.accentColor.toLowerCase()
2411
+ );
2412
+
2413
+ // Determine if we're in dark mode
2414
+ const isDarkMode = currentTheme === "dark";
2415
+
2416
+ // Get the default logo for the current brand - use loaded assets or fallback
2417
+ const currentBrandLogos = currentBrand ? (logoAssetsByBrand[currentBrand.id] || []) : [];
2418
+ const logoFromAssets = currentBrandLogos.length > 0 ? currentBrandLogos[0] : null;
2419
+ const fallbackLogos = currentBrand ? defaultBrandLogos[currentBrand.id] : null;
2420
+ const fallbackLogoPath = fallbackLogos ? (isDarkMode ? fallbackLogos.dark : fallbackLogos.light) : null;
2421
+ const logoPath = logoFromAssets?.path || fallbackLogoPath;
2422
+ const logoName = logoFromAssets?.name || currentBrand?.name || "Brand Logo";
2423
+
2424
+ return (
2425
+ <div className="space-y-5">
2426
+ {/* Brand Presets */}
2427
+ <Section title="Select Brand">
2428
+ <div className="flex gap-1">
2429
+ {brandPresets.map((preset) => (
2430
+ <button
2431
+ key={preset.id}
2432
+ onClick={() => onBrandSelect(preset.id)}
2433
+ className={cn(
2434
+ "flex-1 py-2 px-2 text-xs font-medium rounded transition-colors",
2435
+ "border",
2436
+ currentBrand?.id === preset.id
2437
+ ? "bg-[#333F48] text-white border-[#333F48]"
2438
+ : "bg-white text-gray-600 border-gray-200 hover:bg-gray-50"
2439
+ )}
2440
+ >
2441
+ {preset.name}
2442
+ </button>
2443
+ ))}
2444
+ </div>
2445
+ </Section>
2446
+
2447
+ {/* Brand Logo Preview */}
2448
+ {logoPath && (
2449
+ <Section title="Brand Logo">
2450
+ <div className="flex items-center justify-center p-4 rounded border border-gray-200 bg-gray-50">
2451
+ <img
2452
+ id="section-logo"
2453
+ src={logoPath}
2454
+ alt={logoName}
2455
+ className="max-h-16 max-w-full object-contain"
2456
+ />
2457
+ </div>
2458
+ </Section>
2459
+ )}
2460
+
2461
+ {/* Static Color Display */}
2462
+ <Section title="Brand Colors">
2463
+ <div className="space-y-3">
2464
+ {/* Primary Color */}
2465
+ <div className="flex items-center justify-between p-3 rounded border border-gray-200 bg-gray-50">
2466
+ <div className="flex items-center gap-3">
2467
+ <div
2468
+ className="w-8 h-8 rounded-full border-2 border-white shadow-sm"
2469
+ style={{ backgroundColor: config.baseColor }}
2470
+ />
2471
+ <div>
2472
+ <p id="section-p-primary" className="text-xs font-medium text-gray-700">Primary</p>
2473
+ <p id="brand-panel-p-configbasecolor" className="text-[10px] text-gray-500 uppercase">{config.baseColor}</p>
2474
+ </div>
2475
+ </div>
2476
+ </div>
2477
+
2478
+ {/* Accent Color */}
2479
+ <div className="flex items-center justify-between p-3 rounded border border-gray-200 bg-gray-50">
2480
+ <div className="flex items-center gap-3">
2481
+ <div
2482
+ className="w-8 h-8 rounded-full border-2 border-white shadow-sm"
2483
+ style={{ backgroundColor: config.accentColor }}
2484
+ />
2485
+ <div>
2486
+ <p id="brand-panel-p-accent" className="text-xs font-medium text-gray-700">Accent</p>
2487
+ <p id="brand-panel-p-configaccentcolor" className="text-[10px] text-gray-500 uppercase">{config.accentColor}</p>
2488
+ </div>
2489
+ </div>
2490
+ </div>
2491
+ </div>
2492
+ </Section>
2493
+
2494
+ {/* Brand Description */}
2495
+ {currentBrand && (
2496
+ <div className="p-3 rounded border border-gray-200 bg-gray-50">
2497
+ <p id="brand-panel-p-currentbranddescript" className="text-xs text-gray-600">{currentBrand.description}</p>
2498
+ </div>
2499
+ )}
2500
+ </div>
2501
+ );
2502
+ }
2503
+
2504
+ // ---- Components Panel ----
2505
+
2506
+ type ComponentsSubTab = "inspector" | "design";
2507
+
2508
+ interface ComponentsPanelProps {
2509
+ copiedId: string | null;
2510
+ onCopy: (text: string, id: string) => void;
2511
+ installedComponents: string[];
2512
+ inspectorEnabled: boolean;
2513
+ onToggleInspector: () => void;
2514
+ config: ThemeConfig;
2515
+ updateConfig: (updates: Partial<ThemeConfig>) => void;
2516
+ onReset: () => void;
2517
+ }
2518
+
2519
+ function ComponentsPanel({
2520
+ copiedId,
2521
+ onCopy,
2522
+ installedComponents,
2523
+ inspectorEnabled,
2524
+ onToggleInspector,
2525
+ config,
2526
+ updateConfig,
2527
+ onReset
2528
+ }: ComponentsPanelProps) {
2529
+ const [activeSubTab, setActiveSubTab] = useState<ComponentsSubTab>("inspector");
2530
+ const [tagStatus, setTagStatus] = useState<TagStatus>("idle");
2531
+ const [tagMessage, setTagMessage] = useState<string>("");
2532
+ const [tagStats, setTagStats] = useState<{ tagged: number; untagged: number } | null>(null);
2533
+
2534
+ // Color architecture state
2535
+ const [colorArchitecture, setColorArchitecture] = useState<{
2536
+ primary: string;
2537
+ accent: string;
2538
+ sources: { filePath: string; type: string; variables: { name: string; value: string; lineNumber: number }[] }[];
2539
+ recommendation: string;
2540
+ } | null>(null);
2541
+ const [colorSaveStatus, setColorSaveStatus] = useState<"idle" | "saving" | "success" | "error">("idle");
2542
+ const [colorSaveMessage, setColorSaveMessage] = useState("");
2543
+
2544
+ // Check tagging status and color architecture on mount
2545
+ useEffect(() => {
2546
+ async function fetchAnalysis() {
2547
+ try {
2548
+ const response = await fetch("/api/sonance-analyze");
2549
+ if (response.ok) {
2550
+ const data = await response.json();
2551
+
2552
+ // Update tag stats
2553
+ const defCategory = data.summary?.byCategory?.definition;
2554
+ if (defCategory) {
2555
+ setTagStats({ tagged: defCategory.withId || 0, untagged: defCategory.missingId || 0 });
2556
+ }
2557
+
2558
+ // Update color architecture
2559
+ if (data.colorArchitecture) {
2560
+ setColorArchitecture(data.colorArchitecture);
2561
+ }
2562
+ }
2563
+ } catch {
2564
+ // API might not exist yet
2565
+ }
2566
+ }
2567
+ fetchAnalysis();
2568
+ }, []);
2569
+
2570
+ // Handle saving color changes
2571
+ const handleSaveColors = async () => {
2572
+ if (!colorArchitecture) return;
2573
+
2574
+ setColorSaveStatus("saving");
2575
+ setColorSaveMessage("");
2576
+
2577
+ try {
2578
+ const response = await fetch("/api/sonance-save-colors", {
2579
+ method: "POST",
2580
+ headers: { "Content-Type": "application/json" },
2581
+ body: JSON.stringify({
2582
+ primaryColor: config.baseColor,
2583
+ accentColor: config.accentColor,
2584
+ colorArchitecture,
2585
+ }),
2586
+ });
2587
+
2588
+ const data = await response.json();
2589
+
2590
+ if (!response.ok) {
2591
+ throw new Error(data.error || "Failed to save colors");
2592
+ }
2593
+
2594
+ setColorSaveStatus("success");
2595
+ setColorSaveMessage(data.message || "Colors saved successfully!");
2596
+
2597
+ setTimeout(() => {
2598
+ setColorSaveStatus("idle");
2599
+ setColorSaveMessage("");
2600
+ }, 5000);
2601
+ } catch (error) {
2602
+ setColorSaveStatus("error");
2603
+ setColorSaveMessage(error instanceof Error ? error.message : "Failed to save colors");
2604
+
2605
+ setTimeout(() => {
2606
+ setColorSaveStatus("idle");
2607
+ setColorSaveMessage("");
2608
+ }, 5000);
2609
+ }
2610
+ };
2611
+
2612
+ return (
2613
+ <div className="space-y-4">
2614
+ {/* Sub-Navigation Tabs */}
2615
+ <div className="flex gap-1 p-1 rounded bg-gray-100">
2616
+ <button
2617
+ onClick={() => setActiveSubTab("inspector")}
2618
+ className={cn(
2619
+ "flex-1 py-1.5 px-3 text-xs font-medium rounded transition-colors",
2620
+ activeSubTab === "inspector"
2621
+ ? "bg-white text-gray-900 shadow-sm"
2622
+ : "text-gray-500 hover:text-gray-700"
2623
+ )}
2624
+ >
2625
+ Inspector
2626
+ </button>
2627
+ <button
2628
+ onClick={() => setActiveSubTab("design")}
2629
+ className={cn(
2630
+ "flex-1 py-1.5 px-3 text-xs font-medium rounded transition-colors",
2631
+ activeSubTab === "design"
2632
+ ? "bg-white text-gray-900 shadow-sm"
2633
+ : "text-gray-500 hover:text-gray-700"
2634
+ )}
2635
+ >
2636
+ Design
2637
+ </button>
2638
+ </div>
2639
+
2640
+ {/* Inspector Sub-Tab */}
2641
+ {activeSubTab === "inspector" && (
2642
+ <div className="space-y-4">
2643
+ {/* Inspector Toggle */}
2644
+ <div className="flex items-center justify-between p-3 rounded border border-gray-200 bg-gray-50">
2645
+ <div className="flex items-center gap-2">
2646
+ <Eye className={cn("h-4 w-4", inspectorEnabled ? "text-[#00A3E1]" : "text-gray-400")} />
2647
+ <span id="brand-panel-span-component-inspector" className="text-xs font-medium">Component Inspector</span>
2648
+ </div>
2649
+ <button
2650
+ onClick={onToggleInspector}
2651
+ className={cn(
2652
+ "px-3 py-1 text-xs font-medium rounded transition-colors",
2653
+ inspectorEnabled
2654
+ ? "bg-[#00A3E1] text-white"
2655
+ : "bg-white text-gray-600 border border-gray-200 hover:bg-gray-50"
2656
+ )}
2657
+ >
2658
+ {inspectorEnabled ? "Disable" : "Enable"}
2659
+ </button>
2660
+ </div>
2661
+
2662
+ {/* Component Snippets */}
2663
+ <Section title="Component Snippets">
2664
+ <div className="space-y-2">
2665
+ {Object.entries(componentSnippets).map(([name, snippet]) => (
2666
+ <div
2667
+ key={name}
2668
+ className="p-2 rounded border border-gray-200 bg-gray-50 hover:bg-gray-100 transition-colors cursor-pointer"
2669
+ onClick={() => onCopy(snippet.code, name)}
2670
+ >
2671
+ <div className="flex items-center justify-between">
2672
+ <span id="section-span-snippetname" className="text-xs font-medium">{snippet.name}</span>
2673
+ {copiedId === name ? (
2674
+ <Check className="h-3 w-3 text-green-500" />
2675
+ ) : (
2676
+ <Copy className="h-3 w-3 text-gray-400" />
2677
+ )}
2678
+ </div>
2679
+ <p id="brand-panel-p-snippetdescription" className="text-[10px] text-gray-500 mt-0.5">{snippet.description}</p>
2680
+ </div>
2681
+ ))}
2682
+ </div>
2683
+ </Section>
2684
+ </div>
2685
+ )}
2686
+
2687
+ {/* Design Sub-Tab */}
2688
+ {activeSubTab === "design" && (
2689
+ <div className="space-y-5">
2690
+ {/* Colors */}
2691
+ <Section title="Primary Color">
2692
+ <div className="flex flex-wrap gap-2">
2693
+ {colorPresets.map((preset, index) => (
2694
+ <ColorSwatch
2695
+ key={`base-${index}`}
2696
+ color={preset.value}
2697
+ name={preset.name}
2698
+ selected={config.baseColor.toLowerCase() === preset.value.toLowerCase()}
2699
+ onClick={() => updateConfig({ baseColor: preset.value })}
2700
+ />
2701
+ ))}
2702
+ </div>
2703
+ </Section>
2704
+
2705
+ <Section title="Accent Color">
2706
+ <div className="flex flex-wrap gap-2">
2707
+ {colorPresets.map((preset, index) => (
2708
+ <ColorSwatch
2709
+ key={`accent-${index}`}
2710
+ color={preset.value}
2711
+ name={preset.name}
2712
+ selected={config.accentColor.toLowerCase() === preset.value.toLowerCase()}
2713
+ onClick={() => updateConfig({ accentColor: preset.value })}
2714
+ />
2715
+ ))}
2716
+ </div>
2717
+ </Section>
2718
+
2719
+ {/* Radius */}
2720
+ <Section title="Border Radius">
2721
+ <div className="flex gap-1">
2722
+ {(Object.keys(radiusValues) as Array<keyof typeof radiusValues>).map(
2723
+ (radius) => (
2724
+ <button
2725
+ key={radius}
2726
+ onClick={() => updateConfig({ radius })}
2727
+ className={cn(
2728
+ "flex-1 h-8 text-xs font-medium rounded transition-colors border",
2729
+ config.radius === radius
2730
+ ? "bg-[#333F48] text-white border-[#333F48]"
2731
+ : "bg-white text-gray-600 border-gray-200 hover:bg-gray-50"
2732
+ )}
2733
+ >
2734
+ {radius === "none" ? "0" : radius}
2735
+ </button>
2736
+ )
2737
+ )}
2738
+ </div>
2739
+ </Section>
2740
+
2741
+ {/* Typography */}
2742
+ <Section title="Typography">
2743
+ <div className="space-y-2">
2744
+ <SelectField
2745
+ label="Heading Weight"
2746
+ value={String(config.headingWeight)}
2747
+ options={[
2748
+ { value: "400", label: "Regular" },
2749
+ { value: "500", label: "Medium" },
2750
+ { value: "600", label: "Semibold" },
2751
+ { value: "700", label: "Bold" },
2752
+ ]}
2753
+ onChange={(v) =>
2754
+ updateConfig({
2755
+ headingWeight: parseInt(v) as ThemeConfig["headingWeight"],
2756
+ })
2757
+ }
2758
+ />
2759
+ <SelectField
2760
+ label="Body Weight"
2761
+ value={String(config.bodyWeight)}
2762
+ options={[
2763
+ { value: "300", label: "Light" },
2764
+ { value: "400", label: "Regular" },
2765
+ { value: "500", label: "Medium" },
2766
+ ]}
2767
+ onChange={(v) =>
2768
+ updateConfig({
2769
+ bodyWeight: parseInt(v) as ThemeConfig["bodyWeight"],
2770
+ })
2771
+ }
2772
+ />
2773
+ <SelectField
2774
+ label="Scale"
2775
+ value={config.typographyScale}
2776
+ options={[
2777
+ { value: "compact", label: "Compact" },
2778
+ { value: "default", label: "Default" },
2779
+ { value: "large", label: "Large" },
2780
+ ]}
2781
+ onChange={(v) =>
2782
+ updateConfig({
2783
+ typographyScale: v as ThemeConfig["typographyScale"],
2784
+ })
2785
+ }
2786
+ />
2787
+ </div>
2788
+ </Section>
2789
+
2790
+ {/* Color Architecture Info */}
2791
+ {colorArchitecture && (
2792
+ <Section title="Detected Architecture">
2793
+ <div className="space-y-2">
2794
+ <div className="p-2 rounded border border-gray-200 bg-gray-50">
2795
+ <div className="flex items-center gap-2 mb-1">
2796
+ <span id="section-span-primary-arch" className="text-[10px] text-gray-400 uppercase">Primary:</span>
2797
+ <span id="section-span-primary-arch-value" className="text-xs font-medium">{colorArchitecture.primary}</span>
2798
+ </div>
2799
+ <div className="flex items-center gap-2">
2800
+ <span id="section-span-accent-arch" className="text-[10px] text-gray-400 uppercase">Accent:</span>
2801
+ <span id="section-span-accent-arch-value" className="text-xs font-medium">{colorArchitecture.accent}</span>
2802
+ </div>
2803
+ </div>
2804
+ <p id="section-p-color-recommendation" className="text-[10px] text-gray-500">
2805
+ {colorArchitecture.recommendation}
2806
+ </p>
2807
+ {colorArchitecture.sources.length > 0 && (
2808
+ <div className="text-[10px] text-gray-400">
2809
+ Files: {colorArchitecture.sources.map(s => s.filePath.split('/').pop()).join(', ')}
2810
+ </div>
2811
+ )}
2812
+ </div>
2813
+ </Section>
2814
+ )}
2815
+
2816
+ {/* Save Colors Button */}
2817
+ {colorArchitecture && colorArchitecture.sources.length > 0 && (
2818
+ <div className="space-y-2">
2819
+ <button
2820
+ onClick={handleSaveColors}
2821
+ disabled={colorSaveStatus === "saving"}
2822
+ className={cn(
2823
+ "w-full flex items-center justify-center gap-2 py-2.5",
2824
+ "text-xs font-medium rounded transition-colors",
2825
+ "bg-[#333F48] text-white hover:bg-[#2a343c]",
2826
+ "disabled:opacity-50 disabled:cursor-not-allowed"
2827
+ )}
2828
+ >
2829
+ {colorSaveStatus === "saving" ? (
2830
+ <>
2831
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
2832
+ Saving...
2833
+ </>
2834
+ ) : (
2835
+ <>
2836
+ <Save className="h-3.5 w-3.5" />
2837
+ Save Color Changes
2838
+ </>
2839
+ )}
2840
+ </button>
2841
+ {colorSaveMessage && (
2842
+ <div className={cn(
2843
+ "p-2 rounded text-xs",
2844
+ colorSaveStatus === "success" && "bg-green-50 text-green-700 border border-green-200",
2845
+ colorSaveStatus === "error" && "bg-red-50 text-red-700 border border-red-200"
2846
+ )}>
2847
+ {colorSaveMessage}
2848
+ </div>
2849
+ )}
2850
+ </div>
2851
+ )}
2852
+
2853
+ {/* Warning if no color sources detected */}
2854
+ {colorArchitecture && colorArchitecture.sources.length === 0 && (
2855
+ <div className="p-2 rounded border border-amber-200 bg-amber-50">
2856
+ <p id="section-p-no-sources-warning" className="text-[10px] text-amber-700">
2857
+ No color sources detected. Changes are previewed but cannot be saved automatically.
2858
+ Consider adding CSS variables or a theme file.
2859
+ </p>
2860
+ </div>
2861
+ )}
2862
+
2863
+ {/* Reset */}
2864
+ <button
2865
+ onClick={onReset}
2866
+ className={cn(
2867
+ "w-full flex items-center justify-center gap-2 py-2.5",
2868
+ "text-xs font-medium text-gray-500 hover:text-gray-700",
2869
+ "border border-gray-200 rounded hover:bg-gray-50 transition-colors"
2870
+ )}
2871
+ >
2872
+ <RotateCcw className="h-3.5 w-3.5" />
2873
+ Reset to Default
2874
+ </button>
2875
+ </div>
2876
+ )}
2877
+ </div>
2878
+ );
2879
+ }
2880
+
2881
+ // ---- Logos Panel ----
2882
+
2883
+ interface LogosPanelProps extends LogoToolsPanelProps {
2884
+ inspectorEnabled: boolean;
2885
+ onToggleInspector: () => void;
2886
+ }
2887
+
2888
+ function LogosPanel({
2889
+ inspectorEnabled,
2890
+ onToggleInspector,
2891
+ ...logoToolsProps
2892
+ }: LogosPanelProps) {
2893
+ return (
2894
+ <div className="space-y-4">
2895
+ {/* Inspector Toggle */}
2896
+ <div className="flex items-center justify-between p-3 rounded border border-gray-200 bg-gray-50">
2897
+ <div className="flex items-center gap-2">
2898
+ <ImageIcon className={cn("h-4 w-4", inspectorEnabled ? "text-[#FC4C02]" : "text-gray-400")} />
2899
+ <span id="logos-panel-span-logo-inspector" className="text-xs font-medium">Logo Inspector</span>
2900
+ </div>
2901
+ <button
2902
+ onClick={onToggleInspector}
2903
+ className={cn(
2904
+ "px-3 py-1 text-xs font-medium rounded transition-colors",
2905
+ inspectorEnabled
2906
+ ? "bg-[#FC4C02] text-white"
2907
+ : "bg-white text-gray-600 border border-gray-200 hover:bg-gray-50"
2908
+ )}
2909
+ >
2910
+ {inspectorEnabled ? "Disable" : "Enable"}
2911
+ </button>
2912
+ </div>
2913
+
2914
+ {/* Logo Tools Content */}
2915
+ <LogoToolsPanel {...logoToolsProps} />
2916
+ </div>
2917
+ );
2918
+ }
2919
+
2920
+ // ---- Text Panel ----
2921
+
2922
+ interface TextPanelProps {
2923
+ inspectorEnabled: boolean;
2924
+ onToggleInspector: () => void;
2925
+ taggedElements: DetectedElement[];
2926
+ }
2927
+
2928
+ function TextPanel({ inspectorEnabled, onToggleInspector, taggedElements }: TextPanelProps) {
2929
+ const textElements = taggedElements.filter((el) => el.type === "text");
2930
+
2931
+ return (
2932
+ <div className="space-y-4">
2933
+ {/* Inspector Toggle */}
2934
+ <div className="flex items-center justify-between p-3 rounded border border-gray-200 bg-gray-50">
2935
+ <div className="flex items-center gap-2">
2936
+ <Type className={cn("h-4 w-4", inspectorEnabled ? "text-purple-600" : "text-gray-400")} />
2937
+ <span id="text-panel-span-text-inspector" className="text-xs font-medium">Text Inspector</span>
2938
+ </div>
2939
+ <button
2940
+ onClick={onToggleInspector}
2941
+ className={cn(
2942
+ "px-3 py-1 text-xs font-medium rounded transition-colors",
2943
+ inspectorEnabled
2944
+ ? "bg-purple-600 text-white"
2945
+ : "bg-white text-gray-600 border border-gray-200 hover:bg-gray-50"
2946
+ )}
2947
+ >
2948
+ {inspectorEnabled ? "Disable" : "Enable"}
2949
+ </button>
2950
+ </div>
2951
+
2952
+ {/* Info about typography */}
2953
+ <div className="p-3 rounded border border-gray-200 bg-gray-50">
2954
+ <p id="text-panel-p" className="text-xs text-gray-500">
2955
+ Typography settings have been moved to <strong>Components → Design</strong> tab.
2956
+ </p>
2957
+ </div>
2958
+
2959
+ {/* Detected Text Elements */}
2960
+ {inspectorEnabled && textElements.length > 0 && (
2961
+ <Section title="Page Text Elements">
2962
+ <div className="space-y-1 max-h-48 overflow-y-auto">
2963
+ {textElements.slice(0, 20).map((el, index) => (
2964
+ <div
2965
+ key={el.textId || index}
2966
+ className="p-2 rounded border border-gray-200 bg-gray-50"
2967
+ >
2968
+ <div className="text-xs text-gray-700 truncate">{el.name}</div>
2969
+ </div>
2970
+ ))}
2971
+ {textElements.length > 20 && (
2972
+ <p id="section-p-textelementslength-2" className="text-xs text-gray-400 text-center">
2973
+ +{textElements.length - 20} more elements
2974
+ </p>
2975
+ )}
2976
+ </div>
2977
+ </Section>
2978
+ )}
2979
+
2980
+ {inspectorEnabled && textElements.length === 0 && (
2981
+ <div className="text-center py-6 text-gray-400">
2982
+ <Type className="h-8 w-8 mx-auto mb-2 opacity-50" />
2983
+ <p id="text-panel-p-no-text-elements-det" className="text-xs">No text elements detected on this page.</p>
2984
+ </div>
2985
+ )}
2986
+ </div>
2987
+ );
2988
+ }
2989
+
2990
+ // ---- Logo Tools Panel ----
2991
+
2992
+ type AutoFixStatus = "idle" | "fixing" | "success" | "error";
2993
+
2994
+ interface LogoToolsPanelProps {
2995
+ logoAssets: LogoAsset[];
2996
+ logoAssetsByBrand: Record<string, LogoAsset[]>;
2997
+ selectedLogoId: string | null;
2998
+ globalLogoConfig: LogoOverride;
2999
+ individualLogoConfigs: Record<string, LogoOverride>;
3000
+ originalLogoStates: Record<string, OriginalLogoState>;
3001
+ taggedElements: DetectedElement[];
3002
+ onGlobalConfigChange: (config: LogoOverride) => void;
3003
+ onIndividualConfigChange: (logoId: string, config: LogoOverride) => void;
3004
+ onSelectLogo: (logoId: string | null) => void;
3005
+ onResetAll: () => void;
3006
+ onResetLogo: (logoId: string) => void;
3007
+ onSaveChanges: (configOverride?: LogoOverride, brandId?: string, selector?: string, logoId?: string) => void;
3008
+ saveStatus: LogoSaveStatus;
3009
+ saveMessage: string;
3010
+ findComplementaryLogo: (path: string) => { light?: string; dark?: string } | null;
3011
+ currentTheme: string;
3012
+ onAutoFixId: (logoSrc: string, suggestedId: string) => Promise<{ success: boolean; error?: string }>;
3013
+ autoFixStatus: AutoFixStatus;
3014
+ autoFixMessage: string;
3015
+ }
3016
+
3017
+ /**
3018
+ * Generates a context-aware ID suggestion for a logo element based on its
3019
+ * position in the DOM, parent containers, and attributes.
3020
+ */
3021
+ function generateIdSuggestion(logoId: string, brandId?: string): string {
3022
+ // Try to get context from the DOM element
3023
+ const element = document.querySelector(`[data-sonance-logo-id="${logoId}"]`);
3024
+ if (!element) {
3025
+ return brandId ? `${brandId}-logo` : "brand-logo";
3026
+ }
3027
+
3028
+ // Check parent containers for context
3029
+ const parentNames: string[] = [];
3030
+ let current = element.parentElement;
3031
+ let depth = 0;
3032
+
3033
+ while (current && depth < 5) {
3034
+ // Check for semantic elements
3035
+ const tagName = current.tagName.toLowerCase();
3036
+ if (["header", "footer", "nav", "aside", "main", "section"].includes(tagName)) {
3037
+ parentNames.unshift(tagName);
3038
+ break;
3039
+ }
3040
+
3041
+ // Check for data-sonance-name attribute
3042
+ const sonanceName = current.getAttribute("data-sonance-name");
3043
+ if (sonanceName) {
3044
+ parentNames.unshift(sonanceName.toLowerCase().replace(/\s+/g, "-"));
3045
+ break;
3046
+ }
3047
+
3048
+ // Check for common class hints
3049
+ const classList = current.className;
3050
+ if (typeof classList === "string") {
3051
+ if (classList.includes("header")) parentNames.unshift("header");
3052
+ else if (classList.includes("footer")) parentNames.unshift("footer");
3053
+ else if (classList.includes("sidebar")) parentNames.unshift("sidebar");
3054
+ else if (classList.includes("nav")) parentNames.unshift("nav");
3055
+ }
3056
+
3057
+ current = current.parentElement;
3058
+ depth++;
3059
+ }
3060
+
3061
+ // Check alt text for context
3062
+ const altText = element.getAttribute("alt");
3063
+ if (altText) {
3064
+ const cleanAlt = altText.toLowerCase()
3065
+ .replace(/[^a-z0-9\s]/g, "")
3066
+ .replace(/\s+/g, "-")
3067
+ .substring(0, 20);
3068
+ if (cleanAlt && cleanAlt !== "logo") {
3069
+ parentNames.push(cleanAlt);
3070
+ }
3071
+ }
3072
+
3073
+ // Build the suggested ID
3074
+ const parts: string[] = [];
3075
+ if (parentNames.length > 0) {
3076
+ parts.push(parentNames[0]);
3077
+ }
3078
+ if (brandId) {
3079
+ parts.push(brandId);
3080
+ }
3081
+ parts.push("logo");
3082
+
3083
+ return parts.join("-");
3084
+ }
3085
+
3086
+ function LogoToolsPanel({
3087
+ logoAssets,
3088
+ logoAssetsByBrand,
3089
+ selectedLogoId,
3090
+ globalLogoConfig,
3091
+ individualLogoConfigs,
3092
+ originalLogoStates,
3093
+ taggedElements,
3094
+ onGlobalConfigChange,
3095
+ onIndividualConfigChange,
3096
+ onSelectLogo,
3097
+ onResetAll,
3098
+ onResetLogo,
3099
+ onSaveChanges,
3100
+ saveStatus,
3101
+ saveMessage,
3102
+ findComplementaryLogo,
3103
+ currentTheme,
3104
+ onAutoFixId,
3105
+ autoFixStatus,
3106
+ autoFixMessage,
3107
+ }: LogoToolsPanelProps) {
3108
+ const logoElements = taggedElements.filter((el) => el.type === "logo" && el.logoId);
3109
+ const selectedLogo = logoElements.find((el) => el.logoId === selectedLogoId);
3110
+ const selectedConfig = selectedLogoId ? individualLogoConfigs[selectedLogoId] || {} : {};
3111
+ const selectedOriginal = selectedLogoId ? originalLogoStates[selectedLogoId] : null;
3112
+
3113
+ // Attempt to identify the brand and element ID of the selected logo
3114
+ let selectedBrandId: string | undefined;
3115
+ let selectedElementId: string | undefined;
3116
+
3117
+ if (selectedLogoId) {
3118
+ // Check if the element has an ID attribute for targeted CSS
3119
+ const logoElement = document.querySelector(`[data-sonance-logo-id="${selectedLogoId}"]`);
3120
+ if (logoElement && logoElement.id) {
3121
+ selectedElementId = logoElement.id;
3122
+ }
3123
+ }
3124
+
3125
+ if (selectedOriginal && selectedOriginal.src) {
3126
+ // Try to find matching asset
3127
+ // Normalize paths for comparison (remove protocol/domain if present)
3128
+ const normalize = (p: string) => p.split("?")[0].split("#")[0]; // remove query/hash
3129
+ const originalPath = normalize(selectedOriginal.src);
3130
+
3131
+ // Find asset where path ends with the original src filename or vice versa
3132
+ const asset = logoAssets.find(a =>
3133
+ originalPath.endsWith(a.path) || a.path.endsWith(originalPath) || originalPath.includes(a.name)
3134
+ );
3135
+ if (asset) {
3136
+ selectedBrandId = asset.brand;
3137
+ } else {
3138
+ // Fallback: guess from path
3139
+ if (originalPath.toLowerCase().includes("sonance")) selectedBrandId = "sonance";
3140
+ else if (originalPath.toLowerCase().includes("iport")) selectedBrandId = "iport";
3141
+ else if (originalPath.toLowerCase().includes("blaze")) selectedBrandId = "blaze";
3142
+ }
3143
+ }
3144
+
3145
+ // Get sorted brand keys
3146
+ const brandKeys = Object.keys(logoAssetsByBrand).sort();
3147
+
3148
+ return (
3149
+ <div className="space-y-5">
3150
+ {/* Header info */}
3151
+ <div className="p-3 rounded border border-orange-200 bg-orange-50">
3152
+ <p id="analysis-modal-p" className="text-xs text-orange-700">
3153
+ <strong>{logoElements.length}</strong> logo{logoElements.length !== 1 ? "s" : ""} detected on this page.
3154
+ Click a logo on the page to select it for editing.
3155
+ </p>
3156
+ </div>
3157
+
3158
+ {/* Global Controls */}
3159
+ <Section title="Replace All Logos">
3160
+ <div className="space-y-3">
3161
+ {/* Theme indicator */}
3162
+ <div className="flex items-center gap-2 text-[10px] text-gray-400">
3163
+ <span id="section-span-currenttheme-dark-da" className={cn("px-1.5 py-0.5 rounded", currentTheme === "light" ? "bg-yellow-100 text-yellow-700" : "bg-gray-700 text-gray-200")}>
3164
+ {currentTheme === "dark" ? "🌙 Dark Mode" : "☀️ Light Mode"}
3165
+ </span>
3166
+ <span id="section-span-current-preview">Current preview</span>
3167
+ </div>
3168
+
3169
+ {/* Light Mode Logo */}
3170
+ <div className="space-y-1.5">
3171
+ <label className="text-xs text-gray-500 flex items-center gap-1.5">
3172
+ <Sun className="h-3 w-3 text-yellow-500" />
3173
+ Light Mode Logo:
3174
+ </label>
3175
+ <select
3176
+ value={globalLogoConfig.srcLight || ""}
3177
+ onChange={(e) => {
3178
+ const newPath = e.target.value || undefined;
3179
+ if (newPath) {
3180
+ // Auto-fill dark variant if available
3181
+ const complementary = findComplementaryLogo(newPath);
3182
+ onGlobalConfigChange({
3183
+ ...globalLogoConfig,
3184
+ srcLight: newPath,
3185
+ srcDark: complementary?.dark || globalLogoConfig.srcDark,
3186
+ });
3187
+ } else {
3188
+ onGlobalConfigChange({ ...globalLogoConfig, srcLight: undefined });
3189
+ }
3190
+ }}
3191
+ className={cn(
3192
+ "w-full h-8 px-2 text-xs rounded",
3193
+ "border border-gray-200 bg-white text-gray-700",
3194
+ "focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
3195
+ )}
3196
+ >
3197
+ <option value="">-- Select light mode logo --</option>
3198
+ {brandKeys.map((brand) => (
3199
+ <optgroup key={brand} label={brand.charAt(0).toUpperCase() + brand.slice(1)}>
3200
+ {logoAssetsByBrand[brand].map((asset) => (
3201
+ <option key={asset.id} value={asset.path}>
3202
+ {asset.name}
3203
+ </option>
3204
+ ))}
3205
+ </optgroup>
3206
+ ))}
3207
+ </select>
3208
+ </div>
3209
+
3210
+ {/* Dark Mode Logo */}
3211
+ <div className="space-y-1.5">
3212
+ <label className="text-xs text-gray-500 flex items-center gap-1.5">
3213
+ <Moon className="h-3 w-3 text-blue-400" />
3214
+ Dark Mode Logo:
3215
+ </label>
3216
+ <select
3217
+ value={globalLogoConfig.srcDark || ""}
3218
+ onChange={(e) => {
3219
+ const newPath = e.target.value || undefined;
3220
+ if (newPath) {
3221
+ // Auto-fill light variant if available
3222
+ const complementary = findComplementaryLogo(newPath);
3223
+ onGlobalConfigChange({
3224
+ ...globalLogoConfig,
3225
+ srcDark: newPath,
3226
+ srcLight: complementary?.light || globalLogoConfig.srcLight,
3227
+ });
3228
+ } else {
3229
+ onGlobalConfigChange({ ...globalLogoConfig, srcDark: undefined });
3230
+ }
3231
+ }}
3232
+ className={cn(
3233
+ "w-full h-8 px-2 text-xs rounded",
3234
+ "border border-gray-200 bg-white text-gray-700",
3235
+ "focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
3236
+ )}
3237
+ >
3238
+ <option value="">-- Select dark mode logo --</option>
3239
+ {brandKeys.map((brand) => (
3240
+ <optgroup key={brand} label={brand.charAt(0).toUpperCase() + brand.slice(1)}>
3241
+ {logoAssetsByBrand[brand].map((asset) => (
3242
+ <option key={asset.id} value={asset.path}>
3243
+ {asset.name}
3244
+ </option>
3245
+ ))}
3246
+ </optgroup>
3247
+ ))}
3248
+ </select>
3249
+ </div>
3250
+
3251
+ <div className="space-y-1.5">
3252
+ <label className="text-xs text-gray-500">Scale: {Math.round((globalLogoConfig.scale || 1) * 100)}%</label>
3253
+ <input
3254
+ type="range"
3255
+ min="25"
3256
+ max="200"
3257
+ value={(globalLogoConfig.scale || 1) * 100}
3258
+ onChange={(e) => onGlobalConfigChange({ ...globalLogoConfig, scale: parseInt(e.target.value) / 100 })}
3259
+ className="w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-200"
3260
+ />
3261
+ </div>
3262
+ </div>
3263
+ </Section>
3264
+
3265
+ {/* Save Status Message */}
3266
+ {saveMessage && (
3267
+ <div
3268
+ className={cn(
3269
+ "flex items-start gap-2 p-2 rounded text-xs",
3270
+ saveStatus === "success" && "bg-green-50 text-green-700 border border-green-200",
3271
+ saveStatus === "error" && "bg-red-50 text-red-700 border border-red-200"
3272
+ )}
3273
+ >
3274
+ {saveStatus === "success" && <CheckCircle className="h-3.5 w-3.5 shrink-0 mt-0.5" />}
3275
+ {saveStatus === "error" && <AlertCircle className="h-3.5 w-3.5 shrink-0 mt-0.5" />}
3276
+ <span id="analysis-modal-span-savemessage">{saveMessage}</span>
3277
+ </div>
3278
+ )}
3279
+
3280
+ {/* Save Button */}
3281
+ {(globalLogoConfig.reset || globalLogoConfig.srcLight || globalLogoConfig.srcDark) && (
3282
+ <button
3283
+ onClick={() => onSaveChanges()}
3284
+ disabled={saveStatus === "saving"}
3285
+ className={cn(
3286
+ "w-full flex items-center justify-center gap-2 py-2.5",
3287
+ "text-sm font-medium text-white rounded transition-colors",
3288
+ globalLogoConfig.reset ? "bg-amber-600 hover:bg-amber-700" : "bg-[#333F48] hover:bg-[#2a343c]",
3289
+ "disabled:opacity-50 disabled:cursor-not-allowed"
3290
+ )}
3291
+ >
3292
+ {saveStatus === "saving" ? (
3293
+ <>
3294
+ <Loader2 className="h-4 w-4 animate-spin" />
3295
+ Saving...
3296
+ </>
3297
+ ) : (
3298
+ <>
3299
+ {globalLogoConfig.reset ? <RotateCcw className="h-4 w-4" /> : <Save className="h-4 w-4" />}
3300
+ {globalLogoConfig.reset ? "Save Reset" : "Save to Brand System"}
3301
+ </>
3302
+ )}
3303
+ </button>
3304
+ )}
3305
+
3306
+ {/* Logo List */}
3307
+ <Section title="Page Logos">
3308
+ <div className="space-y-1.5 max-h-32 overflow-y-auto">
3309
+ {logoElements.length === 0 ? (
3310
+ <p id="section-p-no-logos-detected" className="text-xs text-gray-400 italic">No logos detected</p>
3311
+ ) : (
3312
+ logoElements.map((el) => (
3313
+ <button
3314
+ key={el.logoId}
3315
+ onClick={() => onSelectLogo(el.logoId === selectedLogoId ? null : el.logoId!)}
3316
+ className={cn(
3317
+ "w-full flex items-center justify-between px-3 py-2",
3318
+ "text-left text-xs rounded border transition-colors",
3319
+ el.logoId === selectedLogoId
3320
+ ? "border-[#FC4C02] bg-orange-50"
3321
+ : "border-gray-200 hover:bg-gray-50"
3322
+ )}
3323
+ >
3324
+ <div className="flex items-center gap-2">
3325
+ <ImageIcon className="h-3.5 w-3.5 text-gray-400" />
3326
+ <span id="analysis-modal-span-elname" className="font-medium text-gray-700 truncate max-w-[180px]">{el.name}</span>
3327
+ </div>
3328
+ {individualLogoConfigs[el.logoId!] && (
3329
+ <span id="analysis-modal-span-modified" className="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-600">
3330
+ Modified
3331
+ </span>
3332
+ )}
3333
+ </button>
3334
+ ))
3335
+ )}
3336
+ </div>
3337
+ </Section>
3338
+
3339
+ {/* Selected Logo Controls */}
3340
+ {selectedLogoId && selectedLogo && (
3341
+ <Section title={`Edit: ${selectedLogo.name}`}>
3342
+ <div className="space-y-3 p-3 rounded border border-[#FC4C02] bg-orange-50/50">
3343
+ {/* Original info */}
3344
+ {selectedOriginal && (
3345
+ <div className="text-[10px] text-gray-500">
3346
+ Original: {selectedOriginal.width} × {selectedOriginal.height}px
3347
+ </div>
3348
+ )}
3349
+
3350
+ {/* Theme indicator */}
3351
+ <div className="flex items-center gap-2 text-[10px] text-gray-400">
3352
+ <span id="analysis-modal-span-currenttheme-dark-da" className={cn("px-1.5 py-0.5 rounded", currentTheme === "light" ? "bg-yellow-100 text-yellow-700" : "bg-gray-700 text-gray-200")}>
3353
+ {currentTheme === "dark" ? "🌙 Dark Mode" : "☀️ Light Mode"}
3354
+ </span>
3355
+ <span id="analysis-modal-span-current-preview">Current preview</span>
3356
+ </div>
3357
+
3358
+ {/* Light Mode Logo */}
3359
+ <div className="space-y-1.5">
3360
+ <label className="text-xs text-gray-500 flex items-center gap-1.5">
3361
+ <Sun className="h-3 w-3 text-yellow-500" />
3362
+ Light Mode Logo:
3363
+ </label>
3364
+ <select
3365
+ value={selectedConfig.srcLight || ""}
3366
+ onChange={(e) => {
3367
+ const newPath = e.target.value || undefined;
3368
+ if (newPath) {
3369
+ const complementary = findComplementaryLogo(newPath);
3370
+ onIndividualConfigChange(selectedLogoId, {
3371
+ ...selectedConfig,
3372
+ srcLight: newPath,
3373
+ srcDark: complementary?.dark || selectedConfig.srcDark,
3374
+ });
3375
+ } else {
3376
+ onIndividualConfigChange(selectedLogoId, { ...selectedConfig, srcLight: undefined });
3377
+ }
3378
+ }}
3379
+ className={cn(
3380
+ "w-full h-8 px-2 text-xs rounded",
3381
+ "border border-gray-200 bg-white text-gray-700",
3382
+ "focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
3383
+ )}
3384
+ >
3385
+ <option value="">-- Use global/original --</option>
3386
+ {brandKeys.map((brand) => (
3387
+ <optgroup key={brand} label={brand.charAt(0).toUpperCase() + brand.slice(1)}>
3388
+ {logoAssetsByBrand[brand].map((asset) => (
3389
+ <option key={asset.id} value={asset.path}>
3390
+ {asset.name}
3391
+ </option>
3392
+ ))}
3393
+ </optgroup>
3394
+ ))}
3395
+ </select>
3396
+ </div>
3397
+
3398
+ {/* Dark Mode Logo */}
3399
+ <div className="space-y-1.5">
3400
+ <label className="text-xs text-gray-500 flex items-center gap-1.5">
3401
+ <Moon className="h-3 w-3 text-blue-400" />
3402
+ Dark Mode Logo:
3403
+ </label>
3404
+ <select
3405
+ value={selectedConfig.srcDark || ""}
3406
+ onChange={(e) => {
3407
+ const newPath = e.target.value || undefined;
3408
+ if (newPath) {
3409
+ const complementary = findComplementaryLogo(newPath);
3410
+ onIndividualConfigChange(selectedLogoId, {
3411
+ ...selectedConfig,
3412
+ srcDark: newPath,
3413
+ srcLight: complementary?.light || selectedConfig.srcLight,
3414
+ });
3415
+ } else {
3416
+ onIndividualConfigChange(selectedLogoId, { ...selectedConfig, srcDark: undefined });
3417
+ }
3418
+ }}
3419
+ className={cn(
3420
+ "w-full h-8 px-2 text-xs rounded",
3421
+ "border border-gray-200 bg-white text-gray-700",
3422
+ "focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
3423
+ )}
3424
+ >
3425
+ <option value="">-- Use global/original --</option>
3426
+ {brandKeys.map((brand) => (
3427
+ <optgroup key={brand} label={brand.charAt(0).toUpperCase() + brand.slice(1)}>
3428
+ {logoAssetsByBrand[brand].map((asset) => (
3429
+ <option key={asset.id} value={asset.path}>
3430
+ {asset.name}
3431
+ </option>
3432
+ ))}
3433
+ </optgroup>
3434
+ ))}
3435
+ </select>
3436
+ </div>
3437
+
3438
+ {/* Dimensions */}
3439
+ <div className="grid grid-cols-2 gap-2">
3440
+ <div className="space-y-1">
3441
+ <label className="text-xs text-gray-500">Width (px)</label>
3442
+ <input
3443
+ type="number"
3444
+ min="10"
3445
+ max="1000"
3446
+ value={selectedConfig.width || ""}
3447
+ placeholder="auto"
3448
+ onChange={(e) => onIndividualConfigChange(selectedLogoId, {
3449
+ ...selectedConfig,
3450
+ width: e.target.value ? parseInt(e.target.value) : undefined
3451
+ })}
3452
+ className={cn(
3453
+ "w-full h-8 px-2 text-xs rounded",
3454
+ "border border-gray-200 bg-white text-gray-700",
3455
+ "focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
3456
+ )}
3457
+ />
3458
+ </div>
3459
+ <div className="space-y-1">
3460
+ <label className="text-xs text-gray-500">Height (px)</label>
3461
+ <input
3462
+ type="number"
3463
+ min="10"
3464
+ max="1000"
3465
+ value={selectedConfig.height || ""}
3466
+ placeholder="auto"
3467
+ onChange={(e) => onIndividualConfigChange(selectedLogoId, {
3468
+ ...selectedConfig,
3469
+ height: e.target.value ? parseInt(e.target.value) : undefined
3470
+ })}
3471
+ className={cn(
3472
+ "w-full h-8 px-2 text-xs rounded",
3473
+ "border border-gray-200 bg-white text-gray-700",
3474
+ "focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
3475
+ )}
3476
+ />
3477
+ </div>
3478
+ </div>
3479
+
3480
+ {/* Scale */}
3481
+ <div className="space-y-1.5">
3482
+ <label className="text-xs text-gray-500">Scale: {Math.round((selectedConfig.scale || 1) * 100)}%</label>
3483
+ <input
3484
+ type="range"
3485
+ min="25"
3486
+ max="200"
3487
+ value={(selectedConfig.scale || 1) * 100}
3488
+ onChange={(e) => onIndividualConfigChange(selectedLogoId, { ...selectedConfig, scale: parseInt(e.target.value) / 100 })}
3489
+ className="w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-200"
3490
+ />
3491
+ </div>
3492
+
3493
+ {/* Warning if element has no ID for individual persistence */}
3494
+ {!selectedElementId && (selectedConfig.width || selectedConfig.height || selectedConfig.scale) && (() => {
3495
+ const suggestedId = generateIdSuggestion(selectedLogoId, selectedBrandId);
3496
+ const codeSnippet = `id="${suggestedId}"`;
3497
+ const logoSrc = selectedOriginal?.src || "";
3498
+
3499
+ return (
3500
+ <div className="p-2.5 rounded border border-amber-200 bg-amber-50 space-y-2">
3501
+ <p id="analysis-modal-p" className="text-[10px] text-amber-700">
3502
+ <strong>⚠️ No ID found.</strong> Saving will update the <strong>global {selectedBrandId || "brand"} default</strong>, affecting all logos of this brand.
3503
+ </p>
3504
+ <div className="flex items-center gap-2">
3505
+ <code className="flex-1 px-2 py-1 text-[10px] bg-amber-100 text-amber-800 rounded font-mono truncate" title={codeSnippet}>
3506
+ {codeSnippet}
3507
+ </code>
3508
+ <button
3509
+ onClick={() => onAutoFixId(logoSrc, suggestedId)}
3510
+ disabled={autoFixStatus === "fixing"}
3511
+ className={cn(
3512
+ "flex items-center gap-1 px-2 py-1 text-[10px] font-medium rounded transition-colors whitespace-nowrap",
3513
+ autoFixStatus === "success"
3514
+ ? "bg-green-100 text-green-700"
3515
+ : autoFixStatus === "error"
3516
+ ? "bg-red-100 text-red-700"
3517
+ : "text-amber-700 bg-amber-100 hover:bg-amber-200",
3518
+ autoFixStatus === "fixing" && "opacity-50 cursor-not-allowed"
3519
+ )}
3520
+ title="Automatically inject this ID into your source code"
3521
+ >
3522
+ {autoFixStatus === "fixing" ? (
3523
+ <>
3524
+ <Loader2 className="h-3 w-3 animate-spin" />
3525
+ Fixing...
3526
+ </>
3527
+ ) : autoFixStatus === "success" ? (
3528
+ <>
3529
+ <Check className="h-3 w-3" />
3530
+ Fixed!
3531
+ </>
3532
+ ) : autoFixStatus === "error" ? (
3533
+ <>
3534
+ <AlertCircle className="h-3 w-3" />
3535
+ Failed
3536
+ </>
3537
+ ) : (
3538
+ <>
3539
+ <Wand2 className="h-3 w-3" />
3540
+ Auto-Fix
3541
+ </>
3542
+ )}
3543
+ </button>
3544
+ </div>
3545
+ {autoFixMessage && (
3546
+ <p id="analysis-modal-p-autofixmessage" className={cn(
3547
+ "text-[9px]",
3548
+ autoFixStatus === "success" ? "text-green-600" : autoFixStatus === "error" ? "text-red-600" : "text-amber-600"
3549
+ )}>
3550
+ {autoFixMessage}
3551
+ </p>
3552
+ )}
3553
+ {!autoFixMessage && (
3554
+ <p id="analysis-modal-p-click-autofix-to-inj" className="text-[9px] text-amber-600">
3555
+ Click Auto-Fix to inject this ID into your source code automatically.
3556
+ </p>
3557
+ )}
3558
+ </div>
3559
+ );
3560
+ })()}
3561
+
3562
+ {/* Reset this logo */}
3563
+ <div className="flex gap-2">
3564
+ <button
3565
+ onClick={() => {
3566
+ if (selectedLogoId) {
3567
+ onResetLogo(selectedLogoId);
3568
+ }
3569
+ }}
3570
+ className={cn(
3571
+ "flex-1 flex items-center justify-center gap-2 py-2",
3572
+ "text-xs font-medium text-gray-500 hover:text-gray-700",
3573
+ "border border-gray-200 rounded hover:bg-white transition-colors"
3574
+ )}
3575
+ >
3576
+ <RotateCcw className="h-3 w-3" />
3577
+ Reset
3578
+ </button>
3579
+
3580
+ {(selectedConfig.reset || selectedConfig.srcLight || selectedConfig.srcDark || selectedConfig.src || selectedConfig.width || selectedConfig.height || (selectedConfig.scale && selectedConfig.scale !== 1)) && (
3581
+ <button
3582
+ onClick={() => onSaveChanges(selectedConfig, selectedBrandId, selectedElementId ? `#${selectedElementId}` : undefined, selectedLogoId || undefined)}
3583
+ disabled={saveStatus === "saving"}
3584
+ className={cn(
3585
+ "flex-1 flex items-center justify-center gap-2 py-2",
3586
+ "text-xs font-medium text-white rounded transition-colors",
3587
+ selectedConfig.reset ? "bg-amber-600 hover:bg-amber-700" : (selectedElementId ? "bg-[#00A3E1] hover:bg-[#0090c8]" : "bg-[#333F48] hover:bg-[#2a343c]"),
3588
+ "disabled:opacity-50 disabled:cursor-not-allowed"
3589
+ )}
3590
+ title={selectedConfig.reset ? "Save Reset (Delete Override)" : (selectedElementId ? `Save to element #${selectedElementId}` : (selectedBrandId ? `Save as ${selectedBrandId} brand default` : "Save as brand default"))}
3591
+ >
3592
+ {saveStatus === "saving" ? (
3593
+ <Loader2 className="h-3 w-3 animate-spin" />
3594
+ ) : (
3595
+ <>
3596
+ {selectedConfig.reset ? <RotateCcw className="h-3 w-3" /> : <Save className="h-3 w-3" />}
3597
+ {selectedConfig.reset ? "Save Reset" : (selectedElementId ? "Save to Element" : "Save to Brand")}
3598
+ </>
3599
+ )}
3600
+ </button>
3601
+ )}
3602
+ </div>
3603
+
3604
+ {/* Individual Save Status Message */}
3605
+ {saveMessage && (saveStatus === "success" || saveStatus === "error") && (
3606
+ <div
3607
+ className={cn(
3608
+ "flex items-center gap-2 p-2 rounded text-xs mt-2",
3609
+ saveStatus === "success" && "bg-green-50 text-green-700 border border-green-200",
3610
+ saveStatus === "error" && "bg-red-50 text-red-700 border border-red-200"
3611
+ )}
3612
+ >
3613
+ {saveStatus === "success" && <CheckCircle className="h-3.5 w-3.5 shrink-0" />}
3614
+ {saveStatus === "error" && <AlertCircle className="h-3.5 w-3.5 shrink-0" />}
3615
+ <span id="text-panel-span-savemessage">{saveMessage}</span>
3616
+ </div>
3617
+ )}
3618
+ </div>
3619
+ </Section>
3620
+ )}
3621
+
3622
+ {/* Reset All */}
3623
+ <button
3624
+ onClick={onResetAll}
3625
+ className={cn(
3626
+ "w-full flex items-center justify-center gap-2 py-2.5",
3627
+ "text-xs font-medium text-gray-500 hover:text-gray-700",
3628
+ "border border-gray-200 rounded hover:bg-gray-50 transition-colors"
3629
+ )}
3630
+ >
3631
+ <RotateCcw className="h-3.5 w-3.5" />
3632
+ Reset All Logos
3633
+ </button>
3634
+ </div>
3635
+ );
3636
+ }
3637
+
3638
+ // ---- Theme Tab ----
3639
+
3640
+ interface ThemeTabProps {
3641
+ config: ThemeConfig;
3642
+ updateConfig: (updates: Partial<ThemeConfig>) => void;
3643
+ onBrandSelect: (brandId: BrandId) => void;
3644
+ onReset: () => void;
3645
+ }
3646
+
3647
+ function ThemeTab({ config, updateConfig, onBrandSelect, onReset }: ThemeTabProps) {
3648
+ // Find current brand
3649
+ const currentBrand = brandPresets.find(
3650
+ (p) =>
3651
+ p.config.baseColor.toLowerCase() === config.baseColor.toLowerCase() &&
3652
+ p.config.accentColor.toLowerCase() === config.accentColor.toLowerCase()
3653
+ );
3654
+
3655
+ return (
3656
+ <div className="space-y-5">
3657
+ {/* Brand Presets */}
3658
+ <Section title="Brand">
3659
+ <div className="flex gap-1">
3660
+ {brandPresets.map((preset) => (
3661
+ <button
3662
+ key={preset.id}
3663
+ onClick={() => onBrandSelect(preset.id)}
3664
+ className={cn(
3665
+ "flex-1 py-2 px-2 text-xs font-medium rounded transition-colors",
3666
+ "border",
3667
+ currentBrand?.id === preset.id
3668
+ ? "bg-[#333F48] text-white border-[#333F48]"
3669
+ : "bg-white text-gray-600 border-gray-200 hover:bg-gray-50"
3670
+ )}
3671
+ >
3672
+ {preset.name}
3673
+ </button>
3674
+ ))}
3675
+ </div>
3676
+ </Section>
3677
+
3678
+ {/* Colors */}
3679
+ <Section title="Primary Color">
3680
+ <div className="flex flex-wrap gap-2">
3681
+ {colorPresets.map((preset, index) => (
3682
+ <ColorSwatch
3683
+ key={`base-${index}`}
3684
+ color={preset.value}
3685
+ name={preset.name}
3686
+ selected={config.baseColor.toLowerCase() === preset.value.toLowerCase()}
3687
+ onClick={() => updateConfig({ baseColor: preset.value })}
3688
+ />
3689
+ ))}
3690
+ </div>
3691
+ </Section>
3692
+
3693
+ <Section title="Accent Color">
3694
+ <div className="flex flex-wrap gap-2">
3695
+ {colorPresets.map((preset, index) => (
3696
+ <ColorSwatch
3697
+ key={`accent-${index}`}
3698
+ color={preset.value}
3699
+ name={preset.name}
3700
+ selected={config.accentColor.toLowerCase() === preset.value.toLowerCase()}
3701
+ onClick={() => updateConfig({ accentColor: preset.value })}
3702
+ />
3703
+ ))}
3704
+ </div>
3705
+ </Section>
3706
+
3707
+ {/* Radius */}
3708
+ <Section title="Border Radius">
3709
+ <div className="flex gap-1">
3710
+ {(Object.keys(radiusValues) as Array<keyof typeof radiusValues>).map(
3711
+ (radius) => (
3712
+ <button
3713
+ key={radius}
3714
+ onClick={() => updateConfig({ radius })}
3715
+ className={cn(
3716
+ "flex-1 h-8 text-xs font-medium rounded transition-colors border",
3717
+ config.radius === radius
3718
+ ? "bg-[#333F48] text-white border-[#333F48]"
3719
+ : "bg-white text-gray-600 border-gray-200 hover:bg-gray-50"
3720
+ )}
3721
+ >
3722
+ {radius === "none" ? "0" : radius}
3723
+ </button>
3724
+ )
3725
+ )}
3726
+ </div>
3727
+ </Section>
3728
+
3729
+ {/* Typography */}
3730
+ <Section title="Typography">
3731
+ <div className="space-y-2">
3732
+ <SelectField
3733
+ label="Heading Weight"
3734
+ value={String(config.headingWeight)}
3735
+ options={[
3736
+ { value: "400", label: "Regular" },
3737
+ { value: "500", label: "Medium" },
3738
+ { value: "600", label: "Semibold" },
3739
+ { value: "700", label: "Bold" },
3740
+ ]}
3741
+ onChange={(v) =>
3742
+ updateConfig({
3743
+ headingWeight: parseInt(v) as ThemeConfig["headingWeight"],
3744
+ })
3745
+ }
3746
+ />
3747
+ <SelectField
3748
+ label="Body Weight"
3749
+ value={String(config.bodyWeight)}
3750
+ options={[
3751
+ { value: "300", label: "Light" },
3752
+ { value: "400", label: "Regular" },
3753
+ { value: "500", label: "Medium" },
3754
+ ]}
3755
+ onChange={(v) =>
3756
+ updateConfig({
3757
+ bodyWeight: parseInt(v) as ThemeConfig["bodyWeight"],
3758
+ })
3759
+ }
3760
+ />
3761
+ <SelectField
3762
+ label="Scale"
3763
+ value={config.typographyScale}
3764
+ options={[
3765
+ { value: "compact", label: "Compact" },
3766
+ { value: "default", label: "Default" },
3767
+ { value: "large", label: "Large" },
3768
+ ]}
3769
+ onChange={(v) =>
3770
+ updateConfig({
3771
+ typographyScale: v as ThemeConfig["typographyScale"],
3772
+ })
3773
+ }
3774
+ />
3775
+ </div>
3776
+ </Section>
3777
+
3778
+ {/* Reset */}
3779
+ <button
3780
+ onClick={onReset}
3781
+ className={cn(
3782
+ "w-full flex items-center justify-center gap-2 py-2.5",
3783
+ "text-xs font-medium text-gray-500 hover:text-gray-700",
3784
+ "border border-gray-200 rounded hover:bg-gray-50 transition-colors"
3785
+ )}
3786
+ >
3787
+ <RotateCcw className="h-3.5 w-3.5" />
3788
+ Reset to Default
3789
+ </button>
3790
+ </div>
3791
+ );
3792
+ }
3793
+
3794
+ // ---- Components Tab ----
3795
+
3796
+ interface ComponentsTabProps {
3797
+ copiedId: string | null;
3798
+ onCopy: (text: string, id: string) => void;
3799
+ installedComponents: string[];
3800
+ }
3801
+
3802
+ type TagStatus = "idle" | "tagging" | "success" | "error";
3803
+
3804
+ function ComponentsTab({ copiedId, onCopy, installedComponents }: ComponentsTabProps) {
3805
+ const [tagStatus] = useState<TagStatus>("idle");
3806
+ const [tagMessage] = useState<string>("");
3807
+ const [tagStats, setTagStats] = useState<{ tagged: number; untagged: number } | null>(null);
3808
+
3809
+ // Check tagging status on mount using the unified analyze API
3810
+ useEffect(() => {
3811
+ async function checkTagStatus() {
3812
+ try {
3813
+ const response = await fetch("/api/sonance-analyze");
3814
+ if (response.ok) {
3815
+ const data = await response.json();
3816
+ const defCategory = data.summary?.byCategory?.definition;
3817
+ if (defCategory) {
3818
+ setTagStats({ tagged: defCategory.withId || 0, untagged: defCategory.missingId || 0 });
3819
+ }
3820
+ }
3821
+ } catch {
3822
+ // API might not exist yet
3823
+ }
3824
+ }
3825
+ checkTagStatus();
3826
+ }, []);
3827
+
3828
+ // Group snippets by category
3829
+ const snippetsByCategory = componentSnippets.reduce(
3830
+ (acc, snippet) => {
3831
+ if (!acc[snippet.category]) {
3832
+ acc[snippet.category] = [];
3833
+ }
3834
+ acc[snippet.category].push(snippet);
3835
+ return acc;
3836
+ },
3837
+ {} as Record<string, typeof componentSnippets>
3838
+ );
3839
+
3840
+ // Sort categories
3841
+ const sortedCategories = Object.keys(snippetsByCategory).sort();
3842
+
3843
+ // Sort snippets within categories
3844
+ sortedCategories.forEach(cat => {
3845
+ snippetsByCategory[cat].sort((a, b) => a.name.localeCompare(b.name));
3846
+ });
3847
+
3848
+ return (
3849
+ <div className="space-y-4">
3850
+ <div className="space-y-2">
3851
+ <p id="analysis-modal-p-click-to-copy-branda" className="text-xs text-gray-500">
3852
+ Click to copy brand-approved component code.
3853
+ </p>
3854
+ {installedComponents.length > 0 && (
3855
+ <div className="flex items-center gap-1.5 text-xs text-gray-400">
3856
+ <span id="analysis-modal-span" className="flex h-4 w-4 items-center justify-center rounded-full bg-green-100">
3857
+ <Check className="h-2.5 w-2.5 text-green-600" />
3858
+ </span>
3859
+ <span id="analysis-modal-span--installed-in-your-p">= Installed in your project</span>
3860
+ </div>
3861
+ )}
3862
+ </div>
3863
+
3864
+ {/* Visual Inspector tags note - now handled in Analysis tab */}
3865
+ {tagStats && tagStats.untagged > 0 && (
3866
+ <div className="p-3 rounded border border-blue-200 bg-blue-50">
3867
+ <p id="analysis-modal-p" className="text-xs text-blue-700">
3868
+ <strong>{tagStats.untagged}</strong> component definitions need tagging.
3869
+ Use the <strong>Analysis</strong> tab to auto-tag them for the Visual Inspector.
3870
+ </p>
3871
+ </div>
3872
+ )}
3873
+
3874
+ {/* Tag Status Message */}
3875
+ {tagMessage && (
3876
+ <div
3877
+ className={cn(
3878
+ "flex items-start gap-2 p-2 rounded text-xs",
3879
+ tagStatus === "success" && "bg-green-50 text-green-700 border border-green-200",
3880
+ tagStatus === "error" && "bg-red-50 text-red-700 border border-red-200"
3881
+ )}
3882
+ >
3883
+ {tagStatus === "success" && <CheckCircle className="h-3.5 w-3.5 shrink-0 mt-0.5" />}
3884
+ {tagStatus === "error" && <AlertCircle className="h-3.5 w-3.5 shrink-0 mt-0.5" />}
3885
+ <span id="analysis-modal-span-tagmessage">{tagMessage}</span>
3886
+ </div>
3887
+ )}
3888
+ {sortedCategories.map((category) => (
3889
+ <div key={category} className="space-y-2">
3890
+ <h3 id="analysis-modal-h3-category" className="text-xs font-medium uppercase tracking-wide text-gray-400">
3891
+ {category}
3892
+ </h3>
3893
+ <div className="space-y-1.5">
3894
+ {snippetsByCategory[category].map((snippet) => {
3895
+ const isInstalled = snippet.fileName && installedComponents.includes(snippet.fileName);
3896
+ return (
3897
+ <button
3898
+ key={snippet.id}
3899
+ onClick={() => onCopy(snippet.code, snippet.id)}
3900
+ className={cn(
3901
+ "w-full flex items-center justify-between px-3 py-2.5",
3902
+ "text-left text-sm rounded border",
3903
+ isInstalled ? "border-green-200 bg-green-50/50" : "border-gray-200",
3904
+ "hover:bg-gray-50 transition-colors group"
3905
+ )}
3906
+ >
3907
+ <div className="flex items-center gap-2">
3908
+ {isInstalled && (
3909
+ <span id="analysis-modal-span" className="flex h-5 w-5 items-center justify-center rounded-full bg-green-100" title="Installed">
3910
+ <Check className="h-3 w-3 text-green-600" />
3911
+ </span>
3912
+ )}
3913
+ <div>
3914
+ <p id="analysis-modal-p-snippetname" className="font-medium text-gray-700">{snippet.name}</p>
3915
+ <p id="analysis-modal-p-snippetdescription" className="text-xs text-gray-400">{snippet.description}</p>
3916
+ </div>
3917
+ </div>
3918
+ {copiedId === snippet.id ? (
3919
+ <Check className="h-4 w-4 text-green-500 shrink-0" />
3920
+ ) : (
3921
+ <Copy className="h-4 w-4 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
3922
+ )}
3923
+ </button>
3924
+ );
3925
+ })}
3926
+ </div>
3927
+ </div>
3928
+ ))}
3929
+ </div>
3930
+ );
3931
+ }
3932
+
3933
+ // ---- Review Tab ----
3934
+
3935
+ interface ReviewTabProps {
3936
+ config: ThemeConfig;
3937
+ copiedId: string | null;
3938
+ onCopy: (text: string, id: string) => void;
3939
+ onSave: () => void;
3940
+ saveStatus: SaveStatus;
3941
+ saveMessage: string;
3942
+ }
3943
+
3944
+ function ReviewTab({ config, copiedId, onCopy, onSave, saveStatus, saveMessage }: ReviewTabProps) {
3945
+ const cssOutput = generateThemeCSS(config);
3946
+
3947
+ // Find matching brand preset
3948
+ const matchingBrand = brandPresets.find(
3949
+ (p) =>
3950
+ p.config.baseColor.toLowerCase() === config.baseColor.toLowerCase() &&
3951
+ p.config.accentColor.toLowerCase() === config.accentColor.toLowerCase()
3952
+ );
3953
+
3954
+ // Find color names
3955
+ const primaryColorName = colorPresets.find(
3956
+ (c) => c.value.toLowerCase() === config.baseColor.toLowerCase()
3957
+ )?.name || config.baseColor;
3958
+
3959
+ const accentColorName = colorPresets.find(
3960
+ (c) => c.value.toLowerCase() === config.accentColor.toLowerCase()
3961
+ )?.name || config.accentColor;
3962
+
3963
+ return (
3964
+ <div className="space-y-4">
3965
+ <p id="review-tab-p-review-your-theme-co" className="text-xs text-gray-500">
3966
+ Review your theme configuration and apply it to your project.
3967
+ </p>
3968
+
3969
+ {/* Configuration Summary */}
3970
+ <div className="space-y-2">
3971
+ <h3 id="review-tab-h3-configuration-summar" className="text-xs font-medium uppercase tracking-wide text-gray-400">
3972
+ Configuration Summary
3973
+ </h3>
3974
+ <div className="rounded border border-gray-200 divide-y divide-gray-200">
3975
+ <SummaryRow label="Brand" value={matchingBrand?.name || "Custom"} />
3976
+ <SummaryRow
3977
+ label="Primary Color"
3978
+ value={primaryColorName}
3979
+ color={config.baseColor}
3980
+ />
3981
+ <SummaryRow
3982
+ label="Accent Color"
3983
+ value={accentColorName}
3984
+ color={config.accentColor}
3985
+ />
3986
+ <SummaryRow label="Border Radius" value={config.radius} />
3987
+ <SummaryRow label="Heading Weight" value={String(config.headingWeight)} />
3988
+ <SummaryRow label="Body Weight" value={String(config.bodyWeight)} />
3989
+ <SummaryRow label="Typography Scale" value={config.typographyScale} />
3990
+ <SummaryRow label="Spacing" value={config.spacing} />
3991
+ </div>
3992
+ </div>
3993
+
3994
+ {/* Files to be updated */}
3995
+ <div className="space-y-2">
3996
+ <h3 id="review-tab-h3-files-to-update" className="text-xs font-medium uppercase tracking-wide text-gray-400">
3997
+ Files to Update
3998
+ </h3>
3999
+ <div className="rounded border border-gray-200 bg-gray-50 p-3 text-xs space-y-1">
4000
+ <p id="review-tab-p-srcthemesonancetheme" className="font-mono text-gray-600">src/theme/sonance-theme.css</p>
4001
+ <p id="review-tab-p-srcthemesonanceconfi" className="font-mono text-gray-600">src/theme/sonance-config.json</p>
4002
+ </div>
4003
+ </div>
4004
+
4005
+ {/* Status Message */}
4006
+ {saveMessage && (
4007
+ <div
4008
+ className={cn(
4009
+ "flex items-start gap-2 p-3 rounded text-xs",
4010
+ saveStatus === "success" && "bg-green-50 text-green-700 border border-green-200",
4011
+ saveStatus === "error" && "bg-red-50 text-red-700 border border-red-200"
4012
+ )}
4013
+ >
4014
+ {saveStatus === "success" && <CheckCircle className="h-4 w-4 shrink-0 mt-0.5" />}
4015
+ {saveStatus === "error" && <AlertCircle className="h-4 w-4 shrink-0 mt-0.5" />}
4016
+ <span id="review-tab-span-savemessage">{saveMessage}</span>
4017
+ </div>
4018
+ )}
4019
+
4020
+ {/* Apply Button */}
4021
+ <button
4022
+ onClick={onSave}
4023
+ disabled={saveStatus === "saving"}
4024
+ className={cn(
4025
+ "w-full flex items-center justify-center gap-2 py-3",
4026
+ "text-sm font-medium text-white rounded transition-colors",
4027
+ "bg-[#333F48] hover:bg-[#2a343c]",
4028
+ "disabled:opacity-50 disabled:cursor-not-allowed"
4029
+ )}
4030
+ >
4031
+ {saveStatus === "saving" ? (
4032
+ <>
4033
+ <Loader2 className="h-4 w-4 animate-spin" />
4034
+ Saving...
4035
+ </>
4036
+ ) : (
4037
+ <>
4038
+ <Save className="h-4 w-4" />
4039
+ Apply Changes
4040
+ </>
4041
+ )}
4042
+ </button>
4043
+
4044
+ {/* CSS Preview (Collapsible) */}
4045
+ <details className="group">
4046
+ <summary className="flex items-center justify-between cursor-pointer text-xs font-medium uppercase tracking-wide text-gray-400 hover:text-gray-600">
4047
+ <span id="review-tab-span-css-preview">CSS Preview</span>
4048
+ <ChevronDown className="h-3 w-3 transition-transform group-open:rotate-180" />
4049
+ </summary>
4050
+ <div className="mt-2 space-y-2">
4051
+ <div className="flex justify-end">
4052
+ <button
4053
+ onClick={() => onCopy(`:root {\n${cssOutput}\n}`, "css")}
4054
+ className={cn(
4055
+ "flex items-center gap-1 px-2 py-1 text-xs rounded",
4056
+ "border border-gray-200 hover:bg-gray-50 transition-colors"
4057
+ )}
4058
+ >
4059
+ {copiedId === "css" ? (
4060
+ <>
4061
+ <Check className="h-3 w-3 text-green-500" />
4062
+ Copied
4063
+ </>
4064
+ ) : (
4065
+ <>
4066
+ <Copy className="h-3 w-3" />
4067
+ Copy
4068
+ </>
4069
+ )}
4070
+ </button>
4071
+ </div>
4072
+ <pre className="p-3 bg-gray-50 rounded border border-gray-200 text-xs overflow-x-auto max-h-48">
4073
+ <code>{`:root {\n${cssOutput}\n}`}</code>
4074
+ </pre>
4075
+ </div>
4076
+ </details>
4077
+ </div>
4078
+ );
4079
+ }
4080
+
4081
+ // ---- Summary Row Component ----
4082
+
4083
+ function SummaryRow({
4084
+ label,
4085
+ value,
4086
+ color
4087
+ }: {
4088
+ label: string;
4089
+ value: string;
4090
+ color?: string;
4091
+ }) {
4092
+ return (
4093
+ <div className="flex items-center justify-between px-3 py-2">
4094
+ <span id="summary-row-span-label" className="text-xs text-gray-500">{label}</span>
4095
+ <div className="flex items-center gap-2">
4096
+ {color && (
4097
+ <span
4098
+ id="summary-row-span"
4099
+ className="h-4 w-4 rounded-full border border-gray-200"
4100
+ style={{ backgroundColor: color }}
4101
+ />
4102
+ )}
4103
+ <span id="summary-row-span-value" className="text-xs font-medium text-gray-700">{value}</span>
4104
+ </div>
4105
+ </div>
4106
+ );
4107
+ }
4108
+
4109
+ // ---- Utility Components ----
4110
+
4111
+ function Section({
4112
+ title,
4113
+ children,
4114
+ }: {
4115
+ title: string;
4116
+ children: React.ReactNode;
4117
+ }) {
4118
+ return (
4119
+ <div className="space-y-2">
4120
+ <h3 id="section-h3-title" className="text-xs font-medium uppercase tracking-wide text-gray-400">
4121
+ {title}
4122
+ </h3>
4123
+ {children}
4124
+ </div>
4125
+ );
4126
+ }
4127
+
4128
+ function ColorSwatch({
4129
+ color,
4130
+ name,
4131
+ selected,
4132
+ onClick,
4133
+ }: {
4134
+ color: string;
4135
+ name: string;
4136
+ selected: boolean;
4137
+ onClick: () => void;
4138
+ }) {
4139
+ return (
4140
+ <button
4141
+ onClick={onClick}
4142
+ className={cn(
4143
+ "relative h-7 w-7 rounded-full transition-all duration-150",
4144
+ "focus:outline-none focus:ring-2 focus:ring-[#00A3E1] focus:ring-offset-1",
4145
+ selected && "ring-2 ring-[#333F48] ring-offset-2"
4146
+ )}
4147
+ style={{ backgroundColor: color }}
4148
+ aria-label={name}
4149
+ title={name}
4150
+ >
4151
+ {selected && (
4152
+ <Check
4153
+ className="absolute inset-0 m-auto h-3.5 w-3.5"
4154
+ style={{
4155
+ color: isLightColor(color) ? "#000" : "#fff",
4156
+ }}
4157
+ />
4158
+ )}
4159
+ </button>
4160
+ );
4161
+ }
4162
+
4163
+ function SelectField({
4164
+ label,
4165
+ value,
4166
+ options,
4167
+ onChange,
4168
+ }: {
4169
+ label: string;
4170
+ value: string;
4171
+ options: { value: string; label: string }[];
4172
+ onChange: (value: string) => void;
4173
+ }) {
4174
+ return (
4175
+ <div className="flex items-center justify-between">
4176
+ <span id="select-field-span-label" className="text-xs text-gray-500">{label}</span>
4177
+ <div className="relative">
4178
+ <select
4179
+ value={value}
4180
+ onChange={(e) => onChange(e.target.value)}
4181
+ className={cn(
4182
+ "h-7 pl-2 pr-7 text-xs font-medium rounded",
4183
+ "border border-gray-200 bg-white text-gray-700",
4184
+ "focus:outline-none focus:ring-1 focus:ring-[#00A3E1]",
4185
+ "cursor-pointer appearance-none"
4186
+ )}
4187
+ >
4188
+ {options.map((opt) => (
4189
+ <option key={opt.value} value={opt.value}>
4190
+ {opt.label}
4191
+ </option>
4192
+ ))}
4193
+ </select>
4194
+ <ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 h-3 w-3 text-gray-400 pointer-events-none" />
4195
+ </div>
4196
+ </div>
4197
+ );
4198
+ }
4199
+
4200
+ export default SonanceDevTools;
4201
+