vibe-design-system 2.8.79 → 2.8.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/vds-core-template/scan.mjs +45 -10
- package/vds-core-template/story-generator.mjs +131 -30
package/package.json
CHANGED
|
@@ -1429,11 +1429,17 @@ function extractFoundations() {
|
|
|
1429
1429
|
}
|
|
1430
1430
|
}
|
|
1431
1431
|
|
|
1432
|
-
// body { font-family } —
|
|
1433
|
-
|
|
1434
|
-
|
|
1432
|
+
// body { font-family } — Tailwind v4 sets font on html/:host, NOT body
|
|
1433
|
+
// Use [^}]* to stay within block boundaries ([\s\S]*? crosses blocks → mono font false-positive)
|
|
1434
|
+
const bodyMatch =
|
|
1435
|
+
css.match(/\bbody\s*\{[^}]*font-family:\s*([^;]+);/) ||
|
|
1436
|
+
css.match(/html\s*(?:,\s*:host)?\s*\{[^}]*font-family:\s*([^;]+);/); // Tailwind v4
|
|
1435
1437
|
if (bodyMatch) typography.body = bodyMatch[1].trim();
|
|
1436
|
-
|
|
1438
|
+
// Mono: code/kbd/samp/pre block (Tailwind v4 uses code,kbd,samp,pre not code,pre,.font-mono)
|
|
1439
|
+
const monoMatch =
|
|
1440
|
+
css.match(/code,\s*pre,\s*\.font-mono\s*\{[^}]*font-family:\s*([^;]+);/) ||
|
|
1441
|
+
css.match(/code,\s*kbd[^{]*\{[^}]*font-family:\s*([^;]+);/) ||
|
|
1442
|
+
css.match(/\bcode\b[^{,]*\{[^}]*font-family:\s*([^;]+);/);
|
|
1437
1443
|
if (monoMatch) typography.mono = monoMatch[1].trim();
|
|
1438
1444
|
// CSS custom properties for fonts (--font-sans, --font-mono, --font-display, etc.)
|
|
1439
1445
|
const fontVarRe = /--font([\w-]*):\s*([^;\n]+);/g;
|
|
@@ -1444,14 +1450,27 @@ function extractFoundations() {
|
|
|
1444
1450
|
if (!typography[key]) typography[key] = val;
|
|
1445
1451
|
}
|
|
1446
1452
|
// Resolve var(--font-*) references in body/mono to actual font names
|
|
1447
|
-
//
|
|
1453
|
+
// Also handles Tailwind v4 --default-font-family / --default-mono-font-family chain
|
|
1448
1454
|
function resolveTypoVar(val) {
|
|
1449
1455
|
if (!val || !val.startsWith("var(")) return val;
|
|
1456
|
+
// Direct --font-* reference: var(--font-sans) → typography.fontSans
|
|
1450
1457
|
const m = val.match(/var\(--font-([\w-]+)\)/);
|
|
1451
|
-
if (
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1458
|
+
if (m) {
|
|
1459
|
+
const key = `font-${m[1]}`.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
|
|
1460
|
+
return (typography[key] && !typography[key].startsWith("var(")) ? typography[key] : val;
|
|
1461
|
+
}
|
|
1462
|
+
// Tailwind v4: var(--default-font-family, ...) → resolve via fontSans
|
|
1463
|
+
if (val.includes("--default-font-family") && !val.includes("--default-mono")) {
|
|
1464
|
+
if (typography.fontSans && !typography.fontSans.startsWith("var(")) return typography.fontSans;
|
|
1465
|
+
// Fallback: strip the var() wrapper to get the fallback list
|
|
1466
|
+
return val.replace(/^var\(--default-font-family,\s*/, "").replace(/\)\s*$/, "").trim() || val;
|
|
1467
|
+
}
|
|
1468
|
+
// Tailwind v4: var(--default-mono-font-family, ...) → resolve via fontMono
|
|
1469
|
+
if (val.includes("--default-mono-font-family")) {
|
|
1470
|
+
if (typography.fontMono && !typography.fontMono.startsWith("var(")) return typography.fontMono;
|
|
1471
|
+
return val.replace(/^var\(--default-mono-font-family,\s*/, "").replace(/\)\s*$/, "").trim() || val;
|
|
1472
|
+
}
|
|
1473
|
+
return val;
|
|
1455
1474
|
}
|
|
1456
1475
|
if (typography.body) typography.body = resolveTypoVar(typography.body);
|
|
1457
1476
|
if (typography.mono) typography.mono = resolveTypoVar(typography.mono);
|
|
@@ -2207,6 +2226,21 @@ function extractVariantUsage(componentResults) {
|
|
|
2207
2226
|
}
|
|
2208
2227
|
}
|
|
2209
2228
|
|
|
2229
|
+
/**
|
|
2230
|
+
* Heuristic: detect complex page-level components that should not get individual stories.
|
|
2231
|
+
* Criteria: 500+ lines AND 4+ internal capitalized component definitions AND no cva()/forwardRef.
|
|
2232
|
+
* Examples: a 900-line MethodologyMap.tsx with 8 inline sub-components.
|
|
2233
|
+
*/
|
|
2234
|
+
function isComplexPageComponent(content) {
|
|
2235
|
+
const lines = content.split("\n").length;
|
|
2236
|
+
if (lines < 500) return false;
|
|
2237
|
+
const internalComps = (content.match(/\bconst\s+[A-Z][A-Za-z]+\s*[:=]\s*\(/g) || []).length;
|
|
2238
|
+
if (internalComps < 4) return false;
|
|
2239
|
+
// Don't flag UI primitives that use cva() or forwardRef — those are styled components
|
|
2240
|
+
if (content.includes("cva(") || content.includes("forwardRef")) return false;
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2210
2244
|
function scan() {
|
|
2211
2245
|
const relativeFiles = COMPONENTS_DIR ? getAllComponentFiles(COMPONENTS_DIR) : [];
|
|
2212
2246
|
if (!COMPONENTS_DIR) {
|
|
@@ -2231,7 +2265,8 @@ function scan() {
|
|
|
2231
2265
|
description = "";
|
|
2232
2266
|
}
|
|
2233
2267
|
const tokens = extractTailwindTokens(content);
|
|
2234
|
-
|
|
2268
|
+
const isPageComponent = isComplexPageComponent(content);
|
|
2269
|
+
results.push({ file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true } : {}) });
|
|
2235
2270
|
}
|
|
2236
2271
|
if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
|
|
2237
2272
|
const pageFiles = getAllComponentFiles(PAGES_DIR);
|
|
@@ -1792,6 +1792,96 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
|
|
|
1792
1792
|
}
|
|
1793
1793
|
lines.push(`};`);
|
|
1794
1794
|
}
|
|
1795
|
+
|
|
1796
|
+
// --- Design Tokens story (same block as generateStoryFile) ---
|
|
1797
|
+
{
|
|
1798
|
+
const compTokens = Array.isArray(comp.tokens) ? comp.tokens : [];
|
|
1799
|
+
const foundColors = FOUNDATIONS_DATA?.colors || {};
|
|
1800
|
+
if (compTokens.length >= 3) {
|
|
1801
|
+
const cleanTokens = compTokens.filter(t => !/:/.test(t));
|
|
1802
|
+
const colorRaw = cleanTokens.filter(t =>
|
|
1803
|
+
/^(bg|text|border|ring|from|to|fill|stroke)-/.test(t) &&
|
|
1804
|
+
!/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\d)/.test(t)
|
|
1805
|
+
);
|
|
1806
|
+
const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]?-|m[xylrbt]?-|gap|space-[xy]|w-|h-|min-[wh]|max-[wh]|size-)/.test(t));
|
|
1807
|
+
const typographyRaw = cleanTokens.filter(t => /^(text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)|font-)/.test(t));
|
|
1808
|
+
const radiusRaw = cleanTokens.filter(t => /^rounded/.test(t));
|
|
1809
|
+
const animRaw = cleanTokens.filter(t => /^(transition|duration|animate|ease|delay)-/.test(t));
|
|
1810
|
+
const resolvedColors = colorRaw.map(token => {
|
|
1811
|
+
const m = token.match(/^(?:bg|text|border|ring|from|to|fill|stroke)-(.+)$/);
|
|
1812
|
+
const key = m ? m[1] : null;
|
|
1813
|
+
const baseKey = key ? key.replace(/\/[\d.]+$/, "") : null;
|
|
1814
|
+
const entry = key ? (foundColors[key] || (baseKey !== key ? foundColors[baseKey] : null)) : null;
|
|
1815
|
+
const isValidCssColor = (v) => /^#[0-9a-fA-F]{3,8}$/.test(v) || /^(rgb|rgba|hsl|hsla|oklch|oklab|lch|lab|color)\s*\(/.test(v) || v === 'transparent';
|
|
1816
|
+
const hex = entry?.hex && isValidCssColor(entry.hex) ? entry.hex : null;
|
|
1817
|
+
return { token, hex, label: baseKey || key };
|
|
1818
|
+
});
|
|
1819
|
+
const hasContent = resolvedColors.length > 0 || spacingRaw.length > 0 || typographyRaw.length > 0 || radiusRaw.length > 0 || animRaw.length > 0;
|
|
1820
|
+
if (hasContent) {
|
|
1821
|
+
lines.push("");
|
|
1822
|
+
lines.push(`export const Tokens: Story = {`);
|
|
1823
|
+
lines.push(` name: "Design Tokens",`);
|
|
1824
|
+
lines.push(` parameters: { layout: "fullscreen" },`);
|
|
1825
|
+
lines.push(` render: () => {`);
|
|
1826
|
+
lines.push(` const colorTokens = ${JSON.stringify(resolvedColors)};`);
|
|
1827
|
+
lines.push(` const spacingTokens = ${JSON.stringify(spacingRaw)};`);
|
|
1828
|
+
lines.push(` const typographyTokens = ${JSON.stringify(typographyRaw)};`);
|
|
1829
|
+
lines.push(` const radiusTokens = ${JSON.stringify(radiusRaw)};`);
|
|
1830
|
+
lines.push(` const animationTokens = ${JSON.stringify(animRaw)};`);
|
|
1831
|
+
lines.push(` const chip = (label: string, bg: string, color: string) => (`);
|
|
1832
|
+
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>`);
|
|
1833
|
+
lines.push(` );`);
|
|
1834
|
+
lines.push(` const section = (title: string, children: any) => (`);
|
|
1835
|
+
lines.push(` <section style={{ marginBottom: 28 }}>`);
|
|
1836
|
+
lines.push(` <p style={{ margin: "0 0 10px", fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.08em" }}>{title}</p>`);
|
|
1837
|
+
lines.push(` {children}`);
|
|
1838
|
+
lines.push(` </section>`);
|
|
1839
|
+
lines.push(` );`);
|
|
1840
|
+
lines.push(` return (`);
|
|
1841
|
+
lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh", width: "100%" }}>`);
|
|
1842
|
+
lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>Design Tokens</h2>`);
|
|
1843
|
+
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>`);
|
|
1844
|
+
lines.push(` {colorTokens.length > 0 && section("Color", (`);
|
|
1845
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>`);
|
|
1846
|
+
lines.push(` {colorTokens.map(({ token, hex, label }) => (`);
|
|
1847
|
+
lines.push(` <div key={token} style={{ display: "flex", alignItems: "center", gap: 7, padding: "7px 12px", border: "1px solid #e5e7eb", borderRadius: 8, background: "#f9fafb" }}>`);
|
|
1848
|
+
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 }} />}`);
|
|
1849
|
+
lines.push(` <code style={{ fontSize: 12, color: "#374151", fontWeight: 600 }}>{token}</code>`);
|
|
1850
|
+
lines.push(` {hex && <span style={{ fontSize: 11, color: "#9ca3af" }}>{hex}</span>}`);
|
|
1851
|
+
lines.push(` </div>`);
|
|
1852
|
+
lines.push(` ))}`);
|
|
1853
|
+
lines.push(` </div>`);
|
|
1854
|
+
lines.push(` ))}`);
|
|
1855
|
+
lines.push(` {spacingTokens.length > 0 && section("Spacing", (`);
|
|
1856
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{spacingTokens.map(t => chip(t, "#faf5ff", "#6d28d9"))}</div>`);
|
|
1857
|
+
lines.push(` ))}`);
|
|
1858
|
+
lines.push(` {typographyTokens.length > 0 && section("Typography", (`);
|
|
1859
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{typographyTokens.map(t => chip(t, "#fffbeb", "#92400e"))}</div>`);
|
|
1860
|
+
lines.push(` ))}`);
|
|
1861
|
+
lines.push(` {radiusTokens.length > 0 && section("Border Radius", (`);
|
|
1862
|
+
lines.push(` <div style={{ display: "flex", gap: 16, flexWrap: "wrap", alignItems: "flex-end" }}>`);
|
|
1863
|
+
lines.push(` {radiusTokens.map(t => {`);
|
|
1864
|
+
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;`);
|
|
1865
|
+
lines.push(` return (`);
|
|
1866
|
+
lines.push(` <div key={t} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>`);
|
|
1867
|
+
lines.push(` <div style={{ width: 44, height: 44, background: "#6366f1", borderRadius: px }} />`);
|
|
1868
|
+
lines.push(` <code style={{ fontSize: 10, color: "#6b7280" }}>{t}</code>`);
|
|
1869
|
+
lines.push(` </div>`);
|
|
1870
|
+
lines.push(` );`);
|
|
1871
|
+
lines.push(` })}`);
|
|
1872
|
+
lines.push(` </div>`);
|
|
1873
|
+
lines.push(` ))}`);
|
|
1874
|
+
lines.push(` {animationTokens.length > 0 && section("Motion", (`);
|
|
1875
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{animationTokens.map(t => chip(t, "#f0fdf4", "#166534"))}</div>`);
|
|
1876
|
+
lines.push(` ))}`);
|
|
1877
|
+
lines.push(` </div>`);
|
|
1878
|
+
lines.push(` );`);
|
|
1879
|
+
lines.push(` },`);
|
|
1880
|
+
lines.push(`};`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1795
1885
|
return lines.join("\n");
|
|
1796
1886
|
}
|
|
1797
1887
|
|
|
@@ -2115,42 +2205,46 @@ function buildStoryFileContent(comp) {
|
|
|
2115
2205
|
// Component-specific stories for non-variant components (Input, Textarea, etc.)
|
|
2116
2206
|
// Skip buildSpecialStories when multi-dimension detection found CVA dimensions —
|
|
2117
2207
|
// generic detection produces better stories than hardcoded ones.
|
|
2208
|
+
let generatedSpecialStories = false;
|
|
2118
2209
|
if (!hasDimensions) {
|
|
2119
2210
|
const specialStories = buildSpecialStories(componentName, variants);
|
|
2120
2211
|
if (specialStories) {
|
|
2121
2212
|
lines.push(specialStories);
|
|
2122
|
-
|
|
2213
|
+
generatedSpecialStories = true;
|
|
2214
|
+
// Do NOT return here — fall through to Tokens + Usage stories below
|
|
2123
2215
|
}
|
|
2124
2216
|
}
|
|
2125
2217
|
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2218
|
+
if (!generatedSpecialStories) {
|
|
2219
|
+
// Profile-driven render + children — single source of truth via getStoryProfile()
|
|
2220
|
+
const useSafeWrapper = profile === "SAFE";
|
|
2221
|
+
const RenderTarget = useSafeWrapper ? "SafeWrapper" : "ComponentRef";
|
|
2222
|
+
const argsFallback = !useSafeWrapper && (componentName && RENDER_ARGS_FALLBACKS[componentName]) || "";
|
|
2223
|
+
const renderLine = buildProfileRenderLine(profile, RenderTarget, argsFallback);
|
|
2224
|
+
const childrenArgLine = buildProfileChildrenArgLine(profile);
|
|
2132
2225
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2226
|
+
if (hasDimensions) {
|
|
2227
|
+
const multiStories = buildMultiDimensionStories(
|
|
2228
|
+
variantMap, renderLine, childrenArgLine, defaultArgLines, componentName,
|
|
2229
|
+
comp.variantUsage || null
|
|
2230
|
+
);
|
|
2231
|
+
if (multiStories) {
|
|
2232
|
+
lines.push(multiStories);
|
|
2233
|
+
}
|
|
2234
|
+
} else {
|
|
2235
|
+
// No dimensions detected: single Default story
|
|
2236
|
+
lines.push(`export const Default: Story = {`);
|
|
2237
|
+
lines.push(renderLine);
|
|
2238
|
+
const storyArgLines = [];
|
|
2239
|
+
if (childrenArgLine(componentName)) storyArgLines.push(childrenArgLine(componentName));
|
|
2240
|
+
for (const line of defaultArgLines) storyArgLines.push(line);
|
|
2241
|
+
if (storyArgLines.length > 0) {
|
|
2242
|
+
lines.push(` args: {`);
|
|
2243
|
+
for (const line of storyArgLines) lines.push(line);
|
|
2244
|
+
lines.push(` },`);
|
|
2245
|
+
}
|
|
2246
|
+
lines.push(`};`);
|
|
2152
2247
|
}
|
|
2153
|
-
lines.push(`};`);
|
|
2154
2248
|
}
|
|
2155
2249
|
|
|
2156
2250
|
// --- Project-specific usage stories ---
|
|
@@ -2190,7 +2284,7 @@ function buildStoryFileContent(comp) {
|
|
|
2190
2284
|
/^(bg|text|border|ring|from|to|fill|stroke)-/.test(t) &&
|
|
2191
2285
|
!/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\d)/.test(t)
|
|
2192
2286
|
);
|
|
2193
|
-
const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]
|
|
2287
|
+
const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]?-|m[xylrbt]?-|gap|space-[xy]|w-|h-|min-[wh]|max-[wh]|size-)/.test(t));
|
|
2194
2288
|
const typographyRaw = cleanTokens.filter(t => /^(text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)|font-)/.test(t));
|
|
2195
2289
|
const radiusRaw = cleanTokens.filter(t => /^rounded/.test(t));
|
|
2196
2290
|
const animRaw = cleanTokens.filter(t => /^(transition|duration|animate|ease|delay)-/.test(t));
|
|
@@ -2202,7 +2296,8 @@ function buildStoryFileContent(comp) {
|
|
|
2202
2296
|
// Strip opacity modifier (e.g. "muted/20" → "muted") as a fallback
|
|
2203
2297
|
const baseKey = key ? key.replace(/\/[\d.]+$/, "") : null;
|
|
2204
2298
|
const entry = key ? (foundColors[key] || (baseKey !== key ? foundColors[baseKey] : null)) : null;
|
|
2205
|
-
const
|
|
2299
|
+
const isValidCssColor = (v) => /^#[0-9a-fA-F]{3,8}$/.test(v) || /^(rgb|rgba|hsl|hsla|oklch|oklab|lch|lab|color)\s*\(/.test(v) || v === 'transparent';
|
|
2300
|
+
const hex = entry?.hex && isValidCssColor(entry.hex) ? entry.hex : null;
|
|
2206
2301
|
return { token, hex, label: baseKey || key };
|
|
2207
2302
|
});
|
|
2208
2303
|
|
|
@@ -4246,7 +4341,8 @@ function writeComponentInventoryStory(components, foundations) {
|
|
|
4246
4341
|
let key = m ? m[1] : null;
|
|
4247
4342
|
if (key) key = key.replace(/\/\d+$/, "");
|
|
4248
4343
|
const entry = key ? foundColors[key] : null;
|
|
4249
|
-
const
|
|
4344
|
+
const isValidCssColor = (v) => /^#[0-9a-fA-F]{3,8}$/.test(v) || /^(rgb|rgba|hsl|hsla|oklch|oklab|lch|lab|color)\s*\(/.test(v) || v === 'transparent';
|
|
4345
|
+
const hex = entry?.hex && isValidCssColor(entry.hex) ? entry.hex : null;
|
|
4250
4346
|
return { token, hex };
|
|
4251
4347
|
})
|
|
4252
4348
|
.filter(s => s.hex);
|
|
@@ -4906,6 +5002,11 @@ function main() {
|
|
|
4906
5002
|
const storyFileName = `${componentName}.stories.tsx`;
|
|
4907
5003
|
const storyPath = path.join(STORIES_DIR, storyFileName);
|
|
4908
5004
|
if (SKIP_LIST.includes(componentName)) continue;
|
|
5005
|
+
// Skip complex page-level components detected by scan (500+ lines, 4+ inline sub-components)
|
|
5006
|
+
if (comp.isPageComponent) {
|
|
5007
|
+
console.log(`[VDS] ${componentName} → skipped (complex page component — add to extraSkipList to suppress this message)`);
|
|
5008
|
+
continue;
|
|
5009
|
+
}
|
|
4909
5010
|
const requiredCount = Array.isArray(comp.props) ? comp.props.filter((p) => p.required === true).length : 0;
|
|
4910
5011
|
if (requiredCount > 3) {
|
|
4911
5012
|
console.log(`[VDS] ${componentName} → skipped (${requiredCount} required props — too complex to auto-generate)`);
|