sonance-brand-mcp 1.3.13 → 1.3.14
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.
|
@@ -51,6 +51,9 @@ interface ScannedElement {
|
|
|
51
51
|
textContent?: string; // Truncated text content (for headings/paragraphs)
|
|
52
52
|
// Component-specific fields
|
|
53
53
|
variant?: string; // variant prop if present
|
|
54
|
+
// Interactive element fields (Button, Link, etc.)
|
|
55
|
+
onClickHandler?: string; // onClick handler name (e.g., "handleReset")
|
|
56
|
+
iconName?: string; // Icon component inside (e.g., "RotateCcw")
|
|
54
57
|
// Input-specific fields
|
|
55
58
|
inputName?: string; // name or placeholder if present
|
|
56
59
|
inputType?: string; // type attribute if present
|
|
@@ -337,13 +340,38 @@ function extractElements(
|
|
|
337
340
|
}
|
|
338
341
|
}
|
|
339
342
|
|
|
340
|
-
//
|
|
341
|
-
if (category === "
|
|
343
|
+
// Interactive element extraction (Button, Link, etc.)
|
|
344
|
+
if (category === "interactive") {
|
|
342
345
|
// Extract variant
|
|
343
346
|
const variantMatch = elementContent.match(/variant=["']([^"']+)["']/);
|
|
344
347
|
if (variantMatch) {
|
|
345
348
|
element.variant = variantMatch[1];
|
|
346
349
|
}
|
|
350
|
+
|
|
351
|
+
// Extract onClick handler name
|
|
352
|
+
const onClickMatch = elementContent.match(/onClick=\{([a-zA-Z_][a-zA-Z0-9_]*)\}/);
|
|
353
|
+
if (onClickMatch) {
|
|
354
|
+
element.onClickHandler = onClickMatch[1];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Extract icon name (first PascalCase component inside the element)
|
|
358
|
+
const iconMatch = elementContent.match(/<([A-Z][a-zA-Z0-9]*)\s/);
|
|
359
|
+
if (iconMatch && iconMatch[1] !== elementType) {
|
|
360
|
+
element.iconName = iconMatch[1];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Extract text content between > and </ElementType>
|
|
364
|
+
const afterOpenTag = fullContent.substring(elementEnd);
|
|
365
|
+
const closeTagMatch = afterOpenTag.match(new RegExp(`^([^<]*)<\\/${elementType}>`));
|
|
366
|
+
if (closeTagMatch && closeTagMatch[1].trim()) {
|
|
367
|
+
element.textContent = closeTagMatch[1].trim().substring(0, 50);
|
|
368
|
+
} else {
|
|
369
|
+
// Try to find text after any icons/nested elements
|
|
370
|
+
const textAfterIcons = afterOpenTag.match(new RegExp(`^(?:<[^>]+>\\s*)*([^<]+)<\\/${elementType}>`));
|
|
371
|
+
if (textAfterIcons && textAfterIcons[1].trim()) {
|
|
372
|
+
element.textContent = textAfterIcons[1].trim().substring(0, 50);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
347
375
|
}
|
|
348
376
|
|
|
349
377
|
// Input-specific extraction
|
|
@@ -443,15 +471,49 @@ function generateSuggestedId(element: ScannedElement): string {
|
|
|
443
471
|
break;
|
|
444
472
|
}
|
|
445
473
|
|
|
446
|
-
case "
|
|
447
|
-
// Add
|
|
448
|
-
|
|
474
|
+
case "interactive": {
|
|
475
|
+
// Add prefix based on element type
|
|
476
|
+
const prefix = elementType === "Button" ? "btn" :
|
|
477
|
+
elementType === "Link" || elementType === "a" ? "link" : "action";
|
|
478
|
+
parts.push(prefix);
|
|
449
479
|
|
|
450
|
-
//
|
|
451
|
-
if (
|
|
480
|
+
// Priority 1: Use text content if available
|
|
481
|
+
if (textContent) {
|
|
482
|
+
const cleanText = textContent.toLowerCase()
|
|
483
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
484
|
+
.replace(/\s+/g, "-")
|
|
485
|
+
.substring(0, 25);
|
|
486
|
+
if (cleanText && cleanText.length > 1) {
|
|
487
|
+
parts.push(cleanText);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Priority 2: Use onClick handler name
|
|
492
|
+
if (parts.length === 1 && element.onClickHandler) {
|
|
493
|
+
// Convert camelCase to kebab-case (handleReset -> handle-reset)
|
|
494
|
+
const handlerName = element.onClickHandler
|
|
495
|
+
.replace(/^(handle|on)/i, "") // Remove handle/on prefix
|
|
496
|
+
.replace(/([A-Z])/g, "-$1")
|
|
497
|
+
.toLowerCase()
|
|
498
|
+
.replace(/^-/, "");
|
|
499
|
+
if (handlerName) parts.push(handlerName);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Priority 3: Use icon name if no text
|
|
503
|
+
if (parts.length === 1 && element.iconName) {
|
|
504
|
+
const iconName = element.iconName
|
|
505
|
+
.replace(/([A-Z])/g, "-$1")
|
|
506
|
+
.toLowerCase()
|
|
507
|
+
.replace(/^-/, "");
|
|
508
|
+
parts.push(iconName);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Priority 4: Use variant
|
|
512
|
+
if (parts.length === 1 && variant) {
|
|
452
513
|
parts.push(variant.toLowerCase());
|
|
453
514
|
}
|
|
454
515
|
|
|
516
|
+
// Fallback: add "action" if still only prefix
|
|
455
517
|
if (parts.length === 1) parts.push("action");
|
|
456
518
|
break;
|
|
457
519
|
}
|
|
@@ -747,7 +809,23 @@ function scanDirectory(
|
|
|
747
809
|
* Tags a component definition file with data-sonance-name attribute.
|
|
748
810
|
* Uses multiple regex patterns to handle different component structures.
|
|
749
811
|
*/
|
|
750
|
-
function tagComponentDefinition(filePath: string, content: string, componentName: string): { success: boolean; content: string; error?: string } {
|
|
812
|
+
function tagComponentDefinition(filePath: string, content: string, componentName: string): { success: boolean; content: string; error?: string; skipped?: boolean } {
|
|
813
|
+
// Check for pure re-exports (no JSX to tag)
|
|
814
|
+
// Pattern: const ComponentName = SomePrimitive.Element or export const X = Y
|
|
815
|
+
const pureReExportPattern = new RegExp(
|
|
816
|
+
`(?:const|export\\s+const)\\s+${componentName}\\s*=\\s*[A-Z][a-zA-Z0-9]*(?:\\.[A-Z][a-zA-Z0-9]*)?\\s*[;\\n]`,
|
|
817
|
+
'g'
|
|
818
|
+
);
|
|
819
|
+
if (pureReExportPattern.test(content) && !content.includes(`<`) ||
|
|
820
|
+
(content.match(/<[A-Z]/g) || []).length === 0) {
|
|
821
|
+
return {
|
|
822
|
+
success: true,
|
|
823
|
+
content,
|
|
824
|
+
skipped: true,
|
|
825
|
+
error: "Pure re-export - no JSX to tag"
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
751
829
|
// Helper to inject tag only if not present in the match
|
|
752
830
|
const injectTag = (match: string, p1: string, p2: string) => {
|
|
753
831
|
if (match.includes("data-sonance-name=")) return match;
|
|
@@ -840,6 +918,39 @@ function tagComponentDefinition(filePath: string, content: string, componentName
|
|
|
840
918
|
}
|
|
841
919
|
}
|
|
842
920
|
|
|
921
|
+
// Pattern 8: forwardRef with implicit return and element without attributes (needs to add space)
|
|
922
|
+
// Matches: ) => (\n <Element.Sub>\n or ) => (\n <Element>\n
|
|
923
|
+
if (!modified) {
|
|
924
|
+
const forwardRefNoAttrsPattern = /(\)\s*=>\s*\(\s*<(?:[A-Z][a-zA-Z0-9]*\.)?[A-Z][a-zA-Z0-9]*)(?![^>]*data-sonance-name=)(>)/;
|
|
925
|
+
if (forwardRefNoAttrsPattern.test(result)) {
|
|
926
|
+
// Different injection - we need to add space before the >
|
|
927
|
+
const newContent = result.replace(forwardRefNoAttrsPattern, (match, p1, p2) => {
|
|
928
|
+
if (match.includes("data-sonance-name=")) return match;
|
|
929
|
+
return `${p1} data-sonance-name="${componentName}"${p2}`;
|
|
930
|
+
});
|
|
931
|
+
if (newContent !== result) {
|
|
932
|
+
result = newContent;
|
|
933
|
+
modified = true;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Pattern 9: Standard function component returning element without attributes
|
|
939
|
+
// Matches: return (\n <DropdownMenu>\n
|
|
940
|
+
if (!modified) {
|
|
941
|
+
const returnNoAttrsPattern = /(return\s*\(\s*<(?:[A-Z][a-zA-Z0-9]*\.)?[A-Z][a-zA-Z0-9]*)(?![^>]*data-sonance-name=)(>)/;
|
|
942
|
+
if (returnNoAttrsPattern.test(result)) {
|
|
943
|
+
const newContent = result.replace(returnNoAttrsPattern, (match, p1, p2) => {
|
|
944
|
+
if (match.includes("data-sonance-name=")) return match;
|
|
945
|
+
return `${p1} data-sonance-name="${componentName}"${p2}`;
|
|
946
|
+
});
|
|
947
|
+
if (newContent !== result) {
|
|
948
|
+
result = newContent;
|
|
949
|
+
modified = true;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
843
954
|
if (modified) {
|
|
844
955
|
return { success: true, content: result };
|
|
845
956
|
} else {
|
|
@@ -1000,7 +1111,10 @@ export async function POST(request: Request) {
|
|
|
1000
1111
|
const componentName = element.context.parentComponent || element.suggestedId;
|
|
1001
1112
|
const tagResult = tagComponentDefinition(fullPath, content, componentName);
|
|
1002
1113
|
|
|
1003
|
-
if (tagResult.
|
|
1114
|
+
if (tagResult.skipped) {
|
|
1115
|
+
// Pure re-export - no JSX to tag, mark as skipped (not a failure)
|
|
1116
|
+
results.push({ id: element.id, success: true, skipped: true });
|
|
1117
|
+
} else if (tagResult.success) {
|
|
1004
1118
|
fs.writeFileSync(fullPath, tagResult.content, "utf-8");
|
|
1005
1119
|
results.push({ id: element.id, success: true });
|
|
1006
1120
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.14",
|
|
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",
|