vibe-design-system 2.5.0 → 2.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -474,6 +474,46 @@ const COMPONENT_SUGGESTION_KEYWORDS = [
474
474
  "stat",
475
475
  ];
476
476
 
477
+ const KNOWN_HTML_TAGS = new Set([
478
+ "div", "span", "h1", "h2", "h3", "h4", "h5", "h6", "p", "a", "img", "ul", "ol", "li",
479
+ "button", "input", "label", "section", "header", "footer", "nav", "main", "article", "aside",
480
+ "form", "svg", "path", "g", "circle", "rect", "strong", "em", "small", "br", "hr",
481
+ "table", "thead", "tbody", "tr", "th", "td", "colgroup", "col", "i", "b", "sub", "sup",
482
+ ]);
483
+
484
+ /** Extract text from inner content for semantic naming (first h2/h3/span text). */
485
+ function extractInnerTextForNaming(innerContent) {
486
+ const text = innerContent
487
+ .replace(/<[^>]+>/g, " ")
488
+ .replace(/\s+/g, " ")
489
+ .replace(/\{[^}]*\}/g, " ")
490
+ .trim();
491
+ return text.slice(0, 200);
492
+ }
493
+
494
+ /** Derive semantic component name from inner text (e.g. "Individual Plans" → IndividualPlansCard). */
495
+ function suggestNameFromContent(innerText) {
496
+ if (!innerText || !innerText.trim()) return null;
497
+ const stopWords = new Set(["the", "a", "an", "and", "or", "for", "with", "to", "in", "on", "at", "of"]);
498
+ const words = innerText
499
+ .trim()
500
+ .split(/\s+/)
501
+ .filter((w) => w.length > 0 && !stopWords.has(w.toLowerCase()))
502
+ .slice(0, 4);
503
+ if (words.length === 0) return null;
504
+ const pascal = words
505
+ .map((w) => w.replace(/[^a-zA-Z0-9]/g, ""))
506
+ .filter((w) => w.length > 0)
507
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
508
+ .join("");
509
+ if (!pascal) return null;
510
+ if (/\bindividual\b/i.test(innerText)) return pascal + "PricingCard";
511
+ if (/\burbannest\b/i.test(innerText)) return pascal + "VentureCard";
512
+ if (/\bpricing\b/i.test(innerText)) return pascal + "Card";
513
+ if (/\bventure\b/i.test(innerText)) return pascal + "Card";
514
+ return pascal + "Card";
515
+ }
516
+
477
517
  function suggestNameFromPattern(pattern) {
478
518
  const words = pattern
479
519
  .split(/\s+/)
@@ -491,6 +531,45 @@ function suggestNameFromPattern(pattern) {
491
531
  return "Container";
492
532
  }
493
533
 
534
+ /** Extract full JSX element (opening tag to matching closing tag) and inner text. */
535
+ function extractFullElement(content, classNameMatchIndex, tagName) {
536
+ const tag = tagName;
537
+ const before = content.substring(0, classNameMatchIndex);
538
+ const openMatch = new RegExp("<" + tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[\\s>]", "g");
539
+ let openStart = -1;
540
+ let m;
541
+ while ((m = openMatch.exec(before)) !== null) openStart = m.index;
542
+ if (openStart < 0) return null;
543
+ const openEnd = content.indexOf(">", classNameMatchIndex);
544
+ if (openEnd < 0) return null;
545
+ const innerStart = openEnd + 1;
546
+ let depth = 1;
547
+ let pos = innerStart;
548
+ const openTagRe = new RegExp("<" + tag + "[\\s>]", "g");
549
+ const closeTagRe = new RegExp("</" + tag + ">", "g");
550
+ while (depth > 0 && pos < content.length) {
551
+ openTagRe.lastIndex = pos;
552
+ closeTagRe.lastIndex = pos;
553
+ const nextOpen = openTagRe.exec(content);
554
+ const nextClose = closeTagRe.exec(content);
555
+ if (!nextClose) break;
556
+ if (nextOpen && nextOpen.index < nextClose.index) {
557
+ depth += 1;
558
+ pos = nextOpen.index + 1;
559
+ } else {
560
+ depth -= 1;
561
+ if (depth === 0) {
562
+ const fullJsx = content.substring(openStart, nextClose.index + nextClose[0].length);
563
+ const innerContent = content.substring(innerStart, nextClose.index);
564
+ const innerText = extractInnerTextForNaming(innerContent);
565
+ return { fullJsx, innerContent, innerText };
566
+ }
567
+ pos = nextClose.index + 1;
568
+ }
569
+ }
570
+ return null;
571
+ }
572
+
494
573
  /** Scan src/pages/*.tsx for repeated className clusters; return component suggestions. */
495
574
  function extractComponentSuggestions() {
496
575
  if (!fs.existsSync(PAGES_DIR)) return [];
@@ -514,11 +593,20 @@ function extractComponentSuggestions() {
514
593
  const lastOpen = before.lastIndexOf("<");
515
594
  const tagMatch = lastOpen >= 0 ? content.slice(lastOpen).match(/<(\w+)/) : null;
516
595
  const tagName = tagMatch ? tagMatch[1] : "div";
596
+ let fullJsx = null;
597
+ let innerText = "";
598
+ const extracted = extractFullElement(content, m.index, tagName);
599
+ if (extracted) {
600
+ fullJsx = extracted.fullJsx;
601
+ innerText = extracted.innerText || "";
602
+ }
517
603
  byPattern.set(raw, {
518
604
  count: 0,
519
605
  files: new Set(),
520
606
  tagName,
521
607
  snippet: `<${tagName} className="${raw}">...</${tagName}>`,
608
+ fullJsx,
609
+ innerText,
522
610
  });
523
611
  }
524
612
  const entry = byPattern.get(raw);
@@ -528,13 +616,14 @@ function extractComponentSuggestions() {
528
616
  }
529
617
 
530
618
  const suggestions = [];
531
- for (const [pattern, { count, files, snippet, tagName }] of byPattern.entries()) {
619
+ for (const [pattern, { count, files, snippet, tagName, fullJsx, innerText }] of byPattern.entries()) {
532
620
  if (count < 2) continue;
533
- const suggestedName = suggestNameFromPattern(pattern);
621
+ const suggestedName = suggestNameFromContent(innerText) || suggestNameFromPattern(pattern);
534
622
  suggestions.push({
535
- suggestedName,
623
+ suggestedName: suggestedName.replace(/\s+/g, ""),
536
624
  tagName: tagName || "div",
537
625
  pattern,
626
+ fullJsx,
538
627
  occurrences: count,
539
628
  foundIn: [...files].sort(),
540
629
  snippet,
@@ -546,7 +635,23 @@ function extractComponentSuggestions() {
546
635
 
547
636
  const VDS_GENERATED_DIR = path.join(COMPONENTS_DIR, "vds-generated");
548
637
 
549
- /** Write extracted components to src/components/vds-generated/*.tsx and return component entries for output. */
638
+ function toKebab(str) {
639
+ return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
640
+ }
641
+
642
+ /** Collect PascalCase tag names from JSX that are not known HTML. */
643
+ function collectComponentTags(jsx) {
644
+ const tags = new Set();
645
+ const tagRe = /<\/?([A-Z][a-zA-Z0-9]*)\s|<\/?([A-Z][a-zA-Z0-9]*)[>/]/g;
646
+ let m;
647
+ while ((m = tagRe.exec(jsx)) !== null) {
648
+ const name = (m[1] || m[2] || "").trim();
649
+ if (name && !KNOWN_HTML_TAGS.has(name)) tags.add(name);
650
+ }
651
+ return [...tags];
652
+ }
653
+
654
+ /** Write extracted components with full JSX body; add Lucide and UI imports as needed. */
550
655
  function writeVdsGeneratedComponents(suggestions) {
551
656
  if (!Array.isArray(suggestions) || suggestions.length === 0) return [];
552
657
  if (!fs.existsSync(VDS_GENERATED_DIR)) fs.mkdirSync(VDS_GENERATED_DIR, { recursive: true });
@@ -554,36 +659,62 @@ function writeVdsGeneratedComponents(suggestions) {
554
659
  const entries = [];
555
660
  for (let i = 0; i < suggestions.length; i++) {
556
661
  const s = suggestions[i];
557
- let baseName = s.suggestedName.replace(/\s+/g, "");
558
- if (!baseName) baseName = "Extracted";
662
+ let baseName = (s.suggestedName || "Extracted").replace(/\s+/g, "");
559
663
  let fileName = baseName + ".tsx";
560
- if (usedNames.has(fileName)) {
561
- fileName = baseName + String(i + 1) + ".tsx";
562
- }
664
+ if (usedNames.has(fileName)) fileName = baseName + String(i + 1) + ".tsx";
563
665
  usedNames.add(fileName);
564
666
  const componentName = fileName.replace(/\.tsx$/, "");
565
- const tagName = s.tagName || "div";
566
- const className = s.pattern || "";
567
667
  const relPath = "vds-generated/" + fileName;
568
668
  const fullPath = path.join(COMPONENTS_DIR, relPath);
569
- const content = `import * as React from "react";
570
669
 
571
- export function ${componentName}({ children }: { children?: React.ReactNode }) {
670
+ let bodyJsx;
671
+ const tokens = [];
672
+ if (s.fullJsx && s.fullJsx.trim().length > 0) {
673
+ bodyJsx = s.fullJsx;
674
+ const componentTags = collectComponentTags(bodyJsx);
675
+ const lucideTags = [];
676
+ const uiTags = [];
677
+ for (const tag of componentTags) {
678
+ if (/^[A-Z][a-z0-9]+$/.test(tag) && tag.length > 2) lucideTags.push(tag);
679
+ else uiTags.push(tag);
680
+ }
681
+ let importLines = ['import * as React from "react";'];
682
+ if (lucideTags.length > 0) {
683
+ importLines.push(`import { ${lucideTags.join(", ")} } from "lucide-react";`);
684
+ }
685
+ for (const tag of uiTags) {
686
+ if (!lucideTags.includes(tag)) {
687
+ importLines.push(`import { ${tag} } from "@/components/ui/${toKebab(tag)}";`);
688
+ }
689
+ }
690
+ const content = importLines.join("\n") + "\n\n" +
691
+ `export function ${componentName}() {\n return (\n ` +
692
+ bodyJsx.replace(/\n/g, "\n ") + "\n );\n}\n";
693
+ fs.writeFileSync(fullPath, content, "utf-8");
694
+ tokens.push(...(s.pattern || "").split(/\s+/).filter(Boolean));
695
+ } else {
696
+ const tagName = s.tagName || "div";
697
+ const className = s.pattern || "";
698
+ bodyJsx = `<${tagName} className="${className.replace(/"/g, '\\"')}">\n {null}\n </${tagName}>`;
699
+ const content = `import * as React from "react";
700
+
701
+ export function ${componentName}() {
572
702
  return (
573
- <${tagName} className={\`${className.replace(/`/g, "\\`").replace(/\$/g, "\\$")}\`}>
574
- {children ?? null}
575
- </${tagName}>
703
+ ${bodyJsx}
576
704
  );
577
705
  }
578
706
  `;
579
- fs.writeFileSync(fullPath, content, "utf-8");
707
+ fs.writeFileSync(fullPath, content, "utf-8");
708
+ tokens.push(...(className || "").split(/\s+/).filter(Boolean));
709
+ }
710
+
580
711
  entries.push({
581
712
  file: relPath,
582
713
  name: componentName.replace(/([A-Z])/g, " $1").trim(),
583
714
  group: "VDS Generated",
584
715
  category: "Extracted",
585
716
  description: "Auto-extracted from repeated className patterns.",
586
- tokens: (className || "").split(/\s+/).filter(Boolean),
717
+ tokens: [...new Set(tokens)],
587
718
  });
588
719
  }
589
720
  return entries;
@@ -549,10 +549,13 @@ function buildStoryFileContent(comp) {
549
549
  const variantProp = props.find((p) => p.name === "variant");
550
550
  let variants = parseUnionLiterals(variantProp && variantProp.type);
551
551
 
552
+ const srcPath = comp.file.startsWith("pages/")
553
+ ? path.join(SRC_DIR, comp.file)
554
+ : path.join(SRC_DIR, "components", comp.file);
555
+
552
556
  // Fallback: if manifest doesn't have variant metadata yet, parse cva() directly from component file.
553
557
  if (!variants.length) {
554
558
  try {
555
- const srcPath = path.join(SRC_DIR, "components", comp.file);
556
559
  if (fs.existsSync(srcPath)) {
557
560
  const code = fs.readFileSync(srcPath, "utf-8");
558
561
  // Roughly match shadcn-style: variants: { variant: { ... }, size: { ... } }
@@ -576,7 +579,6 @@ function buildStoryFileContent(comp) {
576
579
  // Read component source to detect export style for import
577
580
  let source = "";
578
581
  try {
579
- const srcPath = path.join(SRC_DIR, "components", comp.file);
580
582
  if (fs.existsSync(srcPath)) {
581
583
  source = fs.readFileSync(srcPath, "utf-8");
582
584
  }
@@ -586,6 +588,11 @@ function buildStoryFileContent(comp) {
586
588
  const exportStyle = detectExportStyle(source, componentName);
587
589
  const omitChildren = componentWrapsVoidElement(source);
588
590
 
591
+ // Skip story if no export found (e.g. Join.tsx with broken default export)
592
+ if (exportStyle === "unknown" && (!source.includes("export") || !new RegExp(`\\b${componentName}\\b`).test(source))) {
593
+ return null;
594
+ }
595
+
589
596
  if (RECIPES[componentName]) {
590
597
  return buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, RECIPES[componentName]);
591
598
  }
@@ -596,7 +603,7 @@ function buildStoryFileContent(comp) {
596
603
  if (exportStyle === "default") {
597
604
  lines.push(`import ${componentName} from "${importPath}";`);
598
605
  lines.push(`const ComponentRef = ${componentName};`);
599
- } else if (exportStyle === "named") {
606
+ } else if (exportStyle === "named" || exportStyle === "unknown") {
600
607
  lines.push(`import { ${componentName} } from "${importPath}";`);
601
608
  lines.push(`const ComponentRef = ${componentName};`);
602
609
  } else {
@@ -956,6 +963,7 @@ function main() {
956
963
  const storyFileName = `${componentName}.stories.tsx`;
957
964
  const storyPath = path.join(STORIES_DIR, storyFileName);
958
965
  const content = buildStoryFileContent(comp);
966
+ if (content == null) continue;
959
967
  fs.writeFileSync(storyPath, content, "utf-8");
960
968
  console.log(`[VDS] Wrote ${path.relative(PROJECT_ROOT, storyPath)}`);
961
969
  }