vibe-design-system 2.8.21 → 2.8.23
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
|
@@ -375,6 +375,62 @@ function classifyByPath(rel) {
|
|
|
375
375
|
return { group: "Components", category: "Components" };
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Infer the reusability tier of a component from path + content signals.
|
|
380
|
+
* primitive → src/components/ui/ (shadcn atoms)
|
|
381
|
+
* component → small, focused domain component (< 200 lines, ≤ 6 local imports)
|
|
382
|
+
* feature → medium, contains state / multiple sub-parts
|
|
383
|
+
* page → full page/view — reference only, not for direct reuse
|
|
384
|
+
*/
|
|
385
|
+
function inferTier(rel, content) {
|
|
386
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
387
|
+
if (normalized.startsWith("ui/") || normalized.includes("/ui/")) return "primitive";
|
|
388
|
+
|
|
389
|
+
const lines = content.split("\n").length;
|
|
390
|
+
// Count relative imports (../ ./) AND path-alias imports (@/components/) as local dependencies
|
|
391
|
+
const localImports = (content.match(/from\s+['"](?:\.\.?\/?|@\/components\/)/g) || []).length;
|
|
392
|
+
|
|
393
|
+
if (lines >= 400 || localImports >= 12) return "page";
|
|
394
|
+
if (lines >= 200 || localImports >= 7) return "feature";
|
|
395
|
+
return "component";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Tailwind pseudo-class modifiers that appear inside class strings — never valid cva option names
|
|
399
|
+
const CVA_SKIP = new Set([
|
|
400
|
+
"hover","focus","active","disabled","dark","group","peer","placeholder",
|
|
401
|
+
"before","after","visited","checked","required","invalid","valid","not",
|
|
402
|
+
"open","closed","empty","enabled","first","last","odd","even",
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Extract cva() variant options from a file, e.g.:
|
|
407
|
+
* variants: { variant: { default, destructive, ghost }, size: { sm, md, lg } }
|
|
408
|
+
* Returns { variant: ["default","destructive",...], size: [...] } or null.
|
|
409
|
+
* Filters out Tailwind pseudo-class modifiers that appear inside class strings.
|
|
410
|
+
*/
|
|
411
|
+
function extractCvaVariants(content) {
|
|
412
|
+
const block = content.match(/variants\s*:\s*\{([\s\S]*?)\}\s*,?\s*defaultVariants/);
|
|
413
|
+
if (!block) return null;
|
|
414
|
+
const result = {};
|
|
415
|
+
// Each top-level key is a variant dimension (variant, size, intent…)
|
|
416
|
+
// Match: variantName: { optionA: "...", optionB: "..." }
|
|
417
|
+
const keyRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
|
|
418
|
+
let m;
|
|
419
|
+
while ((m = keyRe.exec(block[1])) !== null) {
|
|
420
|
+
const dimName = m[1];
|
|
421
|
+
if (CVA_SKIP.has(dimName)) continue; // skip pseudo-class keys at outer level
|
|
422
|
+
// Extract option names: only words at the start of a line (key: "value")
|
|
423
|
+
const optRe = /^\s{6,}(\w[\w-]*)\s*:/gm;
|
|
424
|
+
let om;
|
|
425
|
+
const options = [];
|
|
426
|
+
while ((om = optRe.exec(m[2])) !== null) {
|
|
427
|
+
if (!CVA_SKIP.has(om[1])) options.push(om[1]);
|
|
428
|
+
}
|
|
429
|
+
if (options.length > 0) result[dimName] = options;
|
|
430
|
+
}
|
|
431
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
432
|
+
}
|
|
433
|
+
|
|
378
434
|
function getAllComponentFiles(dir, baseDir = dir) {
|
|
379
435
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
380
436
|
const files = [];
|
|
@@ -2151,7 +2207,11 @@ function scan() {
|
|
|
2151
2207
|
description = "";
|
|
2152
2208
|
}
|
|
2153
2209
|
const tokens = extractTailwindTokens(content);
|
|
2154
|
-
|
|
2210
|
+
const tier = inferTier(rel, content);
|
|
2211
|
+
const variants = (tier === "primitive") ? extractCvaVariants(content) : null;
|
|
2212
|
+
const lineCount = content.split("\n").length;
|
|
2213
|
+
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 } : {}) });
|
|
2155
2215
|
}
|
|
2156
2216
|
if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
|
|
2157
2217
|
const pageFiles = getAllComponentFiles(PAGES_DIR);
|
|
@@ -2160,6 +2220,7 @@ function scan() {
|
|
|
2160
2220
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
2161
2221
|
const name = humanizeName(rel);
|
|
2162
2222
|
const tokens = extractTailwindTokens(content);
|
|
2223
|
+
const lineCount = content.split("\n").length;
|
|
2163
2224
|
results.push({
|
|
2164
2225
|
file: path.relative(PROJECT_ROOT, PAGES_DIR).replace(/\\/g, "/") + "/" + rel,
|
|
2165
2226
|
name,
|
|
@@ -2167,6 +2228,9 @@ function scan() {
|
|
|
2167
2228
|
category: "Pages",
|
|
2168
2229
|
description: "",
|
|
2169
2230
|
tokens,
|
|
2231
|
+
tier: "page",
|
|
2232
|
+
lines: lineCount,
|
|
2233
|
+
localImports: 0,
|
|
2170
2234
|
});
|
|
2171
2235
|
}
|
|
2172
2236
|
}
|
|
@@ -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
|
" )}",
|
|
@@ -2486,6 +2514,195 @@ function writeChangelogStory(changelog) {
|
|
|
2486
2514
|
console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Changelog.stories.tsx")));
|
|
2487
2515
|
}
|
|
2488
2516
|
|
|
2517
|
+
/** Generate .cursor/rules — structured design system context for AI agents.
|
|
2518
|
+
* Components are split into tiers so the agent knows what to reuse vs reference.
|
|
2519
|
+
* Foundations (colors, icons, spacing, breakpoints) are inlined as tables/lists.
|
|
2520
|
+
* The file is written to PROJECT_ROOT/.cursor/rules so Cursor injects it automatically. */
|
|
2521
|
+
function writeCursorRules(components, foundations) {
|
|
2522
|
+
const lines = [];
|
|
2523
|
+
const now = new Date().toISOString().slice(0, 10);
|
|
2524
|
+
|
|
2525
|
+
lines.push(`# VDS Design System — Agent Context`);
|
|
2526
|
+
lines.push(`> Auto-generated by VDS on ${now}. Do not edit — regenerated on every \`npm run vds\`.`);
|
|
2527
|
+
lines.push(``);
|
|
2528
|
+
|
|
2529
|
+
// ── Ground rules ──────────────────────────────────────────────────────────
|
|
2530
|
+
const gaps = foundations?.gridSystem?.gaps || {};
|
|
2531
|
+
const topGaps = Object.entries(gaps).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([v]) => `gap-${v}`).join(", ");
|
|
2532
|
+
const bpUsed = Object.keys(foundations?.gridSystem?.breakpoints || {});
|
|
2533
|
+
const bpAll = ["sm","md","lg","xl","2xl"];
|
|
2534
|
+
const bpUnused = bpAll.filter(b => !bpUsed.includes(b));
|
|
2535
|
+
|
|
2536
|
+
lines.push(`## ⚙️ Rules`);
|
|
2537
|
+
lines.push(`1. Before creating a new component, check the component lists below.`);
|
|
2538
|
+
lines.push(`2. **Never hardcode colors or hex values** — use the token table.`);
|
|
2539
|
+
lines.push(`3. **Always import icons from \`lucide-react\`** — never other icon libraries.`);
|
|
2540
|
+
if (topGaps) lines.push(`4. **Spacing convention**: ${topGaps} are the standard gaps in this project.`);
|
|
2541
|
+
if (bpUsed.length) lines.push(`5. **Breakpoints in use**: ${bpUsed.map(b=>`\`${b}:\``).join(" ")} — prefer these.`);
|
|
2542
|
+
if (bpUnused.length) lines.push(`6. **Breakpoints NOT used**: ${bpUnused.map(b=>`\`${b}:\``).join(" ")} — avoid unless explicitly needed.`);
|
|
2543
|
+
lines.push(``);
|
|
2544
|
+
lines.push(`---`);
|
|
2545
|
+
lines.push(``);
|
|
2546
|
+
|
|
2547
|
+
// ── Component tiers ───────────────────────────────────────────────────────
|
|
2548
|
+
const byTier = { primitive: [], component: [], feature: [], page: [] };
|
|
2549
|
+
for (const c of components) {
|
|
2550
|
+
const t = c.tier || "component";
|
|
2551
|
+
if (byTier[t]) byTier[t].push(c);
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// Tier 1 — Primitives
|
|
2555
|
+
if (byTier.primitive.length > 0) {
|
|
2556
|
+
lines.push(`## 🧱 Primitives — Use first for all UI elements`);
|
|
2557
|
+
lines.push(`> Atomic components from \`src/components/ui/\`. Always prefer over building from scratch.`);
|
|
2558
|
+
lines.push(``);
|
|
2559
|
+
for (const c of byTier.primitive.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2560
|
+
let entry = `- **${c.name}** \`${c.file}\``;
|
|
2561
|
+
if (c.variants) {
|
|
2562
|
+
const parts = Object.entries(c.variants).map(([k, vs]) => `${k}: ${vs.join(" | ")}`);
|
|
2563
|
+
entry += ` — ${parts.join(" · ")}`;
|
|
2564
|
+
}
|
|
2565
|
+
lines.push(entry);
|
|
2566
|
+
}
|
|
2567
|
+
lines.push(``);
|
|
2568
|
+
lines.push(`---`);
|
|
2569
|
+
lines.push(``);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// Tier 2 — Components
|
|
2573
|
+
if (byTier.component.length > 0) {
|
|
2574
|
+
lines.push(`## 🔷 Components — Reusable domain components`);
|
|
2575
|
+
lines.push(`> Project-specific, small, focused. Use before creating new ones.`);
|
|
2576
|
+
lines.push(``);
|
|
2577
|
+
for (const c of byTier.component.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2578
|
+
let entry = `- **${c.name}** \`${c.file}\``;
|
|
2579
|
+
if (c.description) entry += ` — ${c.description}`;
|
|
2580
|
+
lines.push(entry);
|
|
2581
|
+
}
|
|
2582
|
+
lines.push(``);
|
|
2583
|
+
lines.push(`---`);
|
|
2584
|
+
lines.push(``);
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// Tier 3 — Features
|
|
2588
|
+
if (byTier.feature.length > 0) {
|
|
2589
|
+
lines.push(`## ⚙️ Feature Components — Extract patterns, don't import whole`);
|
|
2590
|
+
lines.push(`> Complex components with internal state. Reuse sub-patterns, not the whole file.`);
|
|
2591
|
+
lines.push(``);
|
|
2592
|
+
for (const c of byTier.feature.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2593
|
+
lines.push(`- **${c.name}** \`${c.file}\``);
|
|
2594
|
+
}
|
|
2595
|
+
lines.push(``);
|
|
2596
|
+
lines.push(`---`);
|
|
2597
|
+
lines.push(``);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// Tier 4 — Pages (just names, no need to list file paths)
|
|
2601
|
+
if (byTier.page.length > 0) {
|
|
2602
|
+
lines.push(`## 📄 Pages & Views — Reference only`);
|
|
2603
|
+
lines.push(`> Full page views. Study for patterns but do not import into new features.`);
|
|
2604
|
+
lines.push(``);
|
|
2605
|
+
lines.push(byTier.page.map(c => c.name).sort().join(", "));
|
|
2606
|
+
lines.push(``);
|
|
2607
|
+
lines.push(`---`);
|
|
2608
|
+
lines.push(``);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// ── Color tokens ──────────────────────────────────────────────────────────
|
|
2612
|
+
const colors = foundations?.colors || {};
|
|
2613
|
+
const colorUsage = foundations?.colorUsage || {};
|
|
2614
|
+
const colorEntries = Object.entries(colors)
|
|
2615
|
+
.filter(([k]) => k !== "_dark" && !k.startsWith("arbitrary-") && !k.startsWith("inline-"))
|
|
2616
|
+
.sort((a, b) => {
|
|
2617
|
+
const ua = colorUsage[a[0]]?.total || 0;
|
|
2618
|
+
const ub = colorUsage[b[0]]?.total || 0;
|
|
2619
|
+
return ub - ua;
|
|
2620
|
+
})
|
|
2621
|
+
.slice(0, 20);
|
|
2622
|
+
|
|
2623
|
+
if (colorEntries.length > 0) {
|
|
2624
|
+
lines.push(`## 🎨 Color Tokens — Never hardcode hex values`);
|
|
2625
|
+
lines.push(`> Use Tailwind utility classes: \`bg-{token}\`, \`text-{token}\`, \`border-{token}\``);
|
|
2626
|
+
lines.push(``);
|
|
2627
|
+
lines.push(`| Token | Value | Usages |`);
|
|
2628
|
+
lines.push(`|-------|-------|--------|`);
|
|
2629
|
+
for (const [token, val] of colorEntries) {
|
|
2630
|
+
const hex = typeof val === "string" ? val : (val?.DEFAULT || val?.value || "");
|
|
2631
|
+
const usage = colorUsage[token]?.total || 0;
|
|
2632
|
+
lines.push(`| \`${token}\` | ${hex} | ${usage > 0 ? `×${usage}` : "—"} |`);
|
|
2633
|
+
}
|
|
2634
|
+
lines.push(``);
|
|
2635
|
+
lines.push(`---`);
|
|
2636
|
+
lines.push(``);
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// ── Typography ────────────────────────────────────────────────────────────
|
|
2640
|
+
const typo = foundations?.typography || {};
|
|
2641
|
+
if (typo.fontFamily && Object.keys(typo.fontFamily).length > 0) {
|
|
2642
|
+
lines.push(`## 🔤 Typography`);
|
|
2643
|
+
const families = Object.entries(typo.fontFamily).map(([k, v]) => `${k}: ${Array.isArray(v) ? v[0] : v}`).join(" · ");
|
|
2644
|
+
lines.push(`Font: ${families}`);
|
|
2645
|
+
lines.push(``);
|
|
2646
|
+
lines.push(`---`);
|
|
2647
|
+
lines.push(``);
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// ── Spacing / Grid ────────────────────────────────────────────────────────
|
|
2651
|
+
const gridSystem = foundations?.gridSystem;
|
|
2652
|
+
if (gridSystem) {
|
|
2653
|
+
lines.push(`## 📐 Spacing & Layout (detected in this project)`);
|
|
2654
|
+
if (Object.keys(gaps).length > 0) {
|
|
2655
|
+
const gapLine = Object.entries(gaps).sort((a,b)=>b[1]-a[1]).slice(0,8)
|
|
2656
|
+
.map(([v, c]) => `\`gap-${v}\` ×${c}`).join(" · ");
|
|
2657
|
+
lines.push(`**Common gaps:** ${gapLine}`);
|
|
2658
|
+
}
|
|
2659
|
+
if (Object.keys(gridSystem.gridCols || {}).length > 0) {
|
|
2660
|
+
const colLine = Object.entries(gridSystem.gridCols).sort((a,b)=>b[1].count-a[1].count).slice(0,6)
|
|
2661
|
+
.map(([v, d]) => `\`grid-cols-${v}\` ×${d.count}`).join(" · ");
|
|
2662
|
+
lines.push(`**Grid columns:** ${colLine}`);
|
|
2663
|
+
}
|
|
2664
|
+
if (Object.keys(gridSystem.maxWidths || {}).length > 0) {
|
|
2665
|
+
const mwLine = Object.entries(gridSystem.maxWidths).slice(0,5)
|
|
2666
|
+
.map(([v, c]) => `\`max-w-${v}\` ×${c}`).join(" · ");
|
|
2667
|
+
lines.push(`**Container widths:** ${mwLine}`);
|
|
2668
|
+
}
|
|
2669
|
+
if (bpUsed.length > 0) {
|
|
2670
|
+
const bpLine = bpUsed.map(bp => {
|
|
2671
|
+
const d = gridSystem.breakpoints[bp];
|
|
2672
|
+
return `\`${bp}:\` ×${d.count}`;
|
|
2673
|
+
}).join(" · ");
|
|
2674
|
+
lines.push(`**Active breakpoints:** ${bpLine}`);
|
|
2675
|
+
}
|
|
2676
|
+
lines.push(``);
|
|
2677
|
+
lines.push(`---`);
|
|
2678
|
+
lines.push(``);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// ── Icons ─────────────────────────────────────────────────────────────────
|
|
2682
|
+
const icons = Array.isArray(foundations?.icons) ? foundations.icons : [];
|
|
2683
|
+
const activeIcons = icons.filter(i => i && typeof i === "object" ? i.total > 0 : true);
|
|
2684
|
+
if (activeIcons.length > 0) {
|
|
2685
|
+
lines.push(`## 🎯 Icons — \`import { Name } from "lucide-react"\``);
|
|
2686
|
+
lines.push(`> ${activeIcons.length} icons active in this project. See Foundations/Icons in Storybook for full list.`);
|
|
2687
|
+
lines.push(``);
|
|
2688
|
+
const top = activeIcons.slice(0, 16);
|
|
2689
|
+
const iconLine = top.map(i => {
|
|
2690
|
+
const n = typeof i === "string" ? i : i.name;
|
|
2691
|
+
const t = typeof i === "object" ? i.total : 0;
|
|
2692
|
+
return t > 0 ? `\`${n}\` ×${t}` : `\`${n}\``;
|
|
2693
|
+
}).join(" · ");
|
|
2694
|
+
lines.push(`**Most used:** ${iconLine}`);
|
|
2695
|
+
if (icons.length > top.length) lines.push(`*(+ ${icons.length - top.length} more)*`);
|
|
2696
|
+
lines.push(``);
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
const cursorDir = path.join(PROJECT_ROOT, ".cursor");
|
|
2700
|
+
if (!fs.existsSync(cursorDir)) fs.mkdirSync(cursorDir, { recursive: true });
|
|
2701
|
+
const outPath = path.join(cursorDir, "rules");
|
|
2702
|
+
fs.writeFileSync(outPath, lines.join("\n"), "utf-8");
|
|
2703
|
+
console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, outPath));
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2489
2706
|
function main() {
|
|
2490
2707
|
if (!fs.existsSync(VDS_OUTPUT)) {
|
|
2491
2708
|
console.error("[VDS] vds-output.json not found. Run `npm run vds` first.");
|
|
@@ -2503,6 +2720,7 @@ function main() {
|
|
|
2503
2720
|
ensureDir(STORIES_DIR);
|
|
2504
2721
|
ensureDir(path.join(STORIES_DIR, "foundations"));
|
|
2505
2722
|
writeFoundationsStories(foundations);
|
|
2723
|
+
writeCursorRules(components, foundations);
|
|
2506
2724
|
const componentSuggestions = data.componentSuggestions;
|
|
2507
2725
|
if (componentSuggestions?.length) {
|
|
2508
2726
|
writeComponentSuggestionsStory(componentSuggestions);
|