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 +0 -7
- package/package.json +1 -1
- package/vds-core-template/scan.mjs +22 -3
- package/vds-core-template/story-generator.mjs +148 -25
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
|
@@ -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
|
|
1045
|
-
if (
|
|
1046
|
-
for (const [key, val] of Object.entries(
|
|
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
|
-
|
|
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 (
|
|
493
|
-
lines.push(`import { ${
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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: ${
|
|
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
|
|
623
|
-
if (
|
|
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 (
|
|
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("");
|