vibe-design-system 2.8.61 → 2.8.63
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/story-generator.mjs +703 -127
package/package.json
CHANGED
|
@@ -83,6 +83,72 @@ const SKIP_LIST = VDS_CONFIG.skipList
|
|
|
83
83
|
? [...VDS_CONFIG.skipList]
|
|
84
84
|
: [...DEFAULT_SKIP_LIST, ...(VDS_CONFIG.extraSkipList ?? [])];
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Semantic component classifier.
|
|
88
|
+
* Maps component name + tokens + props to an Atlassian-style functional category.
|
|
89
|
+
* Used for sidebar grouping (shadcn/UI components) and Component Inventory story.
|
|
90
|
+
* Returns the category string, or null if no confident match.
|
|
91
|
+
*/
|
|
92
|
+
function classifyComponent(name, tokens = [], props = []) {
|
|
93
|
+
const n = name.toLowerCase();
|
|
94
|
+
|
|
95
|
+
// Status Indicators — short labels, counts, status metadata
|
|
96
|
+
if (
|
|
97
|
+
/^(badge|tag|lozenge|chip|pill|statusbadge|statuschip|statustag|statuslabel|statusdot|statusicon|countertag|counterbadge|tagging)$/.test(n) ||
|
|
98
|
+
/badge$|lozenge$|statuschip$|statusbadge$|statustag$|statuspill$|countertag$/.test(n)
|
|
99
|
+
) return "Status Indicators";
|
|
100
|
+
|
|
101
|
+
// Forms and Input — interactive user input elements
|
|
102
|
+
if (
|
|
103
|
+
/^(button|input|checkbox|radiogroup|radio|select|form|field|toggle|textarea|switch|slider|range|datepicker|timepicker|datetimepicker|combobox|autocomplete|fileupload|fileinput|otpinput|pinfield|searchinput|searchbar|colorpicker|numberinput|currencyinput|passwordinput|formfield|forminput|searchfield)$/.test(n) ||
|
|
104
|
+
/button$|input$|field$|form$|picker$|^select$|toggle$|checkbox$|^radio$|switch$|slider$|upload$|combobox$/.test(n)
|
|
105
|
+
) return "Forms and Input";
|
|
106
|
+
|
|
107
|
+
// Navigation — page/section navigation patterns
|
|
108
|
+
if (
|
|
109
|
+
/^(navbar|nav|navigation|navmenu|menu|breadcrumb|breadcrumbs|tabs|tabbar|tablist|tabpanel|pagination|sidebar|topbar|link|navlink|menuitem|menubar|stepper|stepbar|stepindicator|progresstracker|sidenav)$/.test(n) ||
|
|
110
|
+
/navbar$|navmenu$|breadcrumb$|^tabs$|pagination$|sidebar$|topbar$|stepper$|stepbar$|sidenav$/.test(n)
|
|
111
|
+
) return "Navigation";
|
|
112
|
+
|
|
113
|
+
// Overlays and Layering — content floating above the page
|
|
114
|
+
if (
|
|
115
|
+
/^(modal|dialog|drawer|popover|tooltip|dropdown|overlay|lightbox|sheet|bottomsheet|contextmenu|actionsheet|flyout|slideover|offcanvas|alertdialog)$/.test(n) ||
|
|
116
|
+
/modal$|dialog$|drawer$|tooltip$|popover$|dropdown$|overlay$|sheet$|lightbox$/.test(n)
|
|
117
|
+
) return "Overlays and Layering";
|
|
118
|
+
|
|
119
|
+
// Loading — loading and skeleton states
|
|
120
|
+
if (
|
|
121
|
+
/^(spinner|skeleton|loader|progressbar|loadingbar|loadingindicator|skeletonloader|skeletoncard|loadingspinner|skeletonlist|skeletonrow|loadingoverlay)$/.test(n) ||
|
|
122
|
+
/spinner$|skeleton$|loader$|loading$|^progressbar$/.test(n)
|
|
123
|
+
) return "Loading";
|
|
124
|
+
|
|
125
|
+
// Messaging — system notifications and inline feedback
|
|
126
|
+
if (
|
|
127
|
+
/^(alert|banner|toast|notification|message|flag|snackbar|callout|announcement|inlinemessage|sectionmessage|toastnotification|errormessage|warningbanner|infobanner|successbanner)$/.test(n) ||
|
|
128
|
+
/alert$|banner$|toast$|notification$|^message$|snackbar$|callout$|^flag$/.test(n)
|
|
129
|
+
) return "Messaging";
|
|
130
|
+
|
|
131
|
+
// Images and Icons — visual assets
|
|
132
|
+
if (
|
|
133
|
+
/^(avatar|avatargroup|icon|image|logo|tile|thumbnail|photo|picture|iconbutton|useravatar|profileimage|brandlogo|appicon|mediaplayer|imagegallery)$/.test(n) ||
|
|
134
|
+
/^avatar$|avatargroup$|^icon$|^image$|^logo$|^tile$|thumbnail$|^photo$/.test(n)
|
|
135
|
+
) return "Images and Icons";
|
|
136
|
+
|
|
137
|
+
// Layout and Structure — structural page / content organisation
|
|
138
|
+
if (
|
|
139
|
+
/^(card|panel|container|grid|layout|page|section|divider|separator|accordion|collapsible|expandable|pagelayout|pageheader|applayout|applayoutshell|layoutgrid|contentblock|featureblock|herosection|featurelist|pricingcard|testimonial)$/.test(n) ||
|
|
140
|
+
/card$|panel$|container$|layout$|section$|divider$|separator$|accordion$|grid$|header$|^pagelayout$/.test(n)
|
|
141
|
+
) return "Layout and Structure";
|
|
142
|
+
|
|
143
|
+
// Text and Data Display — showing information and data
|
|
144
|
+
if (
|
|
145
|
+
/^(table|list|datatable|datagrid|chart|graph|stat|stats|metric|heading|text|label|code|display|summary|detail|statblock|metricscard|kpicard|statscard|datatablerow|tablerow|tablecell|listitem|listrow|leaderboard|scorecard)$/.test(n) ||
|
|
146
|
+
/table$|list$|chart$|graph$|stats$|metric$|^heading$|^code$|display$|detail$|summary$|kpicard$|scorecard$|leaderboard$/.test(n)
|
|
147
|
+
) return "Text and Data Display";
|
|
148
|
+
|
|
149
|
+
return null; // No category matched — caller falls back to original group
|
|
150
|
+
}
|
|
151
|
+
|
|
86
152
|
/** shadcn/ui composite component recipes: component name → imports + render. */
|
|
87
153
|
const RECIPES = {
|
|
88
154
|
Accordion: {
|
|
@@ -932,6 +998,105 @@ function scanComponentUsages(componentName, projectRoot) {
|
|
|
932
998
|
.map(([className, count]) => ({ className, count }));
|
|
933
999
|
}
|
|
934
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* Comprehensive component usage scanner for the "Usage" story.
|
|
1003
|
+
* Walks all src/ .tsx/.jsx files (excluding ui/, stories/, the component file itself)
|
|
1004
|
+
* and returns: which files use the component, total usage count, most-used prop names,
|
|
1005
|
+
* and co-occurring components (other PascalCase components in the same files).
|
|
1006
|
+
*/
|
|
1007
|
+
function scanComponentFileUsages(componentName, projectRoot) {
|
|
1008
|
+
const uiPathFrag = path.join("components", "ui") + path.sep;
|
|
1009
|
+
const storiesPathFrag = path.join("stories") + path.sep;
|
|
1010
|
+
|
|
1011
|
+
const fileUsages = []; // { relPath, basename, count }
|
|
1012
|
+
const propCounts = {}; // propName → count across all usages
|
|
1013
|
+
const coCompCounts = {}; // compName → co-occurrence count
|
|
1014
|
+
|
|
1015
|
+
const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1016
|
+
const usageRe = new RegExp(`<${escapedName}(?:\\s+([\\s\\S]*?))?(?:\\s*/>|\\s*>)`, "g");
|
|
1017
|
+
const coCompRe = /<([A-Z][a-zA-Z0-9]+)(?:\s|\/|>)/g;
|
|
1018
|
+
const SKIP_CO = new Set(["React", "Fragment", "Suspense", "StrictMode", "Component", "ErrorBoundary"]);
|
|
1019
|
+
|
|
1020
|
+
function walk(dir) {
|
|
1021
|
+
let entries;
|
|
1022
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
1023
|
+
for (const e of entries) {
|
|
1024
|
+
if (_SCAN_IGNORE_DIRS.has(e.name)) continue;
|
|
1025
|
+
const full = path.join(dir, e.name);
|
|
1026
|
+
if (e.isDirectory()) { walk(full); continue; }
|
|
1027
|
+
if (!/\.(tsx|jsx)$/.test(e.name)) continue;
|
|
1028
|
+
if (full.includes(uiPathFrag)) continue;
|
|
1029
|
+
if (full.includes(storiesPathFrag)) continue;
|
|
1030
|
+
// Skip the component's own definition file
|
|
1031
|
+
const baseNoExt = path.basename(full, path.extname(full));
|
|
1032
|
+
if (baseNoExt === componentName) continue;
|
|
1033
|
+
|
|
1034
|
+
let src;
|
|
1035
|
+
try { src = fs.readFileSync(full, "utf-8"); } catch { continue; }
|
|
1036
|
+
if (!src.includes(`<${componentName}`)) continue;
|
|
1037
|
+
|
|
1038
|
+
// Count usages and collect prop names
|
|
1039
|
+
let count = 0;
|
|
1040
|
+
let m;
|
|
1041
|
+
usageRe.lastIndex = 0;
|
|
1042
|
+
while ((m = usageRe.exec(src)) !== null) {
|
|
1043
|
+
count++;
|
|
1044
|
+
const propsStr = m[1] || "";
|
|
1045
|
+
// Extract prop names (e.g. variant="..." or isLoading or onClick={...})
|
|
1046
|
+
const propNameRe = /\b([a-z][a-zA-Z0-9]+)(?==|\s*>|\s*\/>|\s+[a-z])/g;
|
|
1047
|
+
let pm;
|
|
1048
|
+
while ((pm = propNameRe.exec(propsStr)) !== null) {
|
|
1049
|
+
const pn = pm[1];
|
|
1050
|
+
if (!["className", "style", "key", "id", "ref"].includes(pn)) {
|
|
1051
|
+
propCounts[pn] = (propCounts[pn] || 0) + 1;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (count > 0) {
|
|
1057
|
+
const relPath = path.relative(projectRoot, full);
|
|
1058
|
+
fileUsages.push({ relPath, basename: path.basename(full), count });
|
|
1059
|
+
|
|
1060
|
+
// Co-occurring components
|
|
1061
|
+
coCompRe.lastIndex = 0;
|
|
1062
|
+
while ((m = coCompRe.exec(src)) !== null) {
|
|
1063
|
+
const cn = m[1];
|
|
1064
|
+
if (cn !== componentName && !SKIP_CO.has(cn)) {
|
|
1065
|
+
coCompCounts[cn] = (coCompCounts[cn] || 0) + 1;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Walk standard src directories
|
|
1073
|
+
const srcDirs = [
|
|
1074
|
+
path.join(projectRoot, "src"),
|
|
1075
|
+
path.join(projectRoot, "client", "src"),
|
|
1076
|
+
path.join(projectRoot, "app"),
|
|
1077
|
+
].filter(d => fs.existsSync(d));
|
|
1078
|
+
for (const d of srcDirs) walk(d);
|
|
1079
|
+
|
|
1080
|
+
const totalCount = fileUsages.reduce((s, f) => s + f.count, 0);
|
|
1081
|
+
|
|
1082
|
+
const topProps = Object.entries(propCounts)
|
|
1083
|
+
.sort((a, b) => b[1] - a[1])
|
|
1084
|
+
.slice(0, 6)
|
|
1085
|
+
.map(([name, count]) => ({ name, count }));
|
|
1086
|
+
|
|
1087
|
+
const coComponents = Object.entries(coCompCounts)
|
|
1088
|
+
.sort((a, b) => b[1] - a[1])
|
|
1089
|
+
.slice(0, 6)
|
|
1090
|
+
.map(([name, count]) => ({ name, count }));
|
|
1091
|
+
|
|
1092
|
+
return {
|
|
1093
|
+
files: fileUsages.sort((a, b) => b.count - a.count).slice(0, 10),
|
|
1094
|
+
totalCount,
|
|
1095
|
+
topProps,
|
|
1096
|
+
coComponents,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
935
1100
|
/** Render'da args'a uygulanacak fallback (useState(estimate.toString()) gibi kullanımlar için; args/Controls undefined yapsa bile). */
|
|
936
1101
|
const RENDER_ARGS_FALLBACKS = {
|
|
937
1102
|
TaskEstimateInput: ", estimate: (args && args.estimate) ?? 0, value: (args && args.value) ?? 0, task: (args && args.task) ?? { id: \"1\", title: \"Example\", estimate: 0 }",
|
|
@@ -1770,12 +1935,17 @@ function buildStoryFileContent(comp) {
|
|
|
1770
1935
|
}
|
|
1771
1936
|
if (!importPath.startsWith(".")) importPath = "./" + importPath;
|
|
1772
1937
|
|
|
1773
|
-
//
|
|
1938
|
+
// Determine semantic category and story title
|
|
1774
1939
|
const rawGroup = comp.group || "Components";
|
|
1775
|
-
const
|
|
1940
|
+
const baseGroup = rawGroup === "shadcn" ? "UI"
|
|
1776
1941
|
: rawGroup.includes(".") ? "Components" // root-level file without subdirectory
|
|
1777
1942
|
: rawGroup;
|
|
1778
|
-
//
|
|
1943
|
+
// Apply Atlassian-style semantic categorization to ALL components (shadcn and project-specific alike).
|
|
1944
|
+
// If no semantic category is detected, fall back to the original folder-based group.
|
|
1945
|
+
const compTokensForClassify = Array.isArray(comp.tokens) ? comp.tokens : [];
|
|
1946
|
+
const compPropsForClassify = Array.isArray(comp.props) ? comp.props : [];
|
|
1947
|
+
const semanticCat = classifyComponent(componentName, compTokensForClassify, compPropsForClassify);
|
|
1948
|
+
const group = semanticCat || baseGroup;
|
|
1779
1949
|
const title = `${group}/${componentName}`;
|
|
1780
1950
|
|
|
1781
1951
|
// Read component source once (used for export style, props, CVA parsing)
|
|
@@ -1940,6 +2110,170 @@ function buildStoryFileContent(comp) {
|
|
|
1940
2110
|
}
|
|
1941
2111
|
}
|
|
1942
2112
|
|
|
2113
|
+
// --- Design Tokens story ---
|
|
2114
|
+
// Generate a "Tokens" export that shows which Tailwind tokens this component uses,
|
|
2115
|
+
// resolved to project-specific color/spacing values from foundations.
|
|
2116
|
+
{
|
|
2117
|
+
const compTokens = Array.isArray(comp.tokens) ? comp.tokens : [];
|
|
2118
|
+
const foundColors = FOUNDATIONS_DATA?.colors || {};
|
|
2119
|
+
if (compTokens.length >= 3) {
|
|
2120
|
+
// Keep only base tokens (no state/responsive prefixes like hover:, focus:, sm:, dark:)
|
|
2121
|
+
const cleanTokens = compTokens.filter(t => !/:/.test(t));
|
|
2122
|
+
|
|
2123
|
+
// Exclude font-size tokens (text-xs, text-sm, text-base…) from color category
|
|
2124
|
+
const colorRaw = cleanTokens.filter(t =>
|
|
2125
|
+
/^(bg|text|border|ring|from|to|fill|stroke)-/.test(t) &&
|
|
2126
|
+
!/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\d)/.test(t)
|
|
2127
|
+
);
|
|
2128
|
+
const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]?|m[xylrbt]?|gap|space-[xy]|w-|h-|min-[wh]|max-[wh]|size-)/.test(t));
|
|
2129
|
+
const typographyRaw = cleanTokens.filter(t => /^(text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)|font-)/.test(t));
|
|
2130
|
+
const radiusRaw = cleanTokens.filter(t => /^rounded/.test(t));
|
|
2131
|
+
const animRaw = cleanTokens.filter(t => /^(transition|duration|animate|ease|delay)-/.test(t));
|
|
2132
|
+
|
|
2133
|
+
// Resolve semantic color tokens → hex swatch from foundations.colors
|
|
2134
|
+
const resolvedColors = colorRaw.map(token => {
|
|
2135
|
+
const m = token.match(/^(?:bg|text|border|ring|from|to|fill|stroke)-(.+)$/);
|
|
2136
|
+
const key = m ? m[1] : null;
|
|
2137
|
+
const entry = key ? foundColors[key] : null;
|
|
2138
|
+
const hex = entry?.hex && /^#[0-9a-fA-F]{3,8}$/.test(entry.hex) ? entry.hex : null;
|
|
2139
|
+
return { token, hex, label: key };
|
|
2140
|
+
});
|
|
2141
|
+
|
|
2142
|
+
const hasContent = resolvedColors.length > 0 || spacingRaw.length > 0 || typographyRaw.length > 0 || radiusRaw.length > 0 || animRaw.length > 0;
|
|
2143
|
+
if (hasContent) {
|
|
2144
|
+
lines.push("");
|
|
2145
|
+
lines.push(`export const Tokens: Story = {`);
|
|
2146
|
+
lines.push(` name: "Design Tokens",`);
|
|
2147
|
+
lines.push(` parameters: { layout: "fullscreen" },`);
|
|
2148
|
+
lines.push(` render: () => {`);
|
|
2149
|
+
lines.push(` const colorTokens = ${JSON.stringify(resolvedColors)};`);
|
|
2150
|
+
lines.push(` const spacingTokens = ${JSON.stringify(spacingRaw)};`);
|
|
2151
|
+
lines.push(` const typographyTokens = ${JSON.stringify(typographyRaw)};`);
|
|
2152
|
+
lines.push(` const radiusTokens = ${JSON.stringify(radiusRaw)};`);
|
|
2153
|
+
lines.push(` const animationTokens = ${JSON.stringify(animRaw)};`);
|
|
2154
|
+
lines.push(` const chip = (label: string, bg: string, color: string) => (`);
|
|
2155
|
+
lines.push(` <span key={label} style={{ fontFamily: "monospace", fontSize: 11, background: bg, color, padding: "3px 9px", borderRadius: 5, border: \`1px solid \${bg === "#f9fafb" ? "#e5e7eb" : bg}\`, whiteSpace: "nowrap" as any }}>{label}</span>`);
|
|
2156
|
+
lines.push(` );`);
|
|
2157
|
+
lines.push(` const section = (title: string, children: any) => (`);
|
|
2158
|
+
lines.push(` <section style={{ marginBottom: 28 }}>`);
|
|
2159
|
+
lines.push(` <p style={{ margin: "0 0 10px", fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.08em" }}>{title}</p>`);
|
|
2160
|
+
lines.push(` {children}`);
|
|
2161
|
+
lines.push(` </section>`);
|
|
2162
|
+
lines.push(` );`);
|
|
2163
|
+
lines.push(` return (`);
|
|
2164
|
+
lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh" }}>`);
|
|
2165
|
+
lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>Design Tokens</h2>`);
|
|
2166
|
+
lines.push(` <p style={{ fontSize: 13, color: "#6b7280", margin: "0 0 32px" }}>Tailwind utilities used in <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>${componentName}</code> — resolved to project values.</p>`);
|
|
2167
|
+
lines.push(` {colorTokens.length > 0 && section("Color", (`);
|
|
2168
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>`);
|
|
2169
|
+
lines.push(` {colorTokens.map(({ token, hex, label }) => (`);
|
|
2170
|
+
lines.push(` <div key={token} style={{ display: "flex", alignItems: "center", gap: 7, padding: "7px 12px", border: "1px solid #e5e7eb", borderRadius: 8, background: "#f9fafb" }}>`);
|
|
2171
|
+
lines.push(` {hex && <span style={{ display: "inline-block", width: 16, height: 16, borderRadius: 4, background: hex, border: "1px solid rgba(0,0,0,0.1)", flexShrink: 0 }} />}`);
|
|
2172
|
+
lines.push(` <code style={{ fontSize: 12, color: "#374151", fontWeight: 600 }}>{token}</code>`);
|
|
2173
|
+
lines.push(` {hex && <span style={{ fontSize: 11, color: "#9ca3af" }}>{hex}</span>}`);
|
|
2174
|
+
lines.push(` </div>`);
|
|
2175
|
+
lines.push(` ))}`);
|
|
2176
|
+
lines.push(` </div>`);
|
|
2177
|
+
lines.push(` ))}`);
|
|
2178
|
+
lines.push(` {spacingTokens.length > 0 && section("Spacing", (`);
|
|
2179
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{spacingTokens.map(t => chip(t, "#faf5ff", "#6d28d9"))}</div>`);
|
|
2180
|
+
lines.push(` ))}`);
|
|
2181
|
+
lines.push(` {typographyTokens.length > 0 && section("Typography", (`);
|
|
2182
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{typographyTokens.map(t => chip(t, "#fffbeb", "#92400e"))}</div>`);
|
|
2183
|
+
lines.push(` ))}`);
|
|
2184
|
+
lines.push(` {radiusTokens.length > 0 && section("Border Radius", (`);
|
|
2185
|
+
lines.push(` <div style={{ display: "flex", gap: 16, flexWrap: "wrap", alignItems: "flex-end" }}>`);
|
|
2186
|
+
lines.push(` {radiusTokens.map(t => {`);
|
|
2187
|
+
lines.push(` const px = t === "rounded-none" ? 0 : t === "rounded-sm" ? 2 : t === "rounded" ? 4 : t === "rounded-md" ? 6 : t === "rounded-lg" ? 8 : t === "rounded-xl" ? 12 : t === "rounded-2xl" ? 16 : t === "rounded-3xl" ? 24 : t === "rounded-full" ? 9999 : 4;`);
|
|
2188
|
+
lines.push(` return (`);
|
|
2189
|
+
lines.push(` <div key={t} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>`);
|
|
2190
|
+
lines.push(` <div style={{ width: 44, height: 44, background: "#6366f1", borderRadius: px }} />`);
|
|
2191
|
+
lines.push(` <code style={{ fontSize: 10, color: "#6b7280" }}>{t}</code>`);
|
|
2192
|
+
lines.push(` </div>`);
|
|
2193
|
+
lines.push(` );`);
|
|
2194
|
+
lines.push(` })}`);
|
|
2195
|
+
lines.push(` </div>`);
|
|
2196
|
+
lines.push(` ))}`);
|
|
2197
|
+
lines.push(` {animationTokens.length > 0 && section("Motion", (`);
|
|
2198
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{animationTokens.map(t => chip(t, "#f0fdf4", "#166534"))}</div>`);
|
|
2199
|
+
lines.push(` ))}`);
|
|
2200
|
+
lines.push(` </div>`);
|
|
2201
|
+
lines.push(` );`);
|
|
2202
|
+
lines.push(` },`);
|
|
2203
|
+
lines.push(`};`);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// --- Usage story ---
|
|
2209
|
+
// Shows where this component is actually used in the project:
|
|
2210
|
+
// files, usage count, most-used props, and co-occurring components.
|
|
2211
|
+
// All data is derived from the project source — no placeholder content.
|
|
2212
|
+
{
|
|
2213
|
+
const usageData = scanComponentFileUsages(componentName, PROJECT_ROOT);
|
|
2214
|
+
if (usageData.totalCount > 0) {
|
|
2215
|
+
lines.push(``);
|
|
2216
|
+
lines.push(`export const Usage: Story = {`);
|
|
2217
|
+
lines.push(` name: "Usage",`);
|
|
2218
|
+
lines.push(` parameters: { layout: "fullscreen" },`);
|
|
2219
|
+
lines.push(` render: () => {`);
|
|
2220
|
+
lines.push(` const usageFiles = ${JSON.stringify(usageData.files)};`);
|
|
2221
|
+
lines.push(` const totalCount = ${usageData.totalCount};`);
|
|
2222
|
+
lines.push(` const topProps = ${JSON.stringify(usageData.topProps)};`);
|
|
2223
|
+
lines.push(` const coComponents = ${JSON.stringify(usageData.coComponents)};`);
|
|
2224
|
+
lines.push(` const chip = (label: string, sub?: string) => (`);
|
|
2225
|
+
lines.push(` <span key={label} style={{ display: "inline-flex", alignItems: "center", gap: 4, background: "#f3f4f6", border: "1px solid #e5e7eb", borderRadius: 6, padding: "3px 10px", fontSize: 12, color: "#374151", fontFamily: "monospace", whiteSpace: "nowrap" as any }}>`);
|
|
2226
|
+
lines.push(` {label}{sub && <span style={{ color: "#9ca3af", fontSize: 11, marginLeft: 3 }}>×{sub}</span>}`);
|
|
2227
|
+
lines.push(` </span>`);
|
|
2228
|
+
lines.push(` );`);
|
|
2229
|
+
lines.push(` const sectionHead = (label: string) => (`);
|
|
2230
|
+
lines.push(` <p style={{ margin: "0 0 12px", fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase" as any, letterSpacing: "0.08em" }}>{label}</p>`);
|
|
2231
|
+
lines.push(` );`);
|
|
2232
|
+
lines.push(` return (`);
|
|
2233
|
+
lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh" }}>`);
|
|
2234
|
+
lines.push(` <div style={{ marginBottom: 32 }}>`);
|
|
2235
|
+
lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>Usage</h2>`);
|
|
2236
|
+
lines.push(` <p style={{ fontSize: 13, color: "#6b7280", margin: 0 }}>`);
|
|
2237
|
+
lines.push(` <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>${componentName}</code> appears <strong>{totalCount}</strong> time{totalCount !== 1 ? "s" : ""} across <strong>{usageFiles.length}</strong> file{usageFiles.length !== 1 ? "s" : ""} in this project.`);
|
|
2238
|
+
lines.push(` </p>`);
|
|
2239
|
+
lines.push(` </div>`);
|
|
2240
|
+
// Found In section
|
|
2241
|
+
lines.push(` <section style={{ marginBottom: 32 }}>`);
|
|
2242
|
+
lines.push(` {sectionHead("Found In")}`);
|
|
2243
|
+
lines.push(` <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>`);
|
|
2244
|
+
lines.push(` {usageFiles.map((f: any) => (`);
|
|
2245
|
+
lines.push(` <div key={f.relPath} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "9px 14px", border: "1px solid #e5e7eb", borderRadius: 8, background: "#f9fafb" }}>`);
|
|
2246
|
+
lines.push(` <code style={{ fontSize: 12, color: "#374151" }}>{f.relPath}</code>`);
|
|
2247
|
+
lines.push(` <span style={{ flexShrink: 0, marginLeft: 12, fontSize: 11, background: "#e0e7ff", color: "#3730a3", padding: "2px 8px", borderRadius: 12, fontWeight: 600 }}>{f.count}×</span>`);
|
|
2248
|
+
lines.push(` </div>`);
|
|
2249
|
+
lines.push(` ))}`);
|
|
2250
|
+
lines.push(` </div>`);
|
|
2251
|
+
lines.push(` </section>`);
|
|
2252
|
+
// Most Used Props section
|
|
2253
|
+
lines.push(` {topProps.length > 0 && (`);
|
|
2254
|
+
lines.push(` <section style={{ marginBottom: 32 }}>`);
|
|
2255
|
+
lines.push(` {sectionHead("Most Used Props")}`);
|
|
2256
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>`);
|
|
2257
|
+
lines.push(` {topProps.map((p: any) => chip(p.name, p.count.toString()))}`);
|
|
2258
|
+
lines.push(` </div>`);
|
|
2259
|
+
lines.push(` </section>`);
|
|
2260
|
+
lines.push(` )}`);
|
|
2261
|
+
// Often Used With section
|
|
2262
|
+
lines.push(` {coComponents.length > 0 && (`);
|
|
2263
|
+
lines.push(` <section style={{ marginBottom: 32 }}>`);
|
|
2264
|
+
lines.push(` {sectionHead("Often Used With")}`);
|
|
2265
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>`);
|
|
2266
|
+
lines.push(` {coComponents.map((c: any) => chip(c.name))}`);
|
|
2267
|
+
lines.push(` </div>`);
|
|
2268
|
+
lines.push(` </section>`);
|
|
2269
|
+
lines.push(` )}`);
|
|
2270
|
+
lines.push(` </div>`);
|
|
2271
|
+
lines.push(` );`);
|
|
2272
|
+
lines.push(` },`);
|
|
2273
|
+
lines.push(`};`);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
1943
2277
|
return lines.join("\n");
|
|
1944
2278
|
}
|
|
1945
2279
|
|
|
@@ -2125,7 +2459,7 @@ function writeFoundationsStories(foundations) {
|
|
|
2125
2459
|
const colorsContent = [
|
|
2126
2460
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
2127
2461
|
"",
|
|
2128
|
-
"const meta = { title: \"Foundations/Colors\" } satisfies Meta;",
|
|
2462
|
+
"const meta = { title: \"Foundations/Colors\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
2129
2463
|
"export default meta;",
|
|
2130
2464
|
"type Story = StoryObj;",
|
|
2131
2465
|
"",
|
|
@@ -2250,7 +2584,7 @@ function writeFoundationsStories(foundations) {
|
|
|
2250
2584
|
const typoContent = [
|
|
2251
2585
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
2252
2586
|
"",
|
|
2253
|
-
"const meta = { title: \"Foundations/Typography\" } satisfies Meta;",
|
|
2587
|
+
"const meta = { title: \"Foundations/Typography\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
2254
2588
|
"export default meta;",
|
|
2255
2589
|
"type Story = StoryObj;",
|
|
2256
2590
|
"",
|
|
@@ -2410,7 +2744,7 @@ function writeFoundationsStories(foundations) {
|
|
|
2410
2744
|
[
|
|
2411
2745
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
2412
2746
|
"",
|
|
2413
|
-
"const meta = { title: \"Foundations/Brand\" } satisfies Meta;",
|
|
2747
|
+
"const meta = { title: \"Foundations/Brand\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
2414
2748
|
"export default meta;",
|
|
2415
2749
|
"type Story = StoryObj;",
|
|
2416
2750
|
"",
|
|
@@ -2444,7 +2778,7 @@ function writeFoundationsStories(foundations) {
|
|
|
2444
2778
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
2445
2779
|
"import * as Lucide from \"lucide-react\";",
|
|
2446
2780
|
"",
|
|
2447
|
-
"const meta = { title: \"Foundations/Icons\" } satisfies Meta;",
|
|
2781
|
+
"const meta = { title: \"Foundations/Icons\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
2448
2782
|
"export default meta;",
|
|
2449
2783
|
"type Story = StoryObj;",
|
|
2450
2784
|
"type IconEntry = { name: string; total: number; topFiles: string[] };",
|
|
@@ -2508,10 +2842,16 @@ function writeFoundationsStories(foundations) {
|
|
|
2508
2842
|
" </div>",
|
|
2509
2843
|
" );",
|
|
2510
2844
|
" };",
|
|
2845
|
+
" const TILE_BKGS = [",
|
|
2846
|
+
" { bg: \"#f3f4f6\", iconColor: \"#374151\", border: \"#e5e7eb\", label: \"neutral\" },",
|
|
2847
|
+
" { bg: \"#fff\", iconColor: \"#374151\", border: \"#e5e7eb\", label: \"white\" },",
|
|
2848
|
+
" { bg: \"#4f46e5\", iconColor: \"#ffffff\", border: \"#4f46e5\", label: \"brand\" },",
|
|
2849
|
+
" ];",
|
|
2850
|
+
" const TILE_SIZES = [16, 20, 24];",
|
|
2511
2851
|
" return (",
|
|
2512
2852
|
" <div style={{ padding: 32, fontFamily: \"system-ui,sans-serif\", background: \"#fff\", minHeight: \"100vh\", color: \"#111\" }}>",
|
|
2513
2853
|
" <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Icons</h2>",
|
|
2514
|
-
" <div style={{ marginBottom:
|
|
2854
|
+
" <div style={{ marginBottom: 28 }}>",
|
|
2515
2855
|
" <p style={{ margin: 0, marginBottom: 4, fontSize: 13, color: \"#6b7280\" }}>",
|
|
2516
2856
|
" {iconData.length} icons imported from <code style={{ fontSize: 12, background: \"#f3f4f6\", padding: \"1px 5px\", borderRadius: 4 }}>lucide-react</code> in app code.",
|
|
2517
2857
|
" </p>",
|
|
@@ -2519,6 +2859,50 @@ function writeFoundationsStories(foundations) {
|
|
|
2519
2859
|
" Sorted by usage frequency · Badge = total JSX usages · Click any card to copy the icon name.",
|
|
2520
2860
|
" </p>",
|
|
2521
2861
|
" </div>",
|
|
2862
|
+
"",
|
|
2863
|
+
" {/* ── Icon Tiles ── */}",
|
|
2864
|
+
" {usedIcons.slice(0, 16).length > 0 && (",
|
|
2865
|
+
" <section style={{ marginBottom: 40 }}>",
|
|
2866
|
+
" <p style={{ margin: \"0 0 4px\", fontSize: 11, fontWeight: 700, color: \"#6b7280\", textTransform: \"uppercase\", letterSpacing: \"0.06em\" }}>",
|
|
2867
|
+
" Icon Tiles",
|
|
2868
|
+
" </p>",
|
|
2869
|
+
" <p style={{ margin: \"0 0 16px\", fontSize: 12, color: \"#9ca3af\" }}>",
|
|
2870
|
+
" Three sizes (16 · 20 · 24 px) × three backgrounds. Click to copy icon name.",
|
|
2871
|
+
" </p>",
|
|
2872
|
+
" <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(110px, 1fr))\", gap: 12 }}>",
|
|
2873
|
+
" {usedIcons.slice(0, 16).map((d) => {",
|
|
2874
|
+
" const Icon = (Lucide as Record<string, any>)[d.name];",
|
|
2875
|
+
" if (!Icon) return null;",
|
|
2876
|
+
" const isCopied = copied === d.name;",
|
|
2877
|
+
" return (",
|
|
2878
|
+
" <div key={d.name} onClick={() => handleCopy(d.name)}",
|
|
2879
|
+
" style={{ cursor: \"pointer\", border: `1px solid ${isCopied ? \"#16a34a\" : \"#e5e7eb\"}`, borderRadius: 10,",
|
|
2880
|
+
" padding: 10, background: isCopied ? \"#f0fdf4\" : \"#fff\", transition: \"border-color 0.15s\" }}>",
|
|
2881
|
+
" {TILE_BKGS.map((bkg) => (",
|
|
2882
|
+
" <div key={bkg.label} style={{ display: \"flex\", gap: 3, marginBottom: 3 }}>",
|
|
2883
|
+
" {TILE_SIZES.map((sz) => (",
|
|
2884
|
+
" <div key={sz} style={{",
|
|
2885
|
+
" width: 34, height: 34, borderRadius: 6,",
|
|
2886
|
+
" background: bkg.bg,",
|
|
2887
|
+
" border: `1px solid ${bkg.border}`,",
|
|
2888
|
+
" display: \"flex\", alignItems: \"center\", justifyContent: \"center\", flexShrink: 0,",
|
|
2889
|
+
" }}>",
|
|
2890
|
+
" <Icon size={sz} strokeWidth={1.75} color={bkg.iconColor} />",
|
|
2891
|
+
" </div>",
|
|
2892
|
+
" ))}",
|
|
2893
|
+
" </div>",
|
|
2894
|
+
" ))}",
|
|
2895
|
+
" <div style={{ fontSize: 10, color: isCopied ? \"#16a34a\" : \"#6b7280\", marginTop: 6,",
|
|
2896
|
+
" textAlign: \"center\", overflow: \"hidden\", textOverflow: \"ellipsis\", whiteSpace: \"nowrap\" as any }}>",
|
|
2897
|
+
" {isCopied ? \"Copied!\" : d.name}",
|
|
2898
|
+
" </div>",
|
|
2899
|
+
" </div>",
|
|
2900
|
+
" );",
|
|
2901
|
+
" })}",
|
|
2902
|
+
" </div>",
|
|
2903
|
+
" </section>",
|
|
2904
|
+
" )}",
|
|
2905
|
+
"",
|
|
2522
2906
|
" {usedIcons.length > 0 && (",
|
|
2523
2907
|
" <>",
|
|
2524
2908
|
" <p style={{ margin: \"0 0 12px\", fontSize: 12, fontWeight: 600, color: \"#374151\",",
|
|
@@ -2567,7 +2951,7 @@ function writeFoundationsStories(foundations) {
|
|
|
2567
2951
|
const gridContent = [
|
|
2568
2952
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
2569
2953
|
"",
|
|
2570
|
-
"const meta = { title: \"Foundations/Grid\" } satisfies Meta;",
|
|
2954
|
+
"const meta = { title: \"Foundations/Grid\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
2571
2955
|
"export default meta;",
|
|
2572
2956
|
"type Story = StoryObj;",
|
|
2573
2957
|
"",
|
|
@@ -2602,7 +2986,7 @@ function writeFoundationsStories(foundations) {
|
|
|
2602
2986
|
" );",
|
|
2603
2987
|
" }",
|
|
2604
2988
|
" return (",
|
|
2605
|
-
" <div style={{ fontFamily: \"monospace\", fontSize: 10, color: \"#475569\", marginBottom: 8, background: \"#
|
|
2989
|
+
" <div style={{ fontFamily: \"monospace\", fontSize: 10, color: \"#475569\", marginBottom: 8, background: \"#f1f5f9\", padding: \"4px 6px\", borderRadius: 4, border: \"1px solid #e5e7eb\" }}>",
|
|
2606
2990
|
" grid-cols-{colVal}",
|
|
2607
2991
|
" </div>",
|
|
2608
2992
|
" );",
|
|
@@ -2618,29 +3002,29 @@ function writeFoundationsStories(foundations) {
|
|
|
2618
3002
|
" <p style={{ margin: \"0 0 14px\", fontSize: 12, color: \"#475569\" }}>",
|
|
2619
3003
|
" Tailwind CSS breakpoint prefixes detected in source.",
|
|
2620
3004
|
" </p>",
|
|
2621
|
-
" <div style={{ position: \"relative\", height: 28, marginBottom: 14, background: \"#
|
|
3005
|
+
" <div style={{ position: \"relative\", height: 28, marginBottom: 14, background: \"#f1f5f9\", borderRadius: 8, border: \"1px solid #e2e8f0\", overflow: \"hidden\" }}>",
|
|
2622
3006
|
" {ALL_BPS.map((bp) => {",
|
|
2623
3007
|
" const cfg = BP_CONFIG[bp];",
|
|
2624
3008
|
" const used = !!(gridSystem.breakpoints as any)?.[bp];",
|
|
2625
3009
|
" const leftPct = (cfg.px / 1600) * 100;",
|
|
2626
3010
|
" return (",
|
|
2627
3011
|
" <div key={bp} style={{ position: \"absolute\", left: `${leftPct}%`, top: 0, bottom: 0,",
|
|
2628
|
-
" borderLeft: `2px solid ${used ? cfg.color : \"#
|
|
3012
|
+
" borderLeft: `2px solid ${used ? cfg.color : \"#cbd5e1\"}`,",
|
|
2629
3013
|
" display: \"flex\", alignItems: \"center\", paddingLeft: 4 }}>",
|
|
2630
|
-
" <span style={{ fontSize: 9, color: used ? cfg.color : \"#
|
|
3014
|
+
" <span style={{ fontSize: 9, color: used ? cfg.color : \"#94a3b8\",",
|
|
2631
3015
|
" whiteSpace: \"nowrap\", fontWeight: 600 }}>{bp}</span>",
|
|
2632
3016
|
" </div>",
|
|
2633
3017
|
" );",
|
|
2634
3018
|
" })}",
|
|
2635
3019
|
" </div>",
|
|
2636
|
-
" <div style={{ borderRadius: 8, border: \"1px solid #
|
|
3020
|
+
" <div style={{ borderRadius: 8, border: \"1px solid #e5e7eb\", overflow: \"hidden\" }}>",
|
|
2637
3021
|
" <table style={{ width: \"100%\", borderCollapse: \"collapse\", fontSize: 12 }}>",
|
|
2638
3022
|
" <thead>",
|
|
2639
|
-
" <tr style={{ background: \"#
|
|
2640
|
-
" <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#
|
|
2641
|
-
" <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#
|
|
2642
|
-
" <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#
|
|
2643
|
-
" <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#
|
|
3023
|
+
" <tr style={{ background: \"#f8fafc\", borderBottom: \"1px solid #e5e7eb\" }}>",
|
|
3024
|
+
" <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#6b7280\", fontWeight: 600 }}>Prefix</th>",
|
|
3025
|
+
" <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#6b7280\", fontWeight: 600 }}>Min-width</th>",
|
|
3026
|
+
" <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#6b7280\", fontWeight: 600 }}>Usages</th>",
|
|
3027
|
+
" <th style={{ textAlign: \"left\", padding: \"8px 12px\", color: \"#6b7280\", fontWeight: 600 }}>Top components</th>",
|
|
2644
3028
|
" </tr>",
|
|
2645
3029
|
" </thead>",
|
|
2646
3030
|
" <tbody>",
|
|
@@ -2648,12 +3032,12 @@ function writeFoundationsStories(foundations) {
|
|
|
2648
3032
|
" const cfg = BP_CONFIG[bp];",
|
|
2649
3033
|
" const data = (gridSystem.breakpoints as any)?.[bp];",
|
|
2650
3034
|
" return (",
|
|
2651
|
-
" <tr key={bp} style={{ borderBottom: i < 4 ? \"1px solid #
|
|
3035
|
+
" <tr key={bp} style={{ borderBottom: i < 4 ? \"1px solid #f0f4f8\" : \"none\", opacity: data ? 1 : 0.4 }}>",
|
|
2652
3036
|
" <td style={{ padding: \"8px 12px\" }}>",
|
|
2653
|
-
" <code style={{ fontSize: 12, fontWeight: 700, color: data ? cfg.color : \"#
|
|
3037
|
+
" <code style={{ fontSize: 12, fontWeight: 700, color: data ? cfg.color : \"#94a3b8\" }}>{bp}:</code>",
|
|
2654
3038
|
" </td>",
|
|
2655
|
-
" <td style={{ padding: \"8px 12px\", color: \"#
|
|
2656
|
-
" <td style={{ padding: \"8px 12px\", color: data ? \"#
|
|
3039
|
+
" <td style={{ padding: \"8px 12px\", color: \"#475569\", fontFamily: \"monospace\" }}>{cfg.px}px</td>",
|
|
3040
|
+
" <td style={{ padding: \"8px 12px\", color: data ? \"#111827\" : \"#94a3b8\" }}>",
|
|
2657
3041
|
" {data ? `×${data.count}` : \"—\"}",
|
|
2658
3042
|
" </td>",
|
|
2659
3043
|
" <td style={{ padding: \"8px 12px\" }}>",
|
|
@@ -2680,12 +3064,12 @@ function writeFoundationsStories(foundations) {
|
|
|
2680
3064
|
" </p>",
|
|
2681
3065
|
" <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(180px, 1fr))\", gap: 12 }}>",
|
|
2682
3066
|
" {(colEntries as [string, { count: number; topFiles: string[] }][]).map(([val, data]) => (",
|
|
2683
|
-
" <div key={val} style={{ background: \"#
|
|
3067
|
+
" <div key={val} style={{ background: \"#f8fafc\", border: \"1px solid #e5e7eb\", borderRadius: 8, padding: 12 }}>",
|
|
2684
3068
|
" {renderColPreview(val)}",
|
|
2685
3069
|
" <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", marginBottom: 6 }}>",
|
|
2686
|
-
" <code style={{ fontSize: 11, color: \"#
|
|
2687
|
-
" <span style={{ fontSize: 11, fontWeight: 600, background: \"#
|
|
2688
|
-
" color: \"#
|
|
3070
|
+
" <code style={{ fontSize: 11, color: \"#4f46e5\" }}>grid-cols-{val}</code>",
|
|
3071
|
+
" <span style={{ fontSize: 11, fontWeight: 600, background: \"#e8f0fe\",",
|
|
3072
|
+
" color: \"#3730a3\", padding: \"1px 6px\", borderRadius: 999 }}>×{data.count}</span>",
|
|
2689
3073
|
" </div>",
|
|
2690
3074
|
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 4 }}>",
|
|
2691
3075
|
" {data.topFiles.map(chip)}",
|
|
@@ -2706,8 +3090,8 @@ function writeFoundationsStories(foundations) {
|
|
|
2706
3090
|
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 8 }}>",
|
|
2707
3091
|
" {(gapEntries as [string, { count: number; topFiles: string[] }][]).map(([val, data]) => (",
|
|
2708
3092
|
" <span key={val} style={{ display: \"flex\", alignItems: \"center\", gap: 6,",
|
|
2709
|
-
" background: \"#
|
|
2710
|
-
" <code style={{ color: \"#
|
|
3093
|
+
" background: \"#f8fafc\", border: \"1px solid #e5e7eb\", borderRadius: 6, padding: \"5px 10px\", fontSize: 12 }}>",
|
|
3094
|
+
" <code style={{ color: \"#0891b2\" }}>gap-{val}</code>",
|
|
2711
3095
|
" <span style={{ color: \"#374151\", fontSize: 11 }}>×{data.count}</span>",
|
|
2712
3096
|
" </span>",
|
|
2713
3097
|
" ))}",
|
|
@@ -2725,8 +3109,8 @@ function writeFoundationsStories(foundations) {
|
|
|
2725
3109
|
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 8 }}>",
|
|
2726
3110
|
" {(maxWEntries as [string, number][]).map(([val, count]) => (",
|
|
2727
3111
|
" <span key={val} style={{ display: \"flex\", alignItems: \"center\", gap: 6,",
|
|
2728
|
-
" background: \"#
|
|
2729
|
-
" <code style={{ color: \"#
|
|
3112
|
+
" background: \"#f8fafc\", border: \"1px solid #e5e7eb\", borderRadius: 6, padding: \"5px 10px\", fontSize: 12 }}>",
|
|
3113
|
+
" <code style={{ color: \"#16a34a\" }}>max-w-{val}</code>",
|
|
2730
3114
|
" <span style={{ color: \"#475569\", fontSize: 11 }}>×{count}</span>",
|
|
2731
3115
|
" </span>",
|
|
2732
3116
|
" ))}",
|
|
@@ -2756,7 +3140,7 @@ function writeFoundationsStories(foundations) {
|
|
|
2756
3140
|
[
|
|
2757
3141
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
2758
3142
|
"",
|
|
2759
|
-
"const meta = { title: \"Foundations/Button Usage\" } satisfies Meta;",
|
|
3143
|
+
"const meta = { title: \"Foundations/Button Usage\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
2760
3144
|
"export default meta;",
|
|
2761
3145
|
"type Story = StoryObj;",
|
|
2762
3146
|
"",
|
|
@@ -2770,19 +3154,19 @@ function writeFoundationsStories(foundations) {
|
|
|
2770
3154
|
" <table style={{ borderCollapse: \"collapse\", width: \"100%\", fontSize: 13 }}>",
|
|
2771
3155
|
" <thead>",
|
|
2772
3156
|
" <tr>",
|
|
2773
|
-
" <th style={{ textAlign: \"left\", padding: \"4px 8px\", borderBottom: \"1px solid #
|
|
2774
|
-
" <th style={{ textAlign: \"left\", padding: \"4px 8px\", borderBottom: \"1px solid #
|
|
2775
|
-
" <th style={{ textAlign: \"left\", padding: \"4px 8px\", borderBottom: \"1px solid #
|
|
2776
|
-
" <th style={{ textAlign: \"right\", padding: \"4px 8px\", borderBottom: \"1px solid #
|
|
3157
|
+
" <th style={{ textAlign: \"left\", padding: \"4px 8px\", borderBottom: \"1px solid #e5e7eb\" }}>Variant</th>",
|
|
3158
|
+
" <th style={{ textAlign: \"left\", padding: \"4px 8px\", borderBottom: \"1px solid #e5e7eb\" }}>Size</th>",
|
|
3159
|
+
" <th style={{ textAlign: \"left\", padding: \"4px 8px\", borderBottom: \"1px solid #e5e7eb\" }}>asChild</th>",
|
|
3160
|
+
" <th style={{ textAlign: \"right\", padding: \"4px 8px\", borderBottom: \"1px solid #e5e7eb\" }}>Count</th>",
|
|
2777
3161
|
" </tr>",
|
|
2778
3162
|
" </thead>",
|
|
2779
3163
|
" <tbody>",
|
|
2780
3164
|
" {combos.map((c) => (",
|
|
2781
3165
|
" <tr key={c.key}>",
|
|
2782
|
-
" <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid #
|
|
2783
|
-
" <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid #
|
|
2784
|
-
" <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid #
|
|
2785
|
-
" <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid #
|
|
3166
|
+
" <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid #f0f0f0\" }}>{c.variant}</td>",
|
|
3167
|
+
" <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid #f0f0f0\" }}>{c.size}</td>",
|
|
3168
|
+
" <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid #f0f0f0\" }}>{c.asChild ? \"yes\" : \"no\"}</td>",
|
|
3169
|
+
" <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid #f0f0f0\", textAlign: \"right\" }}>{c.count}</td>",
|
|
2786
3170
|
" </tr>",
|
|
2787
3171
|
" ))}",
|
|
2788
3172
|
" </tbody>",
|
|
@@ -2831,7 +3215,7 @@ function writeFoundationsStories(foundations) {
|
|
|
2831
3215
|
const spacingContent = [
|
|
2832
3216
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
2833
3217
|
"",
|
|
2834
|
-
"const meta = { title: \"Foundations/Spacing & Layout\" } satisfies Meta;",
|
|
3218
|
+
"const meta = { title: \"Foundations/Spacing & Layout\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
2835
3219
|
"export default meta;",
|
|
2836
3220
|
"type Story = StoryObj;",
|
|
2837
3221
|
"",
|
|
@@ -2938,7 +3322,7 @@ function writeFoundationsStories(foundations) {
|
|
|
2938
3322
|
const elevationContent = [
|
|
2939
3323
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
2940
3324
|
"",
|
|
2941
|
-
"const meta = { title: \"Foundations/Elevation & Shadows\" } satisfies Meta;",
|
|
3325
|
+
"const meta = { title: \"Foundations/Elevation & Shadows\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
2942
3326
|
"export default meta;",
|
|
2943
3327
|
"type Story = StoryObj;",
|
|
2944
3328
|
"",
|
|
@@ -3060,7 +3444,7 @@ function writeFoundationsStories(foundations) {
|
|
|
3060
3444
|
const borderContent = [
|
|
3061
3445
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
3062
3446
|
"",
|
|
3063
|
-
"const meta = { title: \"Foundations/Border & Radius\" } satisfies Meta;",
|
|
3447
|
+
"const meta = { title: \"Foundations/Border & Radius\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
3064
3448
|
"export default meta;",
|
|
3065
3449
|
"type Story = StoryObj;",
|
|
3066
3450
|
"",
|
|
@@ -3118,7 +3502,6 @@ function writeFoundationsStories(foundations) {
|
|
|
3118
3502
|
const motion = foundations?.motion || {};
|
|
3119
3503
|
const durations = motion.transitionDuration || {};
|
|
3120
3504
|
const easings = motion.transitionTimingFunction || {};
|
|
3121
|
-
const animations = motion.animation || {};
|
|
3122
3505
|
|
|
3123
3506
|
const durationRows = Object.entries(durations)
|
|
3124
3507
|
.filter(([k]) => k !== "0" && k !== "DEFAULT")
|
|
@@ -3129,103 +3512,168 @@ function writeFoundationsStories(foundations) {
|
|
|
3129
3512
|
.filter(([k]) => k !== "DEFAULT")
|
|
3130
3513
|
.map(([token, value]) => ({ token, value }));
|
|
3131
3514
|
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3515
|
+
// Tailwind default scales (used when project has no custom tokens)
|
|
3516
|
+
const TAILWIND_DEFAULT_DURATIONS = [
|
|
3517
|
+
{ token: "75", value: "75ms" },
|
|
3518
|
+
{ token: "100", value: "100ms" },
|
|
3519
|
+
{ token: "150", value: "150ms" },
|
|
3520
|
+
{ token: "200", value: "200ms" },
|
|
3521
|
+
{ token: "300", value: "300ms" },
|
|
3522
|
+
{ token: "500", value: "500ms" },
|
|
3523
|
+
{ token: "700", value: "700ms" },
|
|
3524
|
+
{ token: "1000", value: "1000ms" },
|
|
3525
|
+
];
|
|
3526
|
+
const TAILWIND_DEFAULT_EASINGS = [
|
|
3527
|
+
{ token: "linear", value: "linear" },
|
|
3528
|
+
{ token: "ease-in", value: "cubic-bezier(0.4, 0, 1, 1)" },
|
|
3529
|
+
{ token: "ease-out", value: "cubic-bezier(0, 0, 0.2, 1)" },
|
|
3530
|
+
{ token: "ease-in-out", value: "cubic-bezier(0.4, 0, 0.2, 1)" },
|
|
3531
|
+
];
|
|
3532
|
+
|
|
3533
|
+
const effectiveDurations = durationRows.length > 0 ? durationRows : TAILWIND_DEFAULT_DURATIONS;
|
|
3534
|
+
const effectiveEasings = easingRows.length > 0 ? easingRows.slice(0, 4) : TAILWIND_DEFAULT_EASINGS;
|
|
3535
|
+
const hasCustomDurations = durationRows.length > 0;
|
|
3135
3536
|
|
|
3136
|
-
|
|
3537
|
+
// Which duration tokens are actually used in the project?
|
|
3538
|
+
const usedDurationKeys = new Set(
|
|
3539
|
+
(foundations?.tokenUsage?.animations || [])
|
|
3540
|
+
.filter(a => a.token.startsWith("duration-"))
|
|
3541
|
+
.map(a => a.token.replace("duration-", ""))
|
|
3542
|
+
);
|
|
3543
|
+
|
|
3544
|
+
// All motion-related token chips from tokenUsage
|
|
3545
|
+
const motionChips = (foundations?.tokenUsage?.animations || []).slice(0, 24);
|
|
3137
3546
|
|
|
3138
3547
|
const motionContent = [
|
|
3139
3548
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
3140
|
-
"import { useState } from \"react\";",
|
|
3549
|
+
"import { useState, useEffect } from \"react\";",
|
|
3141
3550
|
"",
|
|
3142
|
-
"const meta = { title: \"Foundations/Motion & Interaction\" } satisfies Meta;",
|
|
3551
|
+
"const meta = { title: \"Foundations/Motion & Interaction\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
3143
3552
|
"export default meta;",
|
|
3144
3553
|
"type Story = StoryObj;",
|
|
3145
3554
|
"",
|
|
3146
|
-
`const
|
|
3147
|
-
`const
|
|
3148
|
-
`const
|
|
3149
|
-
`const
|
|
3555
|
+
`const effectiveDurations: { token: string; value: string }[] = ${JSON.stringify(effectiveDurations)};`,
|
|
3556
|
+
`const effectiveEasings: { token: string; value: string }[] = ${JSON.stringify(effectiveEasings)};`,
|
|
3557
|
+
`const usedDurationKeys: Set<string> = new Set(${JSON.stringify([...usedDurationKeys])});`,
|
|
3558
|
+
`const motionChips: { token: string; count: number }[] = ${JSON.stringify(motionChips)};`,
|
|
3559
|
+
`const hasCustomDurations = ${hasCustomDurations};`,
|
|
3150
3560
|
"",
|
|
3561
|
+
"export const Default: Story = {",
|
|
3562
|
+
" render: () => {",
|
|
3563
|
+
" const [replayKey, setReplayKey] = useState(0);",
|
|
3564
|
+
" const [easingKey, setEasingKey] = useState(0);",
|
|
3151
3565
|
"",
|
|
3152
|
-
"
|
|
3153
|
-
"
|
|
3154
|
-
"
|
|
3155
|
-
"
|
|
3156
|
-
"
|
|
3157
|
-
"
|
|
3158
|
-
"
|
|
3159
|
-
"
|
|
3160
|
-
"
|
|
3161
|
-
"
|
|
3162
|
-
"
|
|
3163
|
-
"
|
|
3164
|
-
" top: 4, left: active ? 172 : 4,",
|
|
3165
|
-
" width: 44, height: 24,",
|
|
3166
|
-
" background: \"var(--color-primary, #6366f1)\",",
|
|
3167
|
-
" borderRadius: 6,",
|
|
3168
|
-
" transition: `left ${value} cubic-bezier(0.4,0,0.2,1)`,",
|
|
3169
|
-
" }} />",
|
|
3170
|
-
" </div>",
|
|
3171
|
-
" <span style={{ fontSize: 11, color: \"#d1d5db\" }}>click</span>",
|
|
3172
|
-
" </div>",
|
|
3173
|
-
" );",
|
|
3174
|
-
"}",
|
|
3566
|
+
" // Inject @keyframes once into the document head",
|
|
3567
|
+
" useEffect(() => {",
|
|
3568
|
+
" const id = \"vds-motion-styles\";",
|
|
3569
|
+
" if (document.getElementById(id)) return;",
|
|
3570
|
+
" const el = document.createElement(\"style\");",
|
|
3571
|
+
" el.id = id;",
|
|
3572
|
+
" el.textContent = [",
|
|
3573
|
+
" \"@keyframes vdsEnter { from { opacity: 0; transform: translateY(14px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } }\",",
|
|
3574
|
+
" \"@keyframes vdsSlide { from { opacity: 0.15; transform: translateX(-56px); } to { opacity: 1; transform: translateX(0); } }\",",
|
|
3575
|
+
" ].join(\" \");",
|
|
3576
|
+
" document.head.appendChild(el);",
|
|
3577
|
+
" }, []);",
|
|
3175
3578
|
"",
|
|
3176
|
-
"
|
|
3177
|
-
"
|
|
3178
|
-
"
|
|
3179
|
-
"
|
|
3180
|
-
"
|
|
3181
|
-
"
|
|
3182
|
-
"
|
|
3183
|
-
"
|
|
3184
|
-
"
|
|
3185
|
-
"
|
|
3186
|
-
"
|
|
3187
|
-
"
|
|
3188
|
-
"
|
|
3189
|
-
"
|
|
3579
|
+
" const replayBtn = (onClick: () => void) => (",
|
|
3580
|
+
" <button onClick={onClick} style={{ padding: \"6px 16px\", borderRadius: 8, border: \"1px solid #e5e7eb\",",
|
|
3581
|
+
" background: \"#f9fafb\", fontSize: 12, fontWeight: 600, cursor: \"pointer\", color: \"#374151\", flexShrink: 0 }}>",
|
|
3582
|
+
" ↺ Replay",
|
|
3583
|
+
" </button>",
|
|
3584
|
+
" );",
|
|
3585
|
+
"",
|
|
3586
|
+
" return (",
|
|
3587
|
+
" <div style={{ padding: 40, fontFamily: \"system-ui,sans-serif\", background: \"#fff\", minHeight: \"100vh\", color: \"#111\" }}>",
|
|
3588
|
+
" <h2 style={{ fontSize: 22, fontWeight: 700, margin: \"0 0 4px\" }}>Motion & Interaction</h2>",
|
|
3589
|
+
" <p style={{ fontSize: 13, color: \"#6b7280\", margin: \"0 0 40px\" }}>Consistent timing and easing for smooth, intentional motion.</p>",
|
|
3590
|
+
"",
|
|
3591
|
+
" {/* ── Entering Motion ── */}",
|
|
3592
|
+
" <section style={{ marginBottom: 48 }}>",
|
|
3593
|
+
" <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"flex-start\", marginBottom: 18 }}>",
|
|
3594
|
+
" <div>",
|
|
3595
|
+
" <p style={{ margin: \"0 0 2px\", fontSize: 11, fontWeight: 700, color: \"#6b7280\", textTransform: \"uppercase\", letterSpacing: \"0.08em\" }}>",
|
|
3596
|
+
" {hasCustomDurations ? \"Duration Scale\" : \"Duration Scale — Tailwind defaults\"}",
|
|
3597
|
+
" </p>",
|
|
3598
|
+
" <p style={{ margin: 0, fontSize: 12, color: \"#9ca3af\" }}>",
|
|
3599
|
+
" Each box enters with a different duration. {usedDurationKeys.size > 0 ? \"Solid fill = used in this project.\" : \"\"}",
|
|
3600
|
+
" </p>",
|
|
3601
|
+
" </div>",
|
|
3602
|
+
" {replayBtn(() => setReplayKey(k => k + 1))}",
|
|
3190
3603
|
" </div>",
|
|
3191
|
-
"
|
|
3192
|
-
"
|
|
3193
|
-
"
|
|
3194
|
-
"
|
|
3195
|
-
"
|
|
3196
|
-
"
|
|
3197
|
-
"
|
|
3198
|
-
"
|
|
3199
|
-
"
|
|
3200
|
-
"
|
|
3201
|
-
"
|
|
3202
|
-
"
|
|
3203
|
-
"
|
|
3204
|
-
"
|
|
3205
|
-
"
|
|
3206
|
-
"
|
|
3207
|
-
"
|
|
3208
|
-
"
|
|
3209
|
-
"
|
|
3604
|
+
" <div style={{ display: \"flex\", gap: 12, flexWrap: \"wrap\" }}>",
|
|
3605
|
+
" {effectiveDurations.map(({ token, value }) => {",
|
|
3606
|
+
" const isActive = usedDurationKeys.has(token);",
|
|
3607
|
+
" return (",
|
|
3608
|
+
" <div key={`${token}-${replayKey}`} style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"center\", gap: 8 }}>",
|
|
3609
|
+
" <div style={{",
|
|
3610
|
+
" width: 72, height: 72, borderRadius: 14,",
|
|
3611
|
+
" background: isActive ? \"#4f46e5\" : \"#e0e7ff\",",
|
|
3612
|
+
" border: isActive ? \"2px solid #4f46e5\" : \"2px solid #c7d2fe\",",
|
|
3613
|
+
" animation: `vdsEnter ${value} cubic-bezier(0.2, 0, 0, 1) both`,",
|
|
3614
|
+
" }} />",
|
|
3615
|
+
" <div style={{ textAlign: \"center\" }}>",
|
|
3616
|
+
" <div style={{ fontSize: 12, fontWeight: isActive ? 700 : 500, color: isActive ? \"#4f46e5\" : \"#374151\" }}>{value}</div>",
|
|
3617
|
+
" <div style={{ fontSize: 10, color: \"#9ca3af\", fontFamily: \"monospace\" }}>duration-{token}</div>",
|
|
3618
|
+
" </div>",
|
|
3619
|
+
" </div>",
|
|
3620
|
+
" );",
|
|
3621
|
+
" })}",
|
|
3622
|
+
" </div>",
|
|
3623
|
+
" </section>",
|
|
3624
|
+
"",
|
|
3625
|
+
" {/* ── Easing Comparison ── */}",
|
|
3626
|
+
" <section style={{ marginBottom: 48 }}>",
|
|
3627
|
+
" <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"flex-start\", marginBottom: 18 }}>",
|
|
3628
|
+
" <div>",
|
|
3629
|
+
" <p style={{ margin: \"0 0 2px\", fontSize: 11, fontWeight: 700, color: \"#6b7280\", textTransform: \"uppercase\", letterSpacing: \"0.08em\" }}>Easing</p>",
|
|
3630
|
+
" <p style={{ margin: 0, fontSize: 12, color: \"#9ca3af\" }}>Same duration (400ms), different easing curves — notice how each feels.</p>",
|
|
3210
3631
|
" </div>",
|
|
3211
|
-
"
|
|
3212
|
-
"
|
|
3213
|
-
"
|
|
3214
|
-
"
|
|
3215
|
-
"
|
|
3216
|
-
"
|
|
3217
|
-
"
|
|
3218
|
-
"
|
|
3219
|
-
"
|
|
3220
|
-
" <
|
|
3221
|
-
"
|
|
3632
|
+
" {replayBtn(() => setEasingKey(k => k + 1))}",
|
|
3633
|
+
" </div>",
|
|
3634
|
+
" <div style={{ display: \"flex\", flexDirection: \"column\", gap: 12 }}>",
|
|
3635
|
+
" {effectiveEasings.map(({ token, value }) => (",
|
|
3636
|
+
" <div key={`${token}-${easingKey}`} style={{ display: \"flex\", alignItems: \"center\", gap: 20 }}>",
|
|
3637
|
+
" <div style={{ width: 130, flexShrink: 0 }}>",
|
|
3638
|
+
" <div style={{ fontSize: 13, fontWeight: 600, color: \"#374151\", marginBottom: 2 }}>{token}</div>",
|
|
3639
|
+
" <code style={{ fontSize: 10, color: \"#9ca3af\", wordBreak: \"break-all\" as any }}>{value}</code>",
|
|
3640
|
+
" </div>",
|
|
3641
|
+
" <div style={{ flex: 1, position: \"relative\", height: 52, background: \"#f8fafc\",",
|
|
3642
|
+
" borderRadius: 12, border: \"1px solid #e5e7eb\", overflow: \"hidden\" }}>",
|
|
3643
|
+
" <div style={{",
|
|
3644
|
+
" position: \"absolute\", left: 10, top: \"50%\", transform: \"translateY(-50%)\",",
|
|
3645
|
+
" width: 40, height: 40, background: \"#4f46e5\", borderRadius: 10,",
|
|
3646
|
+
" animation: `vdsSlide 400ms ${value} both`,",
|
|
3647
|
+
" }} />",
|
|
3648
|
+
" </div>",
|
|
3222
3649
|
" </div>",
|
|
3223
3650
|
" ))}",
|
|
3224
3651
|
" </div>",
|
|
3225
|
-
"
|
|
3226
|
-
"
|
|
3227
|
-
"
|
|
3228
|
-
"
|
|
3652
|
+
" </section>",
|
|
3653
|
+
"",
|
|
3654
|
+
" {/* ── Motion Token Usage ── */}",
|
|
3655
|
+
" <section>",
|
|
3656
|
+
" <p style={{ margin: \"0 0 2px\", fontSize: 11, fontWeight: 700, color: \"#6b7280\", textTransform: \"uppercase\", letterSpacing: \"0.08em\" }}>",
|
|
3657
|
+
" Motion Tokens Used in Project",
|
|
3658
|
+
" </p>",
|
|
3659
|
+
" <p style={{ margin: \"0 0 12px\", fontSize: 12, color: \"#9ca3af\" }}>transition-*, duration-*, animate-* utilities detected in source files.</p>",
|
|
3660
|
+
" {motionChips.length === 0 ? (",
|
|
3661
|
+
" <p style={{ fontSize: 13, color: \"#9ca3af\" }}>No transition or animation utilities detected in source files.</p>",
|
|
3662
|
+
" ) : (",
|
|
3663
|
+
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6 }}>",
|
|
3664
|
+
" {motionChips.map(({ token, count }) => (",
|
|
3665
|
+
" <span key={token} style={{ padding: \"4px 10px\", background: \"#f0fdf4\", color: \"#166534\",",
|
|
3666
|
+
" border: \"1px solid #bbf7d0\", borderRadius: 20, fontSize: 12, fontFamily: \"monospace\" }}>",
|
|
3667
|
+
" {token} <span style={{ opacity: 0.7 }}>×{count}</span>",
|
|
3668
|
+
" </span>",
|
|
3669
|
+
" ))}",
|
|
3670
|
+
" </div>",
|
|
3671
|
+
" )}",
|
|
3672
|
+
" </section>",
|
|
3673
|
+
"",
|
|
3674
|
+
" </div>",
|
|
3675
|
+
" );",
|
|
3676
|
+
" },",
|
|
3229
3677
|
"};",
|
|
3230
3678
|
].join("\n");
|
|
3231
3679
|
fs.writeFileSync(path.join(foundationsDir, "Motion.stories.tsx"), motionContent, "utf-8");
|
|
@@ -3250,7 +3698,7 @@ function writeComponentSuggestionsStory(componentSuggestions) {
|
|
|
3250
3698
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
3251
3699
|
"import { useState } from \"react\";",
|
|
3252
3700
|
"",
|
|
3253
|
-
"const meta = { title: \"Foundations/Component Suggestions\" } satisfies Meta;",
|
|
3701
|
+
"const meta = { title: \"Foundations/Component Suggestions\", parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
3254
3702
|
"export default meta;",
|
|
3255
3703
|
"type Story = StoryObj;",
|
|
3256
3704
|
"",
|
|
@@ -3319,6 +3767,133 @@ function writeComponentSuggestionsStory(componentSuggestions) {
|
|
|
3319
3767
|
console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "ComponentSuggestions.stories.tsx")));
|
|
3320
3768
|
}
|
|
3321
3769
|
|
|
3770
|
+
/**
|
|
3771
|
+
* Generates Foundations/Component Inventory story.
|
|
3772
|
+
* Shows all project components classified by Atlassian-style functional category.
|
|
3773
|
+
* Every value is derived from vds-output.json — no placeholder content.
|
|
3774
|
+
*/
|
|
3775
|
+
function writeComponentInventoryStory(components, foundations) {
|
|
3776
|
+
if (!Array.isArray(components) || components.length === 0) return;
|
|
3777
|
+
const foundationsDir = path.join(STORIES_DIR, "foundations");
|
|
3778
|
+
ensureDir(foundationsDir);
|
|
3779
|
+
|
|
3780
|
+
// Build per-component data
|
|
3781
|
+
const CATEGORY_ORDER = [
|
|
3782
|
+
"Forms and Input", "Status Indicators", "Navigation", "Overlays and Layering",
|
|
3783
|
+
"Loading", "Messaging", "Images and Icons", "Layout and Structure", "Text and Data Display",
|
|
3784
|
+
];
|
|
3785
|
+
const categorized = {}; // category → [{ name, group, tokenCount, propCount }]
|
|
3786
|
+
|
|
3787
|
+
for (const comp of components) {
|
|
3788
|
+
const g = comp.group || "Components";
|
|
3789
|
+
const gLower = g.toLowerCase();
|
|
3790
|
+
const isShadcnGroup = gLower === "shadcn" || gLower === "ui";
|
|
3791
|
+
const tokens = Array.isArray(comp.tokens) ? comp.tokens : [];
|
|
3792
|
+
const props = Array.isArray(comp.props) ? comp.props : [];
|
|
3793
|
+
const semantic = classifyComponent(comp.name, tokens, props);
|
|
3794
|
+
const category = semantic || g;
|
|
3795
|
+
|
|
3796
|
+
if (!categorized[category]) categorized[category] = [];
|
|
3797
|
+
categorized[category].push({
|
|
3798
|
+
name: comp.name,
|
|
3799
|
+
group: g,
|
|
3800
|
+
tokenCount: tokens.filter(t => !/:/.test(t)).length,
|
|
3801
|
+
propCount: props.length,
|
|
3802
|
+
});
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
const sortedCategories = Object.keys(categorized).sort((a, b) => {
|
|
3806
|
+
const ai = CATEGORY_ORDER.indexOf(a);
|
|
3807
|
+
const bi = CATEGORY_ORDER.indexOf(b);
|
|
3808
|
+
if (ai >= 0 && bi >= 0) return ai - bi;
|
|
3809
|
+
if (ai >= 0) return -1;
|
|
3810
|
+
if (bi >= 0) return 1;
|
|
3811
|
+
return a.localeCompare(b);
|
|
3812
|
+
});
|
|
3813
|
+
|
|
3814
|
+
const inventoryData = sortedCategories.map(cat => ({
|
|
3815
|
+
category: cat,
|
|
3816
|
+
components: categorized[cat].sort((a, b) => a.name.localeCompare(b.name)),
|
|
3817
|
+
}));
|
|
3818
|
+
|
|
3819
|
+
const totalComponents = components.length;
|
|
3820
|
+
const uniqueTokens = [...new Set(
|
|
3821
|
+
components.flatMap(c => Array.isArray(c.tokens) ? c.tokens.filter(t => !/:/.test(t)) : [])
|
|
3822
|
+
)].length;
|
|
3823
|
+
|
|
3824
|
+
const content = [
|
|
3825
|
+
`import type { Meta, StoryObj } from "@storybook/react";`,
|
|
3826
|
+
``,
|
|
3827
|
+
`const meta = { title: "Foundations/Component Inventory", parameters: { layout: "fullscreen" } } satisfies Meta;`,
|
|
3828
|
+
`export default meta;`,
|
|
3829
|
+
`type Story = StoryObj;`,
|
|
3830
|
+
``,
|
|
3831
|
+
`const inventoryData: { category: string; components: { name: string; group: string; tokenCount: number; propCount: number }[] }[] = ${JSON.stringify(inventoryData)};`,
|
|
3832
|
+
`const totalComponents = ${totalComponents};`,
|
|
3833
|
+
`const uniqueTokens = ${uniqueTokens};`,
|
|
3834
|
+
``,
|
|
3835
|
+
`export const Default: Story = {`,
|
|
3836
|
+
` render: () => (`,
|
|
3837
|
+
` <div style={{ padding: 32, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh" }}>`,
|
|
3838
|
+
` {/* Header */}`,
|
|
3839
|
+
` <div style={{ marginBottom: 40 }}>`,
|
|
3840
|
+
` <h2 style={{ fontSize: 28, fontWeight: 700, margin: "0 0 8px" }}>Component Inventory</h2>`,
|
|
3841
|
+
` <p style={{ fontSize: 14, color: "#6b7280", margin: "0 0 20px" }}>All components detected in this project, classified by function.</p>`,
|
|
3842
|
+
` <div style={{ display: "flex", gap: 12, flexWrap: "wrap" as any }}>`,
|
|
3843
|
+
` {[`,
|
|
3844
|
+
` { value: totalComponents.toString(), label: "Components" },`,
|
|
3845
|
+
` { value: inventoryData.length.toString(), label: "Categories" },`,
|
|
3846
|
+
` { value: uniqueTokens.toString(), label: "Unique Tokens" },`,
|
|
3847
|
+
` ].map(stat => (`,
|
|
3848
|
+
` <div key={stat.label} style={{ padding: "12px 20px", background: "#f3f4f6", borderRadius: 10, textAlign: "center" as any, minWidth: 90 }}>`,
|
|
3849
|
+
` <div style={{ fontSize: 26, fontWeight: 800, color: "#111", lineHeight: 1 }}>{stat.value}</div>`,
|
|
3850
|
+
` <div style={{ fontSize: 12, color: "#6b7280", marginTop: 4 }}>{stat.label}</div>`,
|
|
3851
|
+
` </div>`,
|
|
3852
|
+
` ))}`,
|
|
3853
|
+
` </div>`,
|
|
3854
|
+
` </div>`,
|
|
3855
|
+
` {/* Category groups */}`,
|
|
3856
|
+
` {inventoryData.map(group => (`,
|
|
3857
|
+
` <div key={group.category} style={{ marginBottom: 40 }}>`,
|
|
3858
|
+
` <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16, paddingBottom: 10, borderBottom: "2px solid #f3f4f6" }}>`,
|
|
3859
|
+
` <h3 style={{ margin: 0, fontSize: 15, fontWeight: 700, color: "#111" }}>{group.category}</h3>`,
|
|
3860
|
+
` <span style={{ fontSize: 12, background: "#e0e7ff", color: "#3730a3", padding: "2px 9px", borderRadius: 12, fontWeight: 600 }}>{group.components.length}</span>`,
|
|
3861
|
+
` </div>`,
|
|
3862
|
+
` <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(190px, 1fr))", gap: 10 }}>`,
|
|
3863
|
+
` {group.components.map(comp => (`,
|
|
3864
|
+
` <div key={comp.name} style={{ border: "1px solid #e5e7eb", borderRadius: 10, padding: "14px 16px", background: "#fafafa" }}>`,
|
|
3865
|
+
` <div style={{ fontWeight: 700, fontSize: 14, color: "#111", marginBottom: 8 }}>{comp.name}</div>`,
|
|
3866
|
+
` <div style={{ display: "flex", gap: 5, flexWrap: "wrap" as any }}>`,
|
|
3867
|
+
` {comp.propCount > 0 && (`,
|
|
3868
|
+
` <span style={{ fontSize: 10, background: "#f3f4f6", color: "#6b7280", padding: "2px 7px", borderRadius: 10, border: "1px solid #e5e7eb" }}>`,
|
|
3869
|
+
` {comp.propCount} props`,
|
|
3870
|
+
` </span>`,
|
|
3871
|
+
` )}`,
|
|
3872
|
+
` {comp.tokenCount > 0 && (`,
|
|
3873
|
+
` <span style={{ fontSize: 10, background: "#eff6ff", color: "#1d4ed8", padding: "2px 7px", borderRadius: 10, border: "1px solid #dbeafe" }}>`,
|
|
3874
|
+
` {comp.tokenCount} tokens`,
|
|
3875
|
+
` </span>`,
|
|
3876
|
+
` )}`,
|
|
3877
|
+
` {comp.group && comp.group !== "shadcn" && comp.group !== "UI" && comp.group !== "Components" && (`,
|
|
3878
|
+
` <span style={{ fontSize: 10, background: "#f0fdf4", color: "#166534", padding: "2px 7px", borderRadius: 10, border: "1px solid #bbf7d0" }}>`,
|
|
3879
|
+
` {comp.group}`,
|
|
3880
|
+
` </span>`,
|
|
3881
|
+
` )}`,
|
|
3882
|
+
` </div>`,
|
|
3883
|
+
` </div>`,
|
|
3884
|
+
` ))}`,
|
|
3885
|
+
` </div>`,
|
|
3886
|
+
` </div>`,
|
|
3887
|
+
` ))}`,
|
|
3888
|
+
` </div>`,
|
|
3889
|
+
` ),`,
|
|
3890
|
+
`};`,
|
|
3891
|
+
].join("\n");
|
|
3892
|
+
|
|
3893
|
+
fs.writeFileSync(path.join(foundationsDir, "ComponentInventory.stories.tsx"), content, "utf-8");
|
|
3894
|
+
console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "ComponentInventory.stories.tsx")));
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3322
3897
|
function writeChangelogStory(changelog) {
|
|
3323
3898
|
const foundationsDir = path.join(STORIES_DIR, "foundations");
|
|
3324
3899
|
ensureDir(foundationsDir);
|
|
@@ -3327,7 +3902,7 @@ function writeChangelogStory(changelog) {
|
|
|
3327
3902
|
[
|
|
3328
3903
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
3329
3904
|
"",
|
|
3330
|
-
"const meta = { title: \"Foundations/Changelog\", tags: [\"autodocs\"] } satisfies Meta;",
|
|
3905
|
+
"const meta = { title: \"Foundations/Changelog\", tags: [\"autodocs\"], parameters: { layout: \"fullscreen\" } } satisfies Meta;",
|
|
3331
3906
|
"export default meta;",
|
|
3332
3907
|
"type Story = StoryObj;",
|
|
3333
3908
|
"",
|
|
@@ -3591,6 +4166,7 @@ function main() {
|
|
|
3591
4166
|
if (componentSuggestions?.length) {
|
|
3592
4167
|
writeComponentSuggestionsStory(componentSuggestions);
|
|
3593
4168
|
}
|
|
4169
|
+
writeComponentInventoryStory(components, foundations);
|
|
3594
4170
|
writeChangelogStory(data.changelog);
|
|
3595
4171
|
try {
|
|
3596
4172
|
const fd = path.join(STORIES_DIR, "foundations");
|