vibe-design-system 2.5.0 → 2.5.2
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
|
@@ -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: sanitizeComponentName(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,32 @@ function extractComponentSuggestions() {
|
|
|
546
635
|
|
|
547
636
|
const VDS_GENERATED_DIR = path.join(COMPONENTS_DIR, "vds-generated");
|
|
548
637
|
|
|
549
|
-
/**
|
|
638
|
+
/** Ensure component name is valid JS: no leading digits/special chars, PascalCase. */
|
|
639
|
+
function sanitizeComponentName(name) {
|
|
640
|
+
if (!name || typeof name !== "string") return "Extracted";
|
|
641
|
+
let s = name.replace(/\s+/g, "").replace(/^[0-9\W_]+/, "");
|
|
642
|
+
if (!s) return "Extracted";
|
|
643
|
+
s = s.charAt(0).toUpperCase() + s.slice(1);
|
|
644
|
+
return s;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function toKebab(str) {
|
|
648
|
+
return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Collect PascalCase tag names from JSX that are not known HTML. */
|
|
652
|
+
function collectComponentTags(jsx) {
|
|
653
|
+
const tags = new Set();
|
|
654
|
+
const tagRe = /<\/?([A-Z][a-zA-Z0-9]*)\s|<\/?([A-Z][a-zA-Z0-9]*)[>/]/g;
|
|
655
|
+
let m;
|
|
656
|
+
while ((m = tagRe.exec(jsx)) !== null) {
|
|
657
|
+
const name = (m[1] || m[2] || "").trim();
|
|
658
|
+
if (name && !KNOWN_HTML_TAGS.has(name)) tags.add(name);
|
|
659
|
+
}
|
|
660
|
+
return [...tags];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Write extracted components with full JSX body; add Lucide and UI imports as needed. */
|
|
550
664
|
function writeVdsGeneratedComponents(suggestions) {
|
|
551
665
|
if (!Array.isArray(suggestions) || suggestions.length === 0) return [];
|
|
552
666
|
if (!fs.existsSync(VDS_GENERATED_DIR)) fs.mkdirSync(VDS_GENERATED_DIR, { recursive: true });
|
|
@@ -554,36 +668,62 @@ function writeVdsGeneratedComponents(suggestions) {
|
|
|
554
668
|
const entries = [];
|
|
555
669
|
for (let i = 0; i < suggestions.length; i++) {
|
|
556
670
|
const s = suggestions[i];
|
|
557
|
-
let baseName = s.suggestedName
|
|
558
|
-
if (!baseName) baseName = "Extracted";
|
|
671
|
+
let baseName = sanitizeComponentName(s.suggestedName || "Extracted");
|
|
559
672
|
let fileName = baseName + ".tsx";
|
|
560
|
-
if (usedNames.has(fileName))
|
|
561
|
-
fileName = baseName + String(i + 1) + ".tsx";
|
|
562
|
-
}
|
|
673
|
+
if (usedNames.has(fileName)) fileName = baseName + String(i + 1) + ".tsx";
|
|
563
674
|
usedNames.add(fileName);
|
|
564
675
|
const componentName = fileName.replace(/\.tsx$/, "");
|
|
565
|
-
const tagName = s.tagName || "div";
|
|
566
|
-
const className = s.pattern || "";
|
|
567
676
|
const relPath = "vds-generated/" + fileName;
|
|
568
677
|
const fullPath = path.join(COMPONENTS_DIR, relPath);
|
|
569
|
-
const content = `import * as React from "react";
|
|
570
678
|
|
|
571
|
-
|
|
679
|
+
let bodyJsx;
|
|
680
|
+
const tokens = [];
|
|
681
|
+
if (s.fullJsx && s.fullJsx.trim().length > 0) {
|
|
682
|
+
bodyJsx = s.fullJsx;
|
|
683
|
+
const componentTags = collectComponentTags(bodyJsx);
|
|
684
|
+
const lucideTags = [];
|
|
685
|
+
const uiTags = [];
|
|
686
|
+
for (const tag of componentTags) {
|
|
687
|
+
if (/^[A-Z][a-z0-9]+$/.test(tag) && tag.length > 2) lucideTags.push(tag);
|
|
688
|
+
else uiTags.push(tag);
|
|
689
|
+
}
|
|
690
|
+
let importLines = ['import * as React from "react";'];
|
|
691
|
+
if (lucideTags.length > 0) {
|
|
692
|
+
importLines.push(`import { ${lucideTags.join(", ")} } from "lucide-react";`);
|
|
693
|
+
}
|
|
694
|
+
for (const tag of uiTags) {
|
|
695
|
+
if (!lucideTags.includes(tag)) {
|
|
696
|
+
importLines.push(`import { ${tag} } from "@/components/ui/${toKebab(tag)}";`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const content = importLines.join("\n") + "\n\n" +
|
|
700
|
+
`export function ${componentName}() {\n return (\n ` +
|
|
701
|
+
bodyJsx.replace(/\n/g, "\n ") + "\n );\n}\n";
|
|
702
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
703
|
+
tokens.push(...(s.pattern || "").split(/\s+/).filter(Boolean));
|
|
704
|
+
} else {
|
|
705
|
+
const tagName = s.tagName || "div";
|
|
706
|
+
const className = s.pattern || "";
|
|
707
|
+
bodyJsx = `<${tagName} className="${className.replace(/"/g, '\\"')}">\n {null}\n </${tagName}>`;
|
|
708
|
+
const content = `import * as React from "react";
|
|
709
|
+
|
|
710
|
+
export function ${componentName}() {
|
|
572
711
|
return (
|
|
573
|
-
|
|
574
|
-
{children ?? null}
|
|
575
|
-
</${tagName}>
|
|
712
|
+
${bodyJsx}
|
|
576
713
|
);
|
|
577
714
|
}
|
|
578
715
|
`;
|
|
579
|
-
|
|
716
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
717
|
+
tokens.push(...(className || "").split(/\s+/).filter(Boolean));
|
|
718
|
+
}
|
|
719
|
+
|
|
580
720
|
entries.push({
|
|
581
721
|
file: relPath,
|
|
582
722
|
name: componentName.replace(/([A-Z])/g, " $1").trim(),
|
|
583
723
|
group: "VDS Generated",
|
|
584
724
|
category: "Extracted",
|
|
585
725
|
description: "Auto-extracted from repeated className patterns.",
|
|
586
|
-
tokens:
|
|
726
|
+
tokens: [...new Set(tokens)],
|
|
587
727
|
});
|
|
588
728
|
}
|
|
589
729
|
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
|
}
|