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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
574
|
-
{children ?? null}
|
|
575
|
-
</${tagName}>
|
|
703
|
+
${bodyJsx}
|
|
576
704
|
);
|
|
577
705
|
}
|
|
578
706
|
`;
|
|
579
|
-
|
|
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:
|
|
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
|
}
|