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
|
@@ -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" |
|
|
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
|
|
2673
|
-
lines.push(`>
|
|
2768
|
+
lines.push(`## ⚙️ Feature Views — Reuse 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
|
|