vibe-design-system 2.8.23 → 2.8.25

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.23",
3
+ "version": "2.8.25",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -431,6 +431,75 @@ function extractCvaVariants(content) {
431
431
  return Object.keys(result).length > 0 ? result : null;
432
432
  }
433
433
 
434
+ // Common HTML/React props that are noise for the agent — skip them in cursor rules
435
+ const PROPS_SKIP = new Set([
436
+ "className","style","id","key","ref","children","asChild","slot",
437
+ "tabIndex","role","aria-label","aria-describedby","aria-hidden","aria-labelledby",
438
+ "data-testid","htmlFor","draggable","title","lang","dir","onClick","onChange",
439
+ "onSubmit","onBlur","onFocus","onKeyDown","onKeyUp","onMouseEnter","onMouseLeave",
440
+ ]);
441
+
442
+ /**
443
+ * Extract TypeScript prop types from a component file.
444
+ * Looks for interface *Props* { ... } or type *Props* = { ... }
445
+ * Returns Array<{ name, type, required }> or null.
446
+ */
447
+ function extractTSProps(content) {
448
+ // Find the start of a props interface or type alias
449
+ const startRe = /(?:interface|type)\s+\w*[Pp]rops\w*[^{]*\{/;
450
+ const sm = startRe.exec(content);
451
+ if (!sm) return null;
452
+
453
+ // Walk forward counting braces to find the matching closing }
454
+ const blockStart = sm.index + sm[0].length;
455
+ let depth = 1;
456
+ let i = blockStart;
457
+ while (i < content.length && depth > 0) {
458
+ if (content[i] === "{") depth++;
459
+ if (content[i] === "}") depth--;
460
+ i++;
461
+ }
462
+ const propsBlock = content.slice(blockStart, i - 1);
463
+
464
+ const props = [];
465
+ // Match top-level props: optional leading whitespace (1-6 chars), prop name, optional ?, colon, type
466
+ const propRe = /^[ \t]{1,6}([\w]+)(\?)?:\s*(.+?)[ \t]*;?[ \t]*$/gm;
467
+ let pm;
468
+ while ((pm = propRe.exec(propsBlock)) !== null) {
469
+ const propName = pm[1].trim();
470
+ if (PROPS_SKIP.has(propName)) continue;
471
+ // Skip comment lines that accidentally match (e.g. "// foo:")
472
+ if (propsBlock.slice(pm.index).match(/^[ \t]*\/\//)) continue;
473
+
474
+ const optional = pm[2] === "?";
475
+ let type = pm[3]
476
+ .replace(/\s*\/\/.*$/, "") // strip inline // comments (with any space before //)
477
+ .trim() // remove surrounding whitespace
478
+ .replace(/;$/, "") // strip trailing semicolon (now truly at the end)
479
+ .trim();
480
+
481
+ // Skip multi-line types (unbalanced braces — the rest is on the next line)
482
+ const openBraces = (type.match(/\{/g) || []).length;
483
+ const closeBraces = (type.match(/\}/g) || []).length;
484
+ if (openBraces !== closeBraces) continue;
485
+
486
+ // Simplify verbose React type names
487
+ type = type.replace(/React\.ReactNode/g, "ReactNode");
488
+ type = type.replace(/React\.FC[^;,\n]*/g, "FC");
489
+ type = type.replace(/React\.CSSProperties/g, "CSSProperties");
490
+ type = type.replace(/React\.RefObject<[^>]+>/g, "RefObject");
491
+ type = type.replace(/React\.MouseEvent[^;,\n]*/g, "MouseEvent");
492
+ type = type.replace(/React\.ChangeEvent[^;,\n]*/g, "ChangeEvent");
493
+ type = type.replace(/React\.KeyboardEvent[^;,\n]*/g, "KeyboardEvent");
494
+
495
+ // Truncate very long types
496
+ if (type.length > 50) type = type.slice(0, 47) + "...";
497
+
498
+ props.push({ name: propName, type, required: !optional });
499
+ }
500
+ return props.length > 0 ? props : null;
501
+ }
502
+
434
503
  function getAllComponentFiles(dir, baseDir = dir) {
435
504
  const entries = fs.readdirSync(dir, { withFileTypes: true });
436
505
  const files = [];
@@ -2209,9 +2278,10 @@ function scan() {
2209
2278
  const tokens = extractTailwindTokens(content);
2210
2279
  const tier = inferTier(rel, content);
2211
2280
  const variants = (tier === "primitive") ? extractCvaVariants(content) : null;
2281
+ const props = (tier === "component" || tier === "feature") ? extractTSProps(content) : null;
2212
2282
  const lineCount = content.split("\n").length;
2213
2283
  const localImportCount = (content.match(/from\s+['"]\.\.\?\//g) || []).length;
2214
- results.push({ file: rel, name, group, category, description, tokens, tier, lines: lineCount, localImports: localImportCount, ...(variants ? { variants } : {}) });
2284
+ results.push({ file: rel, name, group, category, description, tokens, tier, lines: lineCount, localImports: localImportCount, ...(variants ? { variants } : {}), ...(props ? { props } : {}) });
2215
2285
  }
2216
2286
  if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
2217
2287
  const pageFiles = getAllComponentFiles(PAGES_DIR);
@@ -742,6 +742,12 @@ function buildDefaultArgsForRequiredProps(props, usageFromPages = null, componen
742
742
  continue;
743
743
  }
744
744
  if (!required) continue;
745
+ // Function/callback types must be caught before name-based type checks (e.g. "Task" in "(task: Task) => void")
746
+ if (isFunctionType(type)) {
747
+ argLines.push(` ${name}: () => {},`);
748
+ added.add(name);
749
+ continue;
750
+ }
745
751
  if (/LucideIcon|lucide-react/.test(type)) {
746
752
  const iconName = LUCIDE_ICON_DEFAULT;
747
753
  lucideImports.add(iconName);
@@ -1071,6 +1077,52 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
1071
1077
  return lines.join("\n");
1072
1078
  }
1073
1079
 
1080
+ /**
1081
+ * Map a single extracted TypeScript prop to a Storybook argType entry string.
1082
+ * Returns something like: `{ control: "text", description: "string" }`
1083
+ */
1084
+ function buildArgTypeEntry(prop) {
1085
+ const t = (prop.type || "").trim();
1086
+ const name = prop.name;
1087
+
1088
+ // Function / callback — disable control widget, show as action
1089
+ if (/=>\s*(void|any|never|Promise)/.test(t) || /^\([^)]*\)\s*=>/.test(t)) {
1090
+ const safe = t.replace(/"/g, '\\"').slice(0, 60);
1091
+ return `{ action: "${name}", control: false, description: "${safe}" }`;
1092
+ }
1093
+ // Boolean
1094
+ if (/^boolean/.test(t)) {
1095
+ return `{ control: "boolean", description: "boolean" }`;
1096
+ }
1097
+ // Number
1098
+ if (/^number/.test(t)) {
1099
+ return `{ control: "number", description: "number" }`;
1100
+ }
1101
+ // String union literals: "a" | "b" | "c" → select control
1102
+ if (/^"[^"]+"(\s*\|\s*"[^"]+")+/.test(t)) {
1103
+ const opts = (t.match(/"([^"]+)"/g) || []).map(s => s.replace(/"/g, ""));
1104
+ return `{ control: "select", options: ${JSON.stringify(opts)}, description: ${JSON.stringify(t)} }`;
1105
+ }
1106
+ // Plain string (including union with null/undefined)
1107
+ if (/^string/.test(t) || t === "string | null" || t === "string | undefined" || t === "string | null | undefined") {
1108
+ return `{ control: "text", description: "string" }`;
1109
+ }
1110
+ // ReactNode
1111
+ if (/ReactNode/.test(t)) {
1112
+ return `{ control: "text", description: "ReactNode" }`;
1113
+ }
1114
+ // Arrays → object control
1115
+ if (t.endsWith("[]") || /^Array</.test(t)) {
1116
+ return `{ control: "object", description: ${JSON.stringify(t.slice(0, 60))} }`;
1117
+ }
1118
+ // Inline object / any
1119
+ if (t === "any" || t.startsWith("{")) {
1120
+ return `{ control: "object", description: ${JSON.stringify(t.slice(0, 60))} }`;
1121
+ }
1122
+ // Imported / custom named types (Circle, Task, etc.) — object control so user can inspect
1123
+ return `{ control: "object", description: ${JSON.stringify(t.slice(0, 60))} }`;
1124
+ }
1125
+
1074
1126
  function buildStoryFileContent(comp) {
1075
1127
  const componentName = toSafeComponentName(comp.name, comp.file);
1076
1128
  const fileNoExt = comp.file.replace(/\.(tsx|jsx)$/, "");
@@ -1173,13 +1225,27 @@ function buildStoryFileContent(comp) {
1173
1225
  lines.push(` title: ${JSON.stringify(title)},`);
1174
1226
  lines.push(` component: ComponentRef,`);
1175
1227
  lines.push(` tags: ["autodocs"],`);
1228
+
1229
+ // Build argTypes from extracted TypeScript props + icon-specific overrides
1230
+ const argTypeEntries = [];
1231
+ for (const p of effectiveProps) {
1232
+ if (!p || !p.name) continue;
1233
+ if (p.name === "children") continue; // Storybook infers children from JSX
1234
+ if (iconPropNames && iconPropNames.includes(p.name)) continue; // handled below
1235
+ const entry = buildArgTypeEntry(p);
1236
+ argTypeEntries.push(` ${p.name}: ${entry},`);
1237
+ }
1176
1238
  if (iconPropNames && iconPropNames.length > 0) {
1177
- lines.push(` argTypes: {`);
1178
1239
  for (const name of iconPropNames) {
1179
- lines.push(` ${name}: { control: false },`);
1240
+ argTypeEntries.push(` ${name}: { control: false },`);
1180
1241
  }
1242
+ }
1243
+ if (argTypeEntries.length > 0) {
1244
+ lines.push(` argTypes: {`);
1245
+ for (const entry of argTypeEntries) lines.push(entry);
1181
1246
  lines.push(` },`);
1182
1247
  }
1248
+
1183
1249
  lines.push(`} satisfies Meta<typeof ComponentRef>;`);
1184
1250
  lines.push("");
1185
1251
  lines.push(`export default meta;`);
@@ -2569,6 +2635,20 @@ function writeCursorRules(components, foundations) {
2569
2635
  lines.push(``);
2570
2636
  }
2571
2637
 
2638
+ // Format a compact props summary for cursor rules (required first, up to 6 props)
2639
+ const formatProps = (props) => {
2640
+ if (!Array.isArray(props) || props.length === 0) return null;
2641
+ const sorted = [...props].sort((a, b) => {
2642
+ if (a.required && !b.required) return -1;
2643
+ if (!a.required && b.required) return 1;
2644
+ return a.name.localeCompare(b.name);
2645
+ });
2646
+ const shown = sorted.slice(0, 6);
2647
+ const parts = shown.map(p => p.required ? `${p.name} (${p.type}, required)` : `${p.name} (${p.type})`);
2648
+ const suffix = props.length > 6 ? ` *(+${props.length - 6} more)*` : "";
2649
+ return ` props: ${parts.join(" · ")}${suffix}`;
2650
+ };
2651
+
2572
2652
  // Tier 2 — Components
2573
2653
  if (byTier.component.length > 0) {
2574
2654
  lines.push(`## 🔷 Components — Reusable domain components`);
@@ -2578,6 +2658,8 @@ function writeCursorRules(components, foundations) {
2578
2658
  let entry = `- **${c.name}** \`${c.file}\``;
2579
2659
  if (c.description) entry += ` — ${c.description}`;
2580
2660
  lines.push(entry);
2661
+ const propsLine = formatProps(c.props);
2662
+ if (propsLine) lines.push(propsLine);
2581
2663
  }
2582
2664
  lines.push(``);
2583
2665
  lines.push(`---`);
@@ -2591,6 +2673,8 @@ function writeCursorRules(components, foundations) {
2591
2673
  lines.push(``);
2592
2674
  for (const c of byTier.feature.sort((a, b) => a.name.localeCompare(b.name))) {
2593
2675
  lines.push(`- **${c.name}** \`${c.file}\``);
2676
+ const propsLine = formatProps(c.props);
2677
+ if (propsLine) lines.push(propsLine);
2594
2678
  }
2595
2679
  lines.push(``);
2596
2680
  lines.push(`---`);