vibe-design-system 2.8.20 → 2.8.21

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.20",
3
+ "version": "2.8.21",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -447,30 +447,163 @@ function extractBrandAssets() {
447
447
  return assets;
448
448
  }
449
449
 
450
- /** Extract icon names from `import { A, B, C } from "lucide-react"` in app code only (exclude stories so generated Star/defaults don't pollute). */
450
+ /** Extract Lucide icons used in app code with per-component JSX usage counts.
451
+ * Returns { name, total, topFiles }[] sorted by total desc.
452
+ * Handles aliased imports: `import { ArrowRight as Arrow }` — counted under original name. */
451
453
  function extractLucideIconsUsed(srcDir) {
452
454
  const allFiles = getAllTsxJsxInDir(srcDir);
453
455
  const files = allFiles.filter((rel) => !/^stories[/\\]/.test(rel.replace(/\\/g, "/")));
454
- const names = new Set();
456
+
457
+ // originalName → { total: number, topFiles: Map<componentName, count> }
458
+ const iconData = new Map();
455
459
  const importRe = /import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/g;
460
+
456
461
  for (const rel of files) {
457
462
  const fullPath = path.join(srcDir, rel);
458
- try {
459
- const content = fs.readFileSync(fullPath, "utf-8");
460
- let m;
461
- while ((m = importRe.exec(content)) !== null) {
462
- const block = m[1];
463
- block.split(",").forEach((part) => {
464
- const trimmed = part.trim();
465
- const asMatch = trimmed.match(/^(\w+)\s+as\s+/);
466
- const name = asMatch ? asMatch[1] : trimmed.split(/\s+/)[0];
467
- if (name && /^[A-Z][a-zA-Z0-9]*$/.test(name)) names.add(name);
468
- });
469
- }
470
- } catch (_) {}
463
+ let content;
464
+ try { content = fs.readFileSync(fullPath, "utf-8"); } catch (_) { continue; }
465
+ const componentName = path.basename(rel).replace(/\.(tsx|jsx|ts|js)$/, "");
466
+
467
+ // Pass 1 — collect local-name → original-name for this file
471
468
  importRe.lastIndex = 0;
469
+ let m;
470
+ const localToOriginal = new Map();
471
+ while ((m = importRe.exec(content)) !== null) {
472
+ m[1].split(",").forEach((part) => {
473
+ const trimmed = part.trim();
474
+ if (!trimmed) return;
475
+ const asMatch = trimmed.match(/^(\w+)\s+as\s+(\w+)$/);
476
+ if (asMatch) {
477
+ const orig = asMatch[1];
478
+ const local = asMatch[2];
479
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(orig)) {
480
+ localToOriginal.set(local, orig);
481
+ if (!iconData.has(orig)) iconData.set(orig, { total: 0, topFiles: new Map() });
482
+ }
483
+ } else {
484
+ const name = trimmed.split(/\s+/)[0];
485
+ if (name && /^[A-Z][a-zA-Z0-9]*$/.test(name)) {
486
+ localToOriginal.set(name, name);
487
+ if (!iconData.has(name)) iconData.set(name, { total: 0, topFiles: new Map() });
488
+ }
489
+ }
490
+ });
491
+ }
492
+
493
+ // Pass 2 — count JSX usages `<LocalName ` / `<LocalName/` / `<LocalName>`
494
+ for (const [localName, originalName] of localToOriginal.entries()) {
495
+ const jsxRe = new RegExp(`<${localName}[\\s/>]`, "g");
496
+ let count = 0;
497
+ while (jsxRe.exec(content) !== null) count++;
498
+ if (count > 0) {
499
+ const data = iconData.get(originalName);
500
+ data.total += count;
501
+ data.topFiles.set(componentName, (data.topFiles.get(componentName) || 0) + count);
502
+ }
503
+ }
472
504
  }
473
- return [...names].sort();
505
+
506
+ return [...iconData.entries()]
507
+ .map(([name, data]) => ({
508
+ name,
509
+ total: data.total,
510
+ topFiles: [...data.topFiles.entries()]
511
+ .sort((a, b) => b[1] - a[1])
512
+ .slice(0, 5)
513
+ .map(([n]) => n),
514
+ }))
515
+ .sort((a, b) => b.total - a.total || a.name.localeCompare(b.name));
516
+ }
517
+
518
+ /** Scan app source for responsive breakpoints, grid column patterns, gaps and max-width usage.
519
+ * Returns { breakpoints, gridCols, gaps, maxWidths, containerCount } or null if nothing found. */
520
+ function extractGridSystem(srcDir) {
521
+ if (!fs.existsSync(srcDir)) return null;
522
+ const allFiles = getAllTsxJsxInDir(srcDir);
523
+ const files = allFiles.filter((rel) => !/^stories[/\\]/.test(rel.replace(/\\/g, "/")));
524
+ if (files.length === 0) return null;
525
+
526
+ const BP_NAMES = ["sm", "md", "lg", "xl", "2xl"];
527
+ const bpData = {};
528
+ for (const bp of BP_NAMES) bpData[bp] = { count: 0, topFiles: new Map() };
529
+
530
+ const colData = {}; // colValue → { count, topFiles: Map }
531
+ const gapCounts = {};
532
+ const maxWCounts = {};
533
+ let containerCount = 0;
534
+
535
+ for (const rel of files) {
536
+ let content;
537
+ try { content = fs.readFileSync(path.join(srcDir, rel), "utf-8"); } catch (_) { continue; }
538
+ const componentName = path.basename(rel).replace(/\.(tsx|jsx|ts|js)$/, "");
539
+
540
+ // Breakpoints: sm: md: lg: xl: 2xl:
541
+ const bpRe = /\b(2xl|xl|lg|md|sm):/g;
542
+ let m;
543
+ while ((m = bpRe.exec(content)) !== null) {
544
+ const name = m[1];
545
+ bpData[name].count++;
546
+ bpData[name].topFiles.set(componentName, (bpData[name].topFiles.get(componentName) || 0) + 1);
547
+ }
548
+
549
+ // grid-cols-{value}
550
+ const gcRe = /\bgrid-cols-((?:\[[\w\s\-.,/()*+]+\]|\w+))/g;
551
+ while ((m = gcRe.exec(content)) !== null) {
552
+ const val = m[1];
553
+ if (!colData[val]) colData[val] = { count: 0, topFiles: new Map() };
554
+ colData[val].count++;
555
+ colData[val].topFiles.set(componentName, (colData[val].topFiles.get(componentName) || 0) + 1);
556
+ }
557
+
558
+ // gap-{n}, gap-x-{n}, gap-y-{n}
559
+ const gapRe = /\bgap(?:-[xy])?-((?:\[[\w\s./]+\]|\d[\w.]*|px))\b/g;
560
+ while ((m = gapRe.exec(content)) !== null) {
561
+ const val = m[1];
562
+ gapCounts[val] = (gapCounts[val] || 0) + 1;
563
+ }
564
+
565
+ // max-w-{value}
566
+ const maxWRe = /\bmax-w-((?:\[[\w\s./]+\]|[\w-]+))/g;
567
+ while ((m = maxWRe.exec(content)) !== null) {
568
+ const val = m[1];
569
+ maxWCounts[val] = (maxWCounts[val] || 0) + 1;
570
+ }
571
+
572
+ // container class
573
+ let ctrM;
574
+ const ctrRe = /\bcontainer\b/g;
575
+ while ((ctrM = ctrRe.exec(content)) !== null) containerCount++;
576
+ }
577
+
578
+ const breakpoints = {};
579
+ for (const bp of BP_NAMES) {
580
+ if (bpData[bp].count > 0) {
581
+ breakpoints[bp] = {
582
+ count: bpData[bp].count,
583
+ topFiles: [...bpData[bp].topFiles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([n]) => n),
584
+ };
585
+ }
586
+ }
587
+
588
+ const gridCols = {};
589
+ for (const [val, data] of Object.entries(colData).sort((a, b) => b[1].count - a[1].count)) {
590
+ gridCols[val] = {
591
+ count: data.count,
592
+ topFiles: [...data.topFiles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([n]) => n),
593
+ };
594
+ }
595
+
596
+ const gaps = Object.fromEntries(
597
+ Object.entries(gapCounts).sort((a, b) => b[1] - a[1]).slice(0, 12)
598
+ );
599
+
600
+ const maxWidths = Object.fromEntries(
601
+ Object.entries(maxWCounts).sort((a, b) => b[1] - a[1]).slice(0, 10)
602
+ );
603
+
604
+ if (Object.keys(breakpoints).length === 0 && Object.keys(gridCols).length === 0) return null;
605
+
606
+ return { breakpoints, gridCols, gaps, maxWidths, containerCount };
474
607
  }
475
608
 
476
609
  function extractVdsTags(content) {
@@ -2051,6 +2184,8 @@ function scan() {
2051
2184
  const colorNames = Object.keys(foundations.colors || {}).filter((k) => k !== "_dark");
2052
2185
  const colorUsage = extractColorUsage(colorNames);
2053
2186
  if (Object.keys(colorUsage).length > 0) foundations.colorUsage = colorUsage;
2187
+ const gridSystem = extractGridSystem(SRC_DIR);
2188
+ if (gridSystem) foundations.gridSystem = gridSystem;
2054
2189
  const componentSuggestions = extractComponentSuggestions();
2055
2190
  const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
2056
2191
  const output = {
@@ -1600,43 +1600,284 @@ function writeFoundationsStories(foundations) {
1600
1600
  fs.writeFileSync(path.join(foundationsDir, "Brand.stories.tsx"), brandContent, "utf-8");
1601
1601
  console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Brand.stories.tsx")));
1602
1602
 
1603
- const iconNames = Array.isArray(foundations?.icons) ? foundations.icons : [];
1604
- if (iconNames.length > 0) {
1605
- const iconsContent =
1606
- [
1607
- "import type { Meta, StoryObj } from \"@storybook/react\";",
1608
- "import * as Lucide from \"lucide-react\";",
1609
- "",
1610
- "const meta = { title: \"Foundations/Icons\" } satisfies Meta;",
1611
- "export default meta;",
1612
- "type Story = StoryObj;",
1613
- "",
1614
- `const iconNames = ${JSON.stringify(iconNames)};`,
1615
- "",
1616
- "export const Default: Story = {",
1617
- " render: () => (",
1618
- " <div style={{ padding: 24 }}>",
1619
- " <p style={{ marginBottom: 16, color: \"#888\", fontSize: 14 }}>Icons imported from lucide-react in app code (src/, excluding stories).</p>",
1620
- " <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(100px, 1fr))\", gap: 16 }}>",
1621
- " {iconNames.map((name) => {",
1622
- " const Icon = Lucide[name];",
1623
- " if (!Icon) return null;",
1624
- " return (",
1625
- " <div key={name} style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", gap: 8, padding: 12, border: \"1px solid #333\", borderRadius: 8 }}>",
1626
- " <Icon size={24} />",
1627
- " <span style={{ fontSize: 11, color: \"#888\" }}>{name}</span>",
1628
- " </div>",
1629
- " );",
1630
- " })}",
1631
- " </div>",
1632
- " </div>",
1633
- " ),",
1634
- "};",
1635
- ].join("\n");
1603
+ const rawIcons = Array.isArray(foundations?.icons) ? foundations.icons : [];
1604
+ if (rawIcons.length > 0) {
1605
+ // Support both old format (string[]) and new format ({ name, total, topFiles }[])
1606
+ const iconData = rawIcons[0] && typeof rawIcons[0] === "object"
1607
+ ? rawIcons
1608
+ : rawIcons.map((name) => ({ name, total: 0, topFiles: [] }));
1609
+
1610
+ const iconsContent = [
1611
+ "import type { Meta, StoryObj } from \"@storybook/react\";",
1612
+ "import * as Lucide from \"lucide-react\";",
1613
+ "",
1614
+ "const meta = { title: \"Foundations/Icons\" } satisfies Meta;",
1615
+ "export default meta;",
1616
+ "type Story = StoryObj;",
1617
+ "",
1618
+ `const iconData = ${JSON.stringify(iconData, null, 2)};`,
1619
+ "",
1620
+ "export const Default: Story = {",
1621
+ " render: () => {",
1622
+ " const usedIcons = iconData.filter((d) => d.total > 0);",
1623
+ " const unusedIcons = iconData.filter((d) => d.total === 0);",
1624
+ " const renderCard = (d: { name: string; total: number; topFiles: string[] }) => {",
1625
+ " const Icon = (Lucide as Record<string, any>)[d.name];",
1626
+ " if (!Icon) return null;",
1627
+ " return (",
1628
+ " <div key={d.name} style={{ display: \"flex\", flexDirection: \"column\", gap: 8, padding: 14,",
1629
+ " border: \"1px solid #1e293b\", borderRadius: 10, background: \"#0f172a\" }}>",
1630
+ " <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"flex-start\" }}>",
1631
+ " <Icon size={22} strokeWidth={1.5} />",
1632
+ " {d.total > 0 && (",
1633
+ " <span style={{ fontSize: 11, fontWeight: 600, background: \"#1e293b\",",
1634
+ " color: \"#94a3b8\", padding: \"2px 7px\", borderRadius: 999 }}>",
1635
+ " ×{d.total}",
1636
+ " </span>",
1637
+ " )}",
1638
+ " </div>",
1639
+ " <span style={{ fontSize: 12, fontWeight: 500, color: \"#e2e8f0\" }}>{d.name}</span>",
1640
+ " {d.topFiles.length > 0 && (",
1641
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 4, marginTop: 2 }}>",
1642
+ " {d.topFiles.map((f) => (",
1643
+ " <span key={f} style={{ fontSize: 10, background: \"#1e293b\", color: \"#64748b\",",
1644
+ " padding: \"1px 6px\", borderRadius: 4 }}>{f}</span>",
1645
+ " ))}",
1646
+ " </div>",
1647
+ " )}",
1648
+ " </div>",
1649
+ " );",
1650
+ " };",
1651
+ " return (",
1652
+ " <div style={{ padding: 24, fontFamily: \"sans-serif\" }}>",
1653
+ " <div style={{ marginBottom: 24 }}>",
1654
+ " <p style={{ margin: 0, marginBottom: 4, fontSize: 14, color: \"#94a3b8\" }}>",
1655
+ " {iconData.length} icons imported from <code style={{ fontSize: 12 }}>lucide-react</code> in app code.",
1656
+ " </p>",
1657
+ " <p style={{ margin: 0, fontSize: 12, color: \"#475569\" }}>",
1658
+ " Sorted by usage frequency · Badge shows total JSX usages · Chips show top components.",
1659
+ " </p>",
1660
+ " </div>",
1661
+ " {usedIcons.length > 0 && (",
1662
+ " <>",
1663
+ " <p style={{ margin: \"0 0 12px\", fontSize: 12, fontWeight: 600, color: \"#64748b\",",
1664
+ " textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>",
1665
+ " Active — {usedIcons.length}",
1666
+ " </p>",
1667
+ " <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(160px, 1fr))\",",
1668
+ " gap: 10, marginBottom: 32 }}>",
1669
+ " {usedIcons.map(renderCard)}",
1670
+ " </div>",
1671
+ " </>",
1672
+ " )}",
1673
+ " {unusedIcons.length > 0 && (",
1674
+ " <>",
1675
+ " <p style={{ margin: \"0 0 12px\", fontSize: 12, fontWeight: 600, color: \"#334155\",",
1676
+ " textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>",
1677
+ " Imported (not used in JSX) — {unusedIcons.length}",
1678
+ " </p>",
1679
+ " <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(140px, 1fr))\",",
1680
+ " gap: 8, opacity: 0.45 }}>",
1681
+ " {unusedIcons.map(renderCard)}",
1682
+ " </div>",
1683
+ " </>",
1684
+ " )}",
1685
+ " </div>",
1686
+ " );",
1687
+ " },",
1688
+ "};",
1689
+ ].join("\n");
1636
1690
  fs.writeFileSync(path.join(foundationsDir, "Icons.stories.tsx"), iconsContent, "utf-8");
1637
1691
  console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Icons.stories.tsx")));
1638
1692
  }
1639
1693
 
1694
+ const gridSystem = foundations?.gridSystem;
1695
+ if (gridSystem && (Object.keys(gridSystem.breakpoints || {}).length > 0 || Object.keys(gridSystem.gridCols || {}).length > 0)) {
1696
+ const bpEntries = Object.entries(gridSystem.breakpoints || {});
1697
+ const colEntries = Object.entries(gridSystem.gridCols || {});
1698
+ const gapEntries = Object.entries(gridSystem.gaps || {});
1699
+ const maxWEntries = Object.entries(gridSystem.maxWidths || {});
1700
+ const containerCount = gridSystem.containerCount || 0;
1701
+
1702
+ const gridContent = [
1703
+ "import type { Meta, StoryObj } from \"@storybook/react\";",
1704
+ "",
1705
+ "const meta = { title: \"Foundations/Grid\" } satisfies Meta;",
1706
+ "export default meta;",
1707
+ "type Story = StoryObj;",
1708
+ "",
1709
+ `const gridSystem = ${JSON.stringify(gridSystem, null, 2)};`,
1710
+ "",
1711
+ "const BP_CONFIG: Record<string, { px: number; label: string; color: string }> = {",
1712
+ " sm: { px: 640, label: \"Small\", color: \"#6366f1\" },",
1713
+ " md: { px: 768, label: \"Medium\", color: \"#8b5cf6\" },",
1714
+ " lg: { px: 1024, label: \"Large\", color: \"#a78bfa\" },",
1715
+ " xl: { px: 1280, label: \"Extra Large\", color: \"#c4b5fd\" },",
1716
+ " \"2xl\": { px: 1536, label: \"2× Extra Large\", color: \"#e9d5ff\" },",
1717
+ "};",
1718
+ "const ALL_BPS = [\"sm\", \"md\", \"lg\", \"xl\", \"2xl\"];",
1719
+ "",
1720
+ "export const Default: Story = {",
1721
+ " render: () => {",
1722
+ " const colEntries = Object.entries(gridSystem.gridCols || {});",
1723
+ " const gapEntries = Object.entries(gridSystem.gaps || {});",
1724
+ " const maxWEntries = Object.entries(gridSystem.maxWidths || {});",
1725
+ " const chip = (label: string) => (",
1726
+ " <span key={label} style={{ fontSize: 10, background: \"#1e293b\", color: \"#64748b\", padding: \"1px 6px\", borderRadius: 4 }}>{label}</span>",
1727
+ " );",
1728
+ " const renderColPreview = (colVal: string) => {",
1729
+ " const n = parseInt(colVal, 10);",
1730
+ " if (!isNaN(n) && n >= 1 && n <= 12) {",
1731
+ " return (",
1732
+ " <div style={{ display: \"grid\", gridTemplateColumns: `repeat(${n}, 1fr)`, gap: 2, marginBottom: 8 }}>",
1733
+ " {Array.from({ length: n }).map((_, i) => (",
1734
+ " <div key={i} style={{ height: 20, borderRadius: 3, background: \"#1e3a5f\" }} />",
1735
+ " ))}",
1736
+ " </div>",
1737
+ " );",
1738
+ " }",
1739
+ " return (",
1740
+ " <div style={{ fontFamily: \"monospace\", fontSize: 10, color: \"#475569\", marginBottom: 8, background: \"#0a0f1a\", padding: \"4px 6px\", borderRadius: 4 }}>",
1741
+ " grid-cols-{colVal}",
1742
+ " </div>",
1743
+ " );",
1744
+ " };",
1745
+ " return (",
1746
+ " <div style={{ padding: 24, fontFamily: \"sans-serif\", maxWidth: 900 }}>",
1747
+ "",
1748
+ " {/* ── Breakpoints ── */}",
1749
+ " <section style={{ marginBottom: 40 }}>",
1750
+ " <p style={{ margin: \"0 0 4px\", fontSize: 12, fontWeight: 600, color: \"#64748b\", textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>",
1751
+ " Responsive Breakpoints",
1752
+ " </p>",
1753
+ " <p style={{ margin: \"0 0 14px\", fontSize: 12, color: \"#475569\" }}>",
1754
+ " Tailwind CSS breakpoint prefixes detected in source.",
1755
+ " </p>",
1756
+ " <div style={{ position: \"relative\", height: 28, marginBottom: 14, background: \"#0a0f1a\", borderRadius: 8, border: \"1px solid #1e293b\", overflow: \"hidden\" }}>",
1757
+ " {ALL_BPS.map((bp) => {",
1758
+ " const cfg = BP_CONFIG[bp];",
1759
+ " const used = !!(gridSystem.breakpoints as any)?.[bp];",
1760
+ " const leftPct = (cfg.px / 1600) * 100;",
1761
+ " return (",
1762
+ " <div key={bp} style={{ position: \"absolute\", left: `${leftPct}%`, top: 0, bottom: 0,",
1763
+ " borderLeft: `2px solid ${used ? cfg.color : \"#1e293b\"}`,",
1764
+ " display: \"flex\", alignItems: \"center\", paddingLeft: 4 }}>",
1765
+ " <span style={{ fontSize: 9, color: used ? cfg.color : \"#334155\",",
1766
+ " whiteSpace: \"nowrap\", fontWeight: 600 }}>{bp}</span>",
1767
+ " </div>",
1768
+ " );",
1769
+ " })}",
1770
+ " </div>",
1771
+ " <div style={{ borderRadius: 8, border: \"1px solid #1e293b\", overflow: \"hidden\" }}>",
1772
+ " <table style={{ width: \"100%\", borderCollapse: \"collapse\", fontSize: 12 }}>",
1773
+ " <thead>",
1774
+ " <tr style={{ background: \"#0a0f1a\", borderBottom: \"1px solid #1e293b\" }}>",
1775
+ " <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#64748b\", fontWeight: 600 }}>Prefix</th>",
1776
+ " <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#64748b\", fontWeight: 600 }}>Min-width</th>",
1777
+ " <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#64748b\", fontWeight: 600 }}>Usages</th>",
1778
+ " <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#64748b\", fontWeight: 600 }}>Top components</th>",
1779
+ " </tr>",
1780
+ " </thead>",
1781
+ " <tbody>",
1782
+ " {ALL_BPS.map((bp, i) => {",
1783
+ " const cfg = BP_CONFIG[bp];",
1784
+ " const data = (gridSystem.breakpoints as any)?.[bp];",
1785
+ " return (",
1786
+ " <tr key={bp} style={{ borderBottom: i < 4 ? \"1px solid #1e293b\" : \"none\", opacity: data ? 1 : 0.3 }}>",
1787
+ " <td style={{ padding: \"8px 12px\" }}>",
1788
+ " <code style={{ fontSize: 12, fontWeight: 700, color: data ? cfg.color : \"#475569\" }}>{bp}:</code>",
1789
+ " </td>",
1790
+ " <td style={{ padding: \"8px 12px\", color: \"#94a3b8\", fontFamily: \"monospace\" }}>{cfg.px}px</td>",
1791
+ " <td style={{ padding: \"8px 12px\", color: data ? \"#e2e8f0\" : \"#334155\" }}>",
1792
+ " {data ? `×${data.count}` : \"—\"}",
1793
+ " </td>",
1794
+ " <td style={{ padding: \"8px 12px\" }}>",
1795
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 4 }}>",
1796
+ " {(data?.topFiles || []).map(chip)}",
1797
+ " </div>",
1798
+ " </td>",
1799
+ " </tr>",
1800
+ " );",
1801
+ " })}",
1802
+ " </tbody>",
1803
+ " </table>",
1804
+ " </div>",
1805
+ " </section>",
1806
+ "",
1807
+ " {/* ── Grid Columns ── */}",
1808
+ " {colEntries.length > 0 && (",
1809
+ " <section style={{ marginBottom: 40 }}>",
1810
+ " <p style={{ margin: \"0 0 4px\", fontSize: 12, fontWeight: 600, color: \"#64748b\", textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>",
1811
+ " Grid Columns",
1812
+ " </p>",
1813
+ " <p style={{ margin: \"0 0 14px\", fontSize: 12, color: \"#475569\" }}>",
1814
+ " grid-cols-* patterns detected in source, sorted by usage.",
1815
+ " </p>",
1816
+ " <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(180px, 1fr))\", gap: 12 }}>",
1817
+ " {(colEntries as [string, { count: number; topFiles: string[] }][]).map(([val, data]) => (",
1818
+ " <div key={val} style={{ background: \"#0a0f1a\", border: \"1px solid #1e293b\", borderRadius: 8, padding: 12 }}>",
1819
+ " {renderColPreview(val)}",
1820
+ " <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", marginBottom: 6 }}>",
1821
+ " <code style={{ fontSize: 11, color: \"#a78bfa\" }}>grid-cols-{val}</code>",
1822
+ " <span style={{ fontSize: 11, fontWeight: 600, background: \"#1e293b\",",
1823
+ " color: \"#94a3b8\", padding: \"1px 6px\", borderRadius: 999 }}>×{data.count}</span>",
1824
+ " </div>",
1825
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 4 }}>",
1826
+ " {data.topFiles.map(chip)}",
1827
+ " </div>",
1828
+ " </div>",
1829
+ " ))}",
1830
+ " </div>",
1831
+ " </section>",
1832
+ " )}",
1833
+ "",
1834
+ " {/* ── Gaps ── */}",
1835
+ " {gapEntries.length > 0 && (",
1836
+ " <section style={{ marginBottom: 40 }}>",
1837
+ " <p style={{ margin: \"0 0 4px\", fontSize: 12, fontWeight: 600, color: \"#64748b\", textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>",
1838
+ " Common Gaps",
1839
+ " </p>",
1840
+ " <p style={{ margin: \"0 0 12px\", fontSize: 12, color: \"#475569\" }}>gap-* values across all components.</p>",
1841
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 8 }}>",
1842
+ " {(gapEntries as [string, number][]).map(([val, count]) => (",
1843
+ " <span key={val} style={{ display: \"flex\", alignItems: \"center\", gap: 6,",
1844
+ " background: \"#0a0f1a\", border: \"1px solid #1e293b\", borderRadius: 6, padding: \"5px 10px\", fontSize: 12 }}>",
1845
+ " <code style={{ color: \"#67e8f9\" }}>gap-{val}</code>",
1846
+ " <span style={{ color: \"#475569\", fontSize: 11 }}>×{count}</span>",
1847
+ " </span>",
1848
+ " ))}",
1849
+ " </div>",
1850
+ " </section>",
1851
+ " )}",
1852
+ "",
1853
+ " {/* ── Container / Max-widths ── */}",
1854
+ " {maxWEntries.length > 0 && (",
1855
+ " <section>",
1856
+ " <p style={{ margin: \"0 0 4px\", fontSize: 12, fontWeight: 600, color: \"#64748b\", textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>",
1857
+ " Container Widths",
1858
+ " </p>",
1859
+ ` <p style={{ margin: "0 0 12px", fontSize: 12, color: "#475569" }}>max-w-* usage${containerCount > 0 ? ` · container ×${containerCount}` : ""}.</p>`,
1860
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 8 }}>",
1861
+ " {(maxWEntries as [string, number][]).map(([val, count]) => (",
1862
+ " <span key={val} style={{ display: \"flex\", alignItems: \"center\", gap: 6,",
1863
+ " background: \"#0a0f1a\", border: \"1px solid #1e293b\", borderRadius: 6, padding: \"5px 10px\", fontSize: 12 }}>",
1864
+ " <code style={{ color: \"#4ade80\" }}>max-w-{val}</code>",
1865
+ " <span style={{ color: \"#475569\", fontSize: 11 }}>×{count}</span>",
1866
+ " </span>",
1867
+ " ))}",
1868
+ " </div>",
1869
+ " </section>",
1870
+ " )}",
1871
+ "",
1872
+ " </div>",
1873
+ " );",
1874
+ " },",
1875
+ "};",
1876
+ ].join("\n");
1877
+ fs.writeFileSync(path.join(foundationsDir, "Grid.stories.tsx"), gridContent, "utf-8");
1878
+ console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Grid.stories.tsx")));
1879
+ }
1880
+
1640
1881
  const buttonUsage = foundations?.buttonUsage;
1641
1882
  if (buttonUsage && Array.isArray(buttonUsage.combos) && buttonUsage.combos.length > 0) {
1642
1883
  const combos = buttonUsage.combos.map((c) => ({