vibe-design-system 2.5.10 → 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 = {
@@ -63,19 +61,14 @@ const preview: Preview = {
63
61
  "Foundations",
64
62
  ["Introduction", "Page to Components", "Colors", "Typography", "Brand", "Icons", "Component Suggestions", "Changelog"],
65
63
  "Layout",
66
- ["Navigation", "Footer", "ScrollToTop"],
67
64
  "Components",
68
65
  "Actions",
69
66
  "Data Display",
70
67
  "Examples",
71
- ["Pages"],
72
68
  ],
73
69
  },
74
70
  },
75
71
  },
76
- decorators: [
77
- (Story) => React.createElement(MemoryRouter, null, React.createElement(Story, null)),
78
- ],
79
72
  };
80
73
 
81
74
  export default preview;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.5.10",
3
+ "version": "2.5.12",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -774,6 +774,24 @@ function parseCssVarBlock(block) {
774
774
  return out;
775
775
  }
776
776
 
777
+ /** Return only theme.extend.colors from tailwind config (user-defined colors), not the default Tailwind palette. */
778
+ function getTailwindExtendColors() {
779
+ try {
780
+ const twPath = path.join(PROJECT_ROOT, "tailwind.config.js");
781
+ const twPathMjs = path.join(PROJECT_ROOT, "tailwind.config.mjs");
782
+ let config = null;
783
+ if (fs.existsSync(twPath)) {
784
+ config = projectRequire(twPath);
785
+ } else if (fs.existsSync(twPathMjs)) {
786
+ config = projectRequire(twPathMjs);
787
+ }
788
+ if (config && typeof config === "object" && config.default) config = config.default;
789
+ return config?.theme?.extend?.colors ?? null;
790
+ } catch (_) {
791
+ return null;
792
+ }
793
+ }
794
+
777
795
  /** Resolve Tailwind theme for boxShadow, spacing, screens, zIndex, motion. Uses resolveConfig when available. */
778
796
  function getTailwindTheme() {
779
797
  const empty = { shadows: {}, spacing: {}, breakpoints: {}, zIndex: {}, transitionDuration: {}, transitionTimingFunction: {}, animation: {}, colors: {} };
@@ -1041,9 +1059,9 @@ function extractFoundations() {
1041
1059
  const foundationsColors = { ...colors };
1042
1060
  if (Object.keys(colorsDark).length > 0) foundationsColors._dark = colorsDark;
1043
1061
 
1044
- const twTheme = getTailwindTheme();
1045
- if (twTheme.colors && typeof twTheme.colors === "object") {
1046
- for (const [key, val] of Object.entries(twTheme.colors)) {
1062
+ const extendColors = getTailwindExtendColors();
1063
+ if (extendColors && typeof extendColors === "object") {
1064
+ for (const [key, val] of Object.entries(extendColors)) {
1047
1065
  if (typeof val === "string" && (val.startsWith("#") || /^[a-z]/.test(val))) {
1048
1066
  foundationsColors[key] = { value: val, hex: val.startsWith("#") ? val : val };
1049
1067
  } else if (typeof val === "object" && val !== null && !Array.isArray(val)) {
@@ -1054,6 +1072,7 @@ function extractFoundations() {
1054
1072
  }
1055
1073
  }
1056
1074
 
1075
+ const twTheme = getTailwindTheme();
1057
1076
  const normalizeThemeObj = (obj) => {
1058
1077
  if (!obj || typeof obj !== "object") return {};
1059
1078
  const out = {};
@@ -350,6 +350,30 @@ function needsRouter(source) {
350
350
  return false;
351
351
  }
352
352
 
353
+ /** Detect if component provides its own router (BrowserRouter or RouterProvider). Preview should not wrap with MemoryRouter. */
354
+ function usesOwnRouter(source) {
355
+ if (!source || typeof source !== "string") return false;
356
+ return /\bBrowserRouter\b|\bRouterProvider\b/.test(source);
357
+ }
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
+
353
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. */
354
378
  function componentWrapsVoidElement(source) {
355
379
  if (!source || typeof source !== "string") return false;
@@ -375,6 +399,74 @@ function parseUnionLiterals(type) {
375
399
  return matches.map((s) => s.replace(/"/g, ""));
376
400
  }
377
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
+
378
470
  function capitalize(str) {
379
471
  if (!str) return "";
380
472
  return str.charAt(0).toUpperCase() + str.slice(1);
@@ -483,42 +575,63 @@ function buildSpecialStories(componentName, variants) {
483
575
  return "";
484
576
  }
485
577
 
486
- function buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, recipe) {
578
+ function buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, recipe, defaultArgLines = [], lucideImports = []) {
487
579
  const lines = [];
488
580
  lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
489
- const useRouterDecorator = needsRouter(source);
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
+
490
603
  if (exportStyle === "default") {
491
604
  lines.push(`import ${componentName} from "${importPath}";`);
492
- if (recipe.imports?.length) {
493
- lines.push(`import { ${recipe.imports.join(", ")} } from "${importPath}";`);
605
+ if (effectiveRecipe.imports?.length) {
606
+ lines.push(`import { ${effectiveRecipe.imports.join(", ")} } from "${importPath}";`);
494
607
  }
495
608
  lines.push(`const ComponentRef = ${componentName};`);
496
609
  } else if (exportStyle === "named") {
497
- const names = recipe.imports?.length ? [componentName, ...recipe.imports] : [componentName];
610
+ const names = effectiveRecipe.imports?.length ? [componentName, ...effectiveRecipe.imports] : [componentName];
498
611
  lines.push(`import { ${names.join(", ")} } from "${importPath}";`);
499
612
  lines.push(`const ComponentRef = ${componentName};`);
500
613
  } else {
501
614
  const defaultAlias = `${componentName}Default`;
502
615
  const namedAlias = `${componentName}Named`;
503
- const extra = recipe.imports?.length ? ", " + recipe.imports.join(", ") : "";
616
+ const extra = effectiveRecipe.imports?.length ? ", " + effectiveRecipe.imports.join(", ") : "";
504
617
  lines.push(`import ${defaultAlias}, { ${componentName} as ${namedAlias}${extra} } from "${importPath}";`);
505
618
  lines.push(`const ComponentRef = ${namedAlias} ?? ${defaultAlias};`);
506
619
  }
507
- for (const ext of recipe.extraImports || []) {
620
+ for (const ext of (effectiveRecipe.extraImports || recipe.extraImports) || []) {
508
621
  lines.push(`import { ${ext.names.join(", ")} } from "${ext.from}";`);
509
622
  }
510
- if (useRouterDecorator) lines.push(`import { MemoryRouter } from "react-router-dom";`);
623
+
624
+ const needsRouterDecorator = hasToOrHrefProp(comp, source);
625
+ if (needsRouterDecorator) {
626
+ lines.push(`import { MemoryRouter } from "react-router-dom";`);
627
+ }
511
628
  lines.push("");
512
629
  lines.push(`const meta = {`);
513
630
  lines.push(` title: ${JSON.stringify(title)},`);
514
631
  lines.push(` component: ComponentRef,`);
515
632
  lines.push(` tags: ["autodocs"],`);
516
- if (useRouterDecorator) {
517
- lines.push(` decorators: [(Story) => (`);
518
- lines.push(` <MemoryRouter>`);
519
- lines.push(` <Story />`);
520
- lines.push(` </MemoryRouter>`);
521
- lines.push(` )],`);
633
+ if (needsRouterDecorator) {
634
+ lines.push(` decorators: [(Story) => <MemoryRouter><Story /></MemoryRouter>],`);
522
635
  }
523
636
  lines.push(`} satisfies Meta<typeof ComponentRef>;`);
524
637
  lines.push("");
@@ -526,7 +639,12 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
526
639
  lines.push(`type Story = StoryObj<typeof meta>;`);
527
640
  lines.push("");
528
641
  lines.push(`export const Default: Story = {`);
529
- 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
+ }
530
648
  lines.push(`};`);
531
649
  return lines.join("\n");
532
650
  }
@@ -589,17 +707,24 @@ function buildStoryFileContent(comp) {
589
707
  const omitChildren = componentWrapsVoidElement(source);
590
708
  const isPage = comp.file.startsWith("pages/");
591
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
+
592
714
  // Skip story only if not a page and no export found
593
715
  if (exportStyle === "unknown" && !isPage && (!source.includes("export") || !new RegExp(`\\b${componentName}\\b`).test(source))) {
594
716
  return null;
595
717
  }
596
718
 
597
719
  if (RECIPES[componentName]) {
598
- return buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, RECIPES[componentName]);
720
+ return buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, RECIPES[componentName], defaultArgLines, lucideImports);
599
721
  }
600
722
 
601
723
  const lines = [];
602
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
+ }
603
728
 
604
729
  if (isPage && exportStyle !== "default") {
605
730
  lines.push(`import * as Named from "${importPath}";`);
@@ -619,22 +744,17 @@ function buildStoryFileContent(comp) {
619
744
  lines.push(`const ComponentRef = ${namedAlias} ?? ${defaultAlias};`);
620
745
  }
621
746
 
622
- const useRouterDecorator = needsRouter(source);
623
- if (useRouterDecorator) {
747
+ const needsRouterDecorator = hasToOrHrefProp(comp, source);
748
+ if (needsRouterDecorator) {
624
749
  lines.push(`import { MemoryRouter } from "react-router-dom";`);
625
750
  }
626
-
627
751
  lines.push("");
628
752
  lines.push(`const meta = {`);
629
753
  lines.push(` title: ${JSON.stringify(title)},`);
630
754
  lines.push(` component: ComponentRef,`);
631
755
  lines.push(` tags: ["autodocs"],`);
632
- if (useRouterDecorator) {
633
- lines.push(` decorators: [(Story) => (`);
634
- lines.push(` <MemoryRouter>`);
635
- lines.push(` <Story />`);
636
- lines.push(` </MemoryRouter>`);
637
- lines.push(` )],`);
756
+ if (needsRouterDecorator) {
757
+ lines.push(` decorators: [(Story) => <MemoryRouter><Story /></MemoryRouter>],`);
638
758
  }
639
759
  lines.push(`} satisfies Meta<typeof ComponentRef>;`);
640
760
  lines.push("");
@@ -655,6 +775,7 @@ function buildStoryFileContent(comp) {
655
775
  lines.push(` render: (args) => <ComponentRef {...args} />,`);
656
776
  lines.push(` args: {`);
657
777
  if (!omitChildren) lines.push(` children: "${componentName}",`);
778
+ for (const line of defaultArgLines) lines.push(line);
658
779
  lines.push(` },`);
659
780
  lines.push(`};`);
660
781
  } else {
@@ -664,6 +785,7 @@ function buildStoryFileContent(comp) {
664
785
  lines.push(` args: {`);
665
786
  lines.push(` variant: "${defaultVariant}",`);
666
787
  if (!omitChildren) lines.push(` children: "${componentName}",`);
788
+ for (const line of defaultArgLines) lines.push(line);
667
789
  lines.push(` },`);
668
790
  lines.push(`};`);
669
791
  lines.push("");
@@ -674,6 +796,7 @@ function buildStoryFileContent(comp) {
674
796
  lines.push(` args: {`);
675
797
  lines.push(` variant: "${v}",`);
676
798
  if (!omitChildren) lines.push(` children: "${storyName}",`);
799
+ for (const line of defaultArgLines) lines.push(line);
677
800
  lines.push(` },`);
678
801
  lines.push(`};`);
679
802
  lines.push("");