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 +1 -1
- package/vds-core-template/scan.mjs +151 -16
- package/vds-core-template/story-generator.mjs +274 -33
package/package.json
CHANGED
|
@@ -447,30 +447,163 @@ function extractBrandAssets() {
|
|
|
447
447
|
return assets;
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
/** Extract
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
|
1604
|
-
if (
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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) => ({
|