sonance-brand-mcp 1.3.15 → 1.3.17

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.
@@ -0,0 +1,332 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Type, RotateCcw, Check, X, Sun, Moon } from "lucide-react";
5
+ import { DetectedElement, TextOverride } from "../types";
6
+ import { Section, SelectField } from "../components/common";
7
+ import { cn } from "@/lib/utils";
8
+
9
+ export interface TextPanelProps {
10
+ inspectorEnabled: boolean;
11
+ taggedElements: DetectedElement[];
12
+ selectedTextId?: string | null;
13
+ onSelectText?: (textId: string | null) => void;
14
+ textOverrides?: Record<string, TextOverride>;
15
+ onTextOverrideChange?: (textId: string, override: TextOverride) => void;
16
+ // Save/Revert handlers
17
+ hasChanges?: boolean;
18
+ onSaveChanges?: () => void;
19
+ onRevertAll?: () => void;
20
+ saveStatus?: "idle" | "saving" | "success" | "error";
21
+ // Theme for color editing
22
+ currentTheme?: "light" | "dark";
23
+ }
24
+
25
+ export function TextPanel({
26
+ inspectorEnabled,
27
+ taggedElements,
28
+ selectedTextId,
29
+ onSelectText,
30
+ textOverrides = {},
31
+ onTextOverrideChange,
32
+ hasChanges = false,
33
+ onSaveChanges,
34
+ onRevertAll,
35
+ saveStatus = "idle",
36
+ currentTheme = "light",
37
+ }: TextPanelProps) {
38
+
39
+ // Find the selected element details
40
+ const selectedElement = selectedTextId
41
+ ? taggedElements.find(el => el.textId === selectedTextId)
42
+ : null;
43
+
44
+ // Get current override or empty object
45
+ const currentOverride = selectedTextId ? (textOverrides[selectedTextId] || {}) : {};
46
+
47
+ // Helper to update override
48
+ const updateOverride = (updates: Partial<TextOverride>) => {
49
+ if (!selectedTextId || !onTextOverrideChange) return;
50
+ onTextOverrideChange(selectedTextId, { ...currentOverride, ...updates });
51
+ };
52
+
53
+ const handleReset = () => {
54
+ if (!selectedTextId || !onTextOverrideChange) return;
55
+ // Mark as reset
56
+ onTextOverrideChange(selectedTextId, { reset: true });
57
+ };
58
+
59
+ // Count number of modified elements
60
+ const modifiedCount = Object.keys(textOverrides).filter(
61
+ key => !textOverrides[key].reset && Object.keys(textOverrides[key]).length > 0
62
+ ).length;
63
+
64
+ if (!inspectorEnabled) {
65
+ return (
66
+ <div className="flex flex-col items-center justify-center h-full text-center p-6 text-gray-500">
67
+ <Type className="h-12 w-12 mb-4 opacity-20" />
68
+ <p className="text-sm font-medium mb-2">Text Inspector Disabled</p>
69
+ <p className="text-xs max-w-[200px]">
70
+ Enable the inspector using the mouse icon in the top bar to select and edit text.
71
+ </p>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ // Show pending changes banner when there are modifications
77
+ const renderPendingChangesBanner = () => {
78
+ if (!hasChanges || modifiedCount === 0) return null;
79
+
80
+ return (
81
+ <div className="p-3 rounded border border-amber-200 bg-amber-50 mb-4">
82
+ <div className="flex items-center justify-between mb-2">
83
+ <div className="flex items-center gap-2">
84
+ <div className="w-2 h-2 bg-amber-500 rounded-full animate-pulse" />
85
+ <span className="text-xs font-medium text-amber-800">
86
+ {modifiedCount} text element{modifiedCount !== 1 ? 's' : ''} modified
87
+ </span>
88
+ </div>
89
+ </div>
90
+ <p className="text-[10px] text-amber-600 mb-3">
91
+ Changes are previewed on the page. Save to keep them or revert to discard.
92
+ </p>
93
+ <div className="flex gap-2">
94
+ <button
95
+ onClick={onSaveChanges}
96
+ disabled={saveStatus === "saving"}
97
+ className={cn(
98
+ "flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors",
99
+ "bg-green-600 text-white hover:bg-green-700",
100
+ saveStatus === "saving" && "opacity-50 cursor-not-allowed"
101
+ )}
102
+ >
103
+ <Check className="h-3 w-3" />
104
+ {saveStatus === "saving" ? "Saving..." : "Save Changes"}
105
+ </button>
106
+ <button
107
+ onClick={onRevertAll}
108
+ disabled={saveStatus === "saving"}
109
+ className={cn(
110
+ "flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors",
111
+ "bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200",
112
+ saveStatus === "saving" && "opacity-50 cursor-not-allowed"
113
+ )}
114
+ >
115
+ <X className="h-3 w-3" />
116
+ Revert All
117
+ </button>
118
+ </div>
119
+ </div>
120
+ );
121
+ };
122
+
123
+ if (!selectedTextId || !selectedElement) {
124
+ return (
125
+ <div className="space-y-4">
126
+ {renderPendingChangesBanner()}
127
+ <div className="flex flex-col items-center justify-center text-center p-6 text-gray-500">
128
+ <div className="relative">
129
+ <Type className="h-12 w-12 mb-4 opacity-20" />
130
+ <div className="absolute -bottom-1 -right-1 bg-blue-500 rounded-full p-1">
131
+ <Type className="h-3 w-3 text-white" />
132
+ </div>
133
+ </div>
134
+ <p className="text-sm font-medium mb-2">Select Text to Edit</p>
135
+ <p className="text-xs max-w-[200px]">
136
+ Click on any text element on the page to adjust its typography and content.
137
+ </p>
138
+ </div>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ // Check if current element has changes
144
+ const currentHasChanges = currentOverride &&
145
+ !currentOverride.reset &&
146
+ Object.keys(currentOverride).length > 0;
147
+
148
+ return (
149
+ <div className="space-y-4">
150
+ {renderPendingChangesBanner()}
151
+
152
+ <div className="space-y-6">
153
+ <div className="flex items-center justify-between pb-2 border-b border-gray-100">
154
+ <div className="flex flex-col">
155
+ <div className="flex items-center gap-2">
156
+ <span className="text-xs font-medium text-gray-900 truncate max-w-[180px]">
157
+ {selectedElement.name.split(':')[0]}
158
+ </span>
159
+ {currentHasChanges && (
160
+ <span className="px-1.5 py-0.5 text-[9px] font-medium bg-amber-100 text-amber-700 rounded">
161
+ Modified
162
+ </span>
163
+ )}
164
+ </div>
165
+ <span className="text-[10px] text-gray-400 font-mono">
166
+ {selectedTextId}
167
+ </span>
168
+ </div>
169
+ <button
170
+ onClick={handleReset}
171
+ disabled={!currentHasChanges}
172
+ className={cn(
173
+ "text-xs flex items-center gap-1 transition-colors",
174
+ currentHasChanges
175
+ ? "text-gray-400 hover:text-red-500"
176
+ : "text-gray-300 cursor-not-allowed"
177
+ )}
178
+ title="Reset to original"
179
+ >
180
+ <RotateCcw className="h-3 w-3" />
181
+ Reset
182
+ </button>
183
+ </div>
184
+
185
+ {/* Content Editor */}
186
+ <Section title="Content">
187
+ <textarea
188
+ value={currentOverride.textContent ?? selectedElement.textContent ?? ""}
189
+ onChange={(e) => updateOverride({ textContent: e.target.value })}
190
+ className={cn(
191
+ "w-full min-h-[80px] p-2 text-sm rounded",
192
+ "border border-gray-200 bg-white text-gray-700",
193
+ "focus:outline-none focus:ring-1 focus:ring-[#00A3E1]",
194
+ "resize-y"
195
+ )}
196
+ placeholder="Edit text content..."
197
+ />
198
+ </Section>
199
+
200
+ {/* Typography Editor */}
201
+ <Section title="Typography">
202
+ <div className="space-y-3">
203
+ {/* Font Size - Dropdown */}
204
+ <SelectField
205
+ label="Size"
206
+ value={currentOverride.fontSize || ""}
207
+ options={[
208
+ { value: "", label: "Default" },
209
+ { value: "12px", label: "Small (12px)" },
210
+ { value: "14px", label: "Body (14px)" },
211
+ { value: "16px", label: "Medium (16px)" },
212
+ { value: "18px", label: "Large (18px)" },
213
+ { value: "20px", label: "XL (20px)" },
214
+ { value: "24px", label: "2XL (24px)" },
215
+ { value: "30px", label: "3XL (30px)" },
216
+ { value: "36px", label: "4XL (36px)" },
217
+ { value: "48px", label: "5XL (48px)" },
218
+ { value: "60px", label: "6XL (60px)" },
219
+ ]}
220
+ onChange={(val) => updateOverride({ fontSize: val })}
221
+ />
222
+
223
+ {/* Font Weight - Already a dropdown */}
224
+ <SelectField
225
+ label="Weight"
226
+ value={currentOverride.fontWeight || ""}
227
+ options={[
228
+ { value: "", label: "Default" },
229
+ { value: "300", label: "Light (300)" },
230
+ { value: "400", label: "Regular (400)" },
231
+ { value: "500", label: "Medium (500)" },
232
+ { value: "600", label: "SemiBold (600)" },
233
+ { value: "700", label: "Bold (700)" },
234
+ ]}
235
+ onChange={(val) => updateOverride({ fontWeight: val })}
236
+ />
237
+
238
+ {/* Line Height - Dropdown */}
239
+ <SelectField
240
+ label="Line Height"
241
+ value={currentOverride.lineHeight || ""}
242
+ options={[
243
+ { value: "", label: "Default" },
244
+ { value: "1", label: "Tight (1)" },
245
+ { value: "1.25", label: "Snug (1.25)" },
246
+ { value: "1.5", label: "Normal (1.5)" },
247
+ { value: "1.75", label: "Relaxed (1.75)" },
248
+ { value: "2", label: "Loose (2)" },
249
+ ]}
250
+ onChange={(val) => updateOverride({ lineHeight: val })}
251
+ />
252
+
253
+ {/* Letter Spacing - Dropdown */}
254
+ <SelectField
255
+ label="Letter Spacing"
256
+ value={currentOverride.letterSpacing || ""}
257
+ options={[
258
+ { value: "", label: "Default" },
259
+ { value: "-0.05em", label: "Tighter" },
260
+ { value: "-0.025em", label: "Tight" },
261
+ { value: "0", label: "Normal" },
262
+ { value: "0.025em", label: "Wide" },
263
+ { value: "0.05em", label: "Wider" },
264
+ { value: "0.1em", label: "Widest" },
265
+ ]}
266
+ onChange={(val) => updateOverride({ letterSpacing: val })}
267
+ />
268
+
269
+ {/* Color - Theme-Aware Color Picker */}
270
+ <div className="space-y-1">
271
+ <div className="flex items-center gap-2">
272
+ <label className="text-xs text-gray-500">Color</label>
273
+ <div className={cn(
274
+ "flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium",
275
+ currentTheme === "dark"
276
+ ? "bg-gray-800 text-gray-200"
277
+ : "bg-gray-100 text-gray-600"
278
+ )}>
279
+ {currentTheme === "dark" ? (
280
+ <Moon className="h-2.5 w-2.5" />
281
+ ) : (
282
+ <Sun className="h-2.5 w-2.5" />
283
+ )}
284
+ {currentTheme === "dark" ? "Dark" : "Light"}
285
+ </div>
286
+ </div>
287
+ <div className="flex gap-2">
288
+ <input
289
+ type="color"
290
+ value={
291
+ currentTheme === "dark"
292
+ ? (currentOverride.colorDark || currentOverride.color || "#FFFFFF")
293
+ : (currentOverride.colorLight || currentOverride.color || "#000000")
294
+ }
295
+ onChange={(e) => {
296
+ if (currentTheme === "dark") {
297
+ updateOverride({ colorDark: e.target.value });
298
+ } else {
299
+ updateOverride({ colorLight: e.target.value });
300
+ }
301
+ }}
302
+ className="w-8 h-7 p-0 border border-gray-200 rounded cursor-pointer"
303
+ title={`Pick color for ${currentTheme} mode`}
304
+ />
305
+ <input
306
+ type="text"
307
+ value={
308
+ currentTheme === "dark"
309
+ ? (currentOverride.colorDark || "")
310
+ : (currentOverride.colorLight || "")
311
+ }
312
+ onChange={(e) => {
313
+ if (currentTheme === "dark") {
314
+ updateOverride({ colorDark: e.target.value });
315
+ } else {
316
+ updateOverride({ colorLight: e.target.value });
317
+ }
318
+ }}
319
+ placeholder={currentTheme === "dark" ? "#FFFFFF" : "#333333"}
320
+ className="flex-1 h-7 px-2 text-xs border border-gray-200 rounded focus:border-[#00A3E1] focus:outline-none font-mono"
321
+ />
322
+ </div>
323
+ <p className="text-[10px] text-gray-400">
324
+ Switch theme to set color for {currentTheme === "dark" ? "light" : "dark"} mode
325
+ </p>
326
+ </div>
327
+ </div>
328
+ </Section>
329
+ </div>
330
+ </div>
331
+ );
332
+ }
@@ -0,0 +1,295 @@
1
+
2
+ import React from "react";
3
+
4
+ export type TabId = "analysis" | "components" | "logos" | "text";
5
+
6
+ export interface TabDefinition {
7
+ id: TabId;
8
+ label: string;
9
+ icon: React.ComponentType<{ className?: string }>;
10
+ }
11
+
12
+ export type SaveStatus = "idle" | "saving" | "success" | "error";
13
+
14
+ // Detected element types for Visual Inspector
15
+ export type DetectedElementType = "component" | "logo" | "text";
16
+
17
+ export interface DetectedElement {
18
+ name: string;
19
+ rect: DOMRect;
20
+ type: DetectedElementType;
21
+ /** Unique ID for logo elements (for selection/editing) */
22
+ logoId?: string;
23
+ /** Unique ID for text elements (for selection/editing) */
24
+ textId?: string;
25
+ /** The actual text content (for text elements) */
26
+ textContent?: string;
27
+ /** Component variant ID (hash of styles/classes) to distinguish visual styles */
28
+ variantId?: string;
29
+ }
30
+
31
+ // Logo asset from the API
32
+ export interface LogoAsset {
33
+ id: string;
34
+ name: string;
35
+ path: string;
36
+ brand: string;
37
+ extension: string;
38
+ }
39
+
40
+ // Logo override configuration
41
+ export interface LogoOverride {
42
+ src?: string; // Single source (fallback, or used when no theme-specific)
43
+ srcLight?: string; // Logo for light mode (dark logo on light bg)
44
+ srcDark?: string; // Logo for dark mode (light logo on dark bg)
45
+ width?: number;
46
+ height?: number;
47
+ scale?: number;
48
+ reset?: boolean; // If true, indicates this config is resetting to defaults
49
+ }
50
+
51
+ // Text override configuration
52
+ export interface TextOverride {
53
+ textContent?: string;
54
+ fontSize?: string;
55
+ fontWeight?: string;
56
+ lineHeight?: string;
57
+ letterSpacing?: string;
58
+ color?: string; // Keep for backwards compatibility
59
+ colorLight?: string; // Color for light mode
60
+ colorDark?: string; // Color for dark mode
61
+ fontFamily?: string;
62
+ reset?: boolean;
63
+ // Element identification for persistence across page reloads
64
+ selector?: string; // CSS selector to find element
65
+ originalText?: string; // Original text content for fallback matching
66
+ tagName?: string; // Element tag name (e.g., 'h1', 'p', 'a')
67
+ }
68
+
69
+ // Save status for logo persistence
70
+ export type LogoSaveStatus = "idle" | "saving" | "success" | "error";
71
+
72
+ // Original logo state for reset
73
+ export interface OriginalLogoState {
74
+ src: string;
75
+ width: number;
76
+ height: number;
77
+ srcset?: string;
78
+ }
79
+
80
+ // Original text state for reset
81
+ export interface OriginalTextState {
82
+ textContent: string | null;
83
+ fontSize: string;
84
+ fontWeight: string;
85
+ lineHeight: string;
86
+ letterSpacing: string;
87
+ color: string;
88
+ fontFamily: string;
89
+ }
90
+
91
+ // Component-specific style overrides for scalable styling
92
+ export interface ComponentStyle {
93
+ backgroundColor?: string;
94
+ textColor?: string;
95
+ borderRadius?: string;
96
+ borderColor?: string;
97
+ fontWeight?: string;
98
+ }
99
+
100
+ // Map of component key -> style override
101
+ export type ComponentStyleOverride = Record<string, ComponentStyle>;
102
+
103
+ // ---- AI Chat Interfaces ----
104
+
105
+ export interface ChatMessage {
106
+ id: string;
107
+ role: "user" | "assistant";
108
+ content: string;
109
+ timestamp: Date;
110
+ editResult?: AIEditResult;
111
+ }
112
+
113
+ export interface AIEditResult {
114
+ success: boolean;
115
+ modifiedCode?: string;
116
+ diff?: string;
117
+ explanation?: string;
118
+ error?: string;
119
+ }
120
+
121
+ export interface PendingEdit {
122
+ filePath: string;
123
+ originalCode: string;
124
+ modifiedCode: string;
125
+ diff: string;
126
+ explanation: string;
127
+ // AI-generated CSS for live preview (no parsing needed)
128
+ previewCSS?: string;
129
+ }
130
+
131
+ // Analysis result from the project analyzer
132
+ export interface AnalysisImageElement {
133
+ id: string;
134
+ filePath: string;
135
+ lineNumber: number;
136
+ elementType: "Image" | "img";
137
+ srcValue: string;
138
+ srcType: "literal" | "variable" | "import";
139
+ hasId: boolean;
140
+ existingId?: string;
141
+ alt?: string;
142
+ suggestedId?: string;
143
+ context: {
144
+ parentComponent?: string;
145
+ semanticContainer?: string;
146
+ };
147
+ }
148
+
149
+ export interface CategorySummary {
150
+ total: number;
151
+ withId: number;
152
+ missingId: number;
153
+ }
154
+
155
+ export interface AnalysisResult {
156
+ timestamp: string;
157
+ scanDuration: number;
158
+ filesScanned: number;
159
+ elements: AnalysisImageElement[];
160
+ images: AnalysisImageElement[];
161
+ themeFiles: { filePath: string; type: string; hasBrandVariables: boolean }[];
162
+ summary: {
163
+ totalElements: number;
164
+ elementsWithId: number;
165
+ elementsMissingId: number;
166
+ byCategory: {
167
+ image: CategorySummary;
168
+ text: CategorySummary;
169
+ interactive: CategorySummary;
170
+ input: CategorySummary;
171
+ definition: CategorySummary;
172
+ };
173
+ // Legacy fields
174
+ totalImages: number;
175
+ imagesWithId: number;
176
+ imagesMissingId: number;
177
+ brandLogosDetected: number;
178
+ };
179
+ }
180
+
181
+ export type AnalysisStatus = "idle" | "scanning" | "complete" | "error";
182
+
183
+ export type ConfigSection = 'colors' | 'radius' | 'typography';
184
+
185
+ export type AutoFixStatus = "idle" | "fixing" | "success" | "error";
186
+
187
+ export interface LogoToolsPanelProps {
188
+ logoAssets: LogoAsset[];
189
+ logoAssetsByBrand: Record<string, LogoAsset[]>;
190
+ selectedLogoId: string | null;
191
+ onSelectLogo: (id: string | null) => void;
192
+ globalLogoConfig: LogoOverride;
193
+ individualLogoConfigs: Record<string, LogoOverride>;
194
+ onGlobalConfigChange: (config: LogoOverride) => void;
195
+ onIndividualConfigChange: (logoId: string, config: LogoOverride) => void;
196
+ onResetAll: () => void;
197
+ onResetLogo: (logoId: string) => void;
198
+ onSaveChanges: () => void;
199
+ saveStatus: LogoSaveStatus;
200
+ saveMessage: string;
201
+ findComplementaryLogo: (path: string) => { light?: string; dark?: string } | null;
202
+ currentTheme: string;
203
+ onAutoFixId?: (element: DetectedElement) => void;
204
+ autoFixStatus?: AutoFixStatus;
205
+ autoFixMessage?: string;
206
+ }
207
+
208
+ export type ComponentsViewMode = "visual" | "inspector";
209
+
210
+ // ---- Vision Mode Types ----
211
+
212
+ export interface VisionFocusedElement {
213
+ name: string;
214
+ type: DetectedElementType;
215
+ variantId?: string;
216
+ coordinates: {
217
+ x: number;
218
+ y: number;
219
+ width: number;
220
+ height: number;
221
+ };
222
+ description?: string;
223
+ }
224
+
225
+ export interface VisionEditRequest {
226
+ action: "analyze" | "edit" | "save";
227
+ screenshot?: string;
228
+ pageRoute: string;
229
+ userPrompt: string;
230
+ focusedElements?: VisionFocusedElement[];
231
+ modifications?: VisionFileModification[];
232
+ }
233
+
234
+ export interface VisionFileModification {
235
+ filePath: string;
236
+ originalContent: string;
237
+ modifiedContent: string;
238
+ diff: string;
239
+ explanation: string;
240
+ previewCSS?: string;
241
+ }
242
+
243
+ export interface VisionEditResponse {
244
+ success: boolean;
245
+ modifications?: VisionFileModification[];
246
+ aggregatedPreviewCSS?: string;
247
+ explanation?: string;
248
+ reasoning?: string;
249
+ error?: string;
250
+ }
251
+
252
+ export interface VisionPendingEdit {
253
+ modifications: VisionFileModification[];
254
+ aggregatedPreviewCSS: string;
255
+ explanation: string;
256
+ }
257
+
258
+ // ---- Apply-First Mode Types (Cursor-style instant preview) ----
259
+
260
+ export interface ApplyFirstSession {
261
+ sessionId: string;
262
+ modifications: VisionFileModification[];
263
+ appliedAt: number;
264
+ status: 'applied' | 'accepted' | 'reverted';
265
+ backupPaths: string[];
266
+ }
267
+
268
+ export type ApplyFirstStatus =
269
+ | 'idle' // No active session
270
+ | 'generating' // AI is generating changes
271
+ | 'applying' // Writing files to disk
272
+ | 'waiting-hmr' // Waiting for HMR to show changes
273
+ | 'reviewing' // User is reviewing actual changes
274
+ | 'accepting' // User clicked accept, cleaning up backups
275
+ | 'reverting' // User clicked revert, restoring backups
276
+ | 'error'; // Something went wrong
277
+
278
+ export interface ApplyFirstRequest {
279
+ action: "apply" | "accept" | "revert";
280
+ sessionId?: string;
281
+ screenshot?: string;
282
+ pageRoute?: string;
283
+ userPrompt?: string;
284
+ focusedElements?: VisionFocusedElement[];
285
+ }
286
+
287
+ export interface ApplyFirstResponse {
288
+ success: boolean;
289
+ sessionId?: string;
290
+ modifications?: VisionFileModification[];
291
+ backupPaths?: string[];
292
+ explanation?: string;
293
+ reasoning?: string;
294
+ error?: string;
295
+ }