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 +2 -2
- package/package.json +1 -1
- package/vds-core-template/scan.mjs +252 -18
- package/vds-core-template/story-generator.mjs +427 -0
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
|
@@ -1211,21 +1211,27 @@ function extractFoundations() {
|
|
|
1211
1211
|
const typography = {};
|
|
1212
1212
|
const cssRadiusVars = {};
|
|
1213
1213
|
const borderRadiusScale = {};
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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 (
|
|
1228
|
-
const css =
|
|
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:
|
|
1534
|
-
spacing:
|
|
1672
|
+
shadows: mergedShadows,
|
|
1673
|
+
spacing: mergedSpacing,
|
|
1535
1674
|
breakpoints: normalizeThemeObj(twTheme.breakpoints),
|
|
1536
1675
|
zIndex: normalizeThemeObj(twTheme.zIndex),
|
|
1537
1676
|
motion: {
|
|
1538
|
-
transitionDuration:
|
|
1539
|
-
transitionTimingFunction:
|
|
1540
|
-
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) {
|