vibe-design-system 2.5.11 → 2.5.12

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/bin/init.js CHANGED
@@ -50,8 +50,6 @@ export default config;
50
50
  `;
51
51
 
52
52
  const STORYBOOK_PREVIEW_TS = `import type { Preview } from "@storybook/react";
53
- import React from "react";
54
- import { MemoryRouter } from "react-router-dom";
55
53
  import "../src/index.css";
56
54
 
57
55
  const preview: Preview = {
@@ -71,14 +69,6 @@ const preview: Preview = {
71
69
  },
72
70
  },
73
71
  },
74
- decorators: [
75
- (Story, context) => {
76
- const needsRouter = context.parameters?.router !== false;
77
- return needsRouter
78
- ? React.createElement(MemoryRouter, null, React.createElement(Story, null))
79
- : React.createElement(Story, null);
80
- },
81
- ],
82
72
  };
83
73
 
84
74
  export default preview;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.5.11",
3
+ "version": "2.5.12",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -356,6 +356,24 @@ function usesOwnRouter(source) {
356
356
  return /\bBrowserRouter\b|\bRouterProvider\b/.test(source);
357
357
  }
358
358
 
359
+ /** Whether component has to or href prop (link-like); story should get MemoryRouter decorator. */
360
+ function hasToOrHrefProp(comp, source) {
361
+ const props = Array.isArray(comp.props) && comp.props.length > 0 ? comp.props : parsePropsFromSource(source);
362
+ return props.some((p) => p.name === "to" || p.name === "href");
363
+ }
364
+
365
+ /** Check if source exports given named exports (export { X } or export const X). */
366
+ function hasNamedExports(source, names) {
367
+ if (!source || !Array.isArray(names) || names.length === 0) return false;
368
+ for (const name of names) {
369
+ const hasExport = new RegExp(`export\\s+\\{[^}]*\\b${name}\\b[^}]*\\}`).test(source) ||
370
+ new RegExp(`export\\s+const\\s+${name}\\b`).test(source) ||
371
+ new RegExp(`export\\s+function\\s+${name}\\b`).test(source);
372
+ if (!hasExport) return false;
373
+ }
374
+ return true;
375
+ }
376
+
359
377
  /** Void HTML elements: img, input, hr, br — must not receive children. If component wraps one and has children in props, omit children in story args. */
360
378
  function componentWrapsVoidElement(source) {
361
379
  if (!source || typeof source !== "string") return false;
@@ -381,6 +399,74 @@ function parseUnionLiterals(type) {
381
399
  return matches.map((s) => s.replace(/"/g, ""));
382
400
  }
383
401
 
402
+ /** Parse props with types from component source (interface/type or inline props). Returns [{ name, type, required }]. */
403
+ function parsePropsFromSource(source) {
404
+ if (!source || typeof source !== "string") return [];
405
+ const props = [];
406
+ const push = (name, type, optional) => {
407
+ const required = !optional;
408
+ if (!props.some((p) => p.name === name)) props.push({ name, type, required });
409
+ };
410
+ // Match interface XProps { ... } or type XProps = { ... }
411
+ const blockRe = /(?:interface|type)\s+\w*Props?\s*=\s*\{([^}]+)\}|(?:interface|type)\s+\w*Props?\s*\{([^}]+)\}/g;
412
+ let m;
413
+ while ((m = blockRe.exec(source)) !== null) {
414
+ const body = (m[1] || m[2] || "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
415
+ const propRe = /(\w+)(\??)\s*:\s*([^;]+?)(?=\s*;|\s*\w+\s*\??\s*:|\s*$)/g;
416
+ let pm;
417
+ while ((pm = propRe.exec(body)) !== null) {
418
+ const optional = pm[2] === "?" || (pm[3] || "").includes("| undefined");
419
+ push(pm[1], pm[3].trim().replace(/\s+/g, " "), optional);
420
+ }
421
+ }
422
+ // Inline (props: { ... })
423
+ const inlineRe = /(?:props|Parameters)\s*:\s*\{\s*([^}]+)\}/;
424
+ const im = source.match(inlineRe);
425
+ if (im) {
426
+ const body = im[1].replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
427
+ const propRe = /(\w+)(\??)\s*:\s*([^;:,]+?)(?=\s*[;,}]|\s*\w+\s*\??\s*:)/g;
428
+ let pm;
429
+ while ((pm = propRe.exec(body)) !== null) {
430
+ if (!props.some((p) => p.name === pm[1])) push(pm[1], pm[3].trim().replace(/\s+/g, " "), pm[2] === "?");
431
+ }
432
+ }
433
+ return props;
434
+ }
435
+
436
+ /** Default icon for LucideIcon prop when name is generic (icon, leftIcon, etc.). */
437
+ const LUCIDE_ICON_DEFAULT = "Star";
438
+
439
+ /** Build default args lines and lucide-react imports for required props (LucideIcon → icon component, X[] → example item). */
440
+ function buildDefaultArgsForRequiredProps(props) {
441
+ const argLines = [];
442
+ const lucideImports = new Set();
443
+ if (!Array.isArray(props) || props.length === 0) return { argLines, lucideImports: [] };
444
+ for (const p of props) {
445
+ if (p.required !== true) continue;
446
+ const type = String(p.type || "").trim();
447
+ const name = p.name;
448
+ if (/LucideIcon|lucide-react/.test(type)) {
449
+ const iconName = LUCIDE_ICON_DEFAULT;
450
+ lucideImports.add(iconName);
451
+ argLines.push(` ${name}: ${iconName},`);
452
+ } else if (/\[\]/.test(type)) {
453
+ const itemTypeMatch = type.match(/([A-Za-z0-9_]+)(?=\s*\[\])/);
454
+ const itemType = itemTypeMatch ? itemTypeMatch[1] : "Item";
455
+ const example = getExampleItemForArrayType(itemType);
456
+ argLines.push(` ${name}: ${JSON.stringify(example)},`);
457
+ }
458
+ }
459
+ return { argLines, lucideImports: [...lucideImports] };
460
+ }
461
+
462
+ function getExampleItemForArrayType(itemType) {
463
+ const lower = itemType.toLowerCase();
464
+ if (lower.includes("stat") || itemType === "StatsSectionItem") return [{ label: "Example", value: "100" }];
465
+ if (lower.includes("item") && !lower.includes("menu")) return [{ id: "1", label: "Example", value: "Sample" }];
466
+ if (lower.includes("menu")) return [{ label: "Item 1", value: "item-1" }];
467
+ return [{ label: "Example", value: "100" }];
468
+ }
469
+
384
470
  function capitalize(str) {
385
471
  if (!str) return "";
386
472
  return str.charAt(0).toUpperCase() + str.slice(1);
@@ -489,37 +575,63 @@ function buildSpecialStories(componentName, variants) {
489
575
  return "";
490
576
  }
491
577
 
492
- function buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, recipe) {
578
+ function buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, recipe, defaultArgLines = [], lucideImports = []) {
493
579
  const lines = [];
494
580
  lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
581
+ if (lucideImports.length > 0) {
582
+ lines.push(`import { ${lucideImports.join(", ")} } from "lucide-react";`);
583
+ }
584
+
585
+ const isCardWithoutNamedExports = componentName === "Card" && !hasNamedExports(source, ["CardHeader", "CardTitle", "CardDescription", "CardContent", "CardFooter"]);
586
+ let effectiveRecipe = recipe;
587
+ if (isCardWithoutNamedExports) {
588
+ effectiveRecipe = {
589
+ imports: [],
590
+ render: `(args) => (
591
+ <ComponentRef className="w-[340px]" {...args}>
592
+ <ComponentRef.Header>
593
+ <ComponentRef.Title>Card title</ComponentRef.Title>
594
+ <ComponentRef.Description>Short description.</ComponentRef.Description>
595
+ </ComponentRef.Header>
596
+ <ComponentRef.Content><p>Card body content here.</p></ComponentRef.Content>
597
+ <ComponentRef.Footer>Footer</ComponentRef.Footer>
598
+ </ComponentRef>
599
+ )`,
600
+ };
601
+ }
602
+
495
603
  if (exportStyle === "default") {
496
604
  lines.push(`import ${componentName} from "${importPath}";`);
497
- if (recipe.imports?.length) {
498
- lines.push(`import { ${recipe.imports.join(", ")} } from "${importPath}";`);
605
+ if (effectiveRecipe.imports?.length) {
606
+ lines.push(`import { ${effectiveRecipe.imports.join(", ")} } from "${importPath}";`);
499
607
  }
500
608
  lines.push(`const ComponentRef = ${componentName};`);
501
609
  } else if (exportStyle === "named") {
502
- const names = recipe.imports?.length ? [componentName, ...recipe.imports] : [componentName];
610
+ const names = effectiveRecipe.imports?.length ? [componentName, ...effectiveRecipe.imports] : [componentName];
503
611
  lines.push(`import { ${names.join(", ")} } from "${importPath}";`);
504
612
  lines.push(`const ComponentRef = ${componentName};`);
505
613
  } else {
506
614
  const defaultAlias = `${componentName}Default`;
507
615
  const namedAlias = `${componentName}Named`;
508
- const extra = recipe.imports?.length ? ", " + recipe.imports.join(", ") : "";
616
+ const extra = effectiveRecipe.imports?.length ? ", " + effectiveRecipe.imports.join(", ") : "";
509
617
  lines.push(`import ${defaultAlias}, { ${componentName} as ${namedAlias}${extra} } from "${importPath}";`);
510
618
  lines.push(`const ComponentRef = ${namedAlias} ?? ${defaultAlias};`);
511
619
  }
512
- for (const ext of recipe.extraImports || []) {
620
+ for (const ext of (effectiveRecipe.extraImports || recipe.extraImports) || []) {
513
621
  lines.push(`import { ${ext.names.join(", ")} } from "${ext.from}";`);
514
622
  }
515
- const skipPreviewRouter = usesOwnRouter(source);
623
+
624
+ const needsRouterDecorator = hasToOrHrefProp(comp, source);
625
+ if (needsRouterDecorator) {
626
+ lines.push(`import { MemoryRouter } from "react-router-dom";`);
627
+ }
516
628
  lines.push("");
517
629
  lines.push(`const meta = {`);
518
630
  lines.push(` title: ${JSON.stringify(title)},`);
519
631
  lines.push(` component: ComponentRef,`);
520
632
  lines.push(` tags: ["autodocs"],`);
521
- if (skipPreviewRouter) {
522
- lines.push(` parameters: { router: false },`);
633
+ if (needsRouterDecorator) {
634
+ lines.push(` decorators: [(Story) => <MemoryRouter><Story /></MemoryRouter>],`);
523
635
  }
524
636
  lines.push(`} satisfies Meta<typeof ComponentRef>;`);
525
637
  lines.push("");
@@ -527,7 +639,12 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
527
639
  lines.push(`type Story = StoryObj<typeof meta>;`);
528
640
  lines.push("");
529
641
  lines.push(`export const Default: Story = {`);
530
- lines.push(` render: ${recipe.render},`);
642
+ lines.push(` render: ${effectiveRecipe.render},`);
643
+ if (defaultArgLines.length > 0) {
644
+ lines.push(` args: {`);
645
+ for (const line of defaultArgLines) lines.push(line);
646
+ lines.push(` },`);
647
+ }
531
648
  lines.push(`};`);
532
649
  return lines.join("\n");
533
650
  }
@@ -590,17 +707,24 @@ function buildStoryFileContent(comp) {
590
707
  const omitChildren = componentWrapsVoidElement(source);
591
708
  const isPage = comp.file.startsWith("pages/");
592
709
 
710
+ // Props: manifest or parse from source for default args (LucideIcon, array types)
711
+ const effectiveProps = Array.isArray(comp.props) && comp.props.length > 0 ? comp.props : parsePropsFromSource(source);
712
+ const { argLines: defaultArgLines, lucideImports } = buildDefaultArgsForRequiredProps(effectiveProps);
713
+
593
714
  // Skip story only if not a page and no export found
594
715
  if (exportStyle === "unknown" && !isPage && (!source.includes("export") || !new RegExp(`\\b${componentName}\\b`).test(source))) {
595
716
  return null;
596
717
  }
597
718
 
598
719
  if (RECIPES[componentName]) {
599
- return buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, RECIPES[componentName]);
720
+ return buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, RECIPES[componentName], defaultArgLines, lucideImports);
600
721
  }
601
722
 
602
723
  const lines = [];
603
724
  lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
725
+ if (lucideImports.length > 0) {
726
+ lines.push(`import { ${lucideImports.join(", ")} } from "lucide-react";`);
727
+ }
604
728
 
605
729
  if (isPage && exportStyle !== "default") {
606
730
  lines.push(`import * as Named from "${importPath}";`);
@@ -620,14 +744,17 @@ function buildStoryFileContent(comp) {
620
744
  lines.push(`const ComponentRef = ${namedAlias} ?? ${defaultAlias};`);
621
745
  }
622
746
 
623
- const skipPreviewRouter = usesOwnRouter(source);
747
+ const needsRouterDecorator = hasToOrHrefProp(comp, source);
748
+ if (needsRouterDecorator) {
749
+ lines.push(`import { MemoryRouter } from "react-router-dom";`);
750
+ }
624
751
  lines.push("");
625
752
  lines.push(`const meta = {`);
626
753
  lines.push(` title: ${JSON.stringify(title)},`);
627
754
  lines.push(` component: ComponentRef,`);
628
755
  lines.push(` tags: ["autodocs"],`);
629
- if (skipPreviewRouter) {
630
- lines.push(` parameters: { router: false },`);
756
+ if (needsRouterDecorator) {
757
+ lines.push(` decorators: [(Story) => <MemoryRouter><Story /></MemoryRouter>],`);
631
758
  }
632
759
  lines.push(`} satisfies Meta<typeof ComponentRef>;`);
633
760
  lines.push("");
@@ -648,6 +775,7 @@ function buildStoryFileContent(comp) {
648
775
  lines.push(` render: (args) => <ComponentRef {...args} />,`);
649
776
  lines.push(` args: {`);
650
777
  if (!omitChildren) lines.push(` children: "${componentName}",`);
778
+ for (const line of defaultArgLines) lines.push(line);
651
779
  lines.push(` },`);
652
780
  lines.push(`};`);
653
781
  } else {
@@ -657,6 +785,7 @@ function buildStoryFileContent(comp) {
657
785
  lines.push(` args: {`);
658
786
  lines.push(` variant: "${defaultVariant}",`);
659
787
  if (!omitChildren) lines.push(` children: "${componentName}",`);
788
+ for (const line of defaultArgLines) lines.push(line);
660
789
  lines.push(` },`);
661
790
  lines.push(`};`);
662
791
  lines.push("");
@@ -667,6 +796,7 @@ function buildStoryFileContent(comp) {
667
796
  lines.push(` args: {`);
668
797
  lines.push(` variant: "${v}",`);
669
798
  if (!omitChildren) lines.push(` children: "${storyName}",`);
799
+ for (const line of defaultArgLines) lines.push(line);
670
800
  lines.push(` },`);
671
801
  lines.push(`};`);
672
802
  lines.push("");