vibe-design-system 2.8.2 → 2.8.4

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.2",
3
+ "version": "2.8.4",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,4 +38,4 @@
38
38
  "typescript": "^5.8.3",
39
39
  "vite": "^5.4.19"
40
40
  }
41
- }
41
+ }
@@ -951,6 +951,17 @@ function writeVdsGeneratedComponents(suggestions) {
951
951
 
952
952
  let bodyJsx;
953
953
  const tokens = [];
954
+ // Known shadcn/UI component names — not Lucide icons
955
+ const KNOWN_UI_COMPONENTS = new Set([
956
+ "Button", "Card", "CardHeader", "CardTitle", "CardContent", "CardFooter",
957
+ "Input", "Label", "Textarea", "Select", "Checkbox", "Switch", "Badge",
958
+ "Avatar", "Dialog", "Sheet", "Drawer", "Tooltip", "Popover", "Tabs",
959
+ "Table", "Accordion", "Alert", "Toast", "Form", "Skeleton", "Separator",
960
+ "Progress", "Slider", "RadioGroup", "Toggle", "Command", "Calendar",
961
+ "DropdownMenu", "ContextMenu", "NavigationMenu", "Menubar", "HoverCard",
962
+ "Collapsible", "ScrollArea", "AspectRatio", "Carousel", "Resizable",
963
+ "Link", "Nav", "Header", "Footer", "Section", "Container", "Wrapper",
964
+ ]);
954
965
  if (s.fullJsx && s.fullJsx.trim().length > 0) {
955
966
  bodyJsx = flattenJsx(s.fullJsx);
956
967
  bodyJsx = convertHtmlStyleToReact(bodyJsx);
@@ -958,17 +969,28 @@ function writeVdsGeneratedComponents(suggestions) {
958
969
  const lucideTags = [];
959
970
  const uiTags = [];
960
971
  for (const tag of componentTags) {
961
- if (/^[A-Z][a-z0-9]+$/.test(tag) && tag.length > 2) lucideTags.push(tag);
962
- else uiTags.push(tag);
972
+ if (KNOWN_UI_COMPONENTS.has(tag)) {
973
+ uiTags.push(tag);
974
+ } else if (/^[A-Z][a-zA-Z0-9]+$/.test(tag) && tag.length > 2) {
975
+ lucideTags.push(tag); // likely a Lucide icon or unknown component
976
+ } else {
977
+ uiTags.push(tag);
978
+ }
979
+ }
980
+ if (lucideTags.length > 0) {
981
+ bodyJsx = applyLucideNamespace(bodyJsx, lucideTags);
982
+ }
983
+ const importLines = ['import React from "react";'];
984
+ if (bodyJsx.includes("motion.") || bodyJsx.includes("<motion")) {
985
+ importLines.push('import { motion } from "framer-motion";');
986
+ }
987
+ if (lucideTags.length > 0) {
988
+ importLines.push('import * as LucideIcons from "lucide-react";');
989
+ }
990
+ for (const tag of uiTags) {
991
+ if (!KNOWN_UI_COMPONENTS.has(tag)) continue;
992
+ importLines.push(`import { ${tag} } from "@/components/ui/${toKebab(tag)}";`);
963
993
  }
964
- bodyJsx = applyLucideNamespace(bodyJsx, lucideTags);
965
- const standardImports = [
966
- 'import React from "react";',
967
- 'import { motion } from "framer-motion";',
968
- 'import * as LucideIcons from "lucide-react";',
969
- ];
970
- const uiImportLines = uiTags.map((tag) => `import { ${tag} } from "@/components/ui/${toKebab(tag)}";`);
971
- const importLines = [...standardImports, ...uiImportLines];
972
994
  const content = importLines.join("\n") + "\n\n" +
973
995
  `export function ${componentName}() {\n return (\n ` +
974
996
  bodyJsx.replace(/\n/g, "\n ") + "\n );\n}\n";
@@ -978,16 +1000,7 @@ function writeVdsGeneratedComponents(suggestions) {
978
1000
  const tagName = s.tagName || "div";
979
1001
  const className = s.pattern || "";
980
1002
  bodyJsx = `<${tagName} className="${className.replace(/"/g, '\\"')}">\n {null}\n </${tagName}>`;
981
- const content = `import React from "react";
982
- import { motion } from "framer-motion";
983
- import * as LucideIcons from "lucide-react";
984
-
985
- export function ${componentName}() {
986
- return (
987
- ${bodyJsx}
988
- );
989
- }
990
- `;
1003
+ const content = `import React from "react";\n\nexport function ${componentName}() {\n return (\n ${bodyJsx}\n );\n}\n`;
991
1004
  fs.writeFileSync(fullPath, content, "utf-8");
992
1005
  tokens.push(...(className || "").split(/\s+/).filter(Boolean));
993
1006
  }
@@ -995,8 +1008,9 @@ export function ${componentName}() {
995
1008
  entries.push({
996
1009
  file: relPath,
997
1010
  name: componentName.replace(/([A-Z])/g, " $1").trim(),
998
- group: "VDS Generated",
999
- category: "Extracted",
1011
+ exportName: componentName,
1012
+ group: "Generated",
1013
+ category: "Generated",
1000
1014
  description: "Auto-extracted from repeated className patterns.",
1001
1015
  tokens: [...new Set(tokens)],
1002
1016
  occurrences: s.occurrences ?? 0,
@@ -1635,6 +1649,7 @@ function scan() {
1635
1649
  engineer: getGitEngineer(),
1636
1650
  scannedAt: new Date().toISOString(),
1637
1651
  totalComponents: results.length,
1652
+ componentsRelDir: COMPONENTS_DIR ? path.relative(PROJECT_ROOT, COMPONENTS_DIR).replace(/\\/g, "/") : "src/components",
1638
1653
  components: results,
1639
1654
  foundations,
1640
1655
  componentSuggestions,
@@ -294,6 +294,29 @@ function buildWithProvidersCode(providers) {
294
294
  return `const withProviders = (Story: any) => ${inner};`;
295
295
  }
296
296
 
297
+ /** ErrorBoundary class – injected into .storybook/preview to catch story render errors (missing providers, undefined props, etc.) */
298
+ function buildErrorBoundaryCode() {
299
+ return [
300
+ "class VdsErrorBoundary extends React.Component {",
301
+ " constructor(props) { super(props); this.state = { error: null }; }",
302
+ " static getDerivedStateFromError(error) { return { error }; }",
303
+ " componentDidCatch(err) { console.error(\"[VDS Story Error]\", err); }",
304
+ " render() {",
305
+ " const s = this.state; const p = this.props;",
306
+ " if (s && s.error) return React.createElement(\"div\", {",
307
+ " style: { padding: 16, color: \"#ff6b6b\", border: \"1px solid #ff6b6b\", borderRadius: 8, fontFamily: \"monospace\", margin: 8 }",
308
+ " }, React.createElement(\"b\", null, \"⚠️ Story Error: \"), React.createElement(\"code\", { style: { whiteSpace: \"pre-wrap\" } }, String(s.error.message)));",
309
+ " return p.children;",
310
+ " }",
311
+ "}",
312
+ "const withErrorBoundary = (Story) => React.createElement(VdsErrorBoundary, null, React.createElement(Story));",
313
+ ].join("\n");
314
+ }
315
+
316
+ // VDS markers for idempotent preview injection
317
+ const VDS_BLOCK_START = "// [VDS:providers-start]";
318
+ const VDS_BLOCK_END = "// [VDS:providers-end]";
319
+
297
320
  function injectProviderDecorators(projectRoot) {
298
321
  const previewPath = getPreviewPath(projectRoot);
299
322
  if (!previewPath) {
@@ -302,44 +325,83 @@ function injectProviderDecorators(projectRoot) {
302
325
  }
303
326
 
304
327
  const { providersToAdd, hooksWithoutProvider, hooksByFile } = collectProvidersAndWarnings(projectRoot);
305
- if (providersToAdd.length === 0 && hooksWithoutProvider.size === 0) return;
306
-
307
- if (providersToAdd.length > 0) {
308
- let content = fs.readFileSync(previewPath, "utf-8");
309
- const uniqueProviders = [...new Map(providersToAdd.map((p) => [p.name, p])).values()];
310
- if (!content.includes("import React") && !content.includes("import React from")) {
311
- const firstImport = content.match(/^import\s+/m);
312
- const insertAt = firstImport ? content.indexOf(firstImport[0]) : 0;
313
- content = content.slice(0, insertAt) + "import React from \"react\";\n" + content.slice(insertAt);
314
- }
315
- for (const p of uniqueProviders) {
316
- if (!content.includes(p.name)) {
317
- const lastImportIdx = content.search(/\nimport\s+.+?;\s*$/m);
318
- const insertAt = lastImportIdx >= 0 ? content.indexOf("\n", lastImportIdx) + 1 : content.indexOf("\n") + 1;
319
- let importLine = `import { ${p.name} } from "${p.importPath}";\n`;
320
- if (p.extraImport && !content.includes(p.extraImport.name)) {
321
- importLine += `import { ${p.extraImport.name} } from "${p.extraImport.from}";\n`;
322
- }
323
- content = content.slice(0, insertAt) + importLine + content.slice(insertAt);
328
+ let content = fs.readFileSync(previewPath, "utf-8");
329
+
330
+ // 1. Ensure React import exists (needed for error boundary + providers)
331
+ if (!content.includes("import React") && !content.includes("import React from")) {
332
+ const firstImport = content.match(/^import\s+/m);
333
+ const insertAt = firstImport ? content.indexOf(firstImport[0]) : 0;
334
+ content = content.slice(0, insertAt) + "import React from \"react\";\n" + content.slice(insertAt);
335
+ }
336
+
337
+ // 2. Add provider imports if not already present (use regex to detect existing import)
338
+ const uniqueProviders = [...new Map(providersToAdd.map((p) => [p.name, p])).values()];
339
+ for (const p of uniqueProviders) {
340
+ const alreadyImported = new RegExp(`\\bimport[^;]*\\b${p.name}\\b[^;]*from`).test(content);
341
+ if (!alreadyImported) {
342
+ const importMatches = [...content.matchAll(/^import\s+[^\n]+;/gm)];
343
+ const lastMatch = importMatches[importMatches.length - 1];
344
+ const insertAt = lastMatch ? lastMatch.index + lastMatch[0].length + 1 : content.indexOf("\n") + 1;
345
+ let importLine = `import { ${p.name} } from "${p.importPath}";\n`;
346
+ if (p.extraImport && !new RegExp(`\\b${p.extraImport.name}\\b`).test(content)) {
347
+ importLine += `import { ${p.extraImport.name} } from "${p.extraImport.from}";\n`;
324
348
  }
349
+ content = content.slice(0, insertAt) + importLine + content.slice(insertAt);
325
350
  }
326
- content = content.replace(/const withProviders\s*=\s*\([^)]*\)\s*=>\s*[\s\S]+?;\s*\n?/g, "").replace(/\n{3,}/g, "\n\n");
327
- const withProvidersCode = buildWithProvidersCode(providersToAdd);
328
- const insertBefore = content.indexOf("const preview");
329
- if (insertBefore !== -1) {
330
- content = content.slice(0, insertBefore) + withProvidersCode + "\n\n" + content.slice(insertBefore);
331
- if (content.includes("decorators:")) {
332
- if (!content.includes("decorators: [withProviders")) {
333
- content = content.replace(/decorators:\s*\[/, "decorators: [withProviders, ");
334
- }
335
- } else {
336
- content = content.replace(/(const preview\s*[^=]*=\s*\{\s*)/, "$1\n decorators: [withProviders],\n ");
351
+ }
352
+
353
+ // 3. Remove previous VDS injections (idempotent re-runs)
354
+ content = content.replace(new RegExp(VDS_BLOCK_START.replace(/[\[\]]/g, "\\$&") + "[\\s\\S]*?" + VDS_BLOCK_END.replace(/[\[\]]/g, "\\$&") + "\\n?"), "");
355
+ // Remove old-style single-line withProviders (pre-marker era)
356
+ content = content.replace(/^const withProviders\s*=\s*\([^)]*\)\s*=>.*;\n?/m, "");
357
+ // Remove VDS decorator names from decorators arrays so we can re-inject cleanly
358
+ content = content.replace(/\bwithErrorBoundary\s*,\s*/g, "").replace(/,\s*\bwithErrorBoundary\b/g, "").replace(/\bwithErrorBoundary\b/g, "");
359
+ content = content.replace(/\bwithProviders\s*,\s*/g, "").replace(/,\s*\bwithProviders\b/g, "").replace(/\bwithProviders\b/g, "");
360
+ // Fix any syntax damage: leading comma, trailing comma, empty decorators
361
+ content = content.replace(/\[\s*,\s*/g, "[").replace(/,\s*\]/g, "]");
362
+ content = content.replace(/\s*decorators:\s*\[\s*\]\s*,?\n?/g, "");
363
+ content = content.replace(/\n{3,}/g, "\n\n");
364
+
365
+ // 4. Build new VDS block (error boundary + optional provider wrapper)
366
+ const errorBoundaryCode = buildErrorBoundaryCode();
367
+ const withProvidersCode = uniqueProviders.length > 0 ? buildWithProvidersCode(providersToAdd) : null;
368
+ const vdsCode = [errorBoundaryCode, withProvidersCode].filter(Boolean).join("\n\n");
369
+ const vdsBlock = VDS_BLOCK_START + "\n" + vdsCode + "\n" + VDS_BLOCK_END;
370
+
371
+ // 5. Find insertion point: before "const preview" or "export default {"
372
+ let insertBefore = content.indexOf("const preview");
373
+ if (insertBefore === -1) {
374
+ const edMatch = content.match(/export\s+default\s*[\{(]/);
375
+ if (edMatch) insertBefore = content.indexOf(edMatch[0]);
376
+ }
377
+ if (insertBefore === -1) insertBefore = content.length;
378
+ content = content.slice(0, insertBefore) + vdsBlock + "\n\n" + content.slice(insertBefore);
379
+
380
+ // 6. Inject decorators into preview object
381
+ const decoratorItems = ["withErrorBoundary", ...(withProvidersCode ? ["withProviders"] : [])];
382
+ const decoratorsStr = decoratorItems.join(", ");
383
+ if (content.includes("decorators:")) {
384
+ // Prepend VDS decorators to existing decorators array
385
+ content = content.replace(/\bdecorators:\s*\[/, `decorators: [${decoratorsStr}, `);
386
+ content = content.replace(/,\s*\]/g, "]");
387
+ } else {
388
+ // Add new decorators key to preview object
389
+ const constPreviewMatch = content.match(/(const\s+preview\s*[^=\n]*=\s*\{)/);
390
+ if (constPreviewMatch) {
391
+ content = content.replace(constPreviewMatch[0], constPreviewMatch[0] + `\n decorators: [${decoratorsStr}],`);
392
+ } else {
393
+ const exportDefaultMatch = content.match(/(export\s+default\s*\{)/);
394
+ if (exportDefaultMatch) {
395
+ content = content.replace(exportDefaultMatch[0], exportDefaultMatch[0] + `\n decorators: [${decoratorsStr}],`);
337
396
  }
338
397
  }
339
- fs.writeFileSync(previewPath, content, "utf-8");
340
- console.log("[VDS] Storybook preview: " + uniqueProviders.map((p) => p.name).join(", "));
341
398
  }
342
399
 
400
+ content = content.replace(/\n{3,}/g, "\n\n");
401
+ fs.writeFileSync(previewPath, content, "utf-8");
402
+ console.log("[VDS] Storybook preview: ErrorBoundary" + (uniqueProviders.length ? " + " + uniqueProviders.map((p) => p.name).join(", ") : ""));
403
+
404
+ // 7. Handle hooks without provider: add warning comment to affected story files
343
405
  if (hooksWithoutProvider.size > 0) {
344
406
  const componentNameFromPath = (rel) => path.basename(rel, path.extname(rel));
345
407
  const storiesDir = path.join(projectRoot, "src", "stories");
@@ -22,6 +22,8 @@ const STORIES_DIR = path.join(SRC_DIR, "stories");
22
22
 
23
23
  // Filled in main() after reading vds-output.json so helpers (buildSpecialStories) can see foundations data (e.g. buttonUsage).
24
24
  let FOUNDATIONS_DATA = null;
25
+ // componentsRelDir from vds-output.json — defaults to "src/components"
26
+ let COMPONENTS_REL_DIR = "src/components";
25
27
 
26
28
  // CSS is loaded from .storybook/preview.tsx — never add CSS import to story files.
27
29
 
@@ -521,7 +523,7 @@ const COMPONENT_EXTRA_ARGS = {
521
523
  " taskId: \"1\",",
522
524
  ],
523
525
  TimeFilterPanel: [
524
- " filter: { dateRange: {}, userIds: [], tags: [], billable: \"all\" },",
526
+ " filter: { preset: null, dateRange: {}, userIds: [], tags: [], billable: \"all\" },",
525
527
  " onFilterChange: () => {},",
526
528
  " onSaveFilter: () => {},",
527
529
  " onDeleteFilter: () => {},",
@@ -546,7 +548,7 @@ const RENDER_ARGS_FALLBACKS = {
546
548
  /** Kalıcı çözüm: Storybook Docs/Controls args'ı geçmezse bile component'a güvenli props veren wrapper. Önce safeDefaults uygulanır, sonra gelen props. */
547
549
  const SAFE_WRAPPER_DEFAULTS = {
548
550
  TaskEstimateInput: `{ estimate: 0, onUpdate: () => {}, value: 0, task: { id: "1", title: "Example", estimate: 0 }, compact: false }`,
549
- TimeFilterPanel: `{ filter: { dateRange: {}, userIds: [], tags: [], billable: "all" }, onFilterChange: () => {}, onSaveFilter: () => {}, onDeleteFilter: () => {}, filteredCount: 0, savedFilters: [] }`,
551
+ TimeFilterPanel: `{ filter: { preset: null, dateRange: {}, userIds: [], tags: [], billable: "all" }, onFilterChange: () => {}, onSaveFilter: () => {}, onDeleteFilter: () => {}, filteredCount: 0, savedFilters: [] }`,
550
552
  TimeStats: `{ totalLogged: 0, totalBillable: 0, totalNonBillable: 0, totalBilled: 0, totalEstimated: 0, projectTotalEstimated: 0 }`,
551
553
  };
552
554
 
@@ -1052,12 +1054,18 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
1052
1054
  function buildStoryFileContent(comp) {
1053
1055
  const componentName = toSafeComponentName(comp.name, comp.file);
1054
1056
  const fileNoExt = comp.file.replace(/\.(tsx|jsx)$/, "");
1057
+ // Compute import path relative to src/stories/ using the actual components directory.
1055
1058
  let importPath = "";
1056
- if (fileNoExt.startsWith("pages/")) {
1057
- importPath = "../" + fileNoExt; // Sonuç: "../pages/Home" olacak
1059
+ const isPageFile = fileNoExt.startsWith("pages/") || fileNoExt.startsWith("src/pages/");
1060
+ if (isPageFile) {
1061
+ const normalized = fileNoExt.replace(/^src\//, "");
1062
+ importPath = path.posix.relative("src/stories", normalized);
1058
1063
  } else {
1059
- importPath = "../components/" + fileNoExt; // Sonuç: "../components/ui/button" olacak
1064
+ const targetPath = path.posix.join(COMPONENTS_REL_DIR, fileNoExt);
1065
+ importPath = path.posix.relative("src/stories", targetPath);
1060
1066
  }
1067
+ if (!importPath.startsWith(".")) importPath = "./" + importPath;
1068
+
1061
1069
  const group = comp.group || "Components";
1062
1070
  const category = comp.category || null;
1063
1071
  const titleParts = [group, category, componentName].filter(Boolean);
@@ -1067,9 +1075,9 @@ function buildStoryFileContent(comp) {
1067
1075
  const variantProp = props.find((p) => p.name === "variant");
1068
1076
  let variants = parseUnionLiterals(variantProp && variantProp.type);
1069
1077
 
1070
- const srcPath = comp.file.startsWith("pages/")
1071
- ? path.join(SRC_DIR, comp.file)
1072
- : path.join(SRC_DIR, "components", comp.file);
1078
+ const srcPath = isPageFile
1079
+ ? path.join(PROJECT_ROOT, fileNoExt.replace(/^src\//, "src/") + ".tsx")
1080
+ : path.join(PROJECT_ROOT, COMPONENTS_REL_DIR, comp.file);
1073
1081
 
1074
1082
  // Fallback: if manifest doesn't have variant metadata yet, parse cva() directly from component file.
1075
1083
  if (!variants.length) {
@@ -1105,7 +1113,7 @@ function buildStoryFileContent(comp) {
1105
1113
  }
1106
1114
  const exportStyle = detectExportStyle(source, componentName);
1107
1115
  const omitChildren = componentWrapsVoidElement(source);
1108
- const isPage = comp.file.startsWith("pages/");
1116
+ const isPage = isPageFile;
1109
1117
 
1110
1118
  // Props: manifest or parse from source for default args (LucideIcon, array types, ReactNode, contextual placeholders)
1111
1119
  const effectiveProps = Array.isArray(comp.props) && comp.props.length > 0 ? comp.props : parsePropsFromSource(source);
@@ -1499,6 +1507,7 @@ function main() {
1499
1507
  }
1500
1508
  const raw = fs.readFileSync(VDS_OUTPUT, "utf-8");
1501
1509
  const data = JSON.parse(raw);
1510
+ COMPONENTS_REL_DIR = data.componentsRelDir || "src/components";
1502
1511
  const components = Array.isArray(data.components) ? data.components : [];
1503
1512
  const foundations = data.foundations || null;
1504
1513
  FOUNDATIONS_DATA = foundations;