vibe-design-system 2.8.27 → 2.8.29

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.27",
3
+ "version": "2.8.29",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -525,6 +525,148 @@ 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
+
615
+ /**
616
+ * Extract named UI patterns from a feature-tier file.
617
+ *
618
+ * Signal 1 — JSX block comments: {/* SECTION TITLE *\/}
619
+ * e.g. {/* CLUSTER 1: REVENUE & BILLING *\/} → label "CLUSTER 1: REVENUE & BILLING"
620
+ *
621
+ * Signal 2 — Nearest <h3>/<h2> text (fallback when no comments found)
622
+ * e.g. <h3>Unbilled WIP</h3> → label "Unbilled WIP"
623
+ *
624
+ * Returns Array<{ label: string, line: number }> or null.
625
+ * Safe on any JSX/TSX file — never throws.
626
+ */
627
+ function extractInlinePatterns(content) {
628
+ const patterns = [];
629
+ const seen = new Set();
630
+
631
+ // Signal 1: JSX block comments — {/* ... */}
632
+ const commentRe = /\{\/\*\s*([\s\S]{3,120}?)\s*\*\/\}/g;
633
+ let cm;
634
+ while ((cm = commentRe.exec(content)) !== null) {
635
+ const raw = cm[1].replace(/\s+/g, " ").trim();
636
+ // Skip too-short, pure annotation, or code-style comments
637
+ if (!raw || raw.length < 5) continue;
638
+ if (/^(TODO|FIXME|HACK|NOTE|@|eslint|prettier|ts-ignore|type-)/i.test(raw)) continue;
639
+ // Skip comments that look like code (contain JSX/HTML tags)
640
+ if (/<[A-Za-z]/.test(raw)) continue;
641
+ const lineNum = content.slice(0, cm.index).split("\n").length;
642
+ const label = raw.slice(0, 80);
643
+ const key = label.toLowerCase().replace(/\s+/g, " ");
644
+ if (!seen.has(key)) {
645
+ seen.add(key);
646
+ patterns.push({ label, line: lineNum });
647
+ }
648
+ }
649
+
650
+ // Signal 2: <h3>/<h2> text content (only if no JSX comments found)
651
+ if (patterns.length === 0) {
652
+ const headingRe = /<h[23][^>]*>\s*([^<{]{4,60}?)\s*<\/h[23]>/g;
653
+ let hm;
654
+ while ((hm = headingRe.exec(content)) !== null) {
655
+ const raw = hm[1].replace(/\s+/g, " ").trim();
656
+ if (!raw || raw.length < 4) continue;
657
+ const lineNum = content.slice(0, hm.index).split("\n").length;
658
+ const label = raw.slice(0, 80);
659
+ const key = label.toLowerCase();
660
+ if (!seen.has(key)) {
661
+ seen.add(key);
662
+ patterns.push({ label, line: lineNum });
663
+ }
664
+ }
665
+ }
666
+
667
+ return patterns.length > 0 ? patterns : null;
668
+ }
669
+
528
670
  function getAllComponentFiles(dir, baseDir = dir) {
529
671
  const entries = fs.readdirSync(dir, { withFileTypes: true });
530
672
  const files = [];
@@ -2304,9 +2446,10 @@ function scan() {
2304
2446
  const tier = inferTier(rel, content);
2305
2447
  const variants = (tier === "primitive") ? extractCvaVariants(content) : null;
2306
2448
  const props = (tier === "component" || tier === "feature") ? extractTSProps(content) : null;
2449
+ const patterns = (tier === "feature") ? extractInlinePatterns(content) : null;
2307
2450
  const lineCount = content.split("\n").length;
2308
2451
  const localImportCount = (content.match(/from\s+['"]\.\.\?\//g) || []).length;
2309
- results.push({ file: rel, name, group, category, description, tokens, tier, lines: lineCount, localImports: localImportCount, ...(variants ? { variants } : {}), ...(props ? { props } : {}) });
2452
+ results.push({ file: rel, name, group, category, description, tokens, tier, lines: lineCount, localImports: localImportCount, ...(variants ? { variants } : {}), ...(props ? { props } : {}), ...(patterns ? { patterns } : {}) });
2310
2453
  }
2311
2454
  if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
2312
2455
  const pageFiles = getAllComponentFiles(PAGES_DIR);
@@ -2345,6 +2488,8 @@ function scan() {
2345
2488
  if (Object.keys(colorUsage).length > 0) foundations.colorUsage = colorUsage;
2346
2489
  const gridSystem = extractGridSystem(SRC_DIR);
2347
2490
  if (gridSystem) foundations.gridSystem = gridSystem;
2491
+ const projectTypes = extractProjectTypes();
2492
+ if (projectTypes) foundations.types = projectTypes;
2348
2493
  const componentSuggestions = extractComponentSuggestions();
2349
2494
  const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
2350
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)
@@ -2669,13 +2765,17 @@ function writeCursorRules(components, foundations) {
2669
2765
 
2670
2766
  // Tier 3 — Features
2671
2767
  if (byTier.feature.length > 0) {
2672
- lines.push(`## ⚙️ Feature ComponentsExtract patterns, don't import whole`);
2673
- lines.push(`> Complex components with internal state. Reuse sub-patterns, not the whole file.`);
2768
+ lines.push(`## ⚙️ Feature ViewsReuse existing UI patterns before building new ones`);
2769
+ lines.push(`> Full views with internal state. When a user story needs a new widget, check the pattern list below first.`);
2674
2770
  lines.push(``);
2675
2771
  for (const c of byTier.feature.sort((a, b) => a.name.localeCompare(b.name))) {
2676
2772
  lines.push(`- **${c.name}** \`${c.file}\``);
2677
2773
  const propsLine = formatProps(c.props);
2678
2774
  if (propsLine) lines.push(propsLine);
2775
+ if (Array.isArray(c.patterns) && c.patterns.length > 0) {
2776
+ const patternParts = c.patterns.slice(0, 8).map(p => `"${p.label}" (line ${p.line})`);
2777
+ lines.push(` patterns: ${patternParts.join(" · ")}`);
2778
+ }
2679
2779
  }
2680
2780
  lines.push(``);
2681
2781
  lines.push(`---`);
@@ -2799,6 +2899,10 @@ function main() {
2799
2899
  const components = Array.isArray(data.components) ? data.components : [];
2800
2900
  const foundations = data.foundations || null;
2801
2901
  FOUNDATIONS_DATA = foundations;
2902
+ // Load project type registry for realistic mock arg generation
2903
+ if (foundations?.types && typeof foundations.types === "object") {
2904
+ PROJECT_TYPE_REGISTRY = foundations.types;
2905
+ }
2802
2906
 
2803
2907
  const onlyName = process.argv[2] || null;
2804
2908