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.
- package/dist/assets/api/sonance-analyze/route.ts +1116 -0
- package/dist/assets/api/sonance-assets/route.ts +113 -0
- package/dist/assets/api/sonance-components/route.ts +41 -0
- package/dist/assets/api/sonance-inject-id/route.ts +363 -0
- package/dist/assets/api/sonance-save-logo/route.ts +426 -0
- package/dist/assets/api/sonance-theme/route.ts +106 -0
- package/dist/assets/brand-system.ts +1265 -0
- package/dist/assets/components/accordion.stories.tsx +26 -26
- package/dist/assets/components/accordion.tsx +3 -3
- package/dist/assets/components/alert-dialog.stories.tsx +142 -0
- package/dist/assets/components/alert-dialog.tsx +143 -0
- package/dist/assets/components/alert.stories.tsx +3 -3
- package/dist/assets/components/alert.tsx +4 -3
- package/dist/assets/components/aspect-ratio.stories.tsx +70 -0
- package/dist/assets/components/aspect-ratio.tsx +8 -0
- package/dist/assets/components/autocomplete.stories.tsx +9 -9
- package/dist/assets/components/autocomplete.tsx +3 -3
- package/dist/assets/components/avatar.stories.tsx +5 -5
- package/dist/assets/components/avatar.tsx +67 -23
- package/dist/assets/components/badge.stories.tsx +10 -10
- package/dist/assets/components/badge.tsx +3 -3
- package/dist/assets/components/breadcrumbs.stories.tsx +7 -7
- package/dist/assets/components/breadcrumbs.tsx +13 -8
- package/dist/assets/components/button.stories.tsx +74 -74
- package/dist/assets/components/button.tsx +2 -0
- package/dist/assets/components/calendar.stories.tsx +11 -11
- package/dist/assets/components/calendar.tsx +4 -4
- package/dist/assets/components/card.stories.tsx +22 -22
- package/dist/assets/components/card.tsx +7 -3
- package/dist/assets/components/carousel.stories.tsx +158 -0
- package/dist/assets/components/carousel.tsx +264 -0
- package/dist/assets/components/chart.stories.tsx +376 -0
- package/dist/assets/components/chart.tsx +384 -0
- package/dist/assets/components/checkbox-group.stories.tsx +6 -6
- package/dist/assets/components/checkbox-group.tsx +3 -3
- package/dist/assets/components/checkbox.stories.tsx +23 -20
- package/dist/assets/components/checkbox.tsx +13 -6
- package/dist/assets/components/code.stories.tsx +24 -24
- package/dist/assets/components/code.tsx +22 -27
- package/dist/assets/components/collapsible.stories.tsx +128 -0
- package/dist/assets/components/collapsible.tsx +10 -0
- package/dist/assets/components/command.stories.tsx +183 -0
- package/dist/assets/components/command.tsx +171 -0
- package/dist/assets/components/context-menu.stories.tsx +159 -0
- package/dist/assets/components/context-menu.tsx +214 -0
- package/dist/assets/components/date-input.stories.tsx +9 -9
- package/dist/assets/components/date-input.tsx +2 -2
- package/dist/assets/components/date-picker.stories.tsx +9 -9
- package/dist/assets/components/date-picker.tsx +3 -3
- package/dist/assets/components/date-range-picker.stories.tsx +12 -12
- package/dist/assets/components/date-range-picker.tsx +3 -3
- package/dist/assets/components/dialog.stories.tsx +40 -40
- package/dist/assets/components/dialog.tsx +8 -12
- package/dist/assets/components/divider.stories.tsx +30 -30
- package/dist/assets/components/divider.tsx +34 -35
- package/dist/assets/components/drawer.stories.tsx +32 -31
- package/dist/assets/components/drawer.tsx +7 -6
- package/dist/assets/components/dropdown-menu.tsx +213 -0
- package/dist/assets/components/dropdown.stories.tsx +12 -12
- package/dist/assets/components/dropdown.tsx +5 -5
- package/dist/assets/components/form.stories.tsx +30 -29
- package/dist/assets/components/form.tsx +5 -5
- package/dist/assets/components/hover-card.stories.tsx +115 -0
- package/dist/assets/components/hover-card.tsx +35 -0
- package/dist/assets/components/image.stories.tsx +48 -25
- package/dist/assets/components/image.tsx +8 -5
- package/dist/assets/components/input-otp.stories.tsx +15 -15
- package/dist/assets/components/input-otp.tsx +5 -5
- package/dist/assets/components/input.stories.tsx +30 -25
- package/dist/assets/components/input.tsx +7 -4
- package/dist/assets/components/kbd.stories.tsx +34 -34
- package/dist/assets/components/kbd.tsx +9 -9
- package/dist/assets/components/link.stories.tsx +36 -36
- package/dist/assets/components/link.tsx +4 -0
- package/dist/assets/components/listbox.stories.tsx +5 -5
- package/dist/assets/components/listbox.tsx +4 -4
- package/dist/assets/components/menubar.stories.tsx +208 -0
- package/dist/assets/components/menubar.tsx +247 -0
- package/dist/assets/components/navbar.stories.tsx +24 -24
- package/dist/assets/components/navbar.tsx +8 -14
- package/dist/assets/components/navigation-menu.stories.tsx +239 -0
- package/dist/assets/components/navigation-menu.tsx +135 -0
- package/dist/assets/components/number-input.stories.tsx +11 -11
- package/dist/assets/components/number-input.tsx +3 -3
- package/dist/assets/components/pagination.stories.tsx +13 -13
- package/dist/assets/components/pagination.tsx +6 -6
- package/dist/assets/components/popover.stories.tsx +35 -35
- package/dist/assets/components/popover.tsx +98 -15
- package/dist/assets/components/progress.stories.tsx +5 -5
- package/dist/assets/components/progress.tsx +5 -5
- package/dist/assets/components/radio-group.stories.tsx +7 -7
- package/dist/assets/components/radio-group.tsx +3 -3
- package/dist/assets/components/range-calendar.stories.tsx +18 -18
- package/dist/assets/components/range-calendar.tsx +3 -3
- package/dist/assets/components/resizable.stories.tsx +197 -0
- package/dist/assets/components/resizable.tsx +47 -0
- package/dist/assets/components/scroll-area.stories.tsx +123 -0
- package/dist/assets/components/scroll-area.tsx +48 -0
- package/dist/assets/components/scroll-shadow.stories.tsx +17 -17
- package/dist/assets/components/scroll-shadow.tsx +31 -9
- package/dist/assets/components/select.stories.tsx +20 -19
- package/dist/assets/components/select.tsx +10 -6
- package/dist/assets/components/separator.tsx +32 -0
- package/dist/assets/components/sheet.tsx +137 -0
- package/dist/assets/components/sidebar.stories.tsx +351 -0
- package/dist/assets/components/sidebar.tsx +757 -0
- package/dist/assets/components/skeleton.stories.tsx +3 -3
- package/dist/assets/components/skeleton.tsx +2 -2
- package/dist/assets/components/slider.stories.tsx +6 -6
- package/dist/assets/components/slider.tsx +3 -3
- package/dist/assets/components/spacer.stories.tsx +11 -11
- package/dist/assets/components/spacer.tsx +2 -2
- package/dist/assets/components/spinner.stories.tsx +8 -8
- package/dist/assets/components/spinner.tsx +5 -5
- package/dist/assets/components/switch.stories.tsx +24 -20
- package/dist/assets/components/switch.tsx +14 -6
- package/dist/assets/components/table.stories.tsx +7 -7
- package/dist/assets/components/table.tsx +8 -8
- package/dist/assets/components/tabs.stories.tsx +37 -37
- package/dist/assets/components/tabs.tsx +3 -3
- package/dist/assets/components/textarea.stories.tsx +13 -12
- package/dist/assets/components/textarea.tsx +3 -3
- package/dist/assets/components/theme-toggle.stories.tsx +31 -30
- package/dist/assets/components/theme-toggle.tsx +2 -2
- package/dist/assets/components/time-input.stories.tsx +16 -16
- package/dist/assets/components/time-input.tsx +2 -2
- package/dist/assets/components/toast.stories.tsx +8 -5
- package/dist/assets/components/toast.tsx +6 -6
- package/dist/assets/components/toggle-group.stories.tsx +153 -0
- package/dist/assets/components/toggle-group.tsx +61 -0
- package/dist/assets/components/toggle.stories.tsx +77 -0
- package/dist/assets/components/toggle.tsx +46 -0
- package/dist/assets/components/tooltip.stories.tsx +49 -27
- package/dist/assets/components/tooltip.tsx +23 -90
- package/dist/assets/components/user.stories.tsx +23 -23
- package/dist/assets/components/user.tsx +7 -4
- package/dist/assets/dev-tools/SonanceDevTools.tsx +4201 -0
- package/dist/assets/dev-tools/index.ts +10 -0
- package/dist/assets/globals.css +39 -0
- package/dist/assets/logos/40th-anniversary/Sonance_40_Logo_CMYK_BEAM_BLUE_40_AND_BEAM_DARK.png +0 -0
- package/dist/assets/logos/Sonance logo dark mode.png +0 -0
- package/dist/assets/logos/Sonance logo light mode.png +0 -0
- package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_2C_Light_RGB_05162025.png +0 -0
- package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_3C_Dark_RGB_05162025.png +0 -0
- package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_White_RGB_05162025.png +0 -0
- package/dist/assets/logos/iport/IPORT_Sonance_LockUp_2C_Dark_RGB.png +0 -0
- package/dist/assets/logos/iport/IPORT_Sonance_LockUp_2C_Light_RGB.png +0 -0
- package/dist/assets/logos/james/James_Logo_Black_CMYK.png +0 -0
- package/dist/assets/logos/james/James_Logo_Black_RGB.png +0 -0
- package/dist/assets/logos/james/James_Logo_LtGray_CMYK.png +0 -0
- package/dist/assets/logos/james/James_Logo_LtGray_RGB.png +0 -0
- package/dist/assets/logos/james/James_Logo_Polished_RGB.png +0 -0
- package/dist/assets/logos/james/James_Logo_Reverse_CMYK.png +0 -0
- package/dist/assets/logos/james/James_Logo_Reverse_RGB.png +0 -0
- package/dist/assets/logos/james/James_Logo_White_CMYK.png +0 -0
- package/dist/assets/logos/life-is-better/Sonance_LifeisBetter_Dark_RGB.png +0 -0
- package/dist/assets/logos/life-is-better/Sonance_LifeisBetter_Light_RGB.png +0 -0
- package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Dark_RGB.png +0 -0
- package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Light_RGB.png +0 -0
- package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Reverse_RGB.png +0 -0
- package/dist/assets/logos/my-sonance/My.Sonance_Logo_Black_RGB.png +0 -0
- package/dist/assets/logos/my-sonance/My.Sonance_Logo_Reverse_RGB.png +0 -0
- package/dist/assets/logos/sonance/Sonance_Logo_2C_Dark_RGB.png +0 -0
- package/dist/assets/logos/sonance/Sonance_Logo_2C_Light_RGB.png +0 -0
- package/dist/assets/logos/sonance/Sonance_Logo_2C_Reverse_RGB.png +0 -0
- package/dist/assets/logos/sonance/Sonance_Logo_Black_RGB.png +0 -0
- package/dist/assets/logos/sonance/Sonance_Logo_Grayscale_RGB.png +0 -0
- package/dist/assets/logos/sonance/Sonance_Logo_Reverse_RGB.png +0 -0
- package/dist/assets/logos/sonance-academy/SonanceAcademy_Logo_Dark_CMYK.png +0 -0
- package/dist/assets/logos/sonance-academy/SonanceAcademy_Logo_Light_CMYK.png +0 -0
- package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Dark_RGB.png +0 -0
- package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Light_RGB.png +0 -0
- package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Reverse_RGB.png +0 -0
- package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Black_RGB.png +0 -0
- package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Grayscale_RGB.png +0 -0
- package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Reverse_RGB.png +0 -0
- package/dist/assets/logos/sonance-james/Sonance_James_Lockup_Dark.png +0 -0
- package/dist/assets/logos/sonance-james/Sonance_James_Lockup_Light.png +0 -0
- package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_LockupStacked_Dark.png +0 -0
- package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_LockupStacked_Light.png +0 -0
- package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Dark.png +0 -0
- package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Light.png +0 -0
- package/dist/assets/logos/trufig/TrufigLogo_Black.png +0 -0
- package/dist/assets/logos/trufig/TrufigLogo_Light.png +0 -0
- package/dist/assets/logos/trufig/TrufigWatermark_Black.png +0 -0
- package/dist/assets/logos/trufig/TrufigWatermark_Light.png +0 -0
- package/dist/assets/styles/brand-overrides.css +37 -0
- package/dist/index.js +2055 -15
- 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
|
+
|