vibe-design-system 2.5.11 → 2.5.13
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 +1 -14
- package/package.json +1 -1
- package/vds-core-template/story-generator.mjs +144 -14
package/bin/init.js
CHANGED
|
@@ -24,9 +24,6 @@ const TEMPLATE_DIR = path.join(INSTALLER_ROOT, "vds-core-template");
|
|
|
24
24
|
const STORYBOOK_MAIN_TS = `import type { StorybookConfig } from "@storybook/react-vite";
|
|
25
25
|
import { mergeConfig } from "vite";
|
|
26
26
|
import path from "path";
|
|
27
|
-
import { fileURLToPath } from "url";
|
|
28
|
-
|
|
29
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
27
|
|
|
31
28
|
const config: StorybookConfig = {
|
|
32
29
|
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
@@ -39,7 +36,7 @@ const config: StorybookConfig = {
|
|
|
39
36
|
return mergeConfig(config, {
|
|
40
37
|
resolve: {
|
|
41
38
|
alias: {
|
|
42
|
-
"@": path.resolve(
|
|
39
|
+
"@": path.resolve(process.cwd(), "src"),
|
|
43
40
|
},
|
|
44
41
|
},
|
|
45
42
|
});
|
|
@@ -50,8 +47,6 @@ export default config;
|
|
|
50
47
|
`;
|
|
51
48
|
|
|
52
49
|
const STORYBOOK_PREVIEW_TS = `import type { Preview } from "@storybook/react";
|
|
53
|
-
import React from "react";
|
|
54
|
-
import { MemoryRouter } from "react-router-dom";
|
|
55
50
|
import "../src/index.css";
|
|
56
51
|
|
|
57
52
|
const preview: Preview = {
|
|
@@ -71,14 +66,6 @@ const preview: Preview = {
|
|
|
71
66
|
},
|
|
72
67
|
},
|
|
73
68
|
},
|
|
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
69
|
};
|
|
83
70
|
|
|
84
71
|
export default preview;
|
package/package.json
CHANGED
|
@@ -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 (
|
|
498
|
-
lines.push(`import { ${
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
522
|
-
lines.push(`
|
|
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: ${
|
|
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
|
|
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 (
|
|
630
|
-
lines.push(`
|
|
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("");
|