vibe-design-system 2.8.22 → 2.8.24
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);
|
|
@@ -1608,37 +1608,65 @@ function writeFoundationsStories(foundations) {
|
|
|
1608
1608
|
: rawIcons.map((name) => ({ name, total: 0, topFiles: [] }));
|
|
1609
1609
|
|
|
1610
1610
|
const iconsContent = [
|
|
1611
|
+
"import { useState } from \"react\";",
|
|
1611
1612
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
1612
1613
|
"import * as Lucide from \"lucide-react\";",
|
|
1613
1614
|
"",
|
|
1614
1615
|
"const meta = { title: \"Foundations/Icons\" } satisfies Meta;",
|
|
1615
1616
|
"export default meta;",
|
|
1616
1617
|
"type Story = StoryObj;",
|
|
1618
|
+
"type IconEntry = { name: string; total: number; topFiles: string[] };",
|
|
1617
1619
|
"",
|
|
1618
|
-
`const iconData = ${JSON.stringify(iconData, null, 2)};`,
|
|
1620
|
+
`const iconData: IconEntry[] = ${JSON.stringify(iconData, null, 2)};`,
|
|
1619
1621
|
"",
|
|
1620
1622
|
"export const Default: Story = {",
|
|
1621
1623
|
" render: () => {",
|
|
1624
|
+
" const [copied, setCopied] = useState<string | null>(null);",
|
|
1622
1625
|
" const usedIcons = iconData.filter((d) => d.total > 0);",
|
|
1623
1626
|
" const unusedIcons = iconData.filter((d) => d.total === 0);",
|
|
1624
|
-
"
|
|
1627
|
+
"",
|
|
1628
|
+
" const handleCopy = (name: string) => {",
|
|
1629
|
+
" navigator.clipboard?.writeText(name).then(() => {",
|
|
1630
|
+
" setCopied(name);",
|
|
1631
|
+
" setTimeout(() => setCopied(null), 1400);",
|
|
1632
|
+
" });",
|
|
1633
|
+
" };",
|
|
1634
|
+
"",
|
|
1635
|
+
" const renderCard = (d: IconEntry, faded = false) => {",
|
|
1625
1636
|
" const Icon = (Lucide as Record<string, any>)[d.name];",
|
|
1626
1637
|
" if (!Icon) return null;",
|
|
1638
|
+
" const isCopied = copied === d.name;",
|
|
1627
1639
|
" return (",
|
|
1628
|
-
" <div
|
|
1629
|
-
"
|
|
1640
|
+
" <div",
|
|
1641
|
+
" key={d.name}",
|
|
1642
|
+
" onClick={() => handleCopy(d.name)}",
|
|
1643
|
+
" title={`Click to copy: ${d.name}`}",
|
|
1644
|
+
" style={{",
|
|
1645
|
+
" display: \"flex\", flexDirection: \"column\", gap: 10, padding: 14,",
|
|
1646
|
+
" border: `1px solid ${isCopied ? \"#4ade80\" : \"#1e293b\"}`,",
|
|
1647
|
+
" borderRadius: 10,",
|
|
1648
|
+
" background: isCopied ? \"#052e16\" : \"#0f172a\",",
|
|
1649
|
+
" cursor: \"pointer\",",
|
|
1650
|
+
" opacity: faded ? 0.4 : 1,",
|
|
1651
|
+
" transition: \"border-color 0.15s, background 0.15s\",",
|
|
1652
|
+
" }}",
|
|
1653
|
+
" >",
|
|
1630
1654
|
" <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"flex-start\" }}>",
|
|
1631
|
-
" <
|
|
1655
|
+
" <div style={{ padding: 8, background: \"#1e293b\", borderRadius: 8, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>",
|
|
1656
|
+
" <Icon size={22} strokeWidth={1.75} color=\"#94a3b8\" />",
|
|
1657
|
+
" </div>",
|
|
1632
1658
|
" {d.total > 0 && (",
|
|
1633
1659
|
" <span style={{ fontSize: 11, fontWeight: 600, background: \"#1e293b\",",
|
|
1634
|
-
" color: \"#94a3b8\", padding: \"2px 7px\", borderRadius: 999 }}>",
|
|
1660
|
+
" color: \"#94a3b8\", padding: \"2px 7px\", borderRadius: 999, alignSelf: \"flex-start\" }}>",
|
|
1635
1661
|
" ×{d.total}",
|
|
1636
1662
|
" </span>",
|
|
1637
1663
|
" )}",
|
|
1638
1664
|
" </div>",
|
|
1639
|
-
" <span style={{ fontSize: 12, fontWeight: 500, color: \"#e2e8f0\" }}>
|
|
1665
|
+
" <span style={{ fontSize: 12, fontWeight: 500, color: isCopied ? \"#4ade80\" : \"#e2e8f0\" }}>",
|
|
1666
|
+
" {isCopied ? \"Copied!\" : d.name}",
|
|
1667
|
+
" </span>",
|
|
1640
1668
|
" {d.topFiles.length > 0 && (",
|
|
1641
|
-
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 4
|
|
1669
|
+
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 4 }}>",
|
|
1642
1670
|
" {d.topFiles.map((f) => (",
|
|
1643
1671
|
" <span key={f} style={{ fontSize: 10, background: \"#1e293b\", color: \"#64748b\",",
|
|
1644
1672
|
" padding: \"1px 6px\", borderRadius: 4 }}>{f}</span>",
|
|
@@ -1655,7 +1683,7 @@ function writeFoundationsStories(foundations) {
|
|
|
1655
1683
|
" {iconData.length} icons imported from <code style={{ fontSize: 12 }}>lucide-react</code> in app code.",
|
|
1656
1684
|
" </p>",
|
|
1657
1685
|
" <p style={{ margin: 0, fontSize: 12, color: \"#475569\" }}>",
|
|
1658
|
-
" Sorted by usage frequency · Badge
|
|
1686
|
+
" Sorted by usage frequency · Badge = total JSX usages · Click any card to copy the icon name.",
|
|
1659
1687
|
" </p>",
|
|
1660
1688
|
" </div>",
|
|
1661
1689
|
" {usedIcons.length > 0 && (",
|
|
@@ -1665,8 +1693,8 @@ function writeFoundationsStories(foundations) {
|
|
|
1665
1693
|
" Active — {usedIcons.length}",
|
|
1666
1694
|
" </p>",
|
|
1667
1695
|
" <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(160px, 1fr))\",",
|
|
1668
|
-
" gap: 10, marginBottom:
|
|
1669
|
-
" {usedIcons.map(renderCard)}",
|
|
1696
|
+
" gap: 10, marginBottom: 36 }}>",
|
|
1697
|
+
" {usedIcons.map((d) => renderCard(d, false))}",
|
|
1670
1698
|
" </div>",
|
|
1671
1699
|
" </>",
|
|
1672
1700
|
" )}",
|
|
@@ -1674,11 +1702,11 @@ function writeFoundationsStories(foundations) {
|
|
|
1674
1702
|
" <>",
|
|
1675
1703
|
" <p style={{ margin: \"0 0 12px\", fontSize: 12, fontWeight: 600, color: \"#334155\",",
|
|
1676
1704
|
" textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>",
|
|
1677
|
-
" Imported
|
|
1705
|
+
" Imported, not used in JSX — {unusedIcons.length}",
|
|
1678
1706
|
" </p>",
|
|
1679
1707
|
" <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(140px, 1fr))\",",
|
|
1680
|
-
" gap: 8
|
|
1681
|
-
" {unusedIcons.map(renderCard)}",
|
|
1708
|
+
" gap: 8 }}>",
|
|
1709
|
+
" {unusedIcons.map((d) => renderCard(d, true))}",
|
|
1682
1710
|
" </div>",
|
|
1683
1711
|
" </>",
|
|
1684
1712
|
" )}",
|
|
@@ -2541,6 +2569,20 @@ function writeCursorRules(components, foundations) {
|
|
|
2541
2569
|
lines.push(``);
|
|
2542
2570
|
}
|
|
2543
2571
|
|
|
2572
|
+
// Format a compact props summary for cursor rules (required first, up to 6 props)
|
|
2573
|
+
const formatProps = (props) => {
|
|
2574
|
+
if (!Array.isArray(props) || props.length === 0) return null;
|
|
2575
|
+
const sorted = [...props].sort((a, b) => {
|
|
2576
|
+
if (a.required && !b.required) return -1;
|
|
2577
|
+
if (!a.required && b.required) return 1;
|
|
2578
|
+
return a.name.localeCompare(b.name);
|
|
2579
|
+
});
|
|
2580
|
+
const shown = sorted.slice(0, 6);
|
|
2581
|
+
const parts = shown.map(p => p.required ? `${p.name} (${p.type}, required)` : `${p.name} (${p.type})`);
|
|
2582
|
+
const suffix = props.length > 6 ? ` *(+${props.length - 6} more)*` : "";
|
|
2583
|
+
return ` props: ${parts.join(" · ")}${suffix}`;
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2544
2586
|
// Tier 2 — Components
|
|
2545
2587
|
if (byTier.component.length > 0) {
|
|
2546
2588
|
lines.push(`## 🔷 Components — Reusable domain components`);
|
|
@@ -2550,6 +2592,8 @@ function writeCursorRules(components, foundations) {
|
|
|
2550
2592
|
let entry = `- **${c.name}** \`${c.file}\``;
|
|
2551
2593
|
if (c.description) entry += ` — ${c.description}`;
|
|
2552
2594
|
lines.push(entry);
|
|
2595
|
+
const propsLine = formatProps(c.props);
|
|
2596
|
+
if (propsLine) lines.push(propsLine);
|
|
2553
2597
|
}
|
|
2554
2598
|
lines.push(``);
|
|
2555
2599
|
lines.push(`---`);
|
|
@@ -2563,6 +2607,8 @@ function writeCursorRules(components, foundations) {
|
|
|
2563
2607
|
lines.push(``);
|
|
2564
2608
|
for (const c of byTier.feature.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2565
2609
|
lines.push(`- **${c.name}** \`${c.file}\``);
|
|
2610
|
+
const propsLine = formatProps(c.props);
|
|
2611
|
+
if (propsLine) lines.push(propsLine);
|
|
2566
2612
|
}
|
|
2567
2613
|
lines.push(``);
|
|
2568
2614
|
lines.push(`---`);
|