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
|
@@ -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
|
}
|