sonance-brand-mcp 1.3.73 → 1.3.75
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.
|
@@ -177,6 +177,48 @@ interface ScreenshotAnalysis {
|
|
|
177
177
|
visibleText: string[];
|
|
178
178
|
componentNames: string[];
|
|
179
179
|
codePatterns: string[];
|
|
180
|
+
elementTypes: string[]; // Detected UI element types: table, button, form, card, list, modal, dropdown, input
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Map element types to JSX/HTML patterns to search for in files
|
|
185
|
+
* This is the key to Cursor-style file discovery - search for WHAT we see, not guessed names
|
|
186
|
+
*/
|
|
187
|
+
const ELEMENT_PATTERNS: Record<string, string[]> = {
|
|
188
|
+
table: ['<Table', 'TableRow', 'TableCell', 'TableHeader', 'TableBody', '<table', '<tr', '<td', '<th'],
|
|
189
|
+
button: ['<Button', '<button', 'onClick={', 'handleClick'],
|
|
190
|
+
form: ['<Form', '<form', 'onSubmit', '<Input', '<input', 'handleSubmit'],
|
|
191
|
+
card: ['<Card', 'CardContent', 'CardHeader', 'CardTitle', 'CardDescription'],
|
|
192
|
+
list: ['<List', '<ul', '<li', 'ListItem', '.map(('],
|
|
193
|
+
modal: ['<Modal', '<Dialog', 'DialogContent', 'isOpen', 'onClose', 'onOpenChange'],
|
|
194
|
+
dropdown: ['<Dropdown', '<Select', 'DropdownMenu', 'SelectContent', 'SelectTrigger'],
|
|
195
|
+
input: ['<Input', '<input', '<Textarea', '<textarea', 'onChange={'],
|
|
196
|
+
header: ['<Header', '<header', '<Navbar', '<Nav', 'navigation'],
|
|
197
|
+
sidebar: ['<Sidebar', '<aside', 'SidebarContent', 'NavigationMenu'],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Score a file based on whether it contains patterns for a specific element type
|
|
202
|
+
* Higher scores indicate the file is more likely to contain the target element
|
|
203
|
+
*/
|
|
204
|
+
function scoreFileByElementType(content: string, elementType: string): number {
|
|
205
|
+
const patterns = ELEMENT_PATTERNS[elementType.toLowerCase()] || [];
|
|
206
|
+
let score = 0;
|
|
207
|
+
let matchCount = 0;
|
|
208
|
+
|
|
209
|
+
for (const pattern of patterns) {
|
|
210
|
+
if (content.includes(pattern)) {
|
|
211
|
+
score += 300; // High score per pattern match
|
|
212
|
+
matchCount++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Bonus for multiple pattern matches (indicates primary component for this element type)
|
|
217
|
+
if (matchCount >= 3) {
|
|
218
|
+
score += 500;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return score;
|
|
180
222
|
}
|
|
181
223
|
|
|
182
224
|
/**
|
|
@@ -216,15 +258,17 @@ Analyze this UI to help find the correct source file. Return:
|
|
|
216
258
|
1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
|
|
217
259
|
2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
|
|
218
260
|
3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
|
|
261
|
+
4. **elementTypes**: What TYPE of UI element is the user's request about? Pick from: table, button, form, card, list, modal, dropdown, input, header, sidebar. List the most relevant first.
|
|
219
262
|
|
|
220
263
|
Return ONLY valid JSON:
|
|
221
264
|
{
|
|
222
265
|
"visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
|
|
223
266
|
"componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
|
|
224
|
-
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
|
|
267
|
+
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"],
|
|
268
|
+
"elementTypes": ["table", "button"]
|
|
225
269
|
}
|
|
226
270
|
|
|
227
|
-
Be smart - use your knowledge of React patterns to
|
|
271
|
+
Be smart - use your knowledge of React patterns to identify what type of element the user wants to modify.`,
|
|
228
272
|
},
|
|
229
273
|
],
|
|
230
274
|
},
|
|
@@ -234,7 +278,7 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
234
278
|
try {
|
|
235
279
|
const textBlock = response.content.find((block) => block.type === "text");
|
|
236
280
|
if (!textBlock || textBlock.type !== "text") {
|
|
237
|
-
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
281
|
+
return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
|
|
238
282
|
}
|
|
239
283
|
|
|
240
284
|
let jsonText = textBlock.text.trim();
|
|
@@ -249,13 +293,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
249
293
|
visibleText: parsed.visibleText || parsed.textStrings || [],
|
|
250
294
|
componentNames: parsed.componentNames || [],
|
|
251
295
|
codePatterns: parsed.codePatterns || [],
|
|
296
|
+
elementTypes: parsed.elementTypes || [],
|
|
252
297
|
};
|
|
253
298
|
|
|
254
299
|
debugLog("Phase 1: Analyzed screenshot for search", result);
|
|
255
300
|
return result;
|
|
256
301
|
} catch (e) {
|
|
257
302
|
debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
|
|
258
|
-
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
303
|
+
return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
|
|
259
304
|
}
|
|
260
305
|
}
|
|
261
306
|
|
|
@@ -547,12 +592,18 @@ function searchFilesSmart(
|
|
|
547
592
|
projectRoot: string,
|
|
548
593
|
maxFiles: number = 10
|
|
549
594
|
): { path: string; content: string; score: number; filenameMatch: boolean }[] {
|
|
550
|
-
const { visibleText, componentNames, codePatterns } = analysis;
|
|
595
|
+
const { visibleText, componentNames, codePatterns, elementTypes } = analysis;
|
|
551
596
|
|
|
552
597
|
// Combine all search terms
|
|
553
598
|
const allSearchTerms = [...visibleText, ...codePatterns];
|
|
554
599
|
|
|
555
|
-
|
|
600
|
+
// Get the primary element type for pattern-based scoring (Cursor-style)
|
|
601
|
+
const primaryElementType = elementTypes?.[0] || null;
|
|
602
|
+
if (primaryElementType) {
|
|
603
|
+
debugLog("Phase 2: Element type detected for pattern matching", { primaryElementType });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (allSearchTerms.length === 0 && componentNames.length === 0 && !primaryElementType) return [];
|
|
556
607
|
|
|
557
608
|
const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
|
|
558
609
|
const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
|
|
@@ -619,23 +670,47 @@ function searchFilesSmart(
|
|
|
619
670
|
}
|
|
620
671
|
}
|
|
621
672
|
|
|
673
|
+
// Calculate content score based on text matches
|
|
674
|
+
let contentScore = 0;
|
|
622
675
|
if (uniqueMatches > 0) {
|
|
623
676
|
// Score: unique matches * 10 + total matches
|
|
624
|
-
|
|
625
|
-
|
|
677
|
+
contentScore = (uniqueMatches * 10) + totalMatches;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// CURSOR-STYLE: Add element type pattern scoring
|
|
681
|
+
// Files containing patterns for the detected element type get a big boost
|
|
682
|
+
let elementTypeScore = 0;
|
|
683
|
+
if (primaryElementType) {
|
|
684
|
+
elementTypeScore = scoreFileByElementType(content, primaryElementType);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const totalScore = contentScore + elementTypeScore;
|
|
688
|
+
|
|
689
|
+
if (totalScore > 0) {
|
|
626
690
|
// Check if we already have this file (from component name search)
|
|
627
691
|
const existing = results.get(entryPath);
|
|
628
692
|
if (existing) {
|
|
629
|
-
// Add
|
|
630
|
-
existing.score +=
|
|
693
|
+
// Add scores to existing entry
|
|
694
|
+
existing.score += totalScore;
|
|
631
695
|
} else {
|
|
632
696
|
results.set(entryPath, {
|
|
633
697
|
path: entryPath,
|
|
634
698
|
content,
|
|
635
|
-
score:
|
|
699
|
+
score: totalScore,
|
|
636
700
|
filenameMatch: false
|
|
637
701
|
});
|
|
638
702
|
}
|
|
703
|
+
|
|
704
|
+
// Log high-confidence element type matches
|
|
705
|
+
if (elementTypeScore >= 800) {
|
|
706
|
+
debugLog("Phase 2: High-confidence element type match", {
|
|
707
|
+
file: entryPath,
|
|
708
|
+
elementType: primaryElementType,
|
|
709
|
+
elementScore: elementTypeScore,
|
|
710
|
+
contentScore,
|
|
711
|
+
totalScore
|
|
712
|
+
});
|
|
713
|
+
}
|
|
639
714
|
}
|
|
640
715
|
} catch {
|
|
641
716
|
// Skip files that can't be read
|
|
@@ -958,7 +1033,7 @@ User Request: "${userPrompt}"
|
|
|
958
1033
|
|
|
959
1034
|
// Search for element IDs in the file to enable precise targeting
|
|
960
1035
|
let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
|
|
961
|
-
|
|
1036
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
962
1037
|
for (const el of focusedElements) {
|
|
963
1038
|
idMatch = findElementIdInFile(content, el.elementId, el.childIds);
|
|
964
1039
|
if (idMatch) break;
|
|
@@ -1049,7 +1124,7 @@ ${linesWithNumbers}
|
|
|
1049
1124
|
// Only include the variants/props section, not the whole file
|
|
1050
1125
|
const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
|
|
1051
1126
|
if (variantsMatch) {
|
|
1052
|
-
|
|
1127
|
+
textContent += `
|
|
1053
1128
|
--- UI Component: ${comp.path} (variants only) ---
|
|
1054
1129
|
${variantsMatch[0]}
|
|
1055
1130
|
---
|
|
@@ -1123,7 +1198,7 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
|
|
|
1123
1198
|
|
|
1124
1199
|
// Call Claude Vision API with retry mechanism
|
|
1125
1200
|
const anthropic = new Anthropic({ apiKey });
|
|
1126
|
-
|
|
1201
|
+
|
|
1127
1202
|
// Build set of valid file paths from page context (needed for validation)
|
|
1128
1203
|
const validFilePaths = new Set<string>();
|
|
1129
1204
|
if (pageContext.pageFile) {
|
|
@@ -173,6 +173,48 @@ interface ScreenshotAnalysis {
|
|
|
173
173
|
visibleText: string[];
|
|
174
174
|
componentNames: string[];
|
|
175
175
|
codePatterns: string[];
|
|
176
|
+
elementTypes: string[]; // Detected UI element types: table, button, form, card, list, modal, dropdown, input
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Map element types to JSX/HTML patterns to search for in files
|
|
181
|
+
* This is the key to Cursor-style file discovery - search for WHAT we see, not guessed names
|
|
182
|
+
*/
|
|
183
|
+
const ELEMENT_PATTERNS: Record<string, string[]> = {
|
|
184
|
+
table: ['<Table', 'TableRow', 'TableCell', 'TableHeader', 'TableBody', '<table', '<tr', '<td', '<th'],
|
|
185
|
+
button: ['<Button', '<button', 'onClick={', 'handleClick'],
|
|
186
|
+
form: ['<Form', '<form', 'onSubmit', '<Input', '<input', 'handleSubmit'],
|
|
187
|
+
card: ['<Card', 'CardContent', 'CardHeader', 'CardTitle', 'CardDescription'],
|
|
188
|
+
list: ['<List', '<ul', '<li', 'ListItem', '.map(('],
|
|
189
|
+
modal: ['<Modal', '<Dialog', 'DialogContent', 'isOpen', 'onClose', 'onOpenChange'],
|
|
190
|
+
dropdown: ['<Dropdown', '<Select', 'DropdownMenu', 'SelectContent', 'SelectTrigger'],
|
|
191
|
+
input: ['<Input', '<input', '<Textarea', '<textarea', 'onChange={'],
|
|
192
|
+
header: ['<Header', '<header', '<Navbar', '<Nav', 'navigation'],
|
|
193
|
+
sidebar: ['<Sidebar', '<aside', 'SidebarContent', 'NavigationMenu'],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Score a file based on whether it contains patterns for a specific element type
|
|
198
|
+
* Higher scores indicate the file is more likely to contain the target element
|
|
199
|
+
*/
|
|
200
|
+
function scoreFileByElementType(content: string, elementType: string): number {
|
|
201
|
+
const patterns = ELEMENT_PATTERNS[elementType.toLowerCase()] || [];
|
|
202
|
+
let score = 0;
|
|
203
|
+
let matchCount = 0;
|
|
204
|
+
|
|
205
|
+
for (const pattern of patterns) {
|
|
206
|
+
if (content.includes(pattern)) {
|
|
207
|
+
score += 300; // High score per pattern match
|
|
208
|
+
matchCount++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Bonus for multiple pattern matches (indicates primary component for this element type)
|
|
213
|
+
if (matchCount >= 3) {
|
|
214
|
+
score += 500;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return score;
|
|
176
218
|
}
|
|
177
219
|
|
|
178
220
|
/**
|
|
@@ -212,15 +254,17 @@ Analyze this UI to help find the correct source file. Return:
|
|
|
212
254
|
1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
|
|
213
255
|
2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
|
|
214
256
|
3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
|
|
257
|
+
4. **elementTypes**: What TYPE of UI element is the user's request about? Pick from: table, button, form, card, list, modal, dropdown, input, header, sidebar. List the most relevant first.
|
|
215
258
|
|
|
216
259
|
Return ONLY valid JSON:
|
|
217
260
|
{
|
|
218
261
|
"visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
|
|
219
262
|
"componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
|
|
220
|
-
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
|
|
263
|
+
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"],
|
|
264
|
+
"elementTypes": ["table", "button"]
|
|
221
265
|
}
|
|
222
266
|
|
|
223
|
-
Be smart - use your knowledge of React patterns to
|
|
267
|
+
Be smart - use your knowledge of React patterns to identify what type of element the user wants to modify.`,
|
|
224
268
|
},
|
|
225
269
|
],
|
|
226
270
|
},
|
|
@@ -230,7 +274,7 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
230
274
|
try {
|
|
231
275
|
const textBlock = response.content.find((block) => block.type === "text");
|
|
232
276
|
if (!textBlock || textBlock.type !== "text") {
|
|
233
|
-
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
277
|
+
return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
|
|
234
278
|
}
|
|
235
279
|
|
|
236
280
|
let jsonText = textBlock.text.trim();
|
|
@@ -245,13 +289,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
245
289
|
visibleText: parsed.visibleText || parsed.textStrings || [],
|
|
246
290
|
componentNames: parsed.componentNames || [],
|
|
247
291
|
codePatterns: parsed.codePatterns || [],
|
|
292
|
+
elementTypes: parsed.elementTypes || [],
|
|
248
293
|
};
|
|
249
294
|
|
|
250
295
|
debugLog("Phase 1: Analyzed screenshot for search", result);
|
|
251
296
|
return result;
|
|
252
297
|
} catch (e) {
|
|
253
298
|
debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
|
|
254
|
-
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
299
|
+
return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
|
|
255
300
|
}
|
|
256
301
|
}
|
|
257
302
|
|
|
@@ -543,12 +588,18 @@ function searchFilesSmart(
|
|
|
543
588
|
projectRoot: string,
|
|
544
589
|
maxFiles: number = 10
|
|
545
590
|
): { path: string; content: string; score: number; filenameMatch: boolean }[] {
|
|
546
|
-
const { visibleText, componentNames, codePatterns } = analysis;
|
|
591
|
+
const { visibleText, componentNames, codePatterns, elementTypes } = analysis;
|
|
547
592
|
|
|
548
593
|
// Combine all search terms
|
|
549
594
|
const allSearchTerms = [...visibleText, ...codePatterns];
|
|
550
595
|
|
|
551
|
-
|
|
596
|
+
// Get the primary element type for pattern-based scoring (Cursor-style)
|
|
597
|
+
const primaryElementType = elementTypes?.[0] || null;
|
|
598
|
+
if (primaryElementType) {
|
|
599
|
+
debugLog("Phase 2: Element type detected for pattern matching", { primaryElementType });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (allSearchTerms.length === 0 && componentNames.length === 0 && !primaryElementType) return [];
|
|
552
603
|
|
|
553
604
|
const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
|
|
554
605
|
const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
|
|
@@ -615,23 +666,47 @@ function searchFilesSmart(
|
|
|
615
666
|
}
|
|
616
667
|
}
|
|
617
668
|
|
|
669
|
+
// Calculate content score based on text matches
|
|
670
|
+
let contentScore = 0;
|
|
618
671
|
if (uniqueMatches > 0) {
|
|
619
672
|
// Score: unique matches * 10 + total matches
|
|
620
|
-
|
|
621
|
-
|
|
673
|
+
contentScore = (uniqueMatches * 10) + totalMatches;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// CURSOR-STYLE: Add element type pattern scoring
|
|
677
|
+
// Files containing patterns for the detected element type get a big boost
|
|
678
|
+
let elementTypeScore = 0;
|
|
679
|
+
if (primaryElementType) {
|
|
680
|
+
elementTypeScore = scoreFileByElementType(content, primaryElementType);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const totalScore = contentScore + elementTypeScore;
|
|
684
|
+
|
|
685
|
+
if (totalScore > 0) {
|
|
622
686
|
// Check if we already have this file (from component name search)
|
|
623
687
|
const existing = results.get(entryPath);
|
|
624
688
|
if (existing) {
|
|
625
|
-
// Add
|
|
626
|
-
existing.score +=
|
|
689
|
+
// Add scores to existing entry
|
|
690
|
+
existing.score += totalScore;
|
|
627
691
|
} else {
|
|
628
692
|
results.set(entryPath, {
|
|
629
693
|
path: entryPath,
|
|
630
694
|
content,
|
|
631
|
-
score:
|
|
695
|
+
score: totalScore,
|
|
632
696
|
filenameMatch: false
|
|
633
697
|
});
|
|
634
698
|
}
|
|
699
|
+
|
|
700
|
+
// Log high-confidence element type matches
|
|
701
|
+
if (elementTypeScore >= 800) {
|
|
702
|
+
debugLog("Phase 2: High-confidence element type match", {
|
|
703
|
+
file: entryPath,
|
|
704
|
+
elementType: primaryElementType,
|
|
705
|
+
elementScore: elementTypeScore,
|
|
706
|
+
contentScore,
|
|
707
|
+
totalScore
|
|
708
|
+
});
|
|
709
|
+
}
|
|
635
710
|
}
|
|
636
711
|
} catch {
|
|
637
712
|
// Skip files that can't be read
|
|
@@ -927,7 +1002,7 @@ User Request: "${userPrompt}"
|
|
|
927
1002
|
|
|
928
1003
|
// Search for element IDs in the file to enable precise targeting
|
|
929
1004
|
let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
|
|
930
|
-
|
|
1005
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
931
1006
|
for (const el of focusedElements) {
|
|
932
1007
|
idMatch = findElementIdInFile(content, el.elementId, el.childIds);
|
|
933
1008
|
if (idMatch) break;
|
|
@@ -1018,7 +1093,7 @@ ${linesWithNumbers}
|
|
|
1018
1093
|
// Only include the variants/props section, not the whole file
|
|
1019
1094
|
const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
|
|
1020
1095
|
if (variantsMatch) {
|
|
1021
|
-
|
|
1096
|
+
textContent += `
|
|
1022
1097
|
--- UI Component: ${comp.path} (variants only) ---
|
|
1023
1098
|
${variantsMatch[0]}
|
|
1024
1099
|
---
|
|
@@ -1092,7 +1167,7 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
|
|
|
1092
1167
|
|
|
1093
1168
|
// Call Claude Vision API with retry mechanism
|
|
1094
1169
|
const anthropic = new Anthropic({ apiKey });
|
|
1095
|
-
|
|
1170
|
+
|
|
1096
1171
|
// Build list of known file paths (for logging)
|
|
1097
1172
|
const knownPaths = new Set<string>();
|
|
1098
1173
|
if (pageContext.pageFile) {
|
|
@@ -271,6 +271,12 @@ export function SonanceDevTools() {
|
|
|
271
271
|
// Check if session is recent (< 1 hour old)
|
|
272
272
|
const MAX_SESSION_AGE = 60 * 60 * 1000; // 1 hour
|
|
273
273
|
if (session.timestamp && Date.now() - session.timestamp < MAX_SESSION_AGE) {
|
|
274
|
+
// Validate session has actual modifications
|
|
275
|
+
if (!session.modifications || session.modifications.length === 0) {
|
|
276
|
+
console.log("[Apply-First] Session has no modifications, auto-clearing:", session.sessionId);
|
|
277
|
+
localStorage.removeItem("sonance-apply-first-session");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
274
280
|
console.log("[Apply-First] Restoring session from localStorage:", session.sessionId);
|
|
275
281
|
setApplyFirstSession(session);
|
|
276
282
|
setApplyFirstStatus("reviewing");
|
|
@@ -1,176 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect
|
|
4
|
-
import { X, Zap, Loader2, Check, ChevronDown, ChevronRight, FileCode, AlertTriangle, Info
|
|
3
|
+
import React, { useState, useEffect } from "react";
|
|
4
|
+
import { X, Zap, Loader2, Check, ChevronDown, ChevronRight, FileCode, AlertTriangle, Info } from "lucide-react";
|
|
5
5
|
import { cn } from "../../../lib/utils";
|
|
6
6
|
import { ApplyFirstSession, ApplyFirstStatus, VisionFileModification } from "../types";
|
|
7
7
|
|
|
8
|
-
// CSS Injection Preview ID
|
|
9
|
-
const PREVIEW_STYLE_ID = "sonance-preview-css";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Extract className changes from a diff
|
|
13
|
-
* Returns: { removed: string[], added: string[] }
|
|
14
|
-
*/
|
|
15
|
-
function extractClassNameChanges(diff: string): { removed: string[]; added: string[] } {
|
|
16
|
-
const removed: string[] = [];
|
|
17
|
-
const added: string[] = [];
|
|
18
|
-
|
|
19
|
-
// Regex to find className="..." patterns
|
|
20
|
-
const classNameRegex = /className=["']([^"']+)["']/g;
|
|
21
|
-
|
|
22
|
-
const lines = diff.split("\n");
|
|
23
|
-
for (const line of lines) {
|
|
24
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
25
|
-
// Removed line - extract className
|
|
26
|
-
let match;
|
|
27
|
-
while ((match = classNameRegex.exec(line)) !== null) {
|
|
28
|
-
removed.push(...match[1].split(/\s+/));
|
|
29
|
-
}
|
|
30
|
-
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
31
|
-
// Added line - extract className
|
|
32
|
-
let match;
|
|
33
|
-
while ((match = classNameRegex.exec(line)) !== null) {
|
|
34
|
-
added.push(...match[1].split(/\s+/));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return { removed, added };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Common Tailwind class to CSS mapping for preview
|
|
44
|
-
* Covers styling classes for instant visual feedback
|
|
45
|
-
*/
|
|
46
|
-
const TAILWIND_TO_CSS: Record<string, string> = {
|
|
47
|
-
// Text colors - basic
|
|
48
|
-
"text-white": "color: white !important;",
|
|
49
|
-
"text-black": "color: black !important;",
|
|
50
|
-
"text-gray-900": "color: rgb(17, 24, 39) !important;",
|
|
51
|
-
"text-gray-800": "color: rgb(31, 41, 55) !important;",
|
|
52
|
-
"text-gray-700": "color: rgb(55, 65, 81) !important;",
|
|
53
|
-
"text-gray-600": "color: rgb(75, 85, 99) !important;",
|
|
54
|
-
"text-gray-500": "color: rgb(107, 114, 128) !important;",
|
|
55
|
-
"text-gray-400": "color: rgb(156, 163, 175) !important;",
|
|
56
|
-
|
|
57
|
-
// Sonance brand colors
|
|
58
|
-
"text-sonance-charcoal": "color: #333F48 !important;",
|
|
59
|
-
"text-sonance-blue": "color: #00D3C8 !important;",
|
|
60
|
-
"border-sonance-charcoal": "border-color: #333F48 !important;",
|
|
61
|
-
"border-sonance-blue": "border-color: #00D3C8 !important;",
|
|
62
|
-
"bg-sonance-charcoal": "background-color: #333F48 !important;",
|
|
63
|
-
"bg-sonance-blue": "background-color: #00D3C8 !important;",
|
|
64
|
-
|
|
65
|
-
// Semantic text colors
|
|
66
|
-
"text-primary": "color: var(--primary) !important;",
|
|
67
|
-
"text-primary-foreground": "color: var(--primary-foreground) !important;",
|
|
68
|
-
"text-secondary": "color: var(--secondary) !important;",
|
|
69
|
-
"text-secondary-foreground": "color: var(--secondary-foreground) !important;",
|
|
70
|
-
"text-accent": "color: var(--accent) !important;",
|
|
71
|
-
"text-accent-foreground": "color: var(--accent-foreground) !important;",
|
|
72
|
-
"text-foreground": "color: var(--foreground) !important;",
|
|
73
|
-
"text-muted": "color: var(--muted) !important;",
|
|
74
|
-
"text-muted-foreground": "color: var(--muted-foreground) !important;",
|
|
75
|
-
"text-destructive": "color: var(--destructive) !important;",
|
|
76
|
-
"text-destructive-foreground": "color: var(--destructive-foreground) !important;",
|
|
77
|
-
|
|
78
|
-
// Semantic backgrounds
|
|
79
|
-
"bg-primary": "background-color: hsl(var(--primary)) !important;",
|
|
80
|
-
"bg-secondary": "background-color: hsl(var(--secondary)) !important;",
|
|
81
|
-
"bg-accent": "background-color: hsl(var(--accent)) !important;",
|
|
82
|
-
"bg-muted": "background-color: hsl(var(--muted)) !important;",
|
|
83
|
-
"bg-destructive": "background-color: hsl(var(--destructive)) !important;",
|
|
84
|
-
"bg-success": "background-color: var(--success) !important;",
|
|
85
|
-
"bg-card": "background-color: hsl(var(--card)) !important;",
|
|
86
|
-
"bg-background": "background-color: hsl(var(--background)) !important;",
|
|
87
|
-
|
|
88
|
-
// Common backgrounds
|
|
89
|
-
"bg-white": "background-color: white !important;",
|
|
90
|
-
"bg-black": "background-color: black !important;",
|
|
91
|
-
"bg-gray-50": "background-color: rgb(249, 250, 251) !important;",
|
|
92
|
-
"bg-gray-100": "background-color: rgb(243, 244, 246) !important;",
|
|
93
|
-
"bg-gray-200": "background-color: rgb(229, 231, 235) !important;",
|
|
94
|
-
"bg-gray-300": "background-color: rgb(209, 213, 219) !important;",
|
|
95
|
-
"bg-gray-800": "background-color: rgb(31, 41, 55) !important;",
|
|
96
|
-
"bg-gray-900": "background-color: rgb(17, 24, 39) !important;",
|
|
97
|
-
|
|
98
|
-
// Status colors
|
|
99
|
-
"bg-green-500": "background-color: rgb(34, 197, 94) !important;",
|
|
100
|
-
"bg-green-600": "background-color: rgb(22, 163, 74) !important;",
|
|
101
|
-
"bg-blue-500": "background-color: rgb(59, 130, 246) !important;",
|
|
102
|
-
"bg-blue-600": "background-color: rgb(37, 99, 235) !important;",
|
|
103
|
-
"bg-red-500": "background-color: rgb(239, 68, 68) !important;",
|
|
104
|
-
"bg-red-600": "background-color: rgb(220, 38, 38) !important;",
|
|
105
|
-
"bg-yellow-500": "background-color: rgb(234, 179, 8) !important;",
|
|
106
|
-
"bg-orange-500": "background-color: rgb(249, 115, 22) !important;",
|
|
107
|
-
|
|
108
|
-
// Borders
|
|
109
|
-
"border-gray-200": "border-color: rgb(229, 231, 235) !important;",
|
|
110
|
-
"border-gray-300": "border-color: rgb(209, 213, 219) !important;",
|
|
111
|
-
"border-primary": "border-color: hsl(var(--primary)) !important;",
|
|
112
|
-
"border-accent": "border-color: hsl(var(--accent)) !important;",
|
|
113
|
-
"border-destructive": "border-color: hsl(var(--destructive)) !important;",
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Generate CSS for preview based on class changes
|
|
118
|
-
*/
|
|
119
|
-
function generatePreviewCSS(modifications: VisionFileModification[]): string {
|
|
120
|
-
const cssRules: string[] = [];
|
|
121
|
-
|
|
122
|
-
for (const mod of modifications) {
|
|
123
|
-
const { added } = extractClassNameChanges(mod.diff);
|
|
124
|
-
|
|
125
|
-
// Generate CSS for added classes
|
|
126
|
-
for (const cls of added) {
|
|
127
|
-
if (TAILWIND_TO_CSS[cls]) {
|
|
128
|
-
// We can't know the exact selector, so we apply to elements with data-sonance-preview
|
|
129
|
-
cssRules.push(TAILWIND_TO_CSS[cls]);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (cssRules.length === 0) return "";
|
|
135
|
-
|
|
136
|
-
// Apply to changed elements (marked by InspectorOverlay)
|
|
137
|
-
return `
|
|
138
|
-
[data-sonance-changed="true"] {
|
|
139
|
-
${cssRules.join("\n ")}
|
|
140
|
-
outline: 2px dashed #00D3C8 !important;
|
|
141
|
-
outline-offset: 2px !important;
|
|
142
|
-
}
|
|
143
|
-
`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Inject preview CSS into the document
|
|
148
|
-
*/
|
|
149
|
-
function injectPreviewCSS(css: string): void {
|
|
150
|
-
// Remove existing preview styles first
|
|
151
|
-
removePreviewCSS();
|
|
152
|
-
|
|
153
|
-
if (!css.trim()) return;
|
|
154
|
-
|
|
155
|
-
const style = document.createElement("style");
|
|
156
|
-
style.id = PREVIEW_STYLE_ID;
|
|
157
|
-
style.textContent = css;
|
|
158
|
-
document.head.appendChild(style);
|
|
159
|
-
|
|
160
|
-
console.log("[Preview CSS] Injected:", css.substring(0, 200));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Remove preview CSS from the document
|
|
165
|
-
*/
|
|
166
|
-
function removePreviewCSS(): void {
|
|
167
|
-
const existing = document.getElementById(PREVIEW_STYLE_ID);
|
|
168
|
-
if (existing) {
|
|
169
|
-
existing.remove();
|
|
170
|
-
console.log("[Preview CSS] Removed");
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
8
|
export interface ApplyFirstPreviewProps {
|
|
175
9
|
session: ApplyFirstSession;
|
|
176
10
|
status: ApplyFirstStatus;
|
|
@@ -260,15 +94,6 @@ function HMRStatusBadge({ status }: { status: ApplyFirstStatus }) {
|
|
|
260
94
|
);
|
|
261
95
|
}
|
|
262
96
|
|
|
263
|
-
if (status === "previewing") {
|
|
264
|
-
return (
|
|
265
|
-
<span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700">
|
|
266
|
-
<Eye className="h-3 w-3" />
|
|
267
|
-
Preview Mode
|
|
268
|
-
</span>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
97
|
return null;
|
|
273
98
|
}
|
|
274
99
|
|
|
@@ -281,7 +106,6 @@ export function ApplyFirstPreview({
|
|
|
281
106
|
onApplyPreview,
|
|
282
107
|
}: ApplyFirstPreviewProps) {
|
|
283
108
|
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
|
284
|
-
const [cssPreviewEnabled, setCssPreviewEnabled] = useState(true);
|
|
285
109
|
|
|
286
110
|
// Expand first file by default
|
|
287
111
|
useEffect(() => {
|
|
@@ -289,31 +113,6 @@ export function ApplyFirstPreview({
|
|
|
289
113
|
setExpandedFiles(new Set([session.modifications[0].filePath]));
|
|
290
114
|
}
|
|
291
115
|
}, [session]);
|
|
292
|
-
|
|
293
|
-
// CSS Preview injection for ALL modes (preview AND applied)
|
|
294
|
-
// This ensures the user can always see visual changes immediately
|
|
295
|
-
useEffect(() => {
|
|
296
|
-
if (cssPreviewEnabled && session.modifications.length > 0) {
|
|
297
|
-
// Generate and inject preview CSS to show visual changes immediately
|
|
298
|
-
const previewCSS = generatePreviewCSS(session.modifications);
|
|
299
|
-
if (previewCSS) {
|
|
300
|
-
injectPreviewCSS(previewCSS);
|
|
301
|
-
}
|
|
302
|
-
} else {
|
|
303
|
-
// Remove preview CSS when disabled
|
|
304
|
-
removePreviewCSS();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Cleanup on unmount
|
|
308
|
-
return () => {
|
|
309
|
-
removePreviewCSS();
|
|
310
|
-
};
|
|
311
|
-
}, [session, cssPreviewEnabled]);
|
|
312
|
-
|
|
313
|
-
// Toggle CSS preview
|
|
314
|
-
const toggleCssPreview = useCallback(() => {
|
|
315
|
-
setCssPreviewEnabled(prev => !prev);
|
|
316
|
-
}, []);
|
|
317
116
|
|
|
318
117
|
const toggleFile = (filePath: string) => {
|
|
319
118
|
setExpandedFiles((prev) => {
|
|
@@ -356,20 +155,6 @@ export function ApplyFirstPreview({
|
|
|
356
155
|
</span>
|
|
357
156
|
</div>
|
|
358
157
|
<div className="flex items-center gap-2">
|
|
359
|
-
{/* CSS Preview Toggle - show for all modes to ensure visual changes are visible */}
|
|
360
|
-
<button
|
|
361
|
-
onClick={toggleCssPreview}
|
|
362
|
-
className={cn(
|
|
363
|
-
"flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded transition-colors",
|
|
364
|
-
cssPreviewEnabled
|
|
365
|
-
? isPreviewMode ? "bg-blue-200 text-blue-700" : "bg-green-200 text-green-700"
|
|
366
|
-
: "bg-gray-200 text-gray-500"
|
|
367
|
-
)}
|
|
368
|
-
title={cssPreviewEnabled ? "Hide visual preview" : "Show visual preview"}
|
|
369
|
-
>
|
|
370
|
-
{cssPreviewEnabled ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3" />}
|
|
371
|
-
Preview
|
|
372
|
-
</button>
|
|
373
158
|
<HMRStatusBadge status={status} />
|
|
374
159
|
</div>
|
|
375
160
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.75",
|
|
4
4
|
"description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|