vibe-design-system 2.8.21 → 2.8.22

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.21",
3
+ "version": "2.8.22",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -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
- results.push({ file: rel, name, group, category, description, tokens });
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
  }
@@ -2486,6 +2486,195 @@ function writeChangelogStory(changelog) {
2486
2486
  console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Changelog.stories.tsx")));
2487
2487
  }
2488
2488
 
2489
+ /** Generate .cursor/rules — structured design system context for AI agents.
2490
+ * Components are split into tiers so the agent knows what to reuse vs reference.
2491
+ * Foundations (colors, icons, spacing, breakpoints) are inlined as tables/lists.
2492
+ * The file is written to PROJECT_ROOT/.cursor/rules so Cursor injects it automatically. */
2493
+ function writeCursorRules(components, foundations) {
2494
+ const lines = [];
2495
+ const now = new Date().toISOString().slice(0, 10);
2496
+
2497
+ lines.push(`# VDS Design System — Agent Context`);
2498
+ lines.push(`> Auto-generated by VDS on ${now}. Do not edit — regenerated on every \`npm run vds\`.`);
2499
+ lines.push(``);
2500
+
2501
+ // ── Ground rules ──────────────────────────────────────────────────────────
2502
+ const gaps = foundations?.gridSystem?.gaps || {};
2503
+ const topGaps = Object.entries(gaps).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([v]) => `gap-${v}`).join(", ");
2504
+ const bpUsed = Object.keys(foundations?.gridSystem?.breakpoints || {});
2505
+ const bpAll = ["sm","md","lg","xl","2xl"];
2506
+ const bpUnused = bpAll.filter(b => !bpUsed.includes(b));
2507
+
2508
+ lines.push(`## ⚙️ Rules`);
2509
+ lines.push(`1. Before creating a new component, check the component lists below.`);
2510
+ lines.push(`2. **Never hardcode colors or hex values** — use the token table.`);
2511
+ lines.push(`3. **Always import icons from \`lucide-react\`** — never other icon libraries.`);
2512
+ if (topGaps) lines.push(`4. **Spacing convention**: ${topGaps} are the standard gaps in this project.`);
2513
+ if (bpUsed.length) lines.push(`5. **Breakpoints in use**: ${bpUsed.map(b=>`\`${b}:\``).join(" ")} — prefer these.`);
2514
+ if (bpUnused.length) lines.push(`6. **Breakpoints NOT used**: ${bpUnused.map(b=>`\`${b}:\``).join(" ")} — avoid unless explicitly needed.`);
2515
+ lines.push(``);
2516
+ lines.push(`---`);
2517
+ lines.push(``);
2518
+
2519
+ // ── Component tiers ───────────────────────────────────────────────────────
2520
+ const byTier = { primitive: [], component: [], feature: [], page: [] };
2521
+ for (const c of components) {
2522
+ const t = c.tier || "component";
2523
+ if (byTier[t]) byTier[t].push(c);
2524
+ }
2525
+
2526
+ // Tier 1 — Primitives
2527
+ if (byTier.primitive.length > 0) {
2528
+ lines.push(`## 🧱 Primitives — Use first for all UI elements`);
2529
+ lines.push(`> Atomic components from \`src/components/ui/\`. Always prefer over building from scratch.`);
2530
+ lines.push(``);
2531
+ for (const c of byTier.primitive.sort((a, b) => a.name.localeCompare(b.name))) {
2532
+ let entry = `- **${c.name}** \`${c.file}\``;
2533
+ if (c.variants) {
2534
+ const parts = Object.entries(c.variants).map(([k, vs]) => `${k}: ${vs.join(" | ")}`);
2535
+ entry += ` — ${parts.join(" · ")}`;
2536
+ }
2537
+ lines.push(entry);
2538
+ }
2539
+ lines.push(``);
2540
+ lines.push(`---`);
2541
+ lines.push(``);
2542
+ }
2543
+
2544
+ // Tier 2 — Components
2545
+ if (byTier.component.length > 0) {
2546
+ lines.push(`## 🔷 Components — Reusable domain components`);
2547
+ lines.push(`> Project-specific, small, focused. Use before creating new ones.`);
2548
+ lines.push(``);
2549
+ for (const c of byTier.component.sort((a, b) => a.name.localeCompare(b.name))) {
2550
+ let entry = `- **${c.name}** \`${c.file}\``;
2551
+ if (c.description) entry += ` — ${c.description}`;
2552
+ lines.push(entry);
2553
+ }
2554
+ lines.push(``);
2555
+ lines.push(`---`);
2556
+ lines.push(``);
2557
+ }
2558
+
2559
+ // Tier 3 — Features
2560
+ if (byTier.feature.length > 0) {
2561
+ lines.push(`## ⚙️ Feature Components — Extract patterns, don't import whole`);
2562
+ lines.push(`> Complex components with internal state. Reuse sub-patterns, not the whole file.`);
2563
+ lines.push(``);
2564
+ for (const c of byTier.feature.sort((a, b) => a.name.localeCompare(b.name))) {
2565
+ lines.push(`- **${c.name}** \`${c.file}\``);
2566
+ }
2567
+ lines.push(``);
2568
+ lines.push(`---`);
2569
+ lines.push(``);
2570
+ }
2571
+
2572
+ // Tier 4 — Pages (just names, no need to list file paths)
2573
+ if (byTier.page.length > 0) {
2574
+ lines.push(`## 📄 Pages & Views — Reference only`);
2575
+ lines.push(`> Full page views. Study for patterns but do not import into new features.`);
2576
+ lines.push(``);
2577
+ lines.push(byTier.page.map(c => c.name).sort().join(", "));
2578
+ lines.push(``);
2579
+ lines.push(`---`);
2580
+ lines.push(``);
2581
+ }
2582
+
2583
+ // ── Color tokens ──────────────────────────────────────────────────────────
2584
+ const colors = foundations?.colors || {};
2585
+ const colorUsage = foundations?.colorUsage || {};
2586
+ const colorEntries = Object.entries(colors)
2587
+ .filter(([k]) => k !== "_dark" && !k.startsWith("arbitrary-") && !k.startsWith("inline-"))
2588
+ .sort((a, b) => {
2589
+ const ua = colorUsage[a[0]]?.total || 0;
2590
+ const ub = colorUsage[b[0]]?.total || 0;
2591
+ return ub - ua;
2592
+ })
2593
+ .slice(0, 20);
2594
+
2595
+ if (colorEntries.length > 0) {
2596
+ lines.push(`## 🎨 Color Tokens — Never hardcode hex values`);
2597
+ lines.push(`> Use Tailwind utility classes: \`bg-{token}\`, \`text-{token}\`, \`border-{token}\``);
2598
+ lines.push(``);
2599
+ lines.push(`| Token | Value | Usages |`);
2600
+ lines.push(`|-------|-------|--------|`);
2601
+ for (const [token, val] of colorEntries) {
2602
+ const hex = typeof val === "string" ? val : (val?.DEFAULT || val?.value || "");
2603
+ const usage = colorUsage[token]?.total || 0;
2604
+ lines.push(`| \`${token}\` | ${hex} | ${usage > 0 ? `×${usage}` : "—"} |`);
2605
+ }
2606
+ lines.push(``);
2607
+ lines.push(`---`);
2608
+ lines.push(``);
2609
+ }
2610
+
2611
+ // ── Typography ────────────────────────────────────────────────────────────
2612
+ const typo = foundations?.typography || {};
2613
+ if (typo.fontFamily && Object.keys(typo.fontFamily).length > 0) {
2614
+ lines.push(`## 🔤 Typography`);
2615
+ const families = Object.entries(typo.fontFamily).map(([k, v]) => `${k}: ${Array.isArray(v) ? v[0] : v}`).join(" · ");
2616
+ lines.push(`Font: ${families}`);
2617
+ lines.push(``);
2618
+ lines.push(`---`);
2619
+ lines.push(``);
2620
+ }
2621
+
2622
+ // ── Spacing / Grid ────────────────────────────────────────────────────────
2623
+ const gridSystem = foundations?.gridSystem;
2624
+ if (gridSystem) {
2625
+ lines.push(`## 📐 Spacing & Layout (detected in this project)`);
2626
+ if (Object.keys(gaps).length > 0) {
2627
+ const gapLine = Object.entries(gaps).sort((a,b)=>b[1]-a[1]).slice(0,8)
2628
+ .map(([v, c]) => `\`gap-${v}\` ×${c}`).join(" · ");
2629
+ lines.push(`**Common gaps:** ${gapLine}`);
2630
+ }
2631
+ if (Object.keys(gridSystem.gridCols || {}).length > 0) {
2632
+ const colLine = Object.entries(gridSystem.gridCols).sort((a,b)=>b[1].count-a[1].count).slice(0,6)
2633
+ .map(([v, d]) => `\`grid-cols-${v}\` ×${d.count}`).join(" · ");
2634
+ lines.push(`**Grid columns:** ${colLine}`);
2635
+ }
2636
+ if (Object.keys(gridSystem.maxWidths || {}).length > 0) {
2637
+ const mwLine = Object.entries(gridSystem.maxWidths).slice(0,5)
2638
+ .map(([v, c]) => `\`max-w-${v}\` ×${c}`).join(" · ");
2639
+ lines.push(`**Container widths:** ${mwLine}`);
2640
+ }
2641
+ if (bpUsed.length > 0) {
2642
+ const bpLine = bpUsed.map(bp => {
2643
+ const d = gridSystem.breakpoints[bp];
2644
+ return `\`${bp}:\` ×${d.count}`;
2645
+ }).join(" · ");
2646
+ lines.push(`**Active breakpoints:** ${bpLine}`);
2647
+ }
2648
+ lines.push(``);
2649
+ lines.push(`---`);
2650
+ lines.push(``);
2651
+ }
2652
+
2653
+ // ── Icons ─────────────────────────────────────────────────────────────────
2654
+ const icons = Array.isArray(foundations?.icons) ? foundations.icons : [];
2655
+ const activeIcons = icons.filter(i => i && typeof i === "object" ? i.total > 0 : true);
2656
+ if (activeIcons.length > 0) {
2657
+ lines.push(`## 🎯 Icons — \`import { Name } from "lucide-react"\``);
2658
+ lines.push(`> ${activeIcons.length} icons active in this project. See Foundations/Icons in Storybook for full list.`);
2659
+ lines.push(``);
2660
+ const top = activeIcons.slice(0, 16);
2661
+ const iconLine = top.map(i => {
2662
+ const n = typeof i === "string" ? i : i.name;
2663
+ const t = typeof i === "object" ? i.total : 0;
2664
+ return t > 0 ? `\`${n}\` ×${t}` : `\`${n}\``;
2665
+ }).join(" · ");
2666
+ lines.push(`**Most used:** ${iconLine}`);
2667
+ if (icons.length > top.length) lines.push(`*(+ ${icons.length - top.length} more)*`);
2668
+ lines.push(``);
2669
+ }
2670
+
2671
+ const cursorDir = path.join(PROJECT_ROOT, ".cursor");
2672
+ if (!fs.existsSync(cursorDir)) fs.mkdirSync(cursorDir, { recursive: true });
2673
+ const outPath = path.join(cursorDir, "rules");
2674
+ fs.writeFileSync(outPath, lines.join("\n"), "utf-8");
2675
+ console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, outPath));
2676
+ }
2677
+
2489
2678
  function main() {
2490
2679
  if (!fs.existsSync(VDS_OUTPUT)) {
2491
2680
  console.error("[VDS] vds-output.json not found. Run `npm run vds` first.");
@@ -2503,6 +2692,7 @@ function main() {
2503
2692
  ensureDir(STORIES_DIR);
2504
2693
  ensureDir(path.join(STORIES_DIR, "foundations"));
2505
2694
  writeFoundationsStories(foundations);
2695
+ writeCursorRules(components, foundations);
2506
2696
  const componentSuggestions = data.componentSuggestions;
2507
2697
  if (componentSuggestions?.length) {
2508
2698
  writeComponentSuggestionsStory(componentSuggestions);