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
- // Component-specific extraction
341
- if (category === "component") {
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 "component": {
447
- // Add element type (button, link, etc.)
448
- parts.push(elementType.toLowerCase());
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
- // Add variant if present
451
- if (variant) {
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.success) {
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.13",
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",