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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.8.48",
3
+ "version": "2.8.50",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -74,7 +74,7 @@ const DEFAULT_SKIP_LIST = [
74
74
  "Form",
75
75
  "Toaster",
76
76
  "Toast",
77
- "Button",
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
- // Fallback: if manifest doesn't have variant metadata yet, parse cva() directly from component file.
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 (inputs, composite components, etc.)
1443
- const specialStories = buildSpecialStories(componentName, variants);
1444
- if (specialStories) {
1445
- lines.push(specialStories);
1446
- return lines.join("\n");
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
- if (!variants.length) {
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
- // Clean up leftover story files for components that are now in the skip group
3059
- const oldStory = path.join(STORIES_DIR, `${componentName}.stories.tsx`);
3060
- if (fs.existsSync(oldStory)) {
3061
- fs.unlinkSync(oldStory);
3062
- console.log(`[VDS] Removed shadcn/ui story: ${componentName}.stories.tsx`);
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