sonance-brand-mcp 1.3.15 → 1.3.16
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 +1 -1
- package/dist/assets/api/sonance-save-logo/route.ts +2 -2
- package/dist/assets/brand-system.ts +4 -1
- package/dist/assets/components/image.tsx +3 -1
- package/dist/assets/components/select.tsx +3 -0
- package/dist/assets/dev-tools/SonanceDevTools.tsx +1837 -3579
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +230 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +455 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +190 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +353 -0
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +199 -0
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +116 -0
- package/dist/assets/dev-tools/components/common.tsx +94 -0
- package/dist/assets/dev-tools/constants.ts +616 -0
- package/dist/assets/dev-tools/index.ts +29 -8
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +329 -0
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +623 -0
- package/dist/assets/dev-tools/panels/LogoToolsPanel.tsx +621 -0
- package/dist/assets/dev-tools/panels/LogosPanel.tsx +16 -0
- package/dist/assets/dev-tools/panels/TextPanel.tsx +332 -0
- package/dist/assets/dev-tools/types.ts +295 -0
- package/dist/assets/dev-tools/utils.ts +360 -0
- package/dist/index.js +268 -0
- package/package.json +1 -1
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Image as ImageIcon, Sun, Moon, CheckCircle, AlertCircle, Loader2, RotateCcw, Save, Check, Wand2 } from "lucide-react";
|
|
5
|
+
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { LogoAsset, LogoOverride, OriginalLogoState, DetectedElement, LogoSaveStatus } from "../types";
|
|
7
|
+
import { Section } from "../components/common";
|
|
8
|
+
|
|
9
|
+
type AutoFixStatus = "idle" | "fixing" | "success" | "error";
|
|
10
|
+
|
|
11
|
+
export interface LogoToolsPanelProps {
|
|
12
|
+
logoAssets: LogoAsset[];
|
|
13
|
+
logoAssetsByBrand: Record<string, LogoAsset[]>;
|
|
14
|
+
selectedLogoId: string | null;
|
|
15
|
+
globalLogoConfig: LogoOverride;
|
|
16
|
+
individualLogoConfigs: Record<string, LogoOverride>;
|
|
17
|
+
originalLogoStates: Record<string, OriginalLogoState>;
|
|
18
|
+
taggedElements: DetectedElement[];
|
|
19
|
+
onGlobalConfigChange: (config: LogoOverride) => void;
|
|
20
|
+
onIndividualConfigChange: (logoId: string, config: LogoOverride) => void;
|
|
21
|
+
onSelectLogo: (logoId: string | null) => void;
|
|
22
|
+
onResetAll: () => void;
|
|
23
|
+
onResetLogo: (logoId: string) => void;
|
|
24
|
+
onSaveChanges: (configOverride?: LogoOverride, brandId?: string, selector?: string, logoId?: string) => void;
|
|
25
|
+
saveStatus: LogoSaveStatus;
|
|
26
|
+
saveMessage: string;
|
|
27
|
+
findComplementaryLogo: (path: string) => { light?: string; dark?: string } | null;
|
|
28
|
+
currentTheme: string;
|
|
29
|
+
onAutoFixId: (logoSrc: string, suggestedId: string) => Promise<{ success: boolean; error?: string }>;
|
|
30
|
+
autoFixStatus: AutoFixStatus;
|
|
31
|
+
autoFixMessage: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generates a context-aware ID suggestion for a logo element based on its
|
|
36
|
+
* position in the DOM, parent containers, and attributes.
|
|
37
|
+
*/
|
|
38
|
+
function generateIdSuggestion(logoId: string, brandId?: string): string {
|
|
39
|
+
// Try to get context from the DOM element
|
|
40
|
+
const element = document.querySelector(`[data-sonance-logo-id="${logoId}"]`);
|
|
41
|
+
if (!element) {
|
|
42
|
+
return brandId ? `${brandId}-logo` : "brand-logo";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check parent containers for context
|
|
46
|
+
const parentNames: string[] = [];
|
|
47
|
+
let current = element.parentElement;
|
|
48
|
+
let depth = 0;
|
|
49
|
+
|
|
50
|
+
while (current && depth < 5) {
|
|
51
|
+
// Check for semantic elements
|
|
52
|
+
const tagName = current.tagName.toLowerCase();
|
|
53
|
+
if (["header", "footer", "nav", "aside", "main", "section"].includes(tagName)) {
|
|
54
|
+
parentNames.unshift(tagName);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for data-sonance-name attribute
|
|
59
|
+
const sonanceName = current.getAttribute("data-sonance-name");
|
|
60
|
+
if (sonanceName) {
|
|
61
|
+
parentNames.unshift(sonanceName.toLowerCase().replace(/\s+/g, "-"));
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for common class hints
|
|
66
|
+
const classList = current.className;
|
|
67
|
+
if (typeof classList === "string") {
|
|
68
|
+
if (classList.includes("header")) parentNames.unshift("header");
|
|
69
|
+
else if (classList.includes("footer")) parentNames.unshift("footer");
|
|
70
|
+
else if (classList.includes("sidebar")) parentNames.unshift("sidebar");
|
|
71
|
+
else if (classList.includes("nav")) parentNames.unshift("nav");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
current = current.parentElement;
|
|
75
|
+
depth++;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check alt text for context
|
|
79
|
+
const altText = element.getAttribute("alt");
|
|
80
|
+
if (altText) {
|
|
81
|
+
const cleanAlt = altText.toLowerCase()
|
|
82
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
83
|
+
.replace(/\s+/g, "-")
|
|
84
|
+
.substring(0, 20);
|
|
85
|
+
if (cleanAlt && cleanAlt !== "logo") {
|
|
86
|
+
parentNames.push(cleanAlt);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Build the suggested ID
|
|
91
|
+
const parts: string[] = [];
|
|
92
|
+
if (parentNames.length > 0) {
|
|
93
|
+
parts.push(parentNames[0]);
|
|
94
|
+
}
|
|
95
|
+
if (brandId) {
|
|
96
|
+
parts.push(brandId);
|
|
97
|
+
}
|
|
98
|
+
parts.push("logo");
|
|
99
|
+
|
|
100
|
+
return parts.join("-");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function LogoToolsPanel({
|
|
104
|
+
logoAssets,
|
|
105
|
+
logoAssetsByBrand,
|
|
106
|
+
selectedLogoId,
|
|
107
|
+
globalLogoConfig,
|
|
108
|
+
individualLogoConfigs,
|
|
109
|
+
originalLogoStates,
|
|
110
|
+
taggedElements,
|
|
111
|
+
onGlobalConfigChange,
|
|
112
|
+
onIndividualConfigChange,
|
|
113
|
+
onSelectLogo,
|
|
114
|
+
onResetAll,
|
|
115
|
+
onResetLogo,
|
|
116
|
+
onSaveChanges,
|
|
117
|
+
saveStatus,
|
|
118
|
+
saveMessage,
|
|
119
|
+
findComplementaryLogo,
|
|
120
|
+
currentTheme,
|
|
121
|
+
onAutoFixId,
|
|
122
|
+
autoFixStatus,
|
|
123
|
+
autoFixMessage,
|
|
124
|
+
}: LogoToolsPanelProps) {
|
|
125
|
+
const logoElements = taggedElements.filter((el) => el.type === "logo" && el.logoId);
|
|
126
|
+
const selectedLogo = logoElements.find((el) => el.logoId === selectedLogoId);
|
|
127
|
+
const selectedConfig = selectedLogoId ? individualLogoConfigs[selectedLogoId] || {} : {};
|
|
128
|
+
const selectedOriginal = selectedLogoId ? originalLogoStates[selectedLogoId] : null;
|
|
129
|
+
|
|
130
|
+
// Attempt to identify the brand and element ID of the selected logo
|
|
131
|
+
let selectedBrandId: string | undefined;
|
|
132
|
+
let selectedElementId: string | undefined;
|
|
133
|
+
|
|
134
|
+
if (selectedLogoId) {
|
|
135
|
+
// Check if the element has an ID attribute for targeted CSS
|
|
136
|
+
const logoElement = document.querySelector(`[data-sonance-logo-id="${selectedLogoId}"]`);
|
|
137
|
+
if (logoElement && logoElement.id) {
|
|
138
|
+
selectedElementId = logoElement.id;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (selectedOriginal && selectedOriginal.src) {
|
|
143
|
+
// Try to find matching asset
|
|
144
|
+
// Normalize paths for comparison (remove protocol/domain if present)
|
|
145
|
+
const normalize = (p: string) => p.split("?")[0].split("#")[0]; // remove query/hash
|
|
146
|
+
const originalPath = normalize(selectedOriginal.src);
|
|
147
|
+
|
|
148
|
+
// Find asset where path ends with the original src filename or vice versa
|
|
149
|
+
const asset = logoAssets.find(a =>
|
|
150
|
+
originalPath.endsWith(a.path) || a.path.endsWith(originalPath) || originalPath.includes(a.name)
|
|
151
|
+
);
|
|
152
|
+
if (asset) {
|
|
153
|
+
selectedBrandId = asset.brand;
|
|
154
|
+
} else {
|
|
155
|
+
// Fallback: guess from path
|
|
156
|
+
if (originalPath.toLowerCase().includes("sonance")) selectedBrandId = "sonance";
|
|
157
|
+
else if (originalPath.toLowerCase().includes("iport")) selectedBrandId = "iport";
|
|
158
|
+
else if (originalPath.toLowerCase().includes("blaze")) selectedBrandId = "blaze";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Get sorted brand keys
|
|
163
|
+
const brandKeys = Object.keys(logoAssetsByBrand).sort();
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="space-y-5">
|
|
167
|
+
{/* Header info */}
|
|
168
|
+
<div className="p-3 rounded border border-orange-200 bg-orange-50">
|
|
169
|
+
<p id="analysis-modal-p" className="text-xs text-orange-700">
|
|
170
|
+
<strong>{logoElements.length}</strong> logo{logoElements.length !== 1 ? "s" : ""} detected on this page.
|
|
171
|
+
Click a logo on the page to select it for editing.
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Global Controls */}
|
|
176
|
+
<Section title="Replace All Logos">
|
|
177
|
+
<div className="space-y-3">
|
|
178
|
+
{/* Theme indicator */}
|
|
179
|
+
<div className="flex items-center gap-2 text-[10px] text-gray-400">
|
|
180
|
+
<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")}>
|
|
181
|
+
{currentTheme === "dark" ? "🌙 Dark Mode" : "☀️ Light Mode"}
|
|
182
|
+
</span>
|
|
183
|
+
<span id="section-span-current-preview">Current preview</span>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* Light Mode Logo */}
|
|
187
|
+
<div className="space-y-1.5">
|
|
188
|
+
<label className="text-xs text-gray-500 flex items-center gap-1.5">
|
|
189
|
+
<Sun className="h-3 w-3 text-yellow-500" />
|
|
190
|
+
Light Mode Logo:
|
|
191
|
+
</label>
|
|
192
|
+
<select
|
|
193
|
+
value={globalLogoConfig.srcLight || ""}
|
|
194
|
+
onChange={(e) => {
|
|
195
|
+
const newPath = e.target.value || undefined;
|
|
196
|
+
if (newPath) {
|
|
197
|
+
// Auto-fill dark variant if available
|
|
198
|
+
const complementary = findComplementaryLogo(newPath);
|
|
199
|
+
onGlobalConfigChange({
|
|
200
|
+
...globalLogoConfig,
|
|
201
|
+
srcLight: newPath,
|
|
202
|
+
srcDark: complementary?.dark || globalLogoConfig.srcDark,
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
onGlobalConfigChange({ ...globalLogoConfig, srcLight: undefined });
|
|
206
|
+
}
|
|
207
|
+
}}
|
|
208
|
+
className={cn(
|
|
209
|
+
"w-full h-8 px-2 text-xs rounded",
|
|
210
|
+
"border border-gray-200 bg-white text-gray-700",
|
|
211
|
+
"focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
<option value="">-- Select light mode logo --</option>
|
|
215
|
+
{brandKeys.map((brand) => (
|
|
216
|
+
<optgroup key={brand} label={brand.charAt(0).toUpperCase() + brand.slice(1)}>
|
|
217
|
+
{logoAssetsByBrand[brand].map((asset) => (
|
|
218
|
+
<option key={asset.id} value={asset.path}>
|
|
219
|
+
{asset.name}
|
|
220
|
+
</option>
|
|
221
|
+
))}
|
|
222
|
+
</optgroup>
|
|
223
|
+
))}
|
|
224
|
+
</select>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Dark Mode Logo */}
|
|
228
|
+
<div className="space-y-1.5">
|
|
229
|
+
<label className="text-xs text-gray-500 flex items-center gap-1.5">
|
|
230
|
+
<Moon className="h-3 w-3 text-blue-400" />
|
|
231
|
+
Dark Mode Logo:
|
|
232
|
+
</label>
|
|
233
|
+
<select
|
|
234
|
+
value={globalLogoConfig.srcDark || ""}
|
|
235
|
+
onChange={(e) => {
|
|
236
|
+
const newPath = e.target.value || undefined;
|
|
237
|
+
if (newPath) {
|
|
238
|
+
// Auto-fill light variant if available
|
|
239
|
+
const complementary = findComplementaryLogo(newPath);
|
|
240
|
+
onGlobalConfigChange({
|
|
241
|
+
...globalLogoConfig,
|
|
242
|
+
srcDark: newPath,
|
|
243
|
+
srcLight: complementary?.light || globalLogoConfig.srcLight,
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
onGlobalConfigChange({ ...globalLogoConfig, srcDark: undefined });
|
|
247
|
+
}
|
|
248
|
+
}}
|
|
249
|
+
className={cn(
|
|
250
|
+
"w-full h-8 px-2 text-xs rounded",
|
|
251
|
+
"border border-gray-200 bg-white text-gray-700",
|
|
252
|
+
"focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
|
|
253
|
+
)}
|
|
254
|
+
>
|
|
255
|
+
<option value="">-- Select dark mode logo --</option>
|
|
256
|
+
{brandKeys.map((brand) => (
|
|
257
|
+
<optgroup key={brand} label={brand.charAt(0).toUpperCase() + brand.slice(1)}>
|
|
258
|
+
{logoAssetsByBrand[brand].map((asset) => (
|
|
259
|
+
<option key={asset.id} value={asset.path}>
|
|
260
|
+
{asset.name}
|
|
261
|
+
</option>
|
|
262
|
+
))}
|
|
263
|
+
</optgroup>
|
|
264
|
+
))}
|
|
265
|
+
</select>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div className="space-y-1.5">
|
|
269
|
+
<label className="text-xs text-gray-500">Scale: {Math.round((globalLogoConfig.scale || 1) * 100)}%</label>
|
|
270
|
+
<input
|
|
271
|
+
type="range"
|
|
272
|
+
min="25"
|
|
273
|
+
max="200"
|
|
274
|
+
value={(globalLogoConfig.scale || 1) * 100}
|
|
275
|
+
onChange={(e) => onGlobalConfigChange({ ...globalLogoConfig, scale: parseInt(e.target.value) / 100 })}
|
|
276
|
+
className="w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-200"
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</Section>
|
|
281
|
+
|
|
282
|
+
{/* Save Status Message */}
|
|
283
|
+
{saveMessage && (
|
|
284
|
+
<div
|
|
285
|
+
className={cn(
|
|
286
|
+
"flex items-start gap-2 p-2 rounded text-xs",
|
|
287
|
+
saveStatus === "success" && "bg-green-50 text-green-700 border border-green-200",
|
|
288
|
+
saveStatus === "error" && "bg-red-50 text-red-700 border border-red-200"
|
|
289
|
+
)}
|
|
290
|
+
>
|
|
291
|
+
{saveStatus === "success" && <CheckCircle className="h-3.5 w-3.5 shrink-0 mt-0.5" />}
|
|
292
|
+
{saveStatus === "error" && <AlertCircle className="h-3.5 w-3.5 shrink-0 mt-0.5" />}
|
|
293
|
+
<span id="analysis-modal-span-savemessage">{saveMessage}</span>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
{/* Save Button */}
|
|
298
|
+
{(globalLogoConfig.reset || globalLogoConfig.srcLight || globalLogoConfig.srcDark) && (
|
|
299
|
+
<button
|
|
300
|
+
onClick={() => onSaveChanges()}
|
|
301
|
+
disabled={saveStatus === "saving"}
|
|
302
|
+
className={cn(
|
|
303
|
+
"w-full flex items-center justify-center gap-2 py-2.5",
|
|
304
|
+
"text-sm font-medium text-white rounded transition-colors",
|
|
305
|
+
globalLogoConfig.reset ? "bg-amber-600 hover:bg-amber-700" : "bg-[#333F48] hover:bg-[#2a343c]",
|
|
306
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
307
|
+
)}
|
|
308
|
+
>
|
|
309
|
+
{saveStatus === "saving" ? (
|
|
310
|
+
<>
|
|
311
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
312
|
+
Saving...
|
|
313
|
+
</>
|
|
314
|
+
) : (
|
|
315
|
+
<>
|
|
316
|
+
{globalLogoConfig.reset ? <RotateCcw className="h-4 w-4" /> : <Save className="h-4 w-4" />}
|
|
317
|
+
{globalLogoConfig.reset ? "Save Reset" : "Save to Brand System"}
|
|
318
|
+
</>
|
|
319
|
+
)}
|
|
320
|
+
</button>
|
|
321
|
+
)}
|
|
322
|
+
|
|
323
|
+
{/* Selected Logo Controls */}
|
|
324
|
+
{selectedLogoId && selectedLogo && (
|
|
325
|
+
<Section title={`Edit: ${selectedLogo.name}`}>
|
|
326
|
+
<div className="space-y-3 p-3 rounded border border-[#FC4C02] bg-orange-50/50">
|
|
327
|
+
{/* Original info */}
|
|
328
|
+
{selectedOriginal && (
|
|
329
|
+
<div className="text-[10px] text-gray-500">
|
|
330
|
+
Original: {selectedOriginal.width} × {selectedOriginal.height}px
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
|
|
334
|
+
{/* Theme indicator */}
|
|
335
|
+
<div className="flex items-center gap-2 text-[10px] text-gray-400">
|
|
336
|
+
<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")}>
|
|
337
|
+
{currentTheme === "dark" ? "🌙 Dark Mode" : "☀️ Light Mode"}
|
|
338
|
+
</span>
|
|
339
|
+
<span id="analysis-modal-span-current-preview">Current preview</span>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
{/* Light Mode Logo */}
|
|
343
|
+
<div className="space-y-1.5">
|
|
344
|
+
<label className="text-xs text-gray-500 flex items-center gap-1.5">
|
|
345
|
+
<Sun className="h-3 w-3 text-yellow-500" />
|
|
346
|
+
Light Mode Logo:
|
|
347
|
+
</label>
|
|
348
|
+
<select
|
|
349
|
+
value={selectedConfig.srcLight || ""}
|
|
350
|
+
onChange={(e) => {
|
|
351
|
+
const newPath = e.target.value || undefined;
|
|
352
|
+
if (newPath) {
|
|
353
|
+
const complementary = findComplementaryLogo(newPath);
|
|
354
|
+
onIndividualConfigChange(selectedLogoId, {
|
|
355
|
+
...selectedConfig,
|
|
356
|
+
srcLight: newPath,
|
|
357
|
+
srcDark: complementary?.dark || selectedConfig.srcDark,
|
|
358
|
+
});
|
|
359
|
+
} else {
|
|
360
|
+
onIndividualConfigChange(selectedLogoId, { ...selectedConfig, srcLight: undefined });
|
|
361
|
+
}
|
|
362
|
+
}}
|
|
363
|
+
className={cn(
|
|
364
|
+
"w-full h-8 px-2 text-xs rounded",
|
|
365
|
+
"border border-gray-200 bg-white text-gray-700",
|
|
366
|
+
"focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
|
|
367
|
+
)}
|
|
368
|
+
>
|
|
369
|
+
<option value="">-- Use global/original --</option>
|
|
370
|
+
{brandKeys.map((brand) => (
|
|
371
|
+
<optgroup key={brand} label={brand.charAt(0).toUpperCase() + brand.slice(1)}>
|
|
372
|
+
{logoAssetsByBrand[brand].map((asset) => (
|
|
373
|
+
<option key={asset.id} value={asset.path}>
|
|
374
|
+
{asset.name}
|
|
375
|
+
</option>
|
|
376
|
+
))}
|
|
377
|
+
</optgroup>
|
|
378
|
+
))}
|
|
379
|
+
</select>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{/* Dark Mode Logo */}
|
|
383
|
+
<div className="space-y-1.5">
|
|
384
|
+
<label className="text-xs text-gray-500 flex items-center gap-1.5">
|
|
385
|
+
<Moon className="h-3 w-3 text-blue-400" />
|
|
386
|
+
Dark Mode Logo:
|
|
387
|
+
</label>
|
|
388
|
+
<select
|
|
389
|
+
value={selectedConfig.srcDark || ""}
|
|
390
|
+
onChange={(e) => {
|
|
391
|
+
const newPath = e.target.value || undefined;
|
|
392
|
+
if (newPath) {
|
|
393
|
+
const complementary = findComplementaryLogo(newPath);
|
|
394
|
+
onIndividualConfigChange(selectedLogoId, {
|
|
395
|
+
...selectedConfig,
|
|
396
|
+
srcDark: newPath,
|
|
397
|
+
srcLight: complementary?.light || selectedConfig.srcLight,
|
|
398
|
+
});
|
|
399
|
+
} else {
|
|
400
|
+
onIndividualConfigChange(selectedLogoId, { ...selectedConfig, srcDark: undefined });
|
|
401
|
+
}
|
|
402
|
+
}}
|
|
403
|
+
className={cn(
|
|
404
|
+
"w-full h-8 px-2 text-xs rounded",
|
|
405
|
+
"border border-gray-200 bg-white text-gray-700",
|
|
406
|
+
"focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
|
|
407
|
+
)}
|
|
408
|
+
>
|
|
409
|
+
<option value="">-- Use global/original --</option>
|
|
410
|
+
{brandKeys.map((brand) => (
|
|
411
|
+
<optgroup key={brand} label={brand.charAt(0).toUpperCase() + brand.slice(1)}>
|
|
412
|
+
{logoAssetsByBrand[brand].map((asset) => (
|
|
413
|
+
<option key={asset.id} value={asset.path}>
|
|
414
|
+
{asset.name}
|
|
415
|
+
</option>
|
|
416
|
+
))}
|
|
417
|
+
</optgroup>
|
|
418
|
+
))}
|
|
419
|
+
</select>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
{/* Dimensions */}
|
|
423
|
+
<div className="grid grid-cols-2 gap-2">
|
|
424
|
+
<div className="space-y-1">
|
|
425
|
+
<label className="text-xs text-gray-500">Width (px)</label>
|
|
426
|
+
<input
|
|
427
|
+
type="number"
|
|
428
|
+
min="10"
|
|
429
|
+
max="1000"
|
|
430
|
+
value={selectedConfig.width || ""}
|
|
431
|
+
placeholder="auto"
|
|
432
|
+
onChange={(e) => onIndividualConfigChange(selectedLogoId, {
|
|
433
|
+
...selectedConfig,
|
|
434
|
+
width: e.target.value ? parseInt(e.target.value) : undefined
|
|
435
|
+
})}
|
|
436
|
+
className={cn(
|
|
437
|
+
"w-full h-8 px-2 text-xs rounded",
|
|
438
|
+
"border border-gray-200 bg-white text-gray-700",
|
|
439
|
+
"focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
|
|
440
|
+
)}
|
|
441
|
+
/>
|
|
442
|
+
</div>
|
|
443
|
+
<div className="space-y-1">
|
|
444
|
+
<label className="text-xs text-gray-500">Height (px)</label>
|
|
445
|
+
<input
|
|
446
|
+
type="number"
|
|
447
|
+
min="10"
|
|
448
|
+
max="1000"
|
|
449
|
+
value={selectedConfig.height || ""}
|
|
450
|
+
placeholder="auto"
|
|
451
|
+
onChange={(e) => onIndividualConfigChange(selectedLogoId, {
|
|
452
|
+
...selectedConfig,
|
|
453
|
+
height: e.target.value ? parseInt(e.target.value) : undefined
|
|
454
|
+
})}
|
|
455
|
+
className={cn(
|
|
456
|
+
"w-full h-8 px-2 text-xs rounded",
|
|
457
|
+
"border border-gray-200 bg-white text-gray-700",
|
|
458
|
+
"focus:outline-none focus:ring-1 focus:ring-[#FC4C02]"
|
|
459
|
+
)}
|
|
460
|
+
/>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
{/* Scale */}
|
|
465
|
+
<div className="space-y-1.5">
|
|
466
|
+
<label className="text-xs text-gray-500">Scale: {Math.round((selectedConfig.scale || 1) * 100)}%</label>
|
|
467
|
+
<input
|
|
468
|
+
type="range"
|
|
469
|
+
min="25"
|
|
470
|
+
max="200"
|
|
471
|
+
value={(selectedConfig.scale || 1) * 100}
|
|
472
|
+
onChange={(e) => onIndividualConfigChange(selectedLogoId, { ...selectedConfig, scale: parseInt(e.target.value) / 100 })}
|
|
473
|
+
className="w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-200"
|
|
474
|
+
/>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
{/* Warning if element has no ID for individual persistence */}
|
|
478
|
+
{!selectedElementId && (selectedConfig.width || selectedConfig.height || selectedConfig.scale) && (() => {
|
|
479
|
+
const suggestedId = generateIdSuggestion(selectedLogoId, selectedBrandId);
|
|
480
|
+
const codeSnippet = `id="${suggestedId}"`;
|
|
481
|
+
const logoSrc = selectedOriginal?.src || "";
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
<div className="p-2.5 rounded border border-amber-200 bg-amber-50 space-y-2">
|
|
485
|
+
<p id="analysis-modal-p" className="text-[10px] text-amber-700">
|
|
486
|
+
<strong>⚠️ No ID found.</strong> Saving will update the <strong>global {selectedBrandId || "brand"} default</strong>, affecting all logos of this brand.
|
|
487
|
+
</p>
|
|
488
|
+
<div className="flex items-center gap-2">
|
|
489
|
+
<code className="flex-1 px-2 py-1 text-[10px] bg-amber-100 text-amber-800 rounded font-mono truncate" title={codeSnippet}>
|
|
490
|
+
{codeSnippet}
|
|
491
|
+
</code>
|
|
492
|
+
<button
|
|
493
|
+
onClick={() => onAutoFixId(logoSrc, suggestedId)}
|
|
494
|
+
disabled={autoFixStatus === "fixing"}
|
|
495
|
+
className={cn(
|
|
496
|
+
"flex items-center gap-1 px-2 py-1 text-[10px] font-medium rounded transition-colors whitespace-nowrap",
|
|
497
|
+
autoFixStatus === "success"
|
|
498
|
+
? "bg-green-100 text-green-700"
|
|
499
|
+
: autoFixStatus === "error"
|
|
500
|
+
? "bg-red-100 text-red-700"
|
|
501
|
+
: "text-amber-700 bg-amber-100 hover:bg-amber-200",
|
|
502
|
+
autoFixStatus === "fixing" && "opacity-50 cursor-not-allowed"
|
|
503
|
+
)}
|
|
504
|
+
title="Automatically inject this ID into your source code"
|
|
505
|
+
>
|
|
506
|
+
{autoFixStatus === "fixing" ? (
|
|
507
|
+
<>
|
|
508
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
509
|
+
Fixing...
|
|
510
|
+
</>
|
|
511
|
+
) : autoFixStatus === "success" ? (
|
|
512
|
+
<>
|
|
513
|
+
<Check className="h-3 w-3" />
|
|
514
|
+
Fixed!
|
|
515
|
+
</>
|
|
516
|
+
) : autoFixStatus === "error" ? (
|
|
517
|
+
<>
|
|
518
|
+
<AlertCircle className="h-3 w-3" />
|
|
519
|
+
Failed
|
|
520
|
+
</>
|
|
521
|
+
) : (
|
|
522
|
+
<>
|
|
523
|
+
<Wand2 className="h-3 w-3" />
|
|
524
|
+
Auto-Fix
|
|
525
|
+
</>
|
|
526
|
+
)}
|
|
527
|
+
</button>
|
|
528
|
+
</div>
|
|
529
|
+
{autoFixMessage && (
|
|
530
|
+
<p id="analysis-modal-p-autofixmessage" className={cn(
|
|
531
|
+
"text-[9px]",
|
|
532
|
+
autoFixStatus === "success" ? "text-green-600" : autoFixStatus === "error" ? "text-red-600" : "text-amber-600"
|
|
533
|
+
)}>
|
|
534
|
+
{autoFixMessage}
|
|
535
|
+
</p>
|
|
536
|
+
)}
|
|
537
|
+
{!autoFixMessage && (
|
|
538
|
+
<p id="analysis-modal-p-click-autofix-to-inj" className="text-[9px] text-amber-600">
|
|
539
|
+
Click Auto-Fix to inject this ID into your source code automatically.
|
|
540
|
+
</p>
|
|
541
|
+
)}
|
|
542
|
+
</div>
|
|
543
|
+
);
|
|
544
|
+
})()}
|
|
545
|
+
|
|
546
|
+
{/* Reset this logo */}
|
|
547
|
+
<div className="flex gap-2">
|
|
548
|
+
<button
|
|
549
|
+
onClick={() => {
|
|
550
|
+
if (selectedLogoId) {
|
|
551
|
+
onResetLogo(selectedLogoId);
|
|
552
|
+
}
|
|
553
|
+
}}
|
|
554
|
+
className={cn(
|
|
555
|
+
"flex-1 flex items-center justify-center gap-2 py-2",
|
|
556
|
+
"text-xs font-medium text-gray-500 hover:text-gray-700",
|
|
557
|
+
"border border-gray-200 rounded hover:bg-white transition-colors"
|
|
558
|
+
)}
|
|
559
|
+
>
|
|
560
|
+
<RotateCcw className="h-3 w-3" />
|
|
561
|
+
Reset
|
|
562
|
+
</button>
|
|
563
|
+
|
|
564
|
+
{(selectedConfig.reset || selectedConfig.srcLight || selectedConfig.srcDark || selectedConfig.src || selectedConfig.width || selectedConfig.height || (selectedConfig.scale && selectedConfig.scale !== 1)) && (
|
|
565
|
+
<button
|
|
566
|
+
onClick={() => onSaveChanges(selectedConfig, selectedBrandId, selectedElementId ? `#${selectedElementId}` : undefined, selectedLogoId || undefined)}
|
|
567
|
+
disabled={saveStatus === "saving"}
|
|
568
|
+
className={cn(
|
|
569
|
+
"flex-1 flex items-center justify-center gap-2 py-2",
|
|
570
|
+
"text-xs font-medium text-white rounded transition-colors",
|
|
571
|
+
selectedConfig.reset ? "bg-amber-600 hover:bg-amber-700" : (selectedElementId ? "bg-[#00A3E1] hover:bg-[#0090c8]" : "bg-[#333F48] hover:bg-[#2a343c]"),
|
|
572
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
573
|
+
)}
|
|
574
|
+
title={selectedConfig.reset ? "Save Reset (Delete Override)" : (selectedElementId ? `Save to element #${selectedElementId}` : (selectedBrandId ? `Save as ${selectedBrandId} brand default` : "Save as brand default"))}
|
|
575
|
+
>
|
|
576
|
+
{saveStatus === "saving" ? (
|
|
577
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
578
|
+
) : (
|
|
579
|
+
<>
|
|
580
|
+
{selectedConfig.reset ? <RotateCcw className="h-3 w-3" /> : <Save className="h-3 w-3" />}
|
|
581
|
+
{selectedConfig.reset ? "Save Reset" : (selectedElementId ? "Save to Element" : "Save to Brand")}
|
|
582
|
+
</>
|
|
583
|
+
)}
|
|
584
|
+
</button>
|
|
585
|
+
)}
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
{/* Individual Save Status Message */}
|
|
589
|
+
{saveMessage && (saveStatus === "success" || saveStatus === "error") && (
|
|
590
|
+
<div
|
|
591
|
+
className={cn(
|
|
592
|
+
"flex items-center gap-2 p-2 rounded text-xs mt-2",
|
|
593
|
+
saveStatus === "success" && "bg-green-50 text-green-700 border border-green-200",
|
|
594
|
+
saveStatus === "error" && "bg-red-50 text-red-700 border border-red-200"
|
|
595
|
+
)}
|
|
596
|
+
>
|
|
597
|
+
{saveStatus === "success" && <CheckCircle className="h-3.5 w-3.5 shrink-0" />}
|
|
598
|
+
{saveStatus === "error" && <AlertCircle className="h-3.5 w-3.5 shrink-0" />}
|
|
599
|
+
<span id="text-panel-span-savemessage">{saveMessage}</span>
|
|
600
|
+
</div>
|
|
601
|
+
)}
|
|
602
|
+
</div>
|
|
603
|
+
</Section>
|
|
604
|
+
)}
|
|
605
|
+
|
|
606
|
+
{/* Reset All */}
|
|
607
|
+
<button
|
|
608
|
+
onClick={onResetAll}
|
|
609
|
+
className={cn(
|
|
610
|
+
"w-full flex items-center justify-center gap-2 py-2.5",
|
|
611
|
+
"text-xs font-medium text-gray-500 hover:text-gray-700",
|
|
612
|
+
"border border-gray-200 rounded hover:bg-gray-50 transition-colors"
|
|
613
|
+
)}
|
|
614
|
+
>
|
|
615
|
+
<RotateCcw className="h-3.5 w-3.5" />
|
|
616
|
+
Reset All Logos
|
|
617
|
+
</button>
|
|
618
|
+
</div>
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { LogoToolsPanel, LogoToolsPanelProps } from "./LogoToolsPanel";
|
|
5
|
+
|
|
6
|
+
// LogosPanel now just wraps LogoToolsPanel - inspector is controlled via header
|
|
7
|
+
export type LogosPanelProps = LogoToolsPanelProps;
|
|
8
|
+
|
|
9
|
+
export function LogosPanel(props: LogosPanelProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="space-y-4">
|
|
12
|
+
<LogoToolsPanel {...props} />
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|