vibe-design-system 2.8.28 → 2.8.30

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.28",
3
+ "version": "2.8.30",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -525,6 +525,93 @@ function extractTSProps(content) {
525
525
  return props.length > 0 ? props : null;
526
526
  }
527
527
 
528
+ /**
529
+ * Scan the entire src/ directory for non-Props TypeScript interface and type alias
530
+ * declarations and build a type registry used by story-generator for realistic mock defaults.
531
+ *
532
+ * Returns { TypeName: Array<{ name, type, required }> } or null.
533
+ * Only includes types with ≥2 fields; skips Props/State/Context/Config/Options/Ref types.
534
+ */
535
+ function extractProjectTypes() {
536
+ const registry = {};
537
+ const seen = new Set();
538
+
539
+ function scanDir(dir) {
540
+ if (!fs.existsSync(dir)) return;
541
+ let entries;
542
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
543
+ for (const e of entries) {
544
+ if (e.isDirectory()) {
545
+ if (IGNORE_DIRS.includes(e.name) || e.name === "__tests__") continue;
546
+ scanDir(path.join(dir, e.name));
547
+ } else if (/\.(ts|tsx)$/.test(e.name) &&
548
+ !e.name.endsWith(".stories.tsx") &&
549
+ !e.name.endsWith(".test.ts") &&
550
+ !e.name.endsWith(".spec.ts")) {
551
+ const fullPath = path.join(dir, e.name);
552
+ let content;
553
+ try { content = fs.readFileSync(fullPath, "utf-8"); } catch { continue; }
554
+
555
+ // Find all export interface / type declarations whose names start with uppercase
556
+ const declRe = /(?:export\s+)?(?:interface|type)\s+([A-Z][A-Za-z0-9_]*)(?:<[^>]*>)?(?:\s+extends\s+[^{]+)?\s*(?:=\s*(?![\|&]))?(?=\s*\{)/g;
557
+ let dm;
558
+ while ((dm = declRe.exec(content)) !== null) {
559
+ const typeName = dm[1];
560
+ // Skip non-data types
561
+ if (/Props$|State$|Context$|Config$|Options$|Params$|Ref$|Handle$|Return$|Result$|Theme$/.test(typeName)) continue;
562
+ if (/^(FC|React|JSX|HTML|Event|Mouse|Key|Touch|Change|Focus|Blur|Scroll|Input|Submit|SVG)/.test(typeName)) continue;
563
+ if (seen.has(typeName)) continue; // first declaration wins
564
+
565
+ // Locate the opening brace of the body
566
+ const braceIdx = content.indexOf("{", dm.index + dm[0].length - 1);
567
+ if (braceIdx === -1) continue;
568
+
569
+ // Walk forward counting braces to find matching closing brace
570
+ let depth = 1, i = braceIdx + 1;
571
+ while (i < content.length && depth > 0) {
572
+ if (content[i] === "{") depth++;
573
+ if (content[i] === "}") depth--;
574
+ i++;
575
+ }
576
+ const block = content.slice(braceIdx + 1, i - 1);
577
+
578
+ // Parse fields — for data types we only skip React/HTML-specific attrs, NOT id/title/name etc.
579
+ const fields = [];
580
+ const fieldRe = /^[ \t]{1,6}(?:readonly\s+)?(\w+)(\?)?:\s*(.+?)[ \t]*;?[ \t]*$/gm;
581
+ let fm;
582
+ while ((fm = fieldRe.exec(block)) !== null) {
583
+ const fieldName = fm[1].trim();
584
+ // Only skip HTML/React presentation attrs — keep data fields like id, title, name, status
585
+ if (/^(className|style|ref|key|htmlFor|tabIndex|role|draggable|aria-|data-)/.test(fieldName)) continue;
586
+ if (block.slice(fm.index).match(/^[ \t]*\/\//)) continue;
587
+ const optional = fm[2] === "?";
588
+ let type = (fm[3] || "")
589
+ .replace(/\s*\/\/.*$/, "")
590
+ .trim()
591
+ .replace(/;$/, "")
592
+ .trim();
593
+ const openB = (type.match(/\{/g) || []).length;
594
+ const closeB = (type.match(/\}/g) || []).length;
595
+ if (openB !== closeB) continue;
596
+ type = type.replace(/React\.ReactNode/g, "ReactNode");
597
+ if (type.length > 60) type = type.slice(0, 57) + "...";
598
+ fields.push({ name: fieldName, type, required: !optional });
599
+ }
600
+
601
+ // Only register types with at least 2 fields (otherwise not useful for mock generation)
602
+ if (fields.length >= 2) {
603
+ registry[typeName] = fields;
604
+ seen.add(typeName);
605
+ }
606
+ }
607
+ }
608
+ }
609
+ }
610
+
611
+ scanDir(SRC_DIR);
612
+ return Object.keys(registry).length > 0 ? registry : null;
613
+ }
614
+
528
615
  /**
529
616
  * Extract named UI patterns from a feature-tier file.
530
617
  *
@@ -2401,6 +2488,8 @@ function scan() {
2401
2488
  if (Object.keys(colorUsage).length > 0) foundations.colorUsage = colorUsage;
2402
2489
  const gridSystem = extractGridSystem(SRC_DIR);
2403
2490
  if (gridSystem) foundations.gridSystem = gridSystem;
2491
+ const projectTypes = extractProjectTypes();
2492
+ if (projectTypes) foundations.types = projectTypes;
2404
2493
  const componentSuggestions = extractComponentSuggestions();
2405
2494
  const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
2406
2495
  const output = {
@@ -24,6 +24,8 @@ const STORIES_DIR = path.join(SRC_DIR, "stories");
24
24
  let FOUNDATIONS_DATA = null;
25
25
  // componentsRelDir from vds-output.json — defaults to "src/components"
26
26
  let COMPONENTS_REL_DIR = "src/components";
27
+ // Type registry extracted by scan.mjs from project's src/ — { TypeName: [{name, type, required}] }
28
+ let PROJECT_TYPE_REGISTRY = {};
27
29
 
28
30
  // CSS is loaded from .storybook/preview.tsx — never add CSS import to story files.
29
31
 
@@ -636,10 +638,83 @@ function isDateLikeField(name) {
636
638
  }
637
639
 
638
640
  /** Build 2-3 mock items for array prop from component source (item type fields). Uses valid ISO date strings for date-like fields to avoid "Invalid time value". */
641
+ /**
642
+ * Generate a mock JavaScript value string from a TypeScript type string.
643
+ * Consults PROJECT_TYPE_REGISTRY for custom object types (Task, Project, etc.).
644
+ * depth prevents infinite recursion on circular types.
645
+ */
646
+ function genMockValue(type, depth = 0) {
647
+ if (depth > 2 || !type) return '"example"';
648
+ type = type.trim();
649
+ if (/^string(\s*\|.*)?$/.test(type)) return '"example"';
650
+ if (/^number/.test(type)) return '0';
651
+ if (/^boolean/.test(type)) return 'false';
652
+ if (/^Date\b/.test(type) || /^(createdAt|updatedAt|startDate|endDate|dueDate)$/.test(type)) return 'new Date().toISOString()';
653
+ // String literal union (single or double quoted) — pick first option
654
+ if (/^['"][^'"]+['"](\s*\|\s*['"][^'"]+['"])*$/.test(type)) {
655
+ const first = type.match(/['"]([^'"]+)['"]/);
656
+ return first ? `"${first[1]}"` : '"example"';
657
+ }
658
+ // ReactNode
659
+ if (/ReactNode/.test(type)) return '"Content"';
660
+ // Array: Type[] or Array<Type>
661
+ if (/\[\]$/.test(type)) {
662
+ const base = type.replace(/\[\]$/, "").trim();
663
+ if (/^string/.test(base)) return '["Example"]';
664
+ const inner = genMockValue(base, depth + 1);
665
+ return `[${inner}]`;
666
+ }
667
+ const arrMatch = type.match(/^Array<(.+)>$/);
668
+ if (arrMatch) {
669
+ const inner = genMockValue(arrMatch[1].trim(), depth + 1);
670
+ return `[${inner}]`;
671
+ }
672
+ // Custom type from registry — generate mock object
673
+ if (PROJECT_TYPE_REGISTRY[type]) return genMockObjectFromRegistry(type, depth);
674
+ return '"example"';
675
+ }
676
+
677
+ /**
678
+ * Generate a JS object literal string for a type defined in PROJECT_TYPE_REGISTRY.
679
+ * Uses only required fields (or first 3 fields if all are optional).
680
+ */
681
+ function genMockObjectFromRegistry(typeName, depth = 0) {
682
+ const fields = PROJECT_TYPE_REGISTRY[typeName];
683
+ if (!fields || fields.length === 0) return "{}";
684
+ const required = fields.filter(f => f.required);
685
+ const toUse = required.length > 0 ? required : fields.slice(0, 3);
686
+ const pairs = toUse.map(f => {
687
+ const val = f.name === "id" ? '"1"' : genMockValue(f.type, depth + 1);
688
+ return `${f.name}: ${val}`;
689
+ });
690
+ return `{ ${pairs.join(", ")} }`;
691
+ }
692
+
639
693
  function buildMockArrayItems(componentSource, itemTypeName, propName) {
640
694
  const fields = getItemTypeFieldsFromSource(componentSource, itemTypeName);
641
695
  const propLower = (propName || "").toLowerCase();
642
696
  const looksLikeTimeEntries = /entries|rows|logs|data|items|times/.test(propLower) || /time|table|log/.test(propLower);
697
+
698
+ // If source-based lookup failed, try the type registry for realistic field names
699
+ if (fields.length === 0 && itemTypeName && PROJECT_TYPE_REGISTRY[itemTypeName]) {
700
+ const regFields = PROJECT_TYPE_REGISTRY[itemTypeName];
701
+ const baseDate = new Date("2024-01-15T12:00:00.000Z").getTime();
702
+ const items = [];
703
+ for (let i = 1; i <= 2; i++) {
704
+ const item = {};
705
+ for (const f of regFields.slice(0, 8)) {
706
+ if (f.name === "id") item[f.name] = String(i);
707
+ else if (isDateLikeField(f.name) || /^Date/.test(f.type)) item[f.name] = new Date(baseDate + i * 86400000).toISOString();
708
+ else if (/^number/.test(f.type)) item[f.name] = i;
709
+ else if (/^boolean/.test(f.type)) item[f.name] = false;
710
+ else if (/^['"][^'"]+['"]/.test(f.type)) { const m = f.type.match(/['"]([^'"]+)['"]/); item[f.name] = m ? m[1] : "example"; }
711
+ else item[f.name] = `Example ${i}`;
712
+ }
713
+ items.push(item);
714
+ }
715
+ return items;
716
+ }
717
+
643
718
  const defaultFields = looksLikeTimeEntries
644
719
  ? ["id", "date", "start", "end", "title", "value"]
645
720
  : ["label", "value", "title", "id", "name", "description"];
@@ -785,6 +860,27 @@ function buildDefaultArgsForRequiredProps(props, usageFromPages = null, componen
785
860
  const type = String(p.type || "").trim();
786
861
  const name = p.name;
787
862
  if (added.has(name)) continue;
863
+
864
+ // ── Registry-based lookup (highest priority for custom object types) ──────
865
+ // Direct type match: prop type IS a known project type (e.g. task: Task)
866
+ if (PROJECT_TYPE_REGISTRY[type]) {
867
+ argLines.push(` ${name}: ${genMockObjectFromRegistry(type)},`);
868
+ added.add(name); continue;
869
+ }
870
+ // Array of known type: tasks: Task[] or tasks: Array<Task>
871
+ const arrTypeMatch = type.match(/^([A-Z][A-Za-z0-9]+)\[\]$/) || type.match(/^Array<([A-Z][A-Za-z0-9]+)>$/);
872
+ if (arrTypeMatch && PROJECT_TYPE_REGISTRY[arrTypeMatch[1]]) {
873
+ argLines.push(` ${name}: [${genMockObjectFromRegistry(arrTypeMatch[1])}],`);
874
+ added.add(name); continue;
875
+ }
876
+ // Prop name matches a registry type (e.g. prop named "task" of type "object")
877
+ const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1);
878
+ if (/object|any|unknown/.test(type) && PROJECT_TYPE_REGISTRY[nameCapitalized]) {
879
+ argLines.push(` ${name}: ${genMockObjectFromRegistry(nameCapitalized)},`);
880
+ added.add(name); continue;
881
+ }
882
+ // ─────────────────────────────────────────────────────────────────────────
883
+
788
884
  if (name === "filters" || /dateRange|Filter.*object/.test(type)) {
789
885
  argLines.push(` ${name}: ${DATE_RANGE_DEFAULT},`);
790
886
  added.add(name);
@@ -1098,9 +1194,9 @@ function buildArgTypeEntry(prop) {
1098
1194
  if (/^number/.test(t)) {
1099
1195
  return `{ control: "number", description: "number" }`;
1100
1196
  }
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, ""));
1197
+ // String union literals (single or double quoted): "a" | "b" or 'a' | 'b' → select control
1198
+ if (/^['"][^'"]+['"](\s*\|\s*['"][^'"]+['"])+/.test(t)) {
1199
+ const opts = (t.match(/['"]([^'"]+)['"]/g) || []).map(s => s.replace(/['"]/g, ""));
1104
1200
  return `{ control: "select", options: ${JSON.stringify(opts)}, description: ${JSON.stringify(t)} }`;
1105
1201
  }
1106
1202
  // Plain string (including union with null/undefined)
@@ -1130,8 +1226,10 @@ function buildStoryFileContent(comp) {
1130
1226
  let importPath = "";
1131
1227
  const isPageFile = fileNoExt.startsWith("pages/") || fileNoExt.startsWith("src/pages/");
1132
1228
  if (isPageFile) {
1133
- const normalized = fileNoExt.replace(/^src\//, "");
1134
- importPath = path.posix.relative("src/stories", normalized);
1229
+ // Keep full path with src/ prefix so relative is computed correctly from src/stories/
1230
+ // e.g. "src/pages/Admin" → "../pages/Admin" (NOT "../../pages/Admin")
1231
+ const fullPath = fileNoExt.startsWith("src/") ? fileNoExt : "src/" + fileNoExt;
1232
+ importPath = path.posix.relative("src/stories", fullPath);
1135
1233
  } else {
1136
1234
  const targetPath = path.posix.join(COMPONENTS_REL_DIR, fileNoExt);
1137
1235
  importPath = path.posix.relative("src/stories", targetPath);
@@ -2803,6 +2901,10 @@ function main() {
2803
2901
  const components = Array.isArray(data.components) ? data.components : [];
2804
2902
  const foundations = data.foundations || null;
2805
2903
  FOUNDATIONS_DATA = foundations;
2904
+ // Load project type registry for realistic mock arg generation
2905
+ if (foundations?.types && typeof foundations.types === "object") {
2906
+ PROJECT_TYPE_REGISTRY = foundations.types;
2907
+ }
2806
2908
 
2807
2909
  const onlyName = process.argv[2] || null;
2808
2910