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
|
@@ -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
|
-
|
|
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(`---`);
|