vibe-design-system 2.8.19 → 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 +199 -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
|
+
}
|
|
504
|
+
}
|
|
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
|
+
};
|
|
472
594
|
}
|
|
473
|
-
|
|
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) {
|
|
@@ -1843,8 +1976,22 @@ function extractColorUsage(colorNames) {
|
|
|
1843
1976
|
usage[name] = { bg: 0, text: 0, border: 0, other: 0, topFiles: new Map() };
|
|
1844
1977
|
}
|
|
1845
1978
|
|
|
1979
|
+
// Build reverse lookup: lowercase hex → color name (for arbitrary-* and inline-* tokens)
|
|
1980
|
+
// e.g. "arbitrary-4f46e5" → hex key "4f46e5"
|
|
1981
|
+
const hexToName = new Map();
|
|
1982
|
+
for (const name of colorNames) {
|
|
1983
|
+
if (name.startsWith("arbitrary-") || name.startsWith("inline-")) {
|
|
1984
|
+
const hexKey = name.replace(/^(arbitrary|inline)-/, "").toLowerCase();
|
|
1985
|
+
hexToName.set(hexKey, name);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1846
1989
|
// Single-pass regex: matches bg-primary, text-card-foreground, border-chart-1, etc.
|
|
1847
1990
|
const colorUtilRe = /\b(bg|text|border|fill|stroke|ring|from|to|via|outline)-([\w][\w-]*)/g;
|
|
1991
|
+
// Bracket-notation: bg-[#4F46E5], text-[#4f46e5/80], etc.
|
|
1992
|
+
const bracketHexRe = /\b(bg|text|border|fill|stroke|ring|from|to|via|outline)-\[#([0-9a-fA-F]{3,8})(?:\/[\d.]+)?\]/g;
|
|
1993
|
+
// Inline style hex colors: color:"#4F46E5", backgroundColor:'#4f46e5', etc.
|
|
1994
|
+
const inlineHexRe = /(?:color|(?:background|border|fill|stroke|ring)(?:Color)?)\s*[:=]\s*['"]#([0-9a-fA-F]{3,8})['"]/g;
|
|
1848
1995
|
|
|
1849
1996
|
for (const rel of files) {
|
|
1850
1997
|
if (rel.includes("stories")) continue;
|
|
@@ -1855,6 +2002,7 @@ function extractColorUsage(colorNames) {
|
|
|
1855
2002
|
const componentName = path.basename(rel).replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
1856
2003
|
|
|
1857
2004
|
let m;
|
|
2005
|
+
// ── Token-based utilities (bg-primary, text-muted-foreground, …) ──
|
|
1858
2006
|
const reCopy = new RegExp(colorUtilRe.source, "g");
|
|
1859
2007
|
while ((m = reCopy.exec(content)) !== null) {
|
|
1860
2008
|
const prefix = m[1];
|
|
@@ -1866,6 +2014,39 @@ function extractColorUsage(colorNames) {
|
|
|
1866
2014
|
else usage[token].other++;
|
|
1867
2015
|
usage[token].topFiles.set(componentName, (usage[token].topFiles.get(componentName) || 0) + 1);
|
|
1868
2016
|
}
|
|
2017
|
+
|
|
2018
|
+
// ── Bracket-notation hex utilities (bg-[#4F46E5], …) ──
|
|
2019
|
+
if (hexToName.size > 0) {
|
|
2020
|
+
const brCopy = new RegExp(bracketHexRe.source, "gi");
|
|
2021
|
+
while ((m = brCopy.exec(content)) !== null) {
|
|
2022
|
+
const prefix = m[1].toLowerCase();
|
|
2023
|
+
const hexKey = m[2].toLowerCase();
|
|
2024
|
+
// Normalise 3-char hex → 6-char for matching
|
|
2025
|
+
const normalKey = hexKey.length === 3
|
|
2026
|
+
? hexKey[0] + hexKey[0] + hexKey[1] + hexKey[1] + hexKey[2] + hexKey[2]
|
|
2027
|
+
: hexKey;
|
|
2028
|
+
const name = hexToName.get(normalKey) || hexToName.get(hexKey);
|
|
2029
|
+
if (!name) continue;
|
|
2030
|
+
if (prefix === "bg") usage[name].bg++;
|
|
2031
|
+
else if (prefix === "text") usage[name].text++;
|
|
2032
|
+
else if (prefix === "border") usage[name].border++;
|
|
2033
|
+
else usage[name].other++;
|
|
2034
|
+
usage[name].topFiles.set(componentName, (usage[name].topFiles.get(componentName) || 0) + 1);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// ── Inline style hex colors (color: "#4F46E5") ──
|
|
2038
|
+
const inCopy = new RegExp(inlineHexRe.source, "gi");
|
|
2039
|
+
while ((m = inCopy.exec(content)) !== null) {
|
|
2040
|
+
const hexKey = m[1].toLowerCase();
|
|
2041
|
+
const normalKey = hexKey.length === 3
|
|
2042
|
+
? hexKey[0] + hexKey[0] + hexKey[1] + hexKey[1] + hexKey[2] + hexKey[2]
|
|
2043
|
+
: hexKey;
|
|
2044
|
+
const name = hexToName.get(normalKey) || hexToName.get(hexKey);
|
|
2045
|
+
if (!name) continue;
|
|
2046
|
+
usage[name].other++;
|
|
2047
|
+
usage[name].topFiles.set(componentName, (usage[name].topFiles.get(componentName) || 0) + 1);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1869
2050
|
}
|
|
1870
2051
|
|
|
1871
2052
|
const result = {};
|
|
@@ -2003,6 +2184,8 @@ function scan() {
|
|
|
2003
2184
|
const colorNames = Object.keys(foundations.colors || {}).filter((k) => k !== "_dark");
|
|
2004
2185
|
const colorUsage = extractColorUsage(colorNames);
|
|
2005
2186
|
if (Object.keys(colorUsage).length > 0) foundations.colorUsage = colorUsage;
|
|
2187
|
+
const gridSystem = extractGridSystem(SRC_DIR);
|
|
2188
|
+
if (gridSystem) foundations.gridSystem = gridSystem;
|
|
2006
2189
|
const componentSuggestions = extractComponentSuggestions();
|
|
2007
2190
|
const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
|
|
2008
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) => ({
|