vibe-design-system 2.8.48 → 2.8.50
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
|
@@ -74,7 +74,7 @@ const DEFAULT_SKIP_LIST = [
|
|
|
74
74
|
"Form",
|
|
75
75
|
"Toaster",
|
|
76
76
|
"Toast",
|
|
77
|
-
|
|
77
|
+
// Button removed — now handled by multi-dimension detection (variant × size × disabled)
|
|
78
78
|
];
|
|
79
79
|
|
|
80
80
|
// Merge default with project-specific overrides from vds.config.js
|
|
@@ -441,6 +441,329 @@ function parseUnionLiterals(type) {
|
|
|
441
441
|
return matches.map((s) => s.replace(/"/g, ""));
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
// ─── Multi-Dimension State Detection ─────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Parse ALL CVA variant dimensions from component source (not just "variant").
|
|
448
|
+
* Returns: { variant: ["default", "secondary", ...], size: ["sm", "md", "lg"], ... }
|
|
449
|
+
*/
|
|
450
|
+
function parseCvaAllDimensions(source) {
|
|
451
|
+
if (!source) return {};
|
|
452
|
+
// Find "variants:" or "variants :" block
|
|
453
|
+
const variantsIdx = source.search(/variants\s*:\s*\{/);
|
|
454
|
+
if (variantsIdx === -1) return {};
|
|
455
|
+
|
|
456
|
+
const braceStart = source.indexOf("{", variantsIdx + 8);
|
|
457
|
+
if (braceStart === -1) return {};
|
|
458
|
+
|
|
459
|
+
// Brace-count to find matching close brace
|
|
460
|
+
let depth = 0, braceEnd = -1;
|
|
461
|
+
for (let i = braceStart; i < source.length; i++) {
|
|
462
|
+
if (source[i] === "{") depth++;
|
|
463
|
+
else if (source[i] === "}") { depth--; if (depth === 0) { braceEnd = i; break; } }
|
|
464
|
+
}
|
|
465
|
+
if (braceEnd === -1) return {};
|
|
466
|
+
|
|
467
|
+
const variantsBody = source.slice(braceStart + 1, braceEnd);
|
|
468
|
+
|
|
469
|
+
// Extract each dimension key and its sub-block keys
|
|
470
|
+
const result = {};
|
|
471
|
+
const dimRe = /(\w+)\s*:\s*\{/g;
|
|
472
|
+
let dm;
|
|
473
|
+
while ((dm = dimRe.exec(variantsBody)) !== null) {
|
|
474
|
+
const dimName = dm[1];
|
|
475
|
+
const subStart = dm.index + dm[0].length - 1;
|
|
476
|
+
// Brace-count for this dimension's sub-block
|
|
477
|
+
let d = 0, subEnd = -1;
|
|
478
|
+
for (let i = subStart; i < variantsBody.length; i++) {
|
|
479
|
+
if (variantsBody[i] === "{") d++;
|
|
480
|
+
else if (variantsBody[i] === "}") { d--; if (d === 0) { subEnd = i; break; } }
|
|
481
|
+
}
|
|
482
|
+
if (subEnd === -1) continue;
|
|
483
|
+
const subBody = variantsBody.slice(subStart + 1, subEnd);
|
|
484
|
+
const keys = [];
|
|
485
|
+
const keyRe = /^\s*([A-Za-z_][\w]*)\s*:/gm;
|
|
486
|
+
let km;
|
|
487
|
+
while ((km = keyRe.exec(subBody)) !== null) {
|
|
488
|
+
keys.push(km[1]);
|
|
489
|
+
}
|
|
490
|
+
if (keys.length > 0) result[dimName] = keys;
|
|
491
|
+
}
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Parse CVA defaultVariants block.
|
|
497
|
+
* Returns: { variant: "default", size: "default" }
|
|
498
|
+
*/
|
|
499
|
+
function parseCvaDefaultVariants(source) {
|
|
500
|
+
if (!source) return {};
|
|
501
|
+
const idx = source.search(/defaultVariants\s*:\s*\{/);
|
|
502
|
+
if (idx === -1) return {};
|
|
503
|
+
const braceStart = source.indexOf("{", idx + 15);
|
|
504
|
+
if (braceStart === -1) return {};
|
|
505
|
+
let depth = 0, braceEnd = -1;
|
|
506
|
+
for (let i = braceStart; i < source.length; i++) {
|
|
507
|
+
if (source[i] === "{") depth++;
|
|
508
|
+
else if (source[i] === "}") { depth--; if (depth === 0) { braceEnd = i; break; } }
|
|
509
|
+
}
|
|
510
|
+
if (braceEnd === -1) return {};
|
|
511
|
+
const body = source.slice(braceStart + 1, braceEnd);
|
|
512
|
+
const result = {};
|
|
513
|
+
const re = /(\w+)\s*:\s*["']([^"']+)["']/g;
|
|
514
|
+
let m;
|
|
515
|
+
while ((m = re.exec(body)) !== null) {
|
|
516
|
+
result[m[1]] = m[2];
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Known boolean prop names that represent visual states worth showing in Storybook. */
|
|
522
|
+
const BOOLEAN_STATE_PROP_NAMES = new Set([
|
|
523
|
+
"disabled", "loading", "isLoading", "active", "isActive",
|
|
524
|
+
"checked", "isChecked", "selected", "isSelected",
|
|
525
|
+
"error", "isError", "readOnly", "isReadOnly",
|
|
526
|
+
]);
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Detect boolean state props from effectiveProps.
|
|
530
|
+
* Returns: ["disabled", "loading"]
|
|
531
|
+
*/
|
|
532
|
+
function detectBooleanStateProps(effectiveProps) {
|
|
533
|
+
if (!Array.isArray(effectiveProps)) return [];
|
|
534
|
+
return effectiveProps
|
|
535
|
+
.filter((p) => {
|
|
536
|
+
const type = (p.type || "").trim();
|
|
537
|
+
const isBool = /^boolean/.test(type) || type === "boolean | undefined" || type === "boolean | null";
|
|
538
|
+
return isBool && BOOLEAN_STATE_PROP_NAMES.has(p.name);
|
|
539
|
+
})
|
|
540
|
+
.map((p) => p.name);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Detect all string union/enum props from effectiveProps (e.g., color: "red" | "blue").
|
|
545
|
+
* Returns: [{ name: "color", values: ["red", "blue", "green"] }]
|
|
546
|
+
*/
|
|
547
|
+
function detectUnionEnumProps(effectiveProps) {
|
|
548
|
+
if (!Array.isArray(effectiveProps)) return [];
|
|
549
|
+
const results = [];
|
|
550
|
+
for (const p of effectiveProps) {
|
|
551
|
+
const type = (p.type || "").trim();
|
|
552
|
+
if (/^['"][^'"]+['"](\s*\|\s*['"][^'"]+['"])+/.test(type)) {
|
|
553
|
+
const values = parseUnionLiterals(type);
|
|
554
|
+
if (values.length >= 2) {
|
|
555
|
+
results.push({ name: p.name, values });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return results;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Priority order for determining which dimension is "primary" (gets individual stories). */
|
|
563
|
+
const DIMENSION_PRIORITY = ["variant", "type", "status", "color", "kind", "mode", "side"];
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Build a complete variant map from source + effectiveProps.
|
|
567
|
+
* Combines CVA dimensions, TypeScript union props, and boolean states.
|
|
568
|
+
*/
|
|
569
|
+
function buildComponentVariantMap(source, effectiveProps) {
|
|
570
|
+
const map = { dimensions: {}, booleanStates: [], primaryDimension: null, secondaryDimensions: [] };
|
|
571
|
+
|
|
572
|
+
// 1. Parse CVA dimensions from source
|
|
573
|
+
const cvaDims = parseCvaAllDimensions(source);
|
|
574
|
+
const cvaDefaults = parseCvaDefaultVariants(source);
|
|
575
|
+
for (const [dimName, values] of Object.entries(cvaDims)) {
|
|
576
|
+
map.dimensions[dimName] = { values, default: cvaDefaults[dimName] || values[0], source: "cva" };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 2. Parse union enum props from TypeScript types (only if not already from CVA)
|
|
580
|
+
const unionProps = detectUnionEnumProps(effectiveProps);
|
|
581
|
+
for (const { name, values } of unionProps) {
|
|
582
|
+
if (!map.dimensions[name]) {
|
|
583
|
+
map.dimensions[name] = { values, default: values[0], source: "typescript" };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// 3. Detect boolean state props
|
|
588
|
+
map.booleanStates = detectBooleanStateProps(effectiveProps);
|
|
589
|
+
|
|
590
|
+
// 4. Determine primary and secondary dimensions
|
|
591
|
+
const dimNames = Object.keys(map.dimensions);
|
|
592
|
+
for (const candidate of DIMENSION_PRIORITY) {
|
|
593
|
+
if (dimNames.includes(candidate)) {
|
|
594
|
+
map.primaryDimension = candidate;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (!map.primaryDimension && dimNames.length > 0) {
|
|
599
|
+
map.primaryDimension = dimNames[0];
|
|
600
|
+
}
|
|
601
|
+
map.secondaryDimensions = dimNames.filter((d) => d !== map.primaryDimension);
|
|
602
|
+
|
|
603
|
+
return map;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Generate multi-dimension stories: individual per variant, per size, per state, + AllVariants grid.
|
|
608
|
+
*/
|
|
609
|
+
function buildMultiDimensionStories(variantMap, renderLine, childrenArgLine, defaultArgLines, componentName) {
|
|
610
|
+
const lines = [];
|
|
611
|
+
const { dimensions, booleanStates, primaryDimension, secondaryDimensions } = variantMap;
|
|
612
|
+
|
|
613
|
+
// Filter defaultArgLines to exclude dimension props (they are set explicitly per story)
|
|
614
|
+
const allDimNames = new Set(Object.keys(dimensions));
|
|
615
|
+
const filteredArgLines = defaultArgLines.filter((line) => {
|
|
616
|
+
const match = line.match(/^\s+(\w+)\s*:/);
|
|
617
|
+
return !match || !allDimNames.has(match[1]);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// --- A. Primary dimension stories (one per value) ---
|
|
621
|
+
if (primaryDimension && dimensions[primaryDimension]) {
|
|
622
|
+
const dim = dimensions[primaryDimension];
|
|
623
|
+
const defaultValue = dim.default || dim.values[0];
|
|
624
|
+
|
|
625
|
+
// Default story uses the default value
|
|
626
|
+
lines.push(`export const Default: Story = {`);
|
|
627
|
+
lines.push(renderLine);
|
|
628
|
+
lines.push(` args: {`);
|
|
629
|
+
lines.push(` ${primaryDimension}: "${defaultValue}",`);
|
|
630
|
+
if (childrenArgLine(componentName)) lines.push(childrenArgLine(componentName));
|
|
631
|
+
for (const line of filteredArgLines) lines.push(line);
|
|
632
|
+
lines.push(` },`);
|
|
633
|
+
lines.push(`};`);
|
|
634
|
+
lines.push("");
|
|
635
|
+
|
|
636
|
+
// Remaining values get their own stories
|
|
637
|
+
for (const val of dim.values) {
|
|
638
|
+
if (val === defaultValue) continue;
|
|
639
|
+
const storyName = capitalize(val);
|
|
640
|
+
lines.push(`export const ${storyName}: Story = {`);
|
|
641
|
+
lines.push(renderLine);
|
|
642
|
+
lines.push(` args: {`);
|
|
643
|
+
lines.push(` ${primaryDimension}: "${val}",`);
|
|
644
|
+
if (childrenArgLine(capitalize(val))) lines.push(childrenArgLine(capitalize(val)));
|
|
645
|
+
for (const line of filteredArgLines) lines.push(line);
|
|
646
|
+
lines.push(` },`);
|
|
647
|
+
lines.push(`};`);
|
|
648
|
+
lines.push("");
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
// No primary dimension — single Default story
|
|
652
|
+
lines.push(`export const Default: Story = {`);
|
|
653
|
+
lines.push(renderLine);
|
|
654
|
+
const storyArgLines = [];
|
|
655
|
+
if (childrenArgLine(componentName)) storyArgLines.push(childrenArgLine(componentName));
|
|
656
|
+
for (const line of filteredArgLines) storyArgLines.push(line);
|
|
657
|
+
if (storyArgLines.length > 0) {
|
|
658
|
+
lines.push(` args: {`);
|
|
659
|
+
for (const line of storyArgLines) lines.push(line);
|
|
660
|
+
lines.push(` },`);
|
|
661
|
+
}
|
|
662
|
+
lines.push(`};`);
|
|
663
|
+
lines.push("");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// --- B. Secondary dimension stories (one per non-default value) ---
|
|
667
|
+
for (const dimName of secondaryDimensions) {
|
|
668
|
+
const dim = dimensions[dimName];
|
|
669
|
+
if (!dim || dim.values.length <= 1) continue;
|
|
670
|
+
for (const val of dim.values) {
|
|
671
|
+
if (val === (dim.default || dim.values[0])) continue;
|
|
672
|
+
const storyName = capitalize(dimName) + capitalize(val);
|
|
673
|
+
lines.push(`export const ${storyName}: Story = {`);
|
|
674
|
+
lines.push(renderLine);
|
|
675
|
+
lines.push(` args: {`);
|
|
676
|
+
if (primaryDimension && dimensions[primaryDimension]) {
|
|
677
|
+
lines.push(` ${primaryDimension}: "${dimensions[primaryDimension].default || dimensions[primaryDimension].values[0]}",`);
|
|
678
|
+
}
|
|
679
|
+
lines.push(` ${dimName}: "${val}",`);
|
|
680
|
+
// For icon size use a short symbol; otherwise use component name
|
|
681
|
+
const childLabel = val === "icon" ? "\u2726" : componentName;
|
|
682
|
+
if (childrenArgLine(childLabel)) lines.push(childrenArgLine(childLabel));
|
|
683
|
+
for (const line of filteredArgLines) lines.push(line);
|
|
684
|
+
lines.push(` },`);
|
|
685
|
+
lines.push(`};`);
|
|
686
|
+
lines.push("");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// --- C. Boolean state stories ---
|
|
691
|
+
for (const boolProp of booleanStates) {
|
|
692
|
+
if (boolProp === "open" || boolProp === "isOpen") continue;
|
|
693
|
+
const storyName = capitalize(boolProp);
|
|
694
|
+
lines.push(`export const ${storyName}: Story = {`);
|
|
695
|
+
lines.push(renderLine);
|
|
696
|
+
lines.push(` args: {`);
|
|
697
|
+
if (primaryDimension && dimensions[primaryDimension]) {
|
|
698
|
+
lines.push(` ${primaryDimension}: "${dimensions[primaryDimension].default || dimensions[primaryDimension].values[0]}",`);
|
|
699
|
+
}
|
|
700
|
+
lines.push(` ${boolProp}: true,`);
|
|
701
|
+
if (childrenArgLine(componentName)) lines.push(childrenArgLine(componentName));
|
|
702
|
+
for (const line of filteredArgLines) lines.push(line);
|
|
703
|
+
lines.push(` },`);
|
|
704
|
+
lines.push(`};`);
|
|
705
|
+
lines.push("");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// --- D. AllVariants grid story (only if primary has 2+ values) ---
|
|
709
|
+
if (primaryDimension && dimensions[primaryDimension] && dimensions[primaryDimension].values.length >= 2) {
|
|
710
|
+
const primaryValues = dimensions[primaryDimension].values;
|
|
711
|
+
const secondaryDim = secondaryDimensions.find((d) => dimensions[d] && dimensions[d].values.length >= 2);
|
|
712
|
+
|
|
713
|
+
lines.push(`export const AllVariants: Story = {`);
|
|
714
|
+
lines.push(` parameters: { layout: "padded" },`);
|
|
715
|
+
lines.push(` render: () => {`);
|
|
716
|
+
|
|
717
|
+
if (secondaryDim) {
|
|
718
|
+
const secondaryValues = dimensions[secondaryDim].values;
|
|
719
|
+
const colCount = secondaryValues.length;
|
|
720
|
+
|
|
721
|
+
// Style constants for the grid
|
|
722
|
+
lines.push(` const hdr: React.CSSProperties = { textAlign: "center", fontSize: 11, fontWeight: 600, color: "#6b7280", fontFamily: "monospace", textTransform: "uppercase", letterSpacing: "0.05em", padding: "0 4px" };`);
|
|
723
|
+
lines.push(` const lbl: React.CSSProperties = { fontSize: 12, fontWeight: 500, color: "#374151", fontFamily: "monospace" };`);
|
|
724
|
+
lines.push(` const cell: React.CSSProperties = { display: "flex", justifyContent: "center", alignItems: "center" };`);
|
|
725
|
+
lines.push(` return (`);
|
|
726
|
+
lines.push(` <div style={{ padding: 24 }}>`);
|
|
727
|
+
lines.push(` <div style={{ display: "grid", gridTemplateColumns: "100px repeat(${colCount}, minmax(80px, 1fr))", gap: "16px 12px", alignItems: "center" }}>`);
|
|
728
|
+
|
|
729
|
+
// Header row: empty corner + secondary dimension labels
|
|
730
|
+
lines.push(` <div />`);
|
|
731
|
+
for (const sVal of secondaryValues) {
|
|
732
|
+
lines.push(` <div style={hdr}>${sVal}</div>`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Data rows: primary label + component cells
|
|
736
|
+
for (const pVal of primaryValues) {
|
|
737
|
+
lines.push(` <div style={lbl}>${pVal}</div>`);
|
|
738
|
+
for (const sVal of secondaryValues) {
|
|
739
|
+
const childText = sVal === "icon" ? "\u2726" : componentName;
|
|
740
|
+
lines.push(` <div style={cell}><ComponentRef ${primaryDimension}="${pVal}" ${secondaryDim}="${sVal}">${childText}</ComponentRef></div>`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
lines.push(` </div>`);
|
|
745
|
+
lines.push(` </div>`);
|
|
746
|
+
lines.push(` );`);
|
|
747
|
+
} else {
|
|
748
|
+
// No secondary dimension — labeled flex layout
|
|
749
|
+
lines.push(` return (`);
|
|
750
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 12, padding: 24, alignItems: "center" }}>`);
|
|
751
|
+
for (const val of primaryValues) {
|
|
752
|
+
lines.push(` <ComponentRef ${primaryDimension}="${val}">${componentName}</ComponentRef>`);
|
|
753
|
+
}
|
|
754
|
+
lines.push(` </div>`);
|
|
755
|
+
lines.push(` );`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
lines.push(` },`);
|
|
759
|
+
lines.push(`};`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return lines.join("\n");
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ─── End Multi-Dimension Detection ───────────────────────────────────────────
|
|
766
|
+
|
|
444
767
|
/** Parse props with types from component source (interface/type or inline props). Returns [{ name, type, required }]. */
|
|
445
768
|
function parsePropsFromSource(source) {
|
|
446
769
|
if (!source || typeof source !== "string") return [];
|
|
@@ -1246,8 +1569,15 @@ function buildArgTypeEntry(prop) {
|
|
|
1246
1569
|
* @param {Array<{name:string,type:string,required:boolean}>} effectiveProps
|
|
1247
1570
|
* @returns {"SAFE"|"SECTION"|"WRAPPER"|"VARIANT"|"CONFIGURED"}
|
|
1248
1571
|
*/
|
|
1249
|
-
function getStoryProfile(componentName, source, effectiveProps) {
|
|
1572
|
+
function getStoryProfile(componentName, source, effectiveProps, variantMap) {
|
|
1250
1573
|
if (componentName && SAFE_WRAPPER_DEFAULTS[componentName]) return "SAFE";
|
|
1574
|
+
// Check for ANY multi-value dimensions (variant, size, color, etc.) or boolean states
|
|
1575
|
+
// This must run BEFORE the SECTION check because CVA components (Button, Badge) may have
|
|
1576
|
+
// empty effectiveProps (types come from VariantProps<typeof ...>) but still have dimensions.
|
|
1577
|
+
if (variantMap) {
|
|
1578
|
+
const hasDimensions = Object.keys(variantMap.dimensions).length > 0;
|
|
1579
|
+
if (hasDimensions || variantMap.booleanStates.length > 0) return "VARIANT";
|
|
1580
|
+
}
|
|
1251
1581
|
if (effectiveProps.length === 0) return "SECTION";
|
|
1252
1582
|
if (hasChildrenPropReactNode(effectiveProps)) return "WRAPPER";
|
|
1253
1583
|
if (effectiveProps.some(p => p.name === "variant")) return "VARIANT";
|
|
@@ -1289,7 +1619,7 @@ function buildProfileRenderLine(profile, RenderTarget, argsFallback) {
|
|
|
1289
1619
|
*/
|
|
1290
1620
|
function buildProfileChildrenArgLine(profile) {
|
|
1291
1621
|
return (label) => {
|
|
1292
|
-
if (profile === "WRAPPER") return ` children: ${JSON.stringify(label)},`;
|
|
1622
|
+
if (profile === "WRAPPER" || profile === "VARIANT") return ` children: ${JSON.stringify(label)},`;
|
|
1293
1623
|
return null;
|
|
1294
1624
|
};
|
|
1295
1625
|
}
|
|
@@ -1319,38 +1649,11 @@ function buildStoryFileContent(comp) {
|
|
|
1319
1649
|
// Title: "Module/ComponentName" (category intentionally dropped — folder is the context)
|
|
1320
1650
|
const title = `${group}/${componentName}`;
|
|
1321
1651
|
|
|
1322
|
-
const props = Array.isArray(comp.props) ? comp.props : [];
|
|
1323
|
-
const variantProp = props.find((p) => p.name === "variant");
|
|
1324
|
-
let variants = parseUnionLiterals(variantProp && variantProp.type);
|
|
1325
|
-
|
|
1326
1652
|
const srcPath = isPageFile
|
|
1327
1653
|
? path.join(PROJECT_ROOT, fileNoExt.replace(/^src\//, "src/") + ".tsx")
|
|
1328
1654
|
: path.join(PROJECT_ROOT, COMPONENTS_REL_DIR, comp.file);
|
|
1329
1655
|
|
|
1330
|
-
//
|
|
1331
|
-
if (!variants.length) {
|
|
1332
|
-
try {
|
|
1333
|
-
if (fs.existsSync(srcPath)) {
|
|
1334
|
-
const code = fs.readFileSync(srcPath, "utf-8");
|
|
1335
|
-
// Roughly match shadcn-style: variants: { variant: { ... }, size: { ... } }
|
|
1336
|
-
const m = code.match(/variant\s*:\s*{([\s\S]*?)}\s*,\s*size\s*:/);
|
|
1337
|
-
if (m) {
|
|
1338
|
-
const body = m[1];
|
|
1339
|
-
const names = [];
|
|
1340
|
-
const lineRe = /^\s*([A-Za-z0-9_]+)\s*:/gm;
|
|
1341
|
-
let lm;
|
|
1342
|
-
while ((lm = lineRe.exec(body))) {
|
|
1343
|
-
names.push(lm[1]);
|
|
1344
|
-
}
|
|
1345
|
-
if (names.length) variants = names;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
} catch {
|
|
1349
|
-
// best-effort; ignore parsing errors
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
// Read component source to detect export style for import
|
|
1656
|
+
// Read component source once (used for export style, props, CVA parsing)
|
|
1354
1657
|
let source = "";
|
|
1355
1658
|
try {
|
|
1356
1659
|
if (fs.existsSync(srcPath)) {
|
|
@@ -1366,9 +1669,15 @@ function buildStoryFileContent(comp) {
|
|
|
1366
1669
|
const effectiveProps = Array.isArray(comp.props) && comp.props.length > 0 ? comp.props : parsePropsFromSource(source);
|
|
1367
1670
|
const usageFromPages = findComponentUsageInPages(componentName, PROJECT_ROOT);
|
|
1368
1671
|
|
|
1672
|
+
// Build complete variant map: CVA dimensions + TypeScript union props + boolean states
|
|
1673
|
+
const variantMap = buildComponentVariantMap(source, effectiveProps);
|
|
1674
|
+
|
|
1675
|
+
// Legacy compat: extract flat variants array for buildSpecialStories
|
|
1676
|
+
const variants = variantMap.dimensions.variant ? variantMap.dimensions.variant.values : [];
|
|
1677
|
+
|
|
1369
1678
|
// Profile: single source of truth — replaces omitChildren, useReactNodeChildrenRender,
|
|
1370
1679
|
// isNoPropsFeature, useSafeWrapper, argsParam, propsArg flags.
|
|
1371
|
-
const profile = getStoryProfile(componentName, source, effectiveProps);
|
|
1680
|
+
const profile = getStoryProfile(componentName, source, effectiveProps, variantMap);
|
|
1372
1681
|
const propSummary = effectiveProps.length > 0 ? ` (${effectiveProps.length} props)` : "";
|
|
1373
1682
|
console.log(`[VDS] ${componentName} → ${profile}${propSummary}`);
|
|
1374
1683
|
|
|
@@ -1412,6 +1721,8 @@ function buildStoryFileContent(comp) {
|
|
|
1412
1721
|
lines.push(` component: ComponentRef,`);
|
|
1413
1722
|
// SECTION: no props/args → autodocs tries to render React.lazy without Suspense → useRef crash
|
|
1414
1723
|
if (profile !== "SECTION") lines.push(` tags: ["autodocs"],`);
|
|
1724
|
+
// Center small components (VARIANT, WRAPPER, CONFIGURED, SAFE) to prevent vertical stretching
|
|
1725
|
+
if (profile !== "SECTION") lines.push(` parameters: { layout: "centered" },`);
|
|
1415
1726
|
|
|
1416
1727
|
// Build argTypes from extracted TypeScript props + icon-specific overrides
|
|
1417
1728
|
const argTypeEntries = [];
|
|
@@ -1439,11 +1750,15 @@ function buildStoryFileContent(comp) {
|
|
|
1439
1750
|
lines.push(`type Story = StoryObj<typeof meta>;`);
|
|
1440
1751
|
lines.push("");
|
|
1441
1752
|
|
|
1442
|
-
// Component-specific stories
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1753
|
+
// Component-specific stories for non-variant components (Input, Textarea, etc.)
|
|
1754
|
+
// Button and Badge are now handled generically by multi-dimension detection.
|
|
1755
|
+
const skipSpecialFor = new Set(["Button", "Badge"]);
|
|
1756
|
+
if (!skipSpecialFor.has(componentName)) {
|
|
1757
|
+
const specialStories = buildSpecialStories(componentName, variants);
|
|
1758
|
+
if (specialStories) {
|
|
1759
|
+
lines.push(specialStories);
|
|
1760
|
+
return lines.join("\n");
|
|
1761
|
+
}
|
|
1447
1762
|
}
|
|
1448
1763
|
|
|
1449
1764
|
// Profile-driven render + children — single source of truth via getStoryProfile()
|
|
@@ -1453,7 +1768,18 @@ function buildStoryFileContent(comp) {
|
|
|
1453
1768
|
const renderLine = buildProfileRenderLine(profile, RenderTarget, argsFallback);
|
|
1454
1769
|
const childrenArgLine = buildProfileChildrenArgLine(profile);
|
|
1455
1770
|
|
|
1456
|
-
|
|
1771
|
+
// Multi-dimension story generation: variant × size × disabled × AllVariants grid
|
|
1772
|
+
const hasDimensions = Object.keys(variantMap.dimensions).length > 0 || variantMap.booleanStates.length > 0;
|
|
1773
|
+
|
|
1774
|
+
if (hasDimensions) {
|
|
1775
|
+
const multiStories = buildMultiDimensionStories(
|
|
1776
|
+
variantMap, renderLine, childrenArgLine, defaultArgLines, componentName
|
|
1777
|
+
);
|
|
1778
|
+
if (multiStories) {
|
|
1779
|
+
lines.push(multiStories);
|
|
1780
|
+
}
|
|
1781
|
+
} else {
|
|
1782
|
+
// No dimensions detected: single Default story
|
|
1457
1783
|
lines.push(`export const Default: Story = {`);
|
|
1458
1784
|
lines.push(renderLine);
|
|
1459
1785
|
const storyArgLines = [];
|
|
@@ -1465,29 +1791,6 @@ function buildStoryFileContent(comp) {
|
|
|
1465
1791
|
lines.push(` },`);
|
|
1466
1792
|
}
|
|
1467
1793
|
lines.push(`};`);
|
|
1468
|
-
} else {
|
|
1469
|
-
const defaultVariant = variants[0];
|
|
1470
|
-
lines.push(`export const ${capitalize(defaultVariant)}: Story = {`);
|
|
1471
|
-
lines.push(renderLine);
|
|
1472
|
-
lines.push(` args: {`);
|
|
1473
|
-
lines.push(` variant: "${defaultVariant}",`);
|
|
1474
|
-
if (childrenArgLine(componentName)) lines.push(childrenArgLine(componentName));
|
|
1475
|
-
for (const line of defaultArgLines) lines.push(line);
|
|
1476
|
-
lines.push(` },`);
|
|
1477
|
-
lines.push(`};`);
|
|
1478
|
-
lines.push("");
|
|
1479
|
-
for (const v of variants.slice(1)) {
|
|
1480
|
-
const storyName = capitalize(v);
|
|
1481
|
-
lines.push(`export const ${storyName}: Story = {`);
|
|
1482
|
-
lines.push(renderLine);
|
|
1483
|
-
lines.push(` args: {`);
|
|
1484
|
-
lines.push(` variant: "${v}",`);
|
|
1485
|
-
if (childrenArgLine(storyName)) lines.push(childrenArgLine(storyName));
|
|
1486
|
-
for (const line of defaultArgLines) lines.push(line);
|
|
1487
|
-
lines.push(` },`);
|
|
1488
|
-
lines.push(`};`);
|
|
1489
|
-
lines.push("");
|
|
1490
|
-
}
|
|
1491
1794
|
}
|
|
1492
1795
|
|
|
1493
1796
|
return lines.join("\n");
|
|
@@ -3054,14 +3357,27 @@ function main() {
|
|
|
3054
3357
|
// Skip unclassified and shadcn/ui primitives (UI group) — they're documented at ui.shadcn.com
|
|
3055
3358
|
// Only project-specific module groups (Circles, Finance, Projects, Time, …) get stories
|
|
3056
3359
|
const g = comp.group || "Components";
|
|
3360
|
+
// Skip shadcn/ui primitives by default — they're documented at ui.shadcn.com
|
|
3361
|
+
// Users can override with vds.config.js: includeGroups: ["shadcn"] or includeComponents: ["Button", "Badge"]
|
|
3362
|
+
const includeGroups = VDS_CONFIG.includeGroups || [];
|
|
3363
|
+
const includeComponents = VDS_CONFIG.includeComponents || [];
|
|
3057
3364
|
if (g === "Uncategorized" || g === "UI" || g === "shadcn" || g === "ui") {
|
|
3058
|
-
|
|
3059
|
-
const
|
|
3060
|
-
if (
|
|
3061
|
-
|
|
3062
|
-
|
|
3365
|
+
const groupIncluded = includeGroups.some((ig) => ig.toLowerCase() === g.toLowerCase());
|
|
3366
|
+
const compIncluded = includeComponents.includes(componentName);
|
|
3367
|
+
if (!groupIncluded && !compIncluded) {
|
|
3368
|
+
// Clean up leftover story files for components that are now in the skip group
|
|
3369
|
+
const oldStory = path.join(STORIES_DIR, `${componentName}.stories.tsx`);
|
|
3370
|
+
if (fs.existsSync(oldStory)) {
|
|
3371
|
+
try {
|
|
3372
|
+
const firstLine = fs.readFileSync(oldStory, "utf-8").split("\n")[0] || "";
|
|
3373
|
+
if (firstLine.includes("@vds-regenerate")) {
|
|
3374
|
+
fs.unlinkSync(oldStory);
|
|
3375
|
+
console.log(`[VDS] Removed shadcn/ui story: ${componentName}.stories.tsx`);
|
|
3376
|
+
}
|
|
3377
|
+
} catch { /* ignore */ }
|
|
3378
|
+
}
|
|
3379
|
+
continue;
|
|
3063
3380
|
}
|
|
3064
|
-
continue;
|
|
3065
3381
|
}
|
|
3066
3382
|
if (onlyName && componentName !== onlyName) continue;
|
|
3067
3383
|
|