vibe-design-system 2.4.11 → 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.4.11",
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,10 +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(),
606
+ tagName,
520
607
  snippet: `<${tagName} className="${raw}">...</${tagName}>`,
608
+ fullJsx,
609
+ innerText,
521
610
  });
522
611
  }
523
612
  const entry = byPattern.get(raw);
@@ -527,14 +616,16 @@ function extractComponentSuggestions() {
527
616
  }
528
617
 
529
618
  const suggestions = [];
530
- for (const [pattern, { count, files, snippet }] of byPattern.entries()) {
619
+ for (const [pattern, { count, files, snippet, tagName, fullJsx, innerText }] of byPattern.entries()) {
531
620
  if (count < 2) continue;
532
- const suggestedName = suggestNameFromPattern(pattern);
621
+ const suggestedName = suggestNameFromContent(innerText) || suggestNameFromPattern(pattern);
533
622
  suggestions.push({
534
- suggestedName,
623
+ suggestedName: suggestedName.replace(/\s+/g, ""),
624
+ tagName: tagName || "div",
625
+ pattern,
626
+ fullJsx,
535
627
  occurrences: count,
536
628
  foundIn: [...files].sort(),
537
- pattern,
538
629
  snippet,
539
630
  reason: `Same className cluster appears ${count} times`,
540
631
  });
@@ -542,6 +633,93 @@ function extractComponentSuggestions() {
542
633
  return suggestions;
543
634
  }
544
635
 
636
+ const VDS_GENERATED_DIR = path.join(COMPONENTS_DIR, "vds-generated");
637
+
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. */
655
+ function writeVdsGeneratedComponents(suggestions) {
656
+ if (!Array.isArray(suggestions) || suggestions.length === 0) return [];
657
+ if (!fs.existsSync(VDS_GENERATED_DIR)) fs.mkdirSync(VDS_GENERATED_DIR, { recursive: true });
658
+ const usedNames = new Set();
659
+ const entries = [];
660
+ for (let i = 0; i < suggestions.length; i++) {
661
+ const s = suggestions[i];
662
+ let baseName = (s.suggestedName || "Extracted").replace(/\s+/g, "");
663
+ let fileName = baseName + ".tsx";
664
+ if (usedNames.has(fileName)) fileName = baseName + String(i + 1) + ".tsx";
665
+ usedNames.add(fileName);
666
+ const componentName = fileName.replace(/\.tsx$/, "");
667
+ const relPath = "vds-generated/" + fileName;
668
+ const fullPath = path.join(COMPONENTS_DIR, relPath);
669
+
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}() {
702
+ return (
703
+ ${bodyJsx}
704
+ );
705
+ }
706
+ `;
707
+ fs.writeFileSync(fullPath, content, "utf-8");
708
+ tokens.push(...(className || "").split(/\s+/).filter(Boolean));
709
+ }
710
+
711
+ entries.push({
712
+ file: relPath,
713
+ name: componentName.replace(/([A-Z])/g, " $1").trim(),
714
+ group: "VDS Generated",
715
+ category: "Extracted",
716
+ description: "Auto-extracted from repeated className patterns.",
717
+ tokens: [...new Set(tokens)],
718
+ });
719
+ }
720
+ return entries;
721
+ }
722
+
545
723
  function extractTailwindTokens(content) {
546
724
  const tokens = new Set();
547
725
  const patterns = [
@@ -942,6 +1120,8 @@ function scan() {
942
1120
  foundations.icons = extractLucideIconsUsed(SRC_DIR);
943
1121
  foundations.brand = { assets: extractBrandAssets() };
944
1122
  const componentSuggestions = extractComponentSuggestions();
1123
+ const vdsGeneratedEntries = writeVdsGeneratedComponents(componentSuggestions);
1124
+ results.push(...vdsGeneratedEntries);
945
1125
  const output = {
946
1126
  branch: getGitBranch(),
947
1127
  engineer: getGitEngineer(),
@@ -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 {
@@ -785,6 +792,40 @@ function writeFoundationsStories(foundations) {
785
792
  ].join("\n");
786
793
  fs.writeFileSync(path.join(foundationsDir, "Brand.stories.tsx"), brandContent, "utf-8");
787
794
  console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Brand.stories.tsx")));
795
+
796
+ const iconNames = Array.isArray(foundations?.icons) ? foundations.icons : [];
797
+ if (iconNames.length > 0) {
798
+ const iconsContent =
799
+ [
800
+ "import type { Meta, StoryObj } from \"@storybook/react\";",
801
+ "import * as Lucide from \"lucide-react\";",
802
+ "",
803
+ "const meta = { title: \"Foundations/Icons\" } satisfies Meta;",
804
+ "export default meta;",
805
+ "type Story = StoryObj;",
806
+ "",
807
+ `const iconNames = ${JSON.stringify(iconNames)};`,
808
+ "",
809
+ "export const Default: Story = {",
810
+ " render: () => (",
811
+ " <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(100px, 1fr))\", gap: 16, padding: 24 }}>",
812
+ " {iconNames.map((name) => {",
813
+ " const Icon = Lucide[name];",
814
+ " if (!Icon) return null;",
815
+ " return (",
816
+ " <div key={name} style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", gap: 8, padding: 12, border: \"1px solid #333\", borderRadius: 8 }}>",
817
+ " <Icon size={24} />",
818
+ " <span style={{ fontSize: 11, color: \"#888\" }}>{name}</span>",
819
+ " </div>",
820
+ " );",
821
+ " })}",
822
+ " </div>",
823
+ " ),",
824
+ "};",
825
+ ].join("\n");
826
+ fs.writeFileSync(path.join(foundationsDir, "Icons.stories.tsx"), iconsContent, "utf-8");
827
+ console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Icons.stories.tsx")));
828
+ }
788
829
  }
789
830
 
790
831
  function writeComponentSuggestionsStory(componentSuggestions) {
@@ -922,6 +963,7 @@ function main() {
922
963
  const storyFileName = `${componentName}.stories.tsx`;
923
964
  const storyPath = path.join(STORIES_DIR, storyFileName);
924
965
  const content = buildStoryFileContent(comp);
966
+ if (content == null) continue;
925
967
  fs.writeFileSync(storyPath, content, "utf-8");
926
968
  console.log(`[VDS] Wrote ${path.relative(PROJECT_ROOT, storyPath)}`);
927
969
  }