vibe-design-system 2.8.12 → 2.8.14

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/bin/init.js CHANGED
@@ -58,7 +58,7 @@ const preview: Preview = {
58
58
  storySort: {
59
59
  order: [
60
60
  "Foundations",
61
- ["Introduction", "Page to Components", "Colors", "Typography", "Brand", "Icons", "Component Suggestions", "Changelog"],
61
+ ["Introduction", "Page to Components", "Colors", "Typography", "Spacing & Layout", "Border & Radius", "Elevation & Shadows", "Motion & Interaction", "Brand", "Icons", "Button Usage", "Component Suggestions", "Changelog"],
62
62
  "Layout",
63
63
  "Components",
64
64
  "Actions",
@@ -304,7 +304,7 @@ const preview: Preview = {
304
304
  storySort: {
305
305
  order: [
306
306
  "Foundations",
307
- ["Introduction", "Page to Components", "Colors", "Typography", "Brand", "Icons", "Component Suggestions", "Changelog"],
307
+ ["Introduction", "Page to Components", "Colors", "Typography", "Spacing & Layout", "Border & Radius", "Elevation & Shadows", "Motion & Interaction", "Brand", "Icons", "Button Usage", "Component Suggestions", "Changelog"],
308
308
  "Layout",
309
309
  "Components",
310
310
  "Actions",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.8.12",
3
+ "version": "2.8.14",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -1211,21 +1211,27 @@ function extractFoundations() {
1211
1211
  const typography = {};
1212
1212
  const cssRadiusVars = {};
1213
1213
  const borderRadiusScale = {};
1214
- const cssPath = path.join(PROJECT_ROOT, "src", "index.css");
1215
- const globalsCss = path.join(PROJECT_ROOT, "src", "globals.css");
1216
- const stylesGlobals = path.join(PROJECT_ROOT, "src", "styles", "globals.css");
1217
- const appGlobals = path.join(PROJECT_ROOT, "app", "globals.css");
1218
- const cssToRead = fs.existsSync(cssPath)
1219
- ? cssPath
1220
- : fs.existsSync(globalsCss)
1221
- ? globalsCss
1222
- : fs.existsSync(stylesGlobals)
1223
- ? stylesGlobals
1224
- : appGlobals;
1214
+
1215
+ // Read ALL existing CSS files and combine them (covers Tailwind v4 split configs)
1216
+ const allCssCandidates = [
1217
+ path.join(PROJECT_ROOT, "src", "index.css"),
1218
+ path.join(PROJECT_ROOT, "src", "globals.css"),
1219
+ path.join(PROJECT_ROOT, "src", "styles", "globals.css"),
1220
+ path.join(PROJECT_ROOT, "src", "App.css"),
1221
+ path.join(PROJECT_ROOT, "app", "globals.css"),
1222
+ ];
1223
+ const cssChunks = [];
1224
+ for (const p of allCssCandidates) {
1225
+ if (fs.existsSync(p)) {
1226
+ try { cssChunks.push(fs.readFileSync(p, "utf-8")); } catch (_) {}
1227
+ }
1228
+ }
1229
+ // Also use first found as primary (for legacy logic below)
1230
+ const cssToRead = allCssCandidates.find((p) => fs.existsSync(p)) || "";
1225
1231
 
1226
1232
  try {
1227
- if (fs.existsSync(cssToRead)) {
1228
- const css = fs.readFileSync(cssToRead, "utf-8");
1233
+ if (cssChunks.length > 0) {
1234
+ const css = cssChunks.join("\n");
1229
1235
 
1230
1236
  const rootMatch = css.match(/:root\s*\{([\s\S]*?)\}/);
1231
1237
  if (rootMatch) {
@@ -1266,6 +1272,73 @@ function extractFoundations() {
1266
1272
  const val = fvm[2].trim();
1267
1273
  if (!typography[key]) typography[key] = val;
1268
1274
  }
1275
+
1276
+ // Tailwind v4: extract design tokens from @layer theme { :root { ... } } or bare :root
1277
+ // Variables: --spacing (base), --radius-*, --shadow-*, --ease-*, --duration-*, --animate-*
1278
+ const v4ThemeVars = {};
1279
+ // Parse the full @layer theme block (may span many lines)
1280
+ const themeLayerRe = /@layer\s+theme\s*\{([\s\S]*?)\n\}/g;
1281
+ let tlm;
1282
+ while ((tlm = themeLayerRe.exec(css)) !== null) {
1283
+ const varRe = /--([\w-]+)\s*:\s*([^;\n]+);/g;
1284
+ let vm;
1285
+ while ((vm = varRe.exec(tlm[1])) !== null) {
1286
+ v4ThemeVars[vm[1]] = vm[2].trim();
1287
+ }
1288
+ }
1289
+ // Also scan the entire CSS for these variable families in case they're outside @layer theme
1290
+ const v4AllVarRe = /--(spacing|radius|shadow|ease|duration|animate|breakpoint|z-index)([\w-]*)\s*:\s*([^;\n]+);/g;
1291
+ let v4m;
1292
+ while ((v4m = v4AllVarRe.exec(css)) !== null) {
1293
+ const fullName = v4m[1] + (v4m[2] || "");
1294
+ v4ThemeVars[fullName] = v4m[3].trim();
1295
+ }
1296
+
1297
+ // ── spacing: store base unit for synthetic scale generation
1298
+ if (v4ThemeVars["spacing"] && !v4ThemeVars["spacing"].includes("calc")) {
1299
+ // Mark as v4 base unit — will expand in final section
1300
+ cssRadiusVars["__v4SpacingBase"] = v4ThemeVars["spacing"];
1301
+ }
1302
+
1303
+ // ── radius-* vars → borderRadiusScale
1304
+ for (const [k, v] of Object.entries(v4ThemeVars)) {
1305
+ if (k.startsWith("radius-") || k === "radius") {
1306
+ const token = k === "radius" ? "DEFAULT" : k.replace(/^radius-/, "");
1307
+ borderRadiusScale[token] = v;
1308
+ }
1309
+ }
1310
+
1311
+ // ── shadow-* vars → v4Shadows (stored in cssRadiusVars with __v4shadow prefix)
1312
+ for (const [k, v] of Object.entries(v4ThemeVars)) {
1313
+ if (k.startsWith("shadow-") || k === "shadow") {
1314
+ const token = k === "shadow" ? "DEFAULT" : k.replace(/^shadow-/, "");
1315
+ cssRadiusVars[`__v4shadow_${token}`] = v;
1316
+ }
1317
+ }
1318
+
1319
+ // ── ease-* vars → v4Easings
1320
+ for (const [k, v] of Object.entries(v4ThemeVars)) {
1321
+ if (k.startsWith("ease-") || k === "ease") {
1322
+ const token = k === "ease" ? "DEFAULT" : k.replace(/^ease-/, "");
1323
+ cssRadiusVars[`__v4ease_${token}`] = v;
1324
+ }
1325
+ }
1326
+
1327
+ // ── duration-* vars
1328
+ for (const [k, v] of Object.entries(v4ThemeVars)) {
1329
+ if (k.startsWith("duration-") || k === "duration") {
1330
+ const token = k === "duration" ? "DEFAULT" : k.replace(/^duration-/, "");
1331
+ cssRadiusVars[`__v4duration_${token}`] = v;
1332
+ }
1333
+ }
1334
+
1335
+ // ── animate-* vars
1336
+ for (const [k, v] of Object.entries(v4ThemeVars)) {
1337
+ if (k.startsWith("animate-")) {
1338
+ const token = k.replace(/^animate-/, "");
1339
+ cssRadiusVars[`__v4animate_${token}`] = v;
1340
+ }
1341
+ }
1269
1342
  }
1270
1343
  } catch (_) {}
1271
1344
 
@@ -1326,6 +1399,29 @@ function extractFoundations() {
1326
1399
  }
1327
1400
  }
1328
1401
 
1402
+ // Resolve calc() radius expressions relative to base radius
1403
+ // e.g. --radius-sm: calc(var(--radius) - 4px) → resolves to base - 4px
1404
+ const baseRadiusVal = cssRadiusVars.radius;
1405
+ if (baseRadiusVal) {
1406
+ const baseRe = /^([\d.]+)(rem|px)$/.exec(baseRadiusVal);
1407
+ const basePx = baseRe
1408
+ ? (baseRe[2] === "rem" ? parseFloat(baseRe[1]) * 16 : parseFloat(baseRe[1]))
1409
+ : null;
1410
+ for (const [k, v] of Object.entries(borderRadiusScale)) {
1411
+ if (v.startsWith("calc(") && basePx !== null) {
1412
+ const addM = v.match(/calc\(var\([^)]+\)\s*([+-])\s*([\d.]+)(rem|px)\)/);
1413
+ if (addM) {
1414
+ const sign = addM[1] === "+" ? 1 : -1;
1415
+ const delta = addM[3] === "rem" ? parseFloat(addM[2]) * 16 : parseFloat(addM[2]);
1416
+ const resolved = Math.max(0, basePx + sign * delta);
1417
+ borderRadiusScale[k] = `${resolved}px`;
1418
+ }
1419
+ } else if (v.includes("var(--radius)") && basePx !== null) {
1420
+ borderRadiusScale[k] = `${basePx}px`;
1421
+ }
1422
+ }
1423
+ }
1424
+
1329
1425
  const radius = {};
1330
1426
  if (cssRadiusVars.radius) radius.base = cssRadiusVars.radius;
1331
1427
  if (Object.keys(borderRadiusScale).length > 0) radius.borderRadius = borderRadiusScale;
@@ -1526,22 +1622,156 @@ function extractFoundations() {
1526
1622
  }
1527
1623
  return out;
1528
1624
  };
1625
+
1626
+ // ── Tailwind v4: extract stored __v4* vars from cssRadiusVars ───────────────
1627
+ const v4Shadows = {};
1628
+ const v4Easings = {};
1629
+ const v4Durations = {};
1630
+ const v4Animations = {};
1631
+ let v4SpacingBase = null;
1632
+
1633
+ for (const [k, v] of Object.entries(cssRadiusVars)) {
1634
+ if (k.startsWith("__v4shadow_")) v4Shadows[k.replace("__v4shadow_", "")] = v;
1635
+ else if (k.startsWith("__v4ease_")) v4Easings[k.replace("__v4ease_", "")] = v;
1636
+ else if (k.startsWith("__v4duration_")) v4Durations[k.replace("__v4duration_", "")] = v;
1637
+ else if (k.startsWith("__v4animate_")) v4Animations[k.replace("__v4animate_", "")] = v;
1638
+ else if (k === "__v4SpacingBase") v4SpacingBase = v;
1639
+ }
1640
+
1641
+ // ── Tailwind v4: generate standard spacing scale from base unit ──────────────
1642
+ let v4SpacingScale = {};
1643
+ if (v4SpacingBase) {
1644
+ const baseRe = /^([\d.]+)(rem|px)$/.exec(v4SpacingBase);
1645
+ if (baseRe) {
1646
+ const baseVal = parseFloat(baseRe[1]);
1647
+ const unit = baseRe[2];
1648
+ // Standard Tailwind scale multipliers
1649
+ const steps = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96];
1650
+ for (const s of steps) {
1651
+ const val = s === 0 ? "0px" : `${Math.round(baseVal * s * 1000) / 1000}${unit}`;
1652
+ v4SpacingScale[s === 0 ? "0" : String(s)] = val;
1653
+ }
1654
+ // Also add 'px' = 1px
1655
+ v4SpacingScale["px"] = "1px";
1656
+ }
1657
+ }
1658
+
1659
+ // ── Merge: Tailwind v3 theme takes priority; v4 CSS vars fill gaps ─────────
1660
+ const mergedShadows = { ...v4Shadows, ...normalizeThemeObj(twTheme.shadows) };
1661
+ const mergedSpacing = Object.keys(normalizeThemeObj(twTheme.spacing)).length > 0
1662
+ ? normalizeThemeObj(twTheme.spacing)
1663
+ : v4SpacingScale;
1664
+ const mergedEasings = { ...v4Easings, ...normalizeThemeObj(twTheme.transitionTimingFunction) };
1665
+ const mergedDurations = { ...v4Durations, ...normalizeThemeObj(twTheme.transitionDuration) };
1666
+ const mergedAnimations = { ...v4Animations, ...normalizeThemeObj(twTheme.animation) };
1667
+
1529
1668
  return {
1530
1669
  colors: foundationsColors,
1531
1670
  typography,
1532
1671
  radius,
1533
- shadows: normalizeThemeObj(twTheme.shadows),
1534
- spacing: normalizeThemeObj(twTheme.spacing),
1672
+ shadows: mergedShadows,
1673
+ spacing: mergedSpacing,
1535
1674
  breakpoints: normalizeThemeObj(twTheme.breakpoints),
1536
1675
  zIndex: normalizeThemeObj(twTheme.zIndex),
1537
1676
  motion: {
1538
- transitionDuration: normalizeThemeObj(twTheme.transitionDuration),
1539
- transitionTimingFunction: normalizeThemeObj(twTheme.transitionTimingFunction),
1540
- animation: normalizeThemeObj(twTheme.animation),
1677
+ transitionDuration: mergedDurations,
1678
+ transitionTimingFunction: mergedEasings,
1679
+ animation: mergedAnimations,
1541
1680
  },
1542
1681
  };
1543
1682
  }
1544
1683
 
1684
+ /**
1685
+ * Counts actual Tailwind utility class usage in src/ files.
1686
+ * Returns top-N used tokens for spacing, shadows, z-index, border-radius.
1687
+ * This shows what the PROJECT ACTUALLY USES — not just theoretical design tokens.
1688
+ */
1689
+ function extractTokenUsage() {
1690
+ if (!fs.existsSync(SRC_DIR)) return null;
1691
+ const files = getAllTsxJsxInDir(SRC_DIR);
1692
+ if (!Array.isArray(files) || files.length === 0) return null;
1693
+
1694
+ const spacingCounts = new Map();
1695
+ const shadowCounts = new Map();
1696
+ const zIndexCounts = new Map();
1697
+ const radiusCounts = new Map();
1698
+ const animateCounts = new Map();
1699
+
1700
+ // Spacing utilities: gap-*, p-*, px-*, py-*, pt-*, pb-*, pl-*, pr-*, m-*, mx-*, my-*, mt-*, mb-*, ml-*, mr-*, space-*, w-*, h-*
1701
+ const spacingRe = /\b(gap|p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|space-x|space-y|w|h|size|inset|top|bottom|left|right|min-w|min-h|max-w|max-h)-((?:\d+(\.\d+)?|px|full|screen|auto|fit|max|min|svh|dvh|svw|dvw))\b/g;
1702
+ // Shadow utilities: shadow-sm, shadow-md, shadow-lg, shadow-xl, shadow-2xl, shadow-inner, shadow-none, shadow (bare)
1703
+ const shadowRe = /\bshadow(?:-(sm|md|lg|xl|2xl|inner|none))?\b(?!-color|-opacity|-ring)/g;
1704
+ // Z-index utilities: z-0, z-10, z-20, z-30, z-40, z-50, z-auto
1705
+ const zRe = /\bz-(\d+|auto)\b/g;
1706
+ // Border-radius utilities: rounded, rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-2xl, rounded-3xl, rounded-full, rounded-none
1707
+ const roundedRe = /\brounded(?:-(none|sm|md|lg|xl|2xl|3xl|full|t|r|b|l|tl|tr|br|bl|s|e|ss|se|es|ee|[a-z]+(?:-(?:none|sm|md|lg|xl|2xl|3xl|full))?))?(?!\w)/g;
1708
+ // Animate utilities: animate-spin, animate-ping, animate-pulse, animate-bounce, animate-none
1709
+ const animateRe = /\banimate-(spin|ping|pulse|bounce|none|[\w-]+)\b/g;
1710
+
1711
+ for (const rel of files) {
1712
+ if (rel.includes("stories")) continue;
1713
+ let content;
1714
+ try {
1715
+ content = fs.readFileSync(path.join(SRC_DIR, rel), "utf-8");
1716
+ } catch (_) { continue; }
1717
+
1718
+ // Spacing
1719
+ let m;
1720
+ const spacingReCopy = new RegExp(spacingRe.source, "g");
1721
+ while ((m = spacingReCopy.exec(content)) !== null) {
1722
+ const key = `${m[1]}-${m[2]}`;
1723
+ spacingCounts.set(key, (spacingCounts.get(key) || 0) + 1);
1724
+ }
1725
+
1726
+ // Shadows
1727
+ const shadowReCopy = new RegExp(shadowRe.source, "g");
1728
+ while ((m = shadowReCopy.exec(content)) !== null) {
1729
+ const key = m[1] ? `shadow-${m[1]}` : "shadow";
1730
+ shadowCounts.set(key, (shadowCounts.get(key) || 0) + 1);
1731
+ }
1732
+
1733
+ // Z-index
1734
+ const zReCopy = new RegExp(zRe.source, "g");
1735
+ while ((m = zReCopy.exec(content)) !== null) {
1736
+ const key = `z-${m[1]}`;
1737
+ zIndexCounts.set(key, (zIndexCounts.get(key) || 0) + 1);
1738
+ }
1739
+
1740
+ // Border radius
1741
+ const roundedReCopy = new RegExp(roundedRe.source, "g");
1742
+ while ((m = roundedReCopy.exec(content)) !== null) {
1743
+ // Normalize: only count base rounded-* (not directional like rounded-t-lg)
1744
+ const suffix = m[1] || "";
1745
+ const isDirectional = /^(t|r|b|l|tl|tr|br|bl|s|e|ss|se|es|ee)(-|$)/.test(suffix);
1746
+ if (!isDirectional) {
1747
+ const key = suffix ? `rounded-${suffix}` : "rounded";
1748
+ radiusCounts.set(key, (radiusCounts.get(key) || 0) + 1);
1749
+ }
1750
+ }
1751
+
1752
+ // Animate
1753
+ const animateReCopy = new RegExp(animateRe.source, "g");
1754
+ while ((m = animateReCopy.exec(content)) !== null) {
1755
+ const key = `animate-${m[1]}`;
1756
+ animateCounts.set(key, (animateCounts.get(key) || 0) + 1);
1757
+ }
1758
+ }
1759
+
1760
+ const toTop = (map, n) =>
1761
+ [...map.entries()]
1762
+ .sort((a, b) => b[1] - a[1])
1763
+ .slice(0, n)
1764
+ .map(([token, count]) => ({ token, count }));
1765
+
1766
+ return {
1767
+ spacing: toTop(spacingCounts, 20),
1768
+ shadows: toTop(shadowCounts, 10),
1769
+ zIndex: toTop(zIndexCounts, 10),
1770
+ radius: toTop(radiusCounts, 10),
1771
+ animations: toTop(animateCounts, 8),
1772
+ };
1773
+ }
1774
+
1545
1775
  function extractButtonUsage() {
1546
1776
  if (!fs.existsSync(SRC_DIR)) return null;
1547
1777
  const files = getAllTsxJsxInDir(SRC_DIR);
@@ -1650,6 +1880,10 @@ function scan() {
1650
1880
  if (buttonUsage) {
1651
1881
  foundations.buttonUsage = buttonUsage;
1652
1882
  }
1883
+ const tokenUsage = extractTokenUsage();
1884
+ if (tokenUsage) {
1885
+ foundations.tokenUsage = tokenUsage;
1886
+ }
1653
1887
  const componentSuggestions = extractComponentSuggestions();
1654
1888
  const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
1655
1889
  const output = {
@@ -1442,6 +1442,433 @@ function writeFoundationsStories(foundations) {
1442
1442
  fs.writeFileSync(path.join(foundationsDir, "Buttons.stories.tsx"), buttonsContent, "utf-8");
1443
1443
  console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Buttons.stories.tsx")));
1444
1444
  }
1445
+
1446
+ // ── SPACING & LAYOUT ────────────────────────────────────────────────────────
1447
+ {
1448
+ const spacing = foundations?.spacing || {};
1449
+ const breakpoints = foundations?.breakpoints || {};
1450
+
1451
+ function remToPxNum(val) {
1452
+ const m = String(val).match(/^([\d.]+)rem$/);
1453
+ if (m) return Math.round(parseFloat(m[1]) * 16);
1454
+ const m2 = String(val).match(/^([\d.]+)px$/);
1455
+ if (m2) return parseInt(m2[1]);
1456
+ return -1;
1457
+ }
1458
+
1459
+ const spacingRows = Object.entries(spacing)
1460
+ .map(([token, value]) => {
1461
+ const px = remToPxNum(value);
1462
+ const label = px >= 0 ? `${value} · ${px}px` : value;
1463
+ const barWidth = Math.min(Math.max(px, 0), 240);
1464
+ return { token, label, barWidth };
1465
+ })
1466
+ .sort((a, b) => {
1467
+ const na = parseFloat(a.token), nb = parseFloat(b.token);
1468
+ if (!isNaN(na) && !isNaN(nb)) return na - nb;
1469
+ if (!isNaN(na)) return -1;
1470
+ if (!isNaN(nb)) return 1;
1471
+ return a.token.localeCompare(b.token);
1472
+ });
1473
+
1474
+ const breakpointRows = Object.entries(breakpoints)
1475
+ .map(([token, value]) => ({ token, value }))
1476
+ .sort((a, b) => parseInt(a.value) - parseInt(b.value));
1477
+
1478
+ const usedSpacing = (foundations?.tokenUsage?.spacing || []).slice(0, 16);
1479
+ const spacingContent = [
1480
+ "import type { Meta, StoryObj } from \"@storybook/react\";",
1481
+ "",
1482
+ "const meta = { title: \"Foundations/Spacing & Layout\" } satisfies Meta;",
1483
+ "export default meta;",
1484
+ "type Story = StoryObj;",
1485
+ "",
1486
+ `const spacingTokens: { token: string; label: string; barWidth: number }[] = ${JSON.stringify(spacingRows)};`,
1487
+ `const breakpointTokens: { token: string; value: string }[] = ${JSON.stringify(breakpointRows)};`,
1488
+ `const usedSpacing: { token: string; count: number }[] = ${JSON.stringify(usedSpacing)};`,
1489
+ "",
1490
+ "export const Default: Story = {",
1491
+ " render: () => (",
1492
+ " <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, maxWidth: 760, color: \"#111\" }}>",
1493
+ " <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Spacing Scale</h2>",
1494
+ " <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 24px\" }}>Base unit: 0.25rem = 4px · built on an 8px grid system</p>",
1495
+ " {usedSpacing.length > 0 && (",
1496
+ " <>",
1497
+ " <h3 style={{ fontSize: 14, fontWeight: 600, margin: \"0 0 10px\", color: \"#374151\" }}>🎯 Most used in this project</h3>",
1498
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6, marginBottom: 28 }}>",
1499
+ " {usedSpacing.map(({ token, count }) => (",
1500
+ " <span key={token} style={{ padding: \"3px 10px\", background: \"#ede9fe\", color: \"#5b21b6\", borderRadius: 20, fontSize: 12, fontWeight: 500 }}>",
1501
+ " {token} <span style={{ opacity: 0.6 }}>×{count}</span>",
1502
+ " </span>",
1503
+ " ))}",
1504
+ " </div>",
1505
+ " </>",
1506
+ " )}",
1507
+ " {spacingTokens.length === 0 ? (",
1508
+ " <p style={{ color: \"#999\", fontSize: 13 }}>No spacing tokens detected. Ensure <code>tailwind.config.ts</code> is present.</p>",
1509
+ " ) : (",
1510
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 3 }}>",
1511
+ " {spacingTokens.map(({ token, label, barWidth }) => (",
1512
+ " <div key={token} style={{ display: \"flex\", alignItems: \"center\", gap: 10, minHeight: 24 }}>",
1513
+ " <span style={{ width: 40, fontSize: 11, color: \"#9ca3af\", textAlign: \"right\", flexShrink: 0, fontVariantNumeric: \"tabular-nums\" }}>{token}</span>",
1514
+ " <div style={{ width: Math.max(barWidth, 2), height: 16, background: barWidth === 0 ? \"transparent\" : \"linear-gradient(90deg,#818cf8,#6366f1)\", borderRadius: 3, flexShrink: 0, border: barWidth === 0 ? \"1px dashed #555\" : \"none\", boxSizing: \"border-box\" as any }} />",
1515
+ " <code style={{ fontSize: 11, color: \"#6b7280\" }}>{label}</code>",
1516
+ " </div>",
1517
+ " ))}",
1518
+ " </div>",
1519
+ " )}",
1520
+ " {breakpointTokens.length > 0 && (",
1521
+ " <>",
1522
+ " <h3 style={{ fontSize: 16, fontWeight: 600, margin: \"40px 0 12px\" }}>Breakpoints</h3>",
1523
+ " <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(130px, 1fr))\", gap: 8 }}>",
1524
+ " {breakpointTokens.map(({ token, value }) => (",
1525
+ " <div key={token} style={{ padding: \"10px 12px\", border: \"1px solid #e5e7eb\", borderRadius: 8, background: \"#fafafa\" }}>",
1526
+ " <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 2 }}>{token}</div>",
1527
+ " <code style={{ fontSize: 12, color: \"#6b7280\" }}>{value}</code>",
1528
+ " </div>",
1529
+ " ))}",
1530
+ " </div>",
1531
+ " </>",
1532
+ " )}",
1533
+ " </div>",
1534
+ " ),",
1535
+ "};",
1536
+ ].join("\n");
1537
+ fs.writeFileSync(path.join(foundationsDir, "Spacing.stories.tsx"), spacingContent, "utf-8");
1538
+ console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Spacing.stories.tsx")));
1539
+ }
1540
+
1541
+ // ── ELEVATION & SHADOWS ─────────────────────────────────────────────────────
1542
+ {
1543
+ const shadows = foundations?.shadows || {};
1544
+ const zIndex = foundations?.zIndex || {};
1545
+
1546
+ const shadowOrder = ["none", "sm", "DEFAULT", "md", "lg", "xl", "2xl", "3xl", "inner"];
1547
+ const shadowRows = Object.entries(shadows)
1548
+ .sort(([a], [b]) => {
1549
+ const ia = shadowOrder.indexOf(a), ib = shadowOrder.indexOf(b);
1550
+ if (ia >= 0 && ib >= 0) return ia - ib;
1551
+ if (ia >= 0) return -1;
1552
+ if (ib >= 0) return 1;
1553
+ return a.localeCompare(b);
1554
+ })
1555
+ .map(([token, value]) => ({ token, value }));
1556
+
1557
+ const zSemanticLabels = { "0": "Base layer", "10": "Low elevation", "20": "Dropdown / popover", "30": "Sticky header", "40": "Fixed overlay", "50": "Modal / dialog", "auto": "Auto" };
1558
+ const zIndexRows = Object.entries(zIndex)
1559
+ .sort(([a], [b]) => {
1560
+ if (a === "auto") return 1;
1561
+ if (b === "auto") return -1;
1562
+ return parseInt(a) - parseInt(b);
1563
+ })
1564
+ .map(([token, value]) => ({ token, value, label: zSemanticLabels[token] || "" }));
1565
+
1566
+ const usedShadows = foundations?.tokenUsage?.shadows || [];
1567
+ const usedZIndex = foundations?.tokenUsage?.zIndex || [];
1568
+
1569
+ // Build shadow CSS values from actual Tailwind defaults (for visual demo when no CSS vars found)
1570
+ const shadowDefaults = {
1571
+ "shadow-sm": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
1572
+ "shadow": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
1573
+ "shadow-md": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
1574
+ "shadow-lg": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
1575
+ "shadow-xl": "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
1576
+ "shadow-2xl": "0 25px 50px -12px rgb(0 0 0 / 0.25)",
1577
+ };
1578
+ // If token CSS values available use those; otherwise use defaults keyed by used token names
1579
+ const shadowDisplayRows = shadowRows.length > 0
1580
+ ? shadowRows
1581
+ : usedShadows.map(({ token }) => ({ token, value: shadowDefaults[token] || "" })).filter((r) => r.value);
1582
+
1583
+ const zIndexSemantics = { "z-0": "Base", "z-10": "Low / hover", "z-20": "Dropdown", "z-30": "Sticky", "z-40": "Fixed", "z-50": "Modal / overlay" };
1584
+
1585
+ const elevationContent = [
1586
+ "import type { Meta, StoryObj } from \"@storybook/react\";",
1587
+ "",
1588
+ "const meta = { title: \"Foundations/Elevation & Shadows\" } satisfies Meta;",
1589
+ "export default meta;",
1590
+ "type Story = StoryObj;",
1591
+ "",
1592
+ `const shadowTokens: { token: string; value: string }[] = ${JSON.stringify(shadowDisplayRows)};`,
1593
+ `const zIndexTokens: { token: string; value: string; label: string }[] = ${JSON.stringify(zIndexRows)};`,
1594
+ `const usedShadows: { token: string; count: number }[] = ${JSON.stringify(usedShadows)};`,
1595
+ `const usedZIndex: { token: string; count: number }[] = ${JSON.stringify(usedZIndex)};`,
1596
+ `const zSemantics: Record<string, string> = ${JSON.stringify(zIndexSemantics)};`,
1597
+ "",
1598
+ "export const Default: Story = {",
1599
+ " render: () => (",
1600
+ " <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, background: \"#f1f5f9\", minHeight: 400 }}>",
1601
+ " <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\", color: \"#111\" }}>Elevation & Shadows</h2>",
1602
+ " <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 32px\" }}>Depth is communicated through layered shadow levels</p>",
1603
+ " <h3 style={{ fontSize: 15, fontWeight: 600, margin: \"0 0 14px\", color: \"#374151\" }}>Shadow Scale</h3>",
1604
+ " {usedShadows.length > 0 && (",
1605
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6, marginBottom: 16 }}>",
1606
+ " {usedShadows.map(({ token, count }) => (",
1607
+ " <span key={token} style={{ padding: \"3px 10px\", background: \"#dbeafe\", color: \"#1e40af\", borderRadius: 20, fontSize: 12, fontWeight: 500 }}>",
1608
+ " {token} <span style={{ opacity: 0.6 }}>×{count}</span>",
1609
+ " </span>",
1610
+ " ))}",
1611
+ " </div>",
1612
+ " )}",
1613
+ " {shadowTokens.length === 0 ? (",
1614
+ " <p style={{ color: \"#999\", fontSize: 13, marginBottom: 32 }}>No shadow tokens detected.</p>",
1615
+ " ) : (",
1616
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 24, marginBottom: 48 }}>",
1617
+ " {shadowTokens.map(({ token, value }) => (",
1618
+ " <div key={token} style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", gap: 10 }}>",
1619
+ " <div style={{ width: 80, height: 80, background: \"#fff\", borderRadius: 10, boxShadow: value, border: token === \"none\" ? \"1px dashed #ccc\" : \"none\" }} />",
1620
+ " <span style={{ fontSize: 12, fontWeight: 600, color: \"#374151\" }}>{token === \"DEFAULT\" ? \"default\" : token}</span>",
1621
+ " </div>",
1622
+ " ))}",
1623
+ " </div>",
1624
+ " )}",
1625
+ " <h3 style={{ fontSize: 15, fontWeight: 600, margin: \"0 0 14px\", color: \"#374151\" }}>Z-Index Scale</h3>",
1626
+ " {usedZIndex.length > 0 && (",
1627
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6, marginBottom: 14 }}>",
1628
+ " {usedZIndex.map(({ token, count }) => (",
1629
+ " <span key={token} style={{ padding: \"3px 10px\", background: \"#d1fae5\", color: \"#065f46\", borderRadius: 20, fontSize: 12, fontWeight: 500 }}>",
1630
+ " {token} <span style={{ opacity: 0.6 }}>×{count}</span>",
1631
+ " </span>",
1632
+ " ))}",
1633
+ " </div>",
1634
+ " )}",
1635
+ " {zIndexTokens.length === 0 && usedZIndex.length === 0 ? (",
1636
+ " <p style={{ color: \"#999\", fontSize: 13 }}>No z-index tokens detected.</p>",
1637
+ " ) : zIndexTokens.length > 0 ? (",
1638
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 6, maxWidth: 380 }}>",
1639
+ " {zIndexTokens.map(({ token, value, label }) => (",
1640
+ " <div key={token} style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: \"8px 14px\", background: \"#fff\", borderRadius: 8, border: \"1px solid #e5e7eb\" }}>",
1641
+ " <span style={{ width: 36, fontSize: 15, fontWeight: 700, color: \"#6366f1\", textAlign: \"right\", flexShrink: 0, fontVariantNumeric: \"tabular-nums\" }}>{value}</span>",
1642
+ " <div>",
1643
+ " <span style={{ fontSize: 12, fontWeight: 600 }}>{token}</span>",
1644
+ " {label ? <span style={{ fontSize: 11, color: \"#9ca3af\", marginLeft: 8 }}>{label}</span> : null}",
1645
+ " </div>",
1646
+ " </div>",
1647
+ " ))}",
1648
+ " </div>",
1649
+ " ) : (",
1650
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 6, maxWidth: 380 }}>",
1651
+ " {usedZIndex.map(({ token, count }) => (",
1652
+ " <div key={token} style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: \"8px 14px\", background: \"#fff\", borderRadius: 8, border: \"1px solid #e5e7eb\" }}>",
1653
+ " <span style={{ width: 36, fontSize: 15, fontWeight: 700, color: \"#6366f1\", textAlign: \"right\", flexShrink: 0 }}>{token.replace('z-','')}</span>",
1654
+ " <div>",
1655
+ " <span style={{ fontSize: 12, fontWeight: 600 }}>{token}</span>",
1656
+ " {zSemantics[token] ? <span style={{ fontSize: 11, color: \"#9ca3af\", marginLeft: 8 }}>{zSemantics[token]}</span> : null}",
1657
+ " <span style={{ fontSize: 11, color: \"#d1d5db\", marginLeft: 8 }}>×{count} usages</span>",
1658
+ " </div>",
1659
+ " </div>",
1660
+ " ))}",
1661
+ " </div>",
1662
+ " )}",
1663
+ " </div>",
1664
+ " ),",
1665
+ "};",
1666
+ ].join("\n");
1667
+ fs.writeFileSync(path.join(foundationsDir, "Elevation.stories.tsx"), elevationContent, "utf-8");
1668
+ console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Elevation.stories.tsx")));
1669
+ }
1670
+
1671
+ // ── BORDER & RADIUS ─────────────────────────────────────────────────────────
1672
+ {
1673
+ const radiusData = foundations?.radius || {};
1674
+ const baseRadius = radiusData.base || null;
1675
+ const borderRadiusObj = radiusData.borderRadius || {};
1676
+
1677
+ const radiusScale = { ...(baseRadius ? { base: baseRadius } : {}), ...borderRadiusObj };
1678
+ const radiusOrder = ["none", "sm", "DEFAULT", "base", "md", "lg", "xl", "2xl", "3xl", "full"];
1679
+
1680
+ const radiusRows = Object.entries(radiusScale)
1681
+ .sort(([a], [b]) => {
1682
+ const ia = radiusOrder.indexOf(a), ib = radiusOrder.indexOf(b);
1683
+ if (ia >= 0 && ib >= 0) return ia - ib;
1684
+ const va = parseFloat(a), vb = parseFloat(b);
1685
+ if (!isNaN(va) && !isNaN(vb)) return va - vb;
1686
+ return a.localeCompare(b);
1687
+ })
1688
+ .map(([token, value]) => {
1689
+ const m = String(value).match(/^([\d.]+)rem$/);
1690
+ const px = m ? Math.round(parseFloat(m[1]) * 16) + "px" : null;
1691
+ const isFull = parseInt(String(value)) >= 999;
1692
+ const displayValue = px && px !== value ? `${value} · ${px}` : value;
1693
+ return { token, value, displayValue, isFull };
1694
+ });
1695
+
1696
+ const usedRadius = (foundations?.tokenUsage?.radius || []).slice(0, 12);
1697
+
1698
+ const borderContent = [
1699
+ "import type { Meta, StoryObj } from \"@storybook/react\";",
1700
+ "",
1701
+ "const meta = { title: \"Foundations/Border & Radius\" } satisfies Meta;",
1702
+ "export default meta;",
1703
+ "type Story = StoryObj;",
1704
+ "",
1705
+ `const radiusTokens: { token: string; value: string; displayValue: string; isFull: boolean }[] = ${JSON.stringify(radiusRows)};`,
1706
+ `const usedRadius: { token: string; count: number }[] = ${JSON.stringify(usedRadius)};`,
1707
+ "",
1708
+ "export const Default: Story = {",
1709
+ " render: () => (",
1710
+ " <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, maxWidth: 800 }}>",
1711
+ " <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Border & Radius</h2>",
1712
+ " <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 32px\" }}>Corner radius scale — from sharp edges to fully rounded</p>",
1713
+ " {usedRadius.length > 0 && (",
1714
+ " <div style={{ marginBottom: 28 }}>",
1715
+ " <p style={{ fontSize: 12, color: \"#6b7280\", margin: \"0 0 8px\", fontWeight: 500 }}>Most used in this project</p>",
1716
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6 }}>",
1717
+ " {usedRadius.map(({ token, count }) => (",
1718
+ " <span key={token} style={{ padding: \"3px 10px\", background: \"#fce7f3\", color: \"#9d174d\", borderRadius: 20, fontSize: 12, fontWeight: 500 }}>",
1719
+ " {token} <span style={{ opacity: 0.6 }}>×{count}</span>",
1720
+ " </span>",
1721
+ " ))}",
1722
+ " </div>",
1723
+ " </div>",
1724
+ " )}",
1725
+ " {radiusTokens.length === 0 ? (",
1726
+ " <p style={{ color: \"#999\", fontSize: 13 }}>No border-radius tokens detected. Add <code>borderRadius</code> to <code>tailwind.config.ts</code>.</p>",
1727
+ " ) : (",
1728
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 28 }}>",
1729
+ " {radiusTokens.map(({ token, value, displayValue, isFull }) => (",
1730
+ " <div key={token} style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", gap: 10 }}>",
1731
+ " <div style={{",
1732
+ " width: isFull ? 64 : 72,",
1733
+ " height: isFull ? 64 : 56,",
1734
+ " background: \"linear-gradient(135deg,#818cf8,#6366f1)\",",
1735
+ " borderRadius: isFull ? \"50%\" : value === \"0px\" || value === \"0\" ? 0 : value,",
1736
+ " flexShrink: 0,",
1737
+ " }} />",
1738
+ " <div style={{ textAlign: \"center\" }}>",
1739
+ " <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 2 }}>{token === \"DEFAULT\" ? \"default\" : token}</div>",
1740
+ " <code style={{ fontSize: 11, color: \"#9ca3af\" }}>{displayValue}</code>",
1741
+ " </div>",
1742
+ " </div>",
1743
+ " ))}",
1744
+ " </div>",
1745
+ " )}",
1746
+ " </div>",
1747
+ " ),",
1748
+ "};",
1749
+ ].join("\n");
1750
+ fs.writeFileSync(path.join(foundationsDir, "BorderRadius.stories.tsx"), borderContent, "utf-8");
1751
+ console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "BorderRadius.stories.tsx")));
1752
+ }
1753
+
1754
+ // ── MOTION & INTERACTION ────────────────────────────────────────────────────
1755
+ {
1756
+ const motion = foundations?.motion || {};
1757
+ const durations = motion.transitionDuration || {};
1758
+ const easings = motion.transitionTimingFunction || {};
1759
+ const animations = motion.animation || {};
1760
+
1761
+ const durationRows = Object.entries(durations)
1762
+ .filter(([k]) => k !== "0" && k !== "DEFAULT")
1763
+ .sort(([a], [b]) => parseInt(a) - parseInt(b))
1764
+ .map(([token, value]) => ({ token, value }));
1765
+
1766
+ const easingRows = Object.entries(easings)
1767
+ .filter(([k]) => k !== "DEFAULT")
1768
+ .map(([token, value]) => ({ token, value }));
1769
+
1770
+ const animationRows = Object.entries(animations)
1771
+ .filter(([k]) => k !== "none")
1772
+ .map(([token, value]) => ({ token, value }));
1773
+
1774
+ const usedAnimations = (foundations?.tokenUsage?.animations || []).slice(0, 8);
1775
+
1776
+ const motionContent = [
1777
+ "import type { Meta, StoryObj } from \"@storybook/react\";",
1778
+ "import { useState } from \"react\";",
1779
+ "",
1780
+ "const meta = { title: \"Foundations/Motion & Interaction\" } satisfies Meta;",
1781
+ "export default meta;",
1782
+ "type Story = StoryObj;",
1783
+ "",
1784
+ `const durationTokens: { token: string; value: string }[] = ${JSON.stringify(durationRows)};`,
1785
+ `const easingTokens: { token: string; value: string }[] = ${JSON.stringify(easingRows)};`,
1786
+ `const animationTokens: { token: string; value: string }[] = ${JSON.stringify(animationRows)};`,
1787
+ `const usedAnimations: { token: string; count: number }[] = ${JSON.stringify(usedAnimations)};`,
1788
+ "",
1789
+ "",
1790
+ "function DurationDemo({ token, value }: { token: string; value: string }) {",
1791
+ " const [active, setActive] = useState(false);",
1792
+ " return (",
1793
+ " <div style={{ display: \"flex\", alignItems: \"center\", gap: 16 }}>",
1794
+ " <span style={{ width: 40, fontSize: 12, color: \"#9ca3af\", textAlign: \"right\", flexShrink: 0, fontVariantNumeric: \"tabular-nums\" }}>{token}</span>",
1795
+ " <code style={{ width: 68, fontSize: 12, color: \"#6b7280\", flexShrink: 0 }}>{value}</code>",
1796
+ " <div",
1797
+ " style={{ position: \"relative\", width: 220, height: 32, background: \"#f1f5f9\", borderRadius: 8, cursor: \"pointer\", overflow: \"hidden\", flexShrink: 0 }}",
1798
+ " onClick={() => setActive((a) => !a)}",
1799
+ " >",
1800
+ " <div style={{",
1801
+ " position: \"absolute\",",
1802
+ " top: 4, left: active ? 172 : 4,",
1803
+ " width: 44, height: 24,",
1804
+ " background: \"linear-gradient(90deg,#818cf8,#6366f1)\",",
1805
+ " borderRadius: 6,",
1806
+ " transition: `left ${value} cubic-bezier(0.4,0,0.2,1)`,",
1807
+ " }} />",
1808
+ " </div>",
1809
+ " <span style={{ fontSize: 11, color: \"#d1d5db\" }}>click</span>",
1810
+ " </div>",
1811
+ " );",
1812
+ "}",
1813
+ "",
1814
+ "export const Default: Story = {",
1815
+ " render: () => (",
1816
+ " <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, maxWidth: 700, color: \"#111\" }}>",
1817
+ " <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Motion & Interaction</h2>",
1818
+ " <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 32px\" }}>Consistent timing and easing for smooth, intentional motion</p>",
1819
+ " {usedAnimations.length > 0 && (",
1820
+ " <div style={{ marginBottom: 28 }}>",
1821
+ " <p style={{ fontSize: 12, color: \"#6b7280\", margin: \"0 0 8px\", fontWeight: 500 }}>Used animations in this project</p>",
1822
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6 }}>",
1823
+ " {usedAnimations.map(({ token, count }) => (",
1824
+ " <span key={token} style={{ padding: \"3px 10px\", background: \"#d1fae5\", color: \"#065f46\", borderRadius: 20, fontSize: 12, fontWeight: 500 }}>",
1825
+ " {token} <span style={{ opacity: 0.6 }}>×{count}</span>",
1826
+ " </span>",
1827
+ " ))}",
1828
+ " </div>",
1829
+ " </div>",
1830
+ " )}",
1831
+ " <h3 style={{ fontSize: 15, fontWeight: 600, margin: \"0 0 14px\", color: \"#374151\" }}>Duration</h3>",
1832
+ " {durationTokens.length === 0 ? (",
1833
+ " <p style={{ color: \"#999\", fontSize: 13, marginBottom: 32 }}>No duration tokens detected.</p>",
1834
+ " ) : (",
1835
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 10, marginBottom: 40 }}>",
1836
+ " {durationTokens.map((d) => <DurationDemo key={d.token} token={d.token} value={d.value} />)}",
1837
+ " </div>",
1838
+ " )}",
1839
+ " <h3 style={{ fontSize: 15, fontWeight: 600, margin: \"0 0 14px\", color: \"#374151\" }}>Easing</h3>",
1840
+ " {easingTokens.length === 0 ? (",
1841
+ " <p style={{ color: \"#999\", fontSize: 13, marginBottom: 32 }}>No easing tokens detected.</p>",
1842
+ " ) : (",
1843
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 6, marginBottom: 40, maxWidth: 500 }}>",
1844
+ " {easingTokens.map(({ token, value }) => (",
1845
+ " <div key={token} style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: \"8px 12px\", background: \"#f8fafc\", borderRadius: 8, border: \"1px solid #e5e7eb\" }}>",
1846
+ " <span style={{ width: 64, fontSize: 12, fontWeight: 600, flexShrink: 0 }}>{token}</span>",
1847
+ " <code style={{ fontSize: 11, color: \"#6b7280\" }}>{value}</code>",
1848
+ " </div>",
1849
+ " ))}",
1850
+ " </div>",
1851
+ " )}",
1852
+ " {animationTokens.length > 0 && (",
1853
+ " <>",
1854
+ " <h3 style={{ fontSize: 15, fontWeight: 600, margin: \"0 0 14px\", color: \"#374151\" }}>Animations</h3>",
1855
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 4 }}>",
1856
+ " {animationTokens.map(({ token, value }) => (",
1857
+ " <div key={token} style={{ display: \"flex\", gap: 12, padding: \"7px 0\", borderBottom: \"1px solid #f0f0f0\", alignItems: \"flex-start\" }}>",
1858
+ " <span style={{ width: 128, fontSize: 12, fontWeight: 600, flexShrink: 0, paddingTop: 1 }}>{token}</span>",
1859
+ " <code style={{ fontSize: 11, color: \"#9ca3af\", wordBreak: \"break-all\" as any }}>{value}</code>",
1860
+ " </div>",
1861
+ " ))}",
1862
+ " </div>",
1863
+ " </>",
1864
+ " )}",
1865
+ " </div>",
1866
+ " ),",
1867
+ "};",
1868
+ ].join("\n");
1869
+ fs.writeFileSync(path.join(foundationsDir, "Motion.stories.tsx"), motionContent, "utf-8");
1870
+ console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Motion.stories.tsx")));
1871
+ }
1445
1872
  }
1446
1873
 
1447
1874
  function writeComponentSuggestionsStory(componentSuggestions) {