vibe-design-system 2.8.82 → 2.9.1
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 +152 -10
- package/package.json +1 -1
- package/vds-core-template/scan.mjs +266 -2
- package/vds-core-template/story-generator.mjs +471 -8
package/bin/init.js
CHANGED
|
@@ -24,6 +24,7 @@ const TEMPLATE_DIR = path.join(INSTALLER_ROOT, "vds-core-template");
|
|
|
24
24
|
const STORYBOOK_MAIN_TS = `import type { StorybookConfig } from "@storybook/react-vite";
|
|
25
25
|
import { mergeConfig } from "vite";
|
|
26
26
|
import path from "path";
|
|
27
|
+
import fs from "fs";
|
|
27
28
|
|
|
28
29
|
const config: StorybookConfig = {
|
|
29
30
|
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
@@ -33,10 +34,48 @@ const config: StorybookConfig = {
|
|
|
33
34
|
options: {},
|
|
34
35
|
},
|
|
35
36
|
async viteFinal(config) {
|
|
37
|
+
// Phase C2 — read tsconfig.json paths and inject as Vite aliases
|
|
38
|
+
const extraAliases = (() => {
|
|
39
|
+
try {
|
|
40
|
+
const tsConfigPaths = [
|
|
41
|
+
path.resolve(process.cwd(), "tsconfig.json"),
|
|
42
|
+
path.resolve(process.cwd(), "tsconfig.app.json"),
|
|
43
|
+
];
|
|
44
|
+
for (const tcp of tsConfigPaths) {
|
|
45
|
+
if (!fs.existsSync(tcp)) continue;
|
|
46
|
+
const raw = JSON.parse(fs.readFileSync(tcp, "utf-8").replace(/\\/\\/[^\\n]*/g, "").replace(/,(\\s*[}\\]])/g, "$1"));
|
|
47
|
+
const paths = raw?.compilerOptions?.paths || {};
|
|
48
|
+
const aliases: Record<string, string> = {};
|
|
49
|
+
for (const [alias, targets] of Object.entries(paths) as [string, string[]][]) {
|
|
50
|
+
const cleanAlias = alias.replace(/\\/\\*$/, "");
|
|
51
|
+
const target = targets[0]?.replace(/\\/\\*$/, "") || "";
|
|
52
|
+
if (cleanAlias && target && cleanAlias !== "@") {
|
|
53
|
+
aliases[cleanAlias] = path.resolve(process.cwd(), target);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (Object.keys(aliases).length > 0) return aliases;
|
|
57
|
+
}
|
|
58
|
+
} catch (_) {}
|
|
59
|
+
return {};
|
|
60
|
+
})();
|
|
36
61
|
return mergeConfig(config, {
|
|
62
|
+
plugins: [
|
|
63
|
+
{
|
|
64
|
+
// Mock figma:asset/* imports — returns empty string so components render without crashing in Storybook
|
|
65
|
+
name: "vds-figma-asset-mock",
|
|
66
|
+
enforce: "pre" as const,
|
|
67
|
+
resolveId(id: string) {
|
|
68
|
+
if (id.startsWith("figma:asset")) return "\\0vds-figma-asset-mock";
|
|
69
|
+
},
|
|
70
|
+
load(id: string) {
|
|
71
|
+
if (id === "\\0vds-figma-asset-mock") return "export default '';";
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
37
75
|
resolve: {
|
|
38
76
|
alias: {
|
|
39
77
|
"@": path.resolve(process.cwd(), "src"),
|
|
78
|
+
...extraAliases,
|
|
40
79
|
},
|
|
41
80
|
},
|
|
42
81
|
});
|
|
@@ -241,12 +280,22 @@ function detectFramework(pkg) {
|
|
|
241
280
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
242
281
|
if (deps["next"]) return "nextjs";
|
|
243
282
|
if (deps["@remix-run/react"] || deps["@remix-run/node"]) return "remix";
|
|
283
|
+
if (deps["react-scripts"]) return "cra"; // Phase E2 — Create React App
|
|
284
|
+
// Phase F — Vue/Svelte/Angular detection
|
|
285
|
+
if (deps["vue"] || deps["nuxt"]) return "vue";
|
|
286
|
+
if (deps["svelte"] || deps["@sveltejs/kit"]) return "svelte";
|
|
287
|
+
if (deps["@angular/core"]) return "angular";
|
|
244
288
|
return "vite";
|
|
245
289
|
}
|
|
246
290
|
|
|
247
291
|
/** Framework'e göre Storybook framework paket adı */
|
|
248
292
|
function storybookFrameworkPackage(framework) {
|
|
249
293
|
if (framework === "nextjs") return "@storybook/nextjs";
|
|
294
|
+
if (framework === "cra") return "@storybook/react-webpack5"; // Phase E2 — CRA uses webpack5
|
|
295
|
+
// Phase F — Cross-framework support
|
|
296
|
+
if (framework === "vue") return "@storybook/vue3-vite";
|
|
297
|
+
if (framework === "svelte") return "@storybook/svelte-vite";
|
|
298
|
+
if (framework === "angular") return "@storybook/angular";
|
|
250
299
|
return "@storybook/react-vite"; // vite & remix
|
|
251
300
|
}
|
|
252
301
|
|
|
@@ -257,6 +306,48 @@ function buildStorybookMainTs(framework, srcPrefix) {
|
|
|
257
306
|
? `\n "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",`
|
|
258
307
|
: "";
|
|
259
308
|
|
|
309
|
+
// Phase F — Vue framework
|
|
310
|
+
if (framework === "vue") {
|
|
311
|
+
return `import type { StorybookConfig } from "@storybook/vue3-vite";
|
|
312
|
+
|
|
313
|
+
const config: StorybookConfig = {
|
|
314
|
+
stories: ["../${srcPrefix}/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
315
|
+
addons: ["@storybook/addon-essentials"],
|
|
316
|
+
framework: { name: "@storybook/vue3-vite", options: {} },
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export default config;
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Phase F — Svelte framework
|
|
324
|
+
if (framework === "svelte") {
|
|
325
|
+
return `import type { StorybookConfig } from "@storybook/svelte-vite";
|
|
326
|
+
|
|
327
|
+
const config: StorybookConfig = {
|
|
328
|
+
stories: ["../${srcPrefix}/**/*.stories.@(js|ts|svelte)"],
|
|
329
|
+
addons: ["@storybook/addon-essentials"],
|
|
330
|
+
framework: { name: "@storybook/svelte-vite", options: {} },
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export default config;
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Phase F — Angular framework
|
|
338
|
+
if (framework === "angular") {
|
|
339
|
+
return `import type { StorybookConfig } from "@storybook/angular";
|
|
340
|
+
|
|
341
|
+
const config: StorybookConfig = {
|
|
342
|
+
stories: ["../${srcPrefix}/**/*.stories.@(js|ts)"],
|
|
343
|
+
addons: ["@storybook/addon-essentials"],
|
|
344
|
+
framework: { name: "@storybook/angular", options: {} },
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export default config;
|
|
348
|
+
`;
|
|
349
|
+
}
|
|
350
|
+
|
|
260
351
|
if (framework === "nextjs") {
|
|
261
352
|
return `import type { StorybookConfig } from "@storybook/nextjs";
|
|
262
353
|
|
|
@@ -286,6 +377,7 @@ export default config;
|
|
|
286
377
|
return `import type { StorybookConfig } from "@storybook/react-vite";
|
|
287
378
|
import { mergeConfig } from "vite";
|
|
288
379
|
import path from "path";
|
|
380
|
+
import fs from "fs";
|
|
289
381
|
|
|
290
382
|
const config: StorybookConfig = {
|
|
291
383
|
stories: [
|
|
@@ -297,10 +389,36 @@ const config: StorybookConfig = {
|
|
|
297
389
|
options: {},
|
|
298
390
|
},
|
|
299
391
|
async viteFinal(config) {
|
|
392
|
+
// Phase C2 — read tsconfig.json paths and inject as Vite aliases
|
|
393
|
+
const extraAliases = (() => {
|
|
394
|
+
try {
|
|
395
|
+
const tsConfigPaths = [
|
|
396
|
+
path.resolve(process.cwd(), "tsconfig.json"),
|
|
397
|
+
path.resolve(process.cwd(), "tsconfig.app.json"),
|
|
398
|
+
path.resolve(process.cwd(), "${srcPrefix}", "..", "tsconfig.json"),
|
|
399
|
+
];
|
|
400
|
+
for (const tcp of tsConfigPaths) {
|
|
401
|
+
if (!fs.existsSync(tcp)) continue;
|
|
402
|
+
const raw = JSON.parse(fs.readFileSync(tcp, "utf-8").replace(/\/\/[^\n]*/g, "").replace(/,(\s*[}\]])/g, "$1"));
|
|
403
|
+
const paths = raw?.compilerOptions?.paths || {};
|
|
404
|
+
const aliases: Record<string, string> = {};
|
|
405
|
+
for (const [alias, targets] of Object.entries(paths) as [string, string[]][]) {
|
|
406
|
+
const cleanAlias = alias.replace(/\/\*$/, "");
|
|
407
|
+
const target = targets[0]?.replace(/\/\*$/, "") || "";
|
|
408
|
+
if (cleanAlias && target && cleanAlias !== "@") {
|
|
409
|
+
aliases[cleanAlias] = path.resolve(process.cwd(), target);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (Object.keys(aliases).length > 0) return aliases;
|
|
413
|
+
}
|
|
414
|
+
} catch (_) {}
|
|
415
|
+
return {};
|
|
416
|
+
})();
|
|
300
417
|
return mergeConfig(config, {
|
|
301
418
|
resolve: {
|
|
302
419
|
alias: {
|
|
303
420
|
"@": path.resolve(process.cwd(), "${srcPrefix}"),
|
|
421
|
+
...extraAliases,
|
|
304
422
|
},
|
|
305
423
|
},
|
|
306
424
|
});
|
|
@@ -673,18 +791,42 @@ if (!pkg) {
|
|
|
673
791
|
// Monorepo kök dizininde mi?
|
|
674
792
|
const monorepoPackages = detectMonorepoPackages(projectRoot);
|
|
675
793
|
if (monorepoPackages.length > 0 && pkg.workspaces) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
794
|
+
// Phase E3 — Monorepo guidance: show all workspaces with React deps, auto-install in first one
|
|
795
|
+
const reactPackages = monorepoPackages.filter(p => {
|
|
796
|
+
try {
|
|
797
|
+
const pkgPath = path.join(p, "package.json");
|
|
798
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
799
|
+
const wpkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
800
|
+
const deps = { ...wpkg.dependencies, ...wpkg.devDependencies };
|
|
801
|
+
return !!deps["react"];
|
|
802
|
+
} catch (_) { return false; }
|
|
803
|
+
});
|
|
804
|
+
const targetPackages = reactPackages.length > 0 ? reactPackages : monorepoPackages;
|
|
805
|
+
|
|
806
|
+
console.log("📦 Monorepo tespit edildi — React içeren workspace'ler:");
|
|
807
|
+
for (const p of targetPackages) {
|
|
808
|
+
const rel = path.relative(projectRoot, p);
|
|
809
|
+
console.log(` → ${rel}`);
|
|
810
|
+
}
|
|
811
|
+
console.log("");
|
|
812
|
+
console.log("💡 Her workspace için ayrı kurulum komutları:");
|
|
813
|
+
for (const p of targetPackages) {
|
|
680
814
|
const rel = path.relative(projectRoot, p);
|
|
681
|
-
console.
|
|
815
|
+
console.log(` cd ${rel} && npx vibe-design-system init`);
|
|
816
|
+
}
|
|
817
|
+
console.log("");
|
|
818
|
+
if (!process.env.VDS_COMPONENTS_DIR) {
|
|
819
|
+
// Auto-select first React workspace and continue installation there
|
|
820
|
+
const firstReact = targetPackages[0];
|
|
821
|
+
if (firstReact) {
|
|
822
|
+
const rel = path.relative(projectRoot, firstReact);
|
|
823
|
+
console.log(`🚀 İlk React workspace'e otomatik kurulum yapılıyor: ${rel}`);
|
|
824
|
+
console.log(" (VDS_COMPONENTS_DIR env değişkeni ile farklı bir workspace seçebilirsiniz)\n");
|
|
825
|
+
process.chdir(firstReact);
|
|
826
|
+
// Update projectRoot for remaining steps
|
|
827
|
+
Object.assign(global, { __vdsProjectRoot: firstReact });
|
|
828
|
+
}
|
|
682
829
|
}
|
|
683
|
-
console.warn("\n Örnek kullanım:");
|
|
684
|
-
const firstRel = path.relative(projectRoot, monorepoPackages[0]);
|
|
685
|
-
console.warn(` cd ${firstRel} && npx vibe-design-system init`);
|
|
686
|
-
console.warn("\n VDS'yi bu dizinden çalıştırmaya devam etmek istiyorsanız,");
|
|
687
|
-
console.warn(" VDS_COMPONENTS_DIR env değişkeni ile hedef klasörü belirtin.\n");
|
|
688
830
|
}
|
|
689
831
|
|
|
690
832
|
const framework = detectFramework(pkg);
|
package/package.json
CHANGED
|
@@ -945,7 +945,7 @@ function extractComponentSuggestions() {
|
|
|
945
945
|
|
|
946
946
|
/** src/pages/ içinde tanımlı ama src/components'a çıkarılmamış visual section'ları listele (component adayı raporu). */
|
|
947
947
|
function extractUnreleasedSectionCandidates() {
|
|
948
|
-
if (!fs.existsSync(PAGES_DIR)) return [];
|
|
948
|
+
if (!PAGES_DIR || !fs.existsSync(PAGES_DIR)) return [];
|
|
949
949
|
const suggestions = extractComponentSuggestions();
|
|
950
950
|
return suggestions.map((s) => ({
|
|
951
951
|
suggestedName: s.suggestedName,
|
|
@@ -1145,6 +1145,8 @@ function extractTailwindTokens(content) {
|
|
|
1145
1145
|
/className\s*=\s*\{\s*["'`]([^"'`]+)["'`]/g,
|
|
1146
1146
|
/cn\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
|
1147
1147
|
/cva\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
|
1148
|
+
// CVA variant values: captures `default: "bg-x text-y"`, `destructive: "border-x..."` etc.
|
|
1149
|
+
/\b\w+\s*:\s*["'`]([a-zA-Z0-9_\-\/\s\[\]&>:%.]+)["'`]/g,
|
|
1148
1150
|
/["'`]([a-zA-Z0-9_\-\/\s\[\]&:%.]+(?:hover|focus|active|disabled|sm|md|lg|xl|2xl|dark:)[a-zA-Z0-9_\-\/\s\[\]&:%.]*)["'`]/g,
|
|
1149
1151
|
];
|
|
1150
1152
|
for (const re of patterns) {
|
|
@@ -1393,6 +1395,58 @@ function extractFoundations() {
|
|
|
1393
1395
|
// Also use first found as primary (for legacy logic below)
|
|
1394
1396
|
const cssToRead = allCssCandidates.find((p) => fs.existsSync(p)) || "";
|
|
1395
1397
|
|
|
1398
|
+
// Phase C — JSON token file reader (W3C DTCG + Style Dictionary)
|
|
1399
|
+
{
|
|
1400
|
+
const tokenFileCandidates = [
|
|
1401
|
+
path.join(PROJECT_ROOT, "tokens.json"),
|
|
1402
|
+
path.join(PROJECT_ROOT, "design-tokens.json"),
|
|
1403
|
+
path.join(PROJECT_ROOT, "src", "tokens.json"),
|
|
1404
|
+
path.join(PROJECT_ROOT, "src", "design-tokens.json"),
|
|
1405
|
+
path.join(PROJECT_ROOT, "tokens", "tokens.json"),
|
|
1406
|
+
path.join(PROJECT_ROOT, "tokens", "design-tokens.json"),
|
|
1407
|
+
];
|
|
1408
|
+
// Also scan tokens/ directory for any .json files
|
|
1409
|
+
const tokensDir = path.join(PROJECT_ROOT, "tokens");
|
|
1410
|
+
if (fs.existsSync(tokensDir)) {
|
|
1411
|
+
try {
|
|
1412
|
+
for (const f of fs.readdirSync(tokensDir)) {
|
|
1413
|
+
if (f.endsWith(".json")) tokenFileCandidates.push(path.join(tokensDir, f));
|
|
1414
|
+
}
|
|
1415
|
+
} catch (_) {}
|
|
1416
|
+
}
|
|
1417
|
+
for (const tf of tokenFileCandidates) {
|
|
1418
|
+
if (!fs.existsSync(tf)) continue;
|
|
1419
|
+
try {
|
|
1420
|
+
const raw = JSON.parse(fs.readFileSync(tf, "utf-8"));
|
|
1421
|
+
// W3C DTCG: { "color": { "primary": { "$value": "#...", "$type": "color" } } }
|
|
1422
|
+
// Style Dictionary: { "color-primary": { "value": "#..." } }
|
|
1423
|
+
const flatTokens = {};
|
|
1424
|
+
function flattenDTCG(obj, prefix) {
|
|
1425
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1426
|
+
if (v && typeof v === "object") {
|
|
1427
|
+
if ("$value" in v) {
|
|
1428
|
+
if (!v.$type || v.$type === "color") {
|
|
1429
|
+
flatTokens[(prefix ? prefix + "-" : "") + k] = v.$value;
|
|
1430
|
+
}
|
|
1431
|
+
} else if ("value" in v) {
|
|
1432
|
+
flatTokens[(prefix ? prefix + "-" : "") + k] = v.value;
|
|
1433
|
+
} else {
|
|
1434
|
+
flattenDTCG(v, (prefix ? prefix + "-" : "") + k);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
flattenDTCG(raw, "");
|
|
1440
|
+
for (const [name, value] of Object.entries(flatTokens)) {
|
|
1441
|
+
const cleanName = name.replace(/^color[-_]/i, "").replace(/^-/, "");
|
|
1442
|
+
if (!colors[cleanName] && (typeof value === "string") && (value.startsWith("#") || /^(rgb|hsl|oklch|transparent)/.test(value))) {
|
|
1443
|
+
colors[cleanName] = { value, hex: value };
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
} catch (_) {}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1396
1450
|
try {
|
|
1397
1451
|
if (cssChunks.length > 0) {
|
|
1398
1452
|
const css = cssChunks.join("\n");
|
|
@@ -1429,6 +1483,22 @@ function extractFoundations() {
|
|
|
1429
1483
|
}
|
|
1430
1484
|
}
|
|
1431
1485
|
|
|
1486
|
+
// Phase B — @font-face declarations (variable fonts, custom webfonts)
|
|
1487
|
+
const fontFaceDecls = [];
|
|
1488
|
+
const fontFaceRe = /@font-face\s*\{([^}]+)\}/g;
|
|
1489
|
+
let ffm;
|
|
1490
|
+
while ((ffm = fontFaceRe.exec(css)) !== null) {
|
|
1491
|
+
const block = ffm[1];
|
|
1492
|
+
const fam = block.match(/font-family:\s*['"]?([^;'"]+)['"]?;/)?.[1]?.trim();
|
|
1493
|
+
const wgt = block.match(/font-weight:\s*([^;]+);/)?.[1]?.trim();
|
|
1494
|
+
const sty = block.match(/font-style:\s*([^;]+);/)?.[1]?.trim() || "normal";
|
|
1495
|
+
if (fam) {
|
|
1496
|
+
const isVariable = wgt ? /^\d+\s+\d+$/.test(wgt.trim()) : false;
|
|
1497
|
+
fontFaceDecls.push({ family: fam, weight: wgt || null, style: sty, isVariable });
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (fontFaceDecls.length > 0) typography.fontFaces = fontFaceDecls;
|
|
1501
|
+
|
|
1432
1502
|
// body { font-family } — Tailwind v4 sets font on html/:host, NOT body
|
|
1433
1503
|
// Use [^}]* to stay within block boundaries ([\s\S]*? crosses blocks → mono font false-positive)
|
|
1434
1504
|
const bodyMatch =
|
|
@@ -1975,6 +2045,41 @@ function extractFoundations() {
|
|
|
1975
2045
|
if (arbFonts.size > 0) typography.arbitraryFonts = Array.from(arbFonts);
|
|
1976
2046
|
}
|
|
1977
2047
|
|
|
2048
|
+
// Phase B — next/font detection
|
|
2049
|
+
{
|
|
2050
|
+
const nextFonts = [];
|
|
2051
|
+
const allTsxForFonts = getAllTsxJsxInDir(SRC_DIR);
|
|
2052
|
+
const nextFontRe = /import\s*\{\s*([\w,\s]+)\s*\}\s*from\s*['"]next\/font\/(google|local)['"]/g;
|
|
2053
|
+
const fontCallRe = /const\s+(\w+)\s*=\s*(\w+)\s*\(\s*\{([^}]+)\}\s*\)/g;
|
|
2054
|
+
for (const file of allTsxForFonts) {
|
|
2055
|
+
let src;
|
|
2056
|
+
try { src = fs.readFileSync(file, "utf-8"); } catch (_) { continue; }
|
|
2057
|
+
nextFontRe.lastIndex = 0;
|
|
2058
|
+
let nm;
|
|
2059
|
+
while ((nm = nextFontRe.exec(src)) !== null) {
|
|
2060
|
+
const source = `next/font/${nm[2]}`;
|
|
2061
|
+
const imported = nm[1].split(",").map(s => s.trim()).filter(Boolean);
|
|
2062
|
+
for (const fontName of imported) {
|
|
2063
|
+
fontCallRe.lastIndex = 0;
|
|
2064
|
+
let cm;
|
|
2065
|
+
while ((cm = fontCallRe.exec(src)) !== null) {
|
|
2066
|
+
if (cm[2] === fontName) {
|
|
2067
|
+
const varM = cm[3].match(/variable:\s*['"]([^'"]+)['"]/);
|
|
2068
|
+
const subM = cm[3].match(/subsets?:\s*\[([^\]]+)\]/);
|
|
2069
|
+
nextFonts.push({
|
|
2070
|
+
source,
|
|
2071
|
+
family: fontName,
|
|
2072
|
+
variable: varM ? varM[1] : null,
|
|
2073
|
+
subsets: subM ? subM[1].replace(/['"]/g, "").split(",").map(s=>s.trim()) : [],
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
if (nextFonts.length > 0) typography.nextFonts = nextFonts;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1978
2083
|
return {
|
|
1979
2084
|
colors: foundationsColors,
|
|
1980
2085
|
typography,
|
|
@@ -2108,6 +2213,88 @@ function extractTokenUsage() {
|
|
|
2108
2213
|
};
|
|
2109
2214
|
}
|
|
2110
2215
|
|
|
2216
|
+
/**
|
|
2217
|
+
* Phase A — Color Intelligence v2.9.0
|
|
2218
|
+
* Counts how many times each color token is used in src/ files.
|
|
2219
|
+
* Tracks bg/text/border/ring/fill/stroke/other categories and dark: usage separately.
|
|
2220
|
+
* O(files) complexity — single readFileSync per file, one combined regex for all color names.
|
|
2221
|
+
*/
|
|
2222
|
+
function extractColorUsage(colorTokenNames) {
|
|
2223
|
+
if (!fs.existsSync(SRC_DIR) || !colorTokenNames || colorTokenNames.length === 0) return null;
|
|
2224
|
+
const allSrcFiles = getAllTsxJsxInDir(SRC_DIR).filter(f => !f.includes("stories"));
|
|
2225
|
+
if (allSrcFiles.length === 0) return null;
|
|
2226
|
+
const prefixes = ["bg","text","border","ring","fill","stroke","from","to","via","shadow","outline","decoration","placeholder","accent"];
|
|
2227
|
+
const escapedNames = colorTokenNames.map(n => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2228
|
+
const combined = new RegExp(
|
|
2229
|
+
`\\b(dark:)?(${prefixes.join("|")})-(${escapedNames.join("|")})(?:\\/\\d+)?\\b`,
|
|
2230
|
+
"g"
|
|
2231
|
+
);
|
|
2232
|
+
const usage = {};
|
|
2233
|
+
const fileMap = {};
|
|
2234
|
+
for (const file of allSrcFiles) {
|
|
2235
|
+
const compName = path.basename(file, path.extname(file));
|
|
2236
|
+
let content;
|
|
2237
|
+
try { content = fs.readFileSync(path.join(SRC_DIR, file), "utf-8"); } catch (_) { continue; }
|
|
2238
|
+
combined.lastIndex = 0;
|
|
2239
|
+
let m;
|
|
2240
|
+
while ((m = combined.exec(content)) !== null) {
|
|
2241
|
+
const isDark = !!m[1];
|
|
2242
|
+
const prefix = m[2];
|
|
2243
|
+
const colorName = m[3];
|
|
2244
|
+
if (!usage[colorName]) {
|
|
2245
|
+
usage[colorName] = { bg:0, text:0, border:0, ring:0, fill:0, stroke:0, other:0, total:0, dark:0 };
|
|
2246
|
+
}
|
|
2247
|
+
if (isDark) { usage[colorName].dark++; continue; }
|
|
2248
|
+
const cat = prefix === "bg" ? "bg" : prefix === "text" ? "text" : prefix === "border" ? "border"
|
|
2249
|
+
: prefix === "ring" ? "ring" : prefix === "fill" ? "fill" : prefix === "stroke" ? "stroke" : "other";
|
|
2250
|
+
usage[colorName][cat]++;
|
|
2251
|
+
usage[colorName].total++;
|
|
2252
|
+
if (!fileMap[colorName]) fileMap[colorName] = new Map();
|
|
2253
|
+
fileMap[colorName].set(compName, (fileMap[colorName].get(compName) || 0) + 1);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
for (const [name, u] of Object.entries(usage)) {
|
|
2257
|
+
const sorted = [...(fileMap[name]?.entries() || [])].sort((a,b) => b[1]-a[1]).slice(0, 5);
|
|
2258
|
+
u.topFiles = sorted.map(([f]) => f);
|
|
2259
|
+
}
|
|
2260
|
+
return Object.keys(usage).length > 0 ? usage : null;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/**
|
|
2264
|
+
* Phase E — Dark mode strategy detection v2.13.0
|
|
2265
|
+
* Detects whether the project uses class-based, media-query, or data-attribute dark mode.
|
|
2266
|
+
*/
|
|
2267
|
+
function detectDarkModeStrategy(projectRoot) {
|
|
2268
|
+
// 1. Check tailwind.config.js/ts
|
|
2269
|
+
const twConfigs = ["tailwind.config.js","tailwind.config.ts","tailwind.config.mjs","tailwind.config.cjs"];
|
|
2270
|
+
for (const cfg of twConfigs) {
|
|
2271
|
+
const p = path.join(projectRoot, cfg);
|
|
2272
|
+
if (!fs.existsSync(p)) continue;
|
|
2273
|
+
try {
|
|
2274
|
+
const src = fs.readFileSync(p, "utf-8");
|
|
2275
|
+
const m = src.match(/darkMode\s*:\s*['"]?(class|media|selector)['"]?/);
|
|
2276
|
+
if (m) return m[1] === "selector" ? "data-attribute" : m[1];
|
|
2277
|
+
} catch (_) {}
|
|
2278
|
+
}
|
|
2279
|
+
// 2. Check CSS files for .dark {} or [data-theme="dark"] or @media (prefers-color-scheme)
|
|
2280
|
+
const cssCandidates = [
|
|
2281
|
+
path.join(projectRoot, "src", "index.css"),
|
|
2282
|
+
path.join(projectRoot, "src", "globals.css"),
|
|
2283
|
+
path.join(projectRoot, "src", "styles", "globals.css"),
|
|
2284
|
+
path.join(projectRoot, "app", "globals.css"),
|
|
2285
|
+
];
|
|
2286
|
+
for (const cp of cssCandidates) {
|
|
2287
|
+
if (!fs.existsSync(cp)) continue;
|
|
2288
|
+
try {
|
|
2289
|
+
const css = fs.readFileSync(cp, "utf-8");
|
|
2290
|
+
if (/\[data-theme\s*=\s*['"]dark['"]\]/.test(css)) return "data-attribute";
|
|
2291
|
+
if (/\.dark\s*[{,]/.test(css)) return "class";
|
|
2292
|
+
if (/@media\s*\(prefers-color-scheme\s*:\s*dark\)/.test(css)) return "media";
|
|
2293
|
+
} catch (_) {}
|
|
2294
|
+
}
|
|
2295
|
+
return "unknown";
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2111
2298
|
function extractButtonUsage() {
|
|
2112
2299
|
if (!fs.existsSync(SRC_DIR)) return null;
|
|
2113
2300
|
const files = getAllTsxJsxInDir(SRC_DIR);
|
|
@@ -2166,6 +2353,50 @@ function extractButtonUsage() {
|
|
|
2166
2353
|
};
|
|
2167
2354
|
}
|
|
2168
2355
|
|
|
2356
|
+
/**
|
|
2357
|
+
* Phase D — CSS Modules Support v2.12.0
|
|
2358
|
+
* Reads .module.css files imported by a component and extracts design-relevant CSS properties.
|
|
2359
|
+
*/
|
|
2360
|
+
function extractCssModuleTokens(compFilePath) {
|
|
2361
|
+
if (!compFilePath || !fs.existsSync(compFilePath)) return null;
|
|
2362
|
+
let src;
|
|
2363
|
+
try { src = fs.readFileSync(compFilePath, "utf-8"); } catch (_) { return null; }
|
|
2364
|
+
// Find `import styles from './Foo.module.css'` or named imports
|
|
2365
|
+
const moduleImportRe = /import\s+(?:[\w{},\s]+\s+from\s+)?['"]([^'"]+\.module\.css)['"]/g;
|
|
2366
|
+
const dir = path.dirname(compFilePath);
|
|
2367
|
+
const classNames = [];
|
|
2368
|
+
const properties = {};
|
|
2369
|
+
let found = false;
|
|
2370
|
+
let m;
|
|
2371
|
+
while ((m = moduleImportRe.exec(src)) !== null) {
|
|
2372
|
+
const cssPath = path.resolve(dir, m[1]);
|
|
2373
|
+
if (!fs.existsSync(cssPath)) continue;
|
|
2374
|
+
let css;
|
|
2375
|
+
try { css = fs.readFileSync(cssPath, "utf-8"); } catch (_) { continue; }
|
|
2376
|
+
found = true;
|
|
2377
|
+
// Extract class names and design-relevant properties
|
|
2378
|
+
const classBlockRe = /\.(\w[\w-]*)\s*\{([^}]+)\}/g;
|
|
2379
|
+
let cb;
|
|
2380
|
+
while ((cb = classBlockRe.exec(css)) !== null) {
|
|
2381
|
+
const className = cb[1];
|
|
2382
|
+
if (!classNames.includes(className)) classNames.push(className);
|
|
2383
|
+
const block = cb[2];
|
|
2384
|
+
const DESIGN_PROPS = ["background","background-color","color","font-family","font-size","font-weight","padding","padding-top","padding-right","padding-bottom","padding-left","margin","border-radius","gap","border","border-color","border-width","box-shadow","opacity","width","height"];
|
|
2385
|
+
for (const prop of DESIGN_PROPS) {
|
|
2386
|
+
const re = new RegExp(`${prop}:\\s*([^;]+);`);
|
|
2387
|
+
const pm = block.match(re);
|
|
2388
|
+
if (pm) {
|
|
2389
|
+
if (!properties[prop]) properties[prop] = [];
|
|
2390
|
+
const val = pm[1].trim();
|
|
2391
|
+
if (!properties[prop].includes(val)) properties[prop].push(val);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
if (!found || classNames.length === 0) return null;
|
|
2397
|
+
return { classNames, properties };
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2169
2400
|
/**
|
|
2170
2401
|
* Scan all non-component source files and count how many times each variant value
|
|
2171
2402
|
* is explicitly used for each component (e.g. <Button variant="destructive">).
|
|
@@ -2241,6 +2472,26 @@ function isComplexPageComponent(content) {
|
|
|
2241
2472
|
return true;
|
|
2242
2473
|
}
|
|
2243
2474
|
|
|
2475
|
+
/**
|
|
2476
|
+
* Extract top-level (no indentation) PascalCase component names from a source file.
|
|
2477
|
+
* Used for isPageComponent files to find internal sub-components that can be auto-exported.
|
|
2478
|
+
* Only matches declarations at column 0 (^) to exclude nested arrow functions.
|
|
2479
|
+
*/
|
|
2480
|
+
function extractTopLevelInternalComponentNames(content) {
|
|
2481
|
+
// Match only unindented `const ComponentName = (` or `const ComponentName: React.FC = (`
|
|
2482
|
+
const re = /^(?:export\s+)?const\s+([A-Z][A-Za-z0-9]+)\s*[:=]\s*(?:\(|\bReact\.)/gm;
|
|
2483
|
+
const names = [];
|
|
2484
|
+
const seen = new Set();
|
|
2485
|
+
let m;
|
|
2486
|
+
while ((m = re.exec(content)) !== null) {
|
|
2487
|
+
if (!seen.has(m[1])) {
|
|
2488
|
+
seen.add(m[1]);
|
|
2489
|
+
names.push(m[1]);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
return names;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2244
2495
|
function scan() {
|
|
2245
2496
|
const relativeFiles = COMPONENTS_DIR ? getAllComponentFiles(COMPONENTS_DIR) : [];
|
|
2246
2497
|
if (!COMPONENTS_DIR) {
|
|
@@ -2266,7 +2517,14 @@ function scan() {
|
|
|
2266
2517
|
}
|
|
2267
2518
|
const tokens = extractTailwindTokens(content);
|
|
2268
2519
|
const isPageComponent = isComplexPageComponent(content);
|
|
2269
|
-
|
|
2520
|
+
const internalComponentNames = isPageComponent
|
|
2521
|
+
? extractTopLevelInternalComponentNames(content).filter(n => n !== name)
|
|
2522
|
+
: undefined;
|
|
2523
|
+
const comp = { file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true, internalComponentNames } : {}) };
|
|
2524
|
+
// Phase D — CSS Module tokens
|
|
2525
|
+
const cssModuleTokens = extractCssModuleTokens(COMPONENTS_DIR ? path.join(COMPONENTS_DIR, rel) : null);
|
|
2526
|
+
if (cssModuleTokens) comp.cssModuleTokens = cssModuleTokens;
|
|
2527
|
+
results.push(comp);
|
|
2270
2528
|
}
|
|
2271
2529
|
if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
|
|
2272
2530
|
const pageFiles = getAllComponentFiles(PAGES_DIR);
|
|
@@ -2290,6 +2548,8 @@ function scan() {
|
|
|
2290
2548
|
|
|
2291
2549
|
const foundations = extractFoundations();
|
|
2292
2550
|
foundations.icons = extractLucideIconsUsed(SRC_DIR);
|
|
2551
|
+
// Phase E — Dark mode strategy
|
|
2552
|
+
foundations.darkModeStrategy = detectDarkModeStrategy(PROJECT_ROOT);
|
|
2293
2553
|
foundations.brand = { assets: extractBrandAssets() };
|
|
2294
2554
|
const buttonUsage = extractButtonUsage();
|
|
2295
2555
|
if (buttonUsage) {
|
|
@@ -2299,6 +2559,10 @@ function scan() {
|
|
|
2299
2559
|
if (tokenUsage) {
|
|
2300
2560
|
foundations.tokenUsage = tokenUsage;
|
|
2301
2561
|
}
|
|
2562
|
+
// Phase A — Color Intelligence
|
|
2563
|
+
const colorTokenNames = Object.keys(foundations.colors || {}).filter(k => k !== "_dark");
|
|
2564
|
+
const colorUsage = extractColorUsage(colorTokenNames);
|
|
2565
|
+
if (colorUsage) foundations.colorUsage = colorUsage;
|
|
2302
2566
|
const componentSuggestions = extractComponentSuggestions();
|
|
2303
2567
|
const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
|
|
2304
2568
|
const output = {
|
|
@@ -1880,6 +1880,48 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
|
|
|
1880
1880
|
lines.push(`};`);
|
|
1881
1881
|
}
|
|
1882
1882
|
}
|
|
1883
|
+
// Phase D2 — CSS Module Classes section
|
|
1884
|
+
const cssModTokens = comp.cssModuleTokens || null;
|
|
1885
|
+
if (cssModTokens && Array.isArray(cssModTokens.classNames) && cssModTokens.classNames.length > 0) {
|
|
1886
|
+
lines.push("");
|
|
1887
|
+
lines.push(`export const CssModuleClasses: Story = {`);
|
|
1888
|
+
lines.push(` name: "CSS Module Classes",`);
|
|
1889
|
+
lines.push(` parameters: { layout: "fullscreen" },`);
|
|
1890
|
+
lines.push(` render: () => {`);
|
|
1891
|
+
lines.push(` const classNames = ${JSON.stringify(cssModTokens.classNames)};`);
|
|
1892
|
+
lines.push(` const properties = ${JSON.stringify(cssModTokens.properties)};`);
|
|
1893
|
+
lines.push(` return (`);
|
|
1894
|
+
lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh" }}>`);
|
|
1895
|
+
lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>CSS Module Classes</h2>`);
|
|
1896
|
+
lines.push(` <p style={{ fontSize: 13, color: "#6b7280", margin: "0 0 24px" }}>Classes from <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>.module.css</code> files imported by <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>${componentName}</code></p>`);
|
|
1897
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 32 }}>`);
|
|
1898
|
+
lines.push(` {classNames.map((cls: string) => (`);
|
|
1899
|
+
lines.push(` <span key={cls} style={{ fontFamily: "monospace", fontSize: 12, background: "#f0fdf4", color: "#166534", padding: "4px 10px", borderRadius: 6, border: "1px solid #bbf7d0" }}>source: CSS Module</span>`);
|
|
1900
|
+
lines.push(` ))}`);
|
|
1901
|
+
lines.push(` {classNames.map((cls: string) => (`);
|
|
1902
|
+
lines.push(` <code key={cls} style={{ fontSize: 12, background: "#f9fafb", color: "#374151", padding: "4px 10px", borderRadius: 6, border: "1px solid #e5e7eb" }}>.{cls}</code>`);
|
|
1903
|
+
lines.push(` ))}`);
|
|
1904
|
+
lines.push(` </div>`);
|
|
1905
|
+
lines.push(` {Object.entries(properties).length > 0 && (`);
|
|
1906
|
+
lines.push(` <div>`);
|
|
1907
|
+
lines.push(` <p style={{ fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 12 }}>Extracted Properties</p>`);
|
|
1908
|
+
lines.push(` <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>`);
|
|
1909
|
+
lines.push(` {Object.entries(properties).map(([prop, values]: [string, string[]]) => (`);
|
|
1910
|
+
lines.push(` <div key={prop} style={{ display: "flex", gap: 8, alignItems: "flex-start", padding: "8px 12px", border: "1px solid #e5e7eb", borderRadius: 6, background: "#f9fafb" }}>`);
|
|
1911
|
+
lines.push(` <code style={{ fontSize: 11, color: "#6b7280", minWidth: 140, flexShrink: 0 }}>{prop}</code>`);
|
|
1912
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>`);
|
|
1913
|
+
lines.push(` {values.map((v: string) => <code key={v} style={{ fontSize: 11, background: "#fff", color: "#374151", padding: "1px 6px", borderRadius: 4, border: "1px solid #e5e7eb" }}>{v}</code>)}`);
|
|
1914
|
+
lines.push(` </div>`);
|
|
1915
|
+
lines.push(` </div>`);
|
|
1916
|
+
lines.push(` ))}`);
|
|
1917
|
+
lines.push(` </div>`);
|
|
1918
|
+
lines.push(` </div>`);
|
|
1919
|
+
lines.push(` )}`);
|
|
1920
|
+
lines.push(` </div>`);
|
|
1921
|
+
lines.push(` );`);
|
|
1922
|
+
lines.push(` },`);
|
|
1923
|
+
lines.push(`};`);
|
|
1924
|
+
}
|
|
1883
1925
|
}
|
|
1884
1926
|
|
|
1885
1927
|
return lines.join("\n");
|
|
@@ -2166,8 +2208,11 @@ function buildStoryFileContent(comp) {
|
|
|
2166
2208
|
lines.push(` component: ComponentRef,`);
|
|
2167
2209
|
// SECTION: no props/args → autodocs tries to render React.lazy without Suspense → useRef crash
|
|
2168
2210
|
if (profile !== "SECTION") lines.push(` tags: ["autodocs"],`);
|
|
2169
|
-
// Center small components
|
|
2170
|
-
if (profile !== "SECTION")
|
|
2211
|
+
// Center small components; fullscreen for complex page-level components
|
|
2212
|
+
if (profile !== "SECTION") {
|
|
2213
|
+
const layout = comp.isPageComponent ? "fullscreen" : "centered";
|
|
2214
|
+
lines.push(` parameters: { layout: "${layout}" },`);
|
|
2215
|
+
}
|
|
2171
2216
|
// Wrap with QueryClientProvider for components that use @tanstack/react-query hooks
|
|
2172
2217
|
if (needsQueryClient) {
|
|
2173
2218
|
lines.push(` decorators: [(Story: any) => React.createElement(QueryClientProvider, { client: _queryClient }, React.createElement(Story))],`);
|
|
@@ -2365,6 +2410,48 @@ function buildStoryFileContent(comp) {
|
|
|
2365
2410
|
lines.push(`};`);
|
|
2366
2411
|
}
|
|
2367
2412
|
}
|
|
2413
|
+
// Phase D2 — CSS Module Classes section
|
|
2414
|
+
const cssModTokens = comp.cssModuleTokens || null;
|
|
2415
|
+
if (cssModTokens && Array.isArray(cssModTokens.classNames) && cssModTokens.classNames.length > 0) {
|
|
2416
|
+
lines.push("");
|
|
2417
|
+
lines.push(`export const CssModuleClasses: Story = {`);
|
|
2418
|
+
lines.push(` name: "CSS Module Classes",`);
|
|
2419
|
+
lines.push(` parameters: { layout: "fullscreen" },`);
|
|
2420
|
+
lines.push(` render: () => {`);
|
|
2421
|
+
lines.push(` const classNames = ${JSON.stringify(cssModTokens.classNames)};`);
|
|
2422
|
+
lines.push(` const properties = ${JSON.stringify(cssModTokens.properties)};`);
|
|
2423
|
+
lines.push(` return (`);
|
|
2424
|
+
lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh" }}>`);
|
|
2425
|
+
lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>CSS Module Classes</h2>`);
|
|
2426
|
+
lines.push(` <p style={{ fontSize: 13, color: "#6b7280", margin: "0 0 24px" }}>Classes from <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>.module.css</code> files imported by <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>${componentName}</code></p>`);
|
|
2427
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 32 }}>`);
|
|
2428
|
+
lines.push(` {classNames.map((cls: string) => (`);
|
|
2429
|
+
lines.push(` <span key={cls} style={{ fontFamily: "monospace", fontSize: 12, background: "#f0fdf4", color: "#166534", padding: "4px 10px", borderRadius: 6, border: "1px solid #bbf7d0" }}>source: CSS Module</span>`);
|
|
2430
|
+
lines.push(` ))}`);
|
|
2431
|
+
lines.push(` {classNames.map((cls: string) => (`);
|
|
2432
|
+
lines.push(` <code key={cls} style={{ fontSize: 12, background: "#f9fafb", color: "#374151", padding: "4px 10px", borderRadius: 6, border: "1px solid #e5e7eb" }}>.{cls}</code>`);
|
|
2433
|
+
lines.push(` ))}`);
|
|
2434
|
+
lines.push(` </div>`);
|
|
2435
|
+
lines.push(` {Object.entries(properties).length > 0 && (`);
|
|
2436
|
+
lines.push(` <div>`);
|
|
2437
|
+
lines.push(` <p style={{ fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 12 }}>Extracted Properties</p>`);
|
|
2438
|
+
lines.push(` <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>`);
|
|
2439
|
+
lines.push(` {Object.entries(properties).map(([prop, values]: [string, string[]]) => (`);
|
|
2440
|
+
lines.push(` <div key={prop} style={{ display: "flex", gap: 8, alignItems: "flex-start", padding: "8px 12px", border: "1px solid #e5e7eb", borderRadius: 6, background: "#f9fafb" }}>`);
|
|
2441
|
+
lines.push(` <code style={{ fontSize: 11, color: "#6b7280", minWidth: 140, flexShrink: 0 }}>{prop}</code>`);
|
|
2442
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>`);
|
|
2443
|
+
lines.push(` {values.map((v: string) => <code key={v} style={{ fontSize: 11, background: "#fff", color: "#374151", padding: "1px 6px", borderRadius: 4, border: "1px solid #e5e7eb" }}>{v}</code>)}`);
|
|
2444
|
+
lines.push(` </div>`);
|
|
2445
|
+
lines.push(` </div>`);
|
|
2446
|
+
lines.push(` ))}`);
|
|
2447
|
+
lines.push(` </div>`);
|
|
2448
|
+
lines.push(` </div>`);
|
|
2449
|
+
lines.push(` )}`);
|
|
2450
|
+
lines.push(` </div>`);
|
|
2451
|
+
lines.push(` );`);
|
|
2452
|
+
lines.push(` },`);
|
|
2453
|
+
lines.push(`};`);
|
|
2454
|
+
}
|
|
2368
2455
|
}
|
|
2369
2456
|
|
|
2370
2457
|
// --- Usage story ---
|
|
@@ -2596,7 +2683,8 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2596
2683
|
const hex = c?.hex || c?.value || "";
|
|
2597
2684
|
const u = colorUsage[name] || null;
|
|
2598
2685
|
return { name, hex, cssVar: `--${name}`, description: DESCRIPTIONS[name] || "", usage: u };
|
|
2599
|
-
})
|
|
2686
|
+
})
|
|
2687
|
+
.sort((a, b) => (b.usage?.total || 0) - (a.usage?.total || 0));
|
|
2600
2688
|
return { key: group.key, label: group.label, colors };
|
|
2601
2689
|
}).filter((g) => g.colors.length > 0);
|
|
2602
2690
|
|
|
@@ -2660,6 +2748,11 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2660
2748
|
" ×{usage.total}",
|
|
2661
2749
|
" </span>",
|
|
2662
2750
|
" )}",
|
|
2751
|
+
" {usage && usage.dark > 0 && (",
|
|
2752
|
+
" <span style={{ position: \"absolute\", top: 8, left: 8, fontSize: 9, fontWeight: 700, padding: \"2px 6px\", borderRadius: 99, background: \"rgba(109,40,217,0.85)\", color: \"#fff\", backdropFilter: \"blur(2px)\" }}>",
|
|
2753
|
+
" dark ×{usage.dark}",
|
|
2754
|
+
" </span>",
|
|
2755
|
+
" )}",
|
|
2663
2756
|
" </div>",
|
|
2664
2757
|
" {/* Info */}",
|
|
2665
2758
|
" <div style={{ padding: \"12px 14px\" }}>",
|
|
@@ -2711,6 +2804,74 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2711
2804
|
" </div>",
|
|
2712
2805
|
" ),",
|
|
2713
2806
|
"};",
|
|
2807
|
+
"",
|
|
2808
|
+
"export const UsedOnly: Story = {",
|
|
2809
|
+
" name: \"Used Colors\",",
|
|
2810
|
+
" render: () => {",
|
|
2811
|
+
` const usedGroups = colorGroups.map(g => ({ ...g, colors: g.colors.filter(c => c.usage && c.usage.total > 0) })).filter(g => g.colors.length > 0);`,
|
|
2812
|
+
" return (",
|
|
2813
|
+
" <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, background: \"#fff\", minHeight: \"100vh\", width: \"100%\", color: \"#111\" }}>",
|
|
2814
|
+
" <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Used Colors</h2>",
|
|
2815
|
+
" <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 40px\" }}>Only colors actually used in source — sorted by usage frequency</p>",
|
|
2816
|
+
" {usedGroups.map(group => (",
|
|
2817
|
+
" <div key={group.key} style={{ marginBottom: 40 }}>",
|
|
2818
|
+
" <h3 style={{ fontSize: 14, fontWeight: 700, textTransform: \"uppercase\", letterSpacing: \"0.08em\", color: \"#6b7280\", margin: \"0 0 16px\", borderBottom: \"1px solid #e5e7eb\", paddingBottom: 8 }}>{group.label}</h3>",
|
|
2819
|
+
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 12 }}>",
|
|
2820
|
+
" {group.colors.map(({ name, hex, usage }) => (",
|
|
2821
|
+
" <div key={name} style={{ display: \"flex\", flexDirection: \"column\", gap: 4, width: 80 }}>",
|
|
2822
|
+
" <div style={{ width: 80, height: 48, borderRadius: 8, background: hex, border: \"1px solid #e5e7eb\" }} />",
|
|
2823
|
+
" <code style={{ fontSize: 10, color: \"#374151\", wordBreak: \"break-all\" as any }}>{name}</code>",
|
|
2824
|
+
" <span style={{ fontSize: 10, color: \"#6b7280\" }}>×{usage?.total}</span>",
|
|
2825
|
+
" </div>",
|
|
2826
|
+
" ))}",
|
|
2827
|
+
" </div>",
|
|
2828
|
+
" </div>",
|
|
2829
|
+
" ))}",
|
|
2830
|
+
" {usedGroups.length === 0 && <p style={{ color: \"#9ca3af\", fontSize: 14 }}>No colorUsage data — run scan.mjs to generate.</p>}",
|
|
2831
|
+
" </div>",
|
|
2832
|
+
" );",
|
|
2833
|
+
" },",
|
|
2834
|
+
"};",
|
|
2835
|
+
"",
|
|
2836
|
+
"export const ColorUsageByComponent: Story = {",
|
|
2837
|
+
" name: \"Component → Color\",",
|
|
2838
|
+
" render: () => {",
|
|
2839
|
+
" // Build reverse map: component → colors used",
|
|
2840
|
+
" const compMap: Record<string, {name:string;hex:string;total:number}[]> = {};",
|
|
2841
|
+
" for (const group of colorGroups) {",
|
|
2842
|
+
" for (const { name, hex, usage } of group.colors) {",
|
|
2843
|
+
" if (!usage || usage.total === 0) continue;",
|
|
2844
|
+
" for (const comp of (usage.topFiles || [])) {",
|
|
2845
|
+
" if (!compMap[comp]) compMap[comp] = [];",
|
|
2846
|
+
" if (!compMap[comp].find(c => c.name === name)) compMap[comp].push({ name, hex, total: usage.total });",
|
|
2847
|
+
" }",
|
|
2848
|
+
" }",
|
|
2849
|
+
" }",
|
|
2850
|
+
" const compEntries = Object.entries(compMap).sort((a,b) => b[1].length - a[1].length).slice(0, 30);",
|
|
2851
|
+
" return (",
|
|
2852
|
+
" <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, background: \"#fff\", minHeight: \"100vh\", width: \"100%\", color: \"#111\" }}>",
|
|
2853
|
+
" <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Component → Color</h2>",
|
|
2854
|
+
" <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 32px\" }}>Which components use which color tokens</p>",
|
|
2855
|
+
" <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>",
|
|
2856
|
+
" {compEntries.map(([comp, colors]) => (",
|
|
2857
|
+
" <div key={comp} style={{ padding: \"14px 18px\", border: \"1px solid #e5e7eb\", borderRadius: 10, background: \"#f9fafb\" }}>",
|
|
2858
|
+
" <div style={{ fontSize: 13, fontWeight: 700, color: \"#111\", marginBottom: 10, fontFamily: \"monospace\" }}>{comp}</div>",
|
|
2859
|
+
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6 }}>",
|
|
2860
|
+
" {colors.map(({ name, hex }) => (",
|
|
2861
|
+
" <div key={name} style={{ display: \"flex\", alignItems: \"center\", gap: 5, background: \"#fff\", border: \"1px solid #e5e7eb\", borderRadius: 6, padding: \"3px 9px\" }}>",
|
|
2862
|
+
" <span style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 2, background: hex, border: \"1px solid rgba(0,0,0,0.1)\", flexShrink: 0 }} />",
|
|
2863
|
+
" <code style={{ fontSize: 11, color: \"#374151\" }}>{name}</code>",
|
|
2864
|
+
" </div>",
|
|
2865
|
+
" ))}",
|
|
2866
|
+
" </div>",
|
|
2867
|
+
" </div>",
|
|
2868
|
+
" ))}",
|
|
2869
|
+
" {compEntries.length === 0 && <p style={{ color: \"#9ca3af\", fontSize: 14 }}>No colorUsage data — run scan.mjs to generate.</p>}",
|
|
2870
|
+
" </div>",
|
|
2871
|
+
" </div>",
|
|
2872
|
+
" );",
|
|
2873
|
+
" },",
|
|
2874
|
+
"};",
|
|
2714
2875
|
].join("\n");
|
|
2715
2876
|
|
|
2716
2877
|
fs.writeFileSync(path.join(foundationsDir, "Colors.stories.tsx"), colorsContent, "utf-8");
|
|
@@ -2802,6 +2963,9 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2802
2963
|
? `'${typo.arbitraryFonts[0]}', sans-serif` : null;
|
|
2803
2964
|
const sansFamily = typo.fontSans || typo.body || firstArbitraryFont || typo.tailwindSans || "system-ui, sans-serif";
|
|
2804
2965
|
|
|
2966
|
+
const fontFaces = Array.isArray(typo.fontFaces) ? typo.fontFaces : [];
|
|
2967
|
+
const nextFonts = Array.isArray(typo.nextFonts) ? typo.nextFonts : [];
|
|
2968
|
+
|
|
2805
2969
|
const typoContent = [
|
|
2806
2970
|
"import React from \"react\";",
|
|
2807
2971
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
@@ -2817,6 +2981,8 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2817
2981
|
`const weightRows: { token: string; value: string }[] = ${JSON.stringify(weightRows)};`,
|
|
2818
2982
|
`const weightRowsIsDefault: boolean = ${weightRowsIsDefault};`,
|
|
2819
2983
|
`const sansFamily = ${JSON.stringify(sansFamily)};`,
|
|
2984
|
+
`const fontFamilyRows: { token: string; value: string }[] = ${JSON.stringify(familyRows)};`,
|
|
2985
|
+
`const fontSourceRows: { type: string; family: string; weight: string | null; isVariable: boolean; variable: string | null }[] = ${JSON.stringify([...fontFaces.map(f => ({ type: "css", family: f.family, weight: f.weight, isVariable: f.isVariable, variable: null })), ...nextFonts.map(f => ({ type: "next", family: f.family, weight: null, isVariable: false, variable: f.variable }))])};`,
|
|
2820
2986
|
"",
|
|
2821
2987
|
"export const Default: Story = {",
|
|
2822
2988
|
" render: () => (",
|
|
@@ -2895,6 +3061,26 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2895
3061
|
" </>",
|
|
2896
3062
|
" )}",
|
|
2897
3063
|
"",
|
|
3064
|
+
" {/* Phase B4 — Letter Specimens */}",
|
|
3065
|
+
" {fontFamilyRows.length > 0 && (",
|
|
3066
|
+
" <div style={{ marginBottom: 48 }}>",
|
|
3067
|
+
" <h3 style={{ fontSize: 14, fontWeight: 700, textTransform: \"uppercase\", letterSpacing: \"0.08em\", color: \"#6b7280\", margin: \"0 0 24px\", borderBottom: \"1px solid #e5e7eb\", paddingBottom: 8 }}>Letter Specimens</h3>",
|
|
3068
|
+
" <div style={{ display: \"flex\", flexDirection: \"column\", gap: 32 }}>",
|
|
3069
|
+
" {fontFamilyRows.map(({ token, value }) => {",
|
|
3070
|
+
" const family = value.split(',')[0].trim().replace(/['\\\"/]/g, '');",
|
|
3071
|
+
" return (",
|
|
3072
|
+
" <div key={token} style={{ padding: 24, border: \"1px solid #e5e7eb\", borderRadius: 12, background: \"#fafafa\" }}>",
|
|
3073
|
+
" <div style={{ fontSize: 11, fontWeight: 600, color: \"#9ca3af\", marginBottom: 12, textTransform: \"uppercase\", letterSpacing: \"0.08em\", fontFamily: \"system-ui\" }}>{token} — {value.length > 50 ? value.slice(0,50)+'…' : value}</div>",
|
|
3074
|
+
" <div style={{ fontFamily: value, fontSize: 48, fontWeight: 700, color: \"#111\", lineHeight: 1.1, marginBottom: 8 }}>Aa Bb Cc</div>",
|
|
3075
|
+
" <div style={{ fontFamily: value, fontSize: 18, color: \"#374151\", lineHeight: 1.6, marginBottom: 8 }}>The quick brown fox jumps over the lazy dog</div>",
|
|
3076
|
+
" <div style={{ fontFamily: value, fontSize: 14, color: \"#6b7280\", letterSpacing: \"0.1em\" }}>0123456789 ! @ # $ %</div>",
|
|
3077
|
+
" </div>",
|
|
3078
|
+
" );",
|
|
3079
|
+
" })}",
|
|
3080
|
+
" </div>",
|
|
3081
|
+
" </div>",
|
|
3082
|
+
" )}",
|
|
3083
|
+
"",
|
|
2898
3084
|
" {/* ── FONT WEIGHTS ── */}",
|
|
2899
3085
|
" {weightRowsIsDefault && <p style={{ fontSize: 12, color: \"#92400e\", background: \"#fef3c7\", border: \"1px solid #fde68a\", borderRadius: 6, padding: \"6px 12px\", marginBottom: 12, display: \"inline-block\" }}>ℹ️ Tailwind defaults — no custom font weights found in this project</p>}",
|
|
2900
3086
|
" {weightRows.length > 0 && (",
|
|
@@ -2913,6 +3099,21 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2913
3099
|
" </div>",
|
|
2914
3100
|
" </>",
|
|
2915
3101
|
" )}",
|
|
3102
|
+
" {fontSourceRows.length > 0 && (",
|
|
3103
|
+
" <div style={{ marginBottom: 48 }}>",
|
|
3104
|
+
" <h3 style={{ fontSize: 14, fontWeight: 700, textTransform: \"uppercase\", letterSpacing: \"0.08em\", color: \"#6b7280\", margin: \"0 0 16px\", borderBottom: \"1px solid #e5e7eb\", paddingBottom: 8 }}>Font Sources</h3>",
|
|
3105
|
+
" <div style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>",
|
|
3106
|
+
" {fontSourceRows.map((f, i) => (",
|
|
3107
|
+
" <div key={i} style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: \"10px 16px\", border: \"1px solid #e5e7eb\", borderRadius: 8, background: \"#f9fafb\" }}>",
|
|
3108
|
+
" <span style={{ fontSize: 10, fontWeight: 700, padding: \"2px 7px\", borderRadius: 4, background: f.type === 'next' ? '#dbeafe' : '#d1fae5', color: f.type === 'next' ? '#1e40af' : '#065f46' }}>{f.type === 'next' ? 'next/font' : '@font-face'}</span>",
|
|
3109
|
+
" <code style={{ fontSize: 12, fontWeight: 600, color: \"#111\" }}>{f.family}</code>",
|
|
3110
|
+
" {f.isVariable && <span style={{ fontSize: 10, padding: \"2px 7px\", borderRadius: 4, background: \"#fef9c3\", color: \"#854d0e\" }}>variable {f.weight}</span>}",
|
|
3111
|
+
" {f.variable && <code style={{ fontSize: 10, color: \"#9ca3af\" }}>{f.variable}</code>}",
|
|
3112
|
+
" </div>",
|
|
3113
|
+
" ))}",
|
|
3114
|
+
" </div>",
|
|
3115
|
+
" </div>",
|
|
3116
|
+
" )}",
|
|
2916
3117
|
" </div>",
|
|
2917
3118
|
" ),",
|
|
2918
3119
|
"};",
|
|
@@ -5002,11 +5203,8 @@ function main() {
|
|
|
5002
5203
|
const storyFileName = `${componentName}.stories.tsx`;
|
|
5003
5204
|
const storyPath = path.join(STORIES_DIR, storyFileName);
|
|
5004
5205
|
if (SKIP_LIST.includes(componentName)) continue;
|
|
5005
|
-
//
|
|
5006
|
-
|
|
5007
|
-
console.log(`[VDS] ${componentName} → skipped (complex page component — add to extraSkipList to suppress this message)`);
|
|
5008
|
-
continue;
|
|
5009
|
-
}
|
|
5206
|
+
// Complex page-level components (500+ lines, 4+ inline sub-components) — generate fullscreen story instead of skipping
|
|
5207
|
+
// (Previously skipped; now a simplified fullscreen story is generated so they appear in Storybook)
|
|
5010
5208
|
const requiredCount = Array.isArray(comp.props) ? comp.props.filter((p) => p.required === true).length : 0;
|
|
5011
5209
|
if (requiredCount > 3) {
|
|
5012
5210
|
console.log(`[VDS] ${componentName} → skipped (${requiredCount} required props — too complex to auto-generate)`);
|
|
@@ -5026,6 +5224,12 @@ function main() {
|
|
|
5026
5224
|
writtenCount++;
|
|
5027
5225
|
}
|
|
5028
5226
|
|
|
5227
|
+
// Phase I — Internal component sub-stories for isPageComponent files
|
|
5228
|
+
for (const comp of components) {
|
|
5229
|
+
if (!comp.isPageComponent || !Array.isArray(comp.internalComponentNames) || comp.internalComponentNames.length === 0) continue;
|
|
5230
|
+
generateInternalComponentStories(comp);
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5029
5233
|
// Summary
|
|
5030
5234
|
if (writtenCount === 0 && components.length > 0) {
|
|
5031
5235
|
const hasShadcnGroup = components.some(c => {
|
|
@@ -5042,5 +5246,264 @@ function main() {
|
|
|
5042
5246
|
}
|
|
5043
5247
|
}
|
|
5044
5248
|
|
|
5249
|
+
// ─── Phase I: Internal Component Sub-Story Engine ─────────────────────────────
|
|
5250
|
+
/**
|
|
5251
|
+
* Adds `export` to top-level internal component declarations in a source file.
|
|
5252
|
+
* Non-destructive: only adds `export` keyword, no logic changes.
|
|
5253
|
+
* Returns the list of names that were newly exported.
|
|
5254
|
+
*/
|
|
5255
|
+
function autoExportInternalComponents(sourceFilePath, names) {
|
|
5256
|
+
let src = fs.readFileSync(sourceFilePath, "utf-8");
|
|
5257
|
+
const newly = [];
|
|
5258
|
+
for (const n of names) {
|
|
5259
|
+
// Already exported? skip
|
|
5260
|
+
if (new RegExp(`^export\\s+const\\s+${n}\\b`, "m").test(src)) continue;
|
|
5261
|
+
// Exists as unexported top-level const?
|
|
5262
|
+
const re = new RegExp(`^(const\\s+${n}\\b)`, "m");
|
|
5263
|
+
if (!re.test(src)) continue;
|
|
5264
|
+
src = src.replace(re, "export $1");
|
|
5265
|
+
newly.push(n);
|
|
5266
|
+
}
|
|
5267
|
+
if (newly.length > 0) fs.writeFileSync(sourceFilePath, src, "utf-8");
|
|
5268
|
+
return newly;
|
|
5269
|
+
}
|
|
5270
|
+
|
|
5271
|
+
/**
|
|
5272
|
+
* Parse top-level relative import statements (local files only).
|
|
5273
|
+
* Returns [{importPath, symbols}]
|
|
5274
|
+
*/
|
|
5275
|
+
function parseLocalDataImports(content) {
|
|
5276
|
+
const re = /^import\s*\{([^}]+)\}\s*from\s*["'](\.[^"']+)["']/gm;
|
|
5277
|
+
const results = [];
|
|
5278
|
+
let m;
|
|
5279
|
+
while ((m = re.exec(content)) !== null) {
|
|
5280
|
+
const symbols = m[1].split(",").map(s => s.trim().replace(/\s+as\s+\w+$/, "").trim()).filter(Boolean);
|
|
5281
|
+
results.push({ importPath: m[2], symbols });
|
|
5282
|
+
}
|
|
5283
|
+
return results;
|
|
5284
|
+
}
|
|
5285
|
+
|
|
5286
|
+
/**
|
|
5287
|
+
* Detect named exports from a data file: returns { exportName → 'array' | 'object' | 'scalar' }
|
|
5288
|
+
*/
|
|
5289
|
+
function parseDataFileExports(filePath) {
|
|
5290
|
+
if (!fs.existsSync(filePath)) return {};
|
|
5291
|
+
const src = fs.readFileSync(filePath, "utf-8");
|
|
5292
|
+
const result = {};
|
|
5293
|
+
const re = /^export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=:]/gm;
|
|
5294
|
+
let m;
|
|
5295
|
+
while ((m = re.exec(src)) !== null) {
|
|
5296
|
+
const name = m[1];
|
|
5297
|
+
const afterIdx = src.indexOf("=", m.index) + 1;
|
|
5298
|
+
const afterSlice = src.slice(afterIdx, afterIdx + 10).trimStart();
|
|
5299
|
+
result[name] = afterSlice.startsWith("[") ? "array" : afterSlice.startsWith("{") ? "object" : "scalar";
|
|
5300
|
+
}
|
|
5301
|
+
return result;
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5304
|
+
/**
|
|
5305
|
+
* Extract inline TypeScript prop types for a named component from source content.
|
|
5306
|
+
* Handles: `const Foo = ({ a, b }: { a: boolean; b: string }) =>`
|
|
5307
|
+
* Handles nested object types (e.g., `data: { subStation: SubStation; } | null`)
|
|
5308
|
+
* Returns [{name, type}]
|
|
5309
|
+
*/
|
|
5310
|
+
function extractComponentProps(content, componentName) {
|
|
5311
|
+
const esc = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5312
|
+
const declRe = new RegExp(`(?:export\\s+)?const\\s+${esc}\\s*[:=]\\s*\\(`, "g");
|
|
5313
|
+
const declMatch = declRe.exec(content);
|
|
5314
|
+
if (!declMatch) return [];
|
|
5315
|
+
const slice = content.slice(declMatch.index, declMatch.index + 5000);
|
|
5316
|
+
// Find `}: {` — start of inline type annotation
|
|
5317
|
+
const typeHeaderIdx = slice.search(/\}\s*:\s*\{/);
|
|
5318
|
+
if (typeHeaderIdx === -1) return [];
|
|
5319
|
+
const braceStart = slice.indexOf("{", typeHeaderIdx);
|
|
5320
|
+
// Walk forward tracking brace depth to find the matching closing brace
|
|
5321
|
+
let depth = 0, braceEnd = -1;
|
|
5322
|
+
for (let i = braceStart; i < slice.length; i++) {
|
|
5323
|
+
if (slice[i] === "{") depth++;
|
|
5324
|
+
else if (slice[i] === "}") { depth--; if (depth === 0) { braceEnd = i; break; } }
|
|
5325
|
+
}
|
|
5326
|
+
if (braceEnd === -1) return [];
|
|
5327
|
+
const typeBody = slice.slice(braceStart + 1, braceEnd);
|
|
5328
|
+
// Smart prop parser: tracks depth so `;` inside nested types is skipped
|
|
5329
|
+
const props = [];
|
|
5330
|
+
let i = 0;
|
|
5331
|
+
while (i < typeBody.length) {
|
|
5332
|
+
while (i < typeBody.length && /\s/.test(typeBody[i])) i++;
|
|
5333
|
+
if (i >= typeBody.length) break;
|
|
5334
|
+
const nameMatch = typeBody.slice(i).match(/^(\w+)\??(\s*:\s*)/);
|
|
5335
|
+
if (!nameMatch) { i++; continue; }
|
|
5336
|
+
const propName = nameMatch[1];
|
|
5337
|
+
i += nameMatch[0].length;
|
|
5338
|
+
let typeDepth = 0;
|
|
5339
|
+
const typeStartIdx = i;
|
|
5340
|
+
while (i < typeBody.length) {
|
|
5341
|
+
const ch = typeBody[i];
|
|
5342
|
+
if ("{([<".includes(ch)) typeDepth++;
|
|
5343
|
+
// `>` only closes angle brackets (generics), not `=>` arrow functions
|
|
5344
|
+
else if ("})]".includes(ch) || (ch === ">" && typeDepth > 0)) typeDepth--;
|
|
5345
|
+
else if (ch === ";" && typeDepth === 0) break;
|
|
5346
|
+
i++;
|
|
5347
|
+
}
|
|
5348
|
+
const type = typeBody.slice(typeStartIdx, i).trim();
|
|
5349
|
+
if (propName && type && !propName.startsWith("//") && propName !== "readonly") {
|
|
5350
|
+
props.push({ name: propName, type });
|
|
5351
|
+
}
|
|
5352
|
+
i++; // skip `;`
|
|
5353
|
+
}
|
|
5354
|
+
return props;
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
/**
|
|
5358
|
+
* Generate a mock value string for a TypeScript type, using available data exports.
|
|
5359
|
+
*/
|
|
5360
|
+
function mockValueForType(type, localExports) {
|
|
5361
|
+
const t = type.trim();
|
|
5362
|
+
if (t === "boolean") return "false";
|
|
5363
|
+
if (t === "string") return '""';
|
|
5364
|
+
if (t === "number") return "0";
|
|
5365
|
+
if (t === "null" || t === "undefined") return "null";
|
|
5366
|
+
if (t.startsWith("() =>") || t === "VoidFunction") return "() => {}";
|
|
5367
|
+
if (t.startsWith("(") && t.includes("=>")) return "() => {}";
|
|
5368
|
+
// Inline object type → null (complex; user fills it in)
|
|
5369
|
+
if (t.startsWith("{")) return "null";
|
|
5370
|
+
// Union ending in null/undefined → null
|
|
5371
|
+
if (t.endsWith("| null") || t.endsWith("| undefined") || t.startsWith("null |")) return "null";
|
|
5372
|
+
// String literal union → pick first quoted value
|
|
5373
|
+
const firstLiteral = t.match(/["']([^"']+)["']/);
|
|
5374
|
+
if (firstLiteral && (t.startsWith('"') || t.startsWith("'"))) return `"${firstLiteral[1]}"`;
|
|
5375
|
+
|
|
5376
|
+
// Try matching named type to available array exports
|
|
5377
|
+
// Strip common suffixes to get the "base" name (e.g., RouteDef→route, SubStation→substation)
|
|
5378
|
+
const typeLower = t.toLowerCase().replace(/def$|id$|key$|type$|interface$|kind$/i, "");
|
|
5379
|
+
if (typeLower.length >= 3) {
|
|
5380
|
+
for (const [expName, expKind] of Object.entries(localExports)) {
|
|
5381
|
+
if (expKind !== "array") continue;
|
|
5382
|
+
const expBase = expName.toLowerCase().replace(/_/g, "").replace(/s$/, "").replace(/data$/, "");
|
|
5383
|
+
// Direct match: RouteDef → ROUTES (routedef→rout vs routes→rout)
|
|
5384
|
+
if (expBase.slice(0, 4) === typeLower.slice(0, 4) || typeLower.slice(0, 4) === expBase.slice(0, 4)) {
|
|
5385
|
+
return `${expName}[0]`;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
return "undefined as any";
|
|
5390
|
+
}
|
|
5391
|
+
|
|
5392
|
+
/**
|
|
5393
|
+
* Build the story file content for a single internal component.
|
|
5394
|
+
*/
|
|
5395
|
+
function buildInternalComponentStoryContent(compName, parentImportPath, dataImportPath, dataSymbols, localExports, props, parentComp) {
|
|
5396
|
+
const lines = [];
|
|
5397
|
+
lines.push(`// @vds-regenerate — VDS auto-generated. Remove this line to prevent overwrite.`);
|
|
5398
|
+
lines.push(`import React from "react";`);
|
|
5399
|
+
lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
|
|
5400
|
+
lines.push(`import { ${compName} } from "${parentImportPath}";`);
|
|
5401
|
+
if (dataImportPath && dataSymbols.length > 0) {
|
|
5402
|
+
lines.push(`import { ${dataSymbols.join(", ")} } from "${dataImportPath}";`);
|
|
5403
|
+
}
|
|
5404
|
+
lines.push(``);
|
|
5405
|
+
lines.push(`/**`);
|
|
5406
|
+
lines.push(` * **${compName}** — sub-component of ${parentComp.name}.`);
|
|
5407
|
+
lines.push(` * Auto-exported by VDS for isolated Storybook documentation.`);
|
|
5408
|
+
lines.push(` */`);
|
|
5409
|
+
lines.push(`const meta: Meta<typeof ${compName}> = {`);
|
|
5410
|
+
lines.push(` title: "${parentComp.group || "Screens"}/${parentComp.name}/${compName}",`);
|
|
5411
|
+
lines.push(` component: ${compName},`);
|
|
5412
|
+
lines.push(` parameters: {`);
|
|
5413
|
+
lines.push(` layout: "centered",`);
|
|
5414
|
+
lines.push(` backgrounds: { default: "dark" },`);
|
|
5415
|
+
lines.push(` },`);
|
|
5416
|
+
lines.push(`};`);
|
|
5417
|
+
lines.push(`export default meta;`);
|
|
5418
|
+
lines.push(`type Story = StoryObj<typeof meta>;`);
|
|
5419
|
+
lines.push(``);
|
|
5420
|
+
|
|
5421
|
+
if (props.length > 0) {
|
|
5422
|
+
const argLines = props.map(p => ` ${p.name}: ${mockValueForType(p.type, localExports)},`).join("\n");
|
|
5423
|
+
lines.push(`export const Default: Story = {`);
|
|
5424
|
+
lines.push(` args: {`);
|
|
5425
|
+
lines.push(argLines);
|
|
5426
|
+
lines.push(` },`);
|
|
5427
|
+
lines.push(`};`);
|
|
5428
|
+
} else {
|
|
5429
|
+
lines.push(`export const Default: Story = {};`);
|
|
5430
|
+
}
|
|
5431
|
+
return lines.join("\n") + "\n";
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5434
|
+
/**
|
|
5435
|
+
* Main orchestrator: auto-exports internal components + generates individual story files.
|
|
5436
|
+
* Called for each comp with isPageComponent: true.
|
|
5437
|
+
*/
|
|
5438
|
+
function generateInternalComponentStories(comp) {
|
|
5439
|
+
const names = comp.internalComponentNames || [];
|
|
5440
|
+
if (names.length === 0) return;
|
|
5441
|
+
|
|
5442
|
+
// Locate the source file (COMPONENTS_REL_DIR is a module-level variable set in main())
|
|
5443
|
+
const sourceFile = path.join(PROJECT_ROOT, COMPONENTS_REL_DIR, comp.file);
|
|
5444
|
+
if (!fs.existsSync(sourceFile)) return;
|
|
5445
|
+
|
|
5446
|
+
const sourceContent = fs.readFileSync(sourceFile, "utf-8");
|
|
5447
|
+
|
|
5448
|
+
// Step 1: Auto-export internal components
|
|
5449
|
+
const newly = autoExportInternalComponents(sourceFile, names);
|
|
5450
|
+
if (newly.length > 0) {
|
|
5451
|
+
console.log(`[VDS] ${comp.name} → auto-exported ${newly.length} sub-components: ${newly.join(", ")}`);
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
// Step 2: Find best local data import (most array-typed exports)
|
|
5455
|
+
const localImports = parseLocalDataImports(sourceContent);
|
|
5456
|
+
let bestImportPath = null;
|
|
5457
|
+
let bestSymbols = [];
|
|
5458
|
+
let bestExports = {};
|
|
5459
|
+
for (const li of localImports) {
|
|
5460
|
+
const resolved = path.resolve(path.dirname(sourceFile), li.importPath);
|
|
5461
|
+
const exts = ["", ".ts", ".tsx", ".js", ".jsx"];
|
|
5462
|
+
let found = null;
|
|
5463
|
+
for (const ext of exts) {
|
|
5464
|
+
const p = resolved + ext;
|
|
5465
|
+
if (fs.existsSync(p)) { found = p; break; }
|
|
5466
|
+
}
|
|
5467
|
+
if (!found) continue;
|
|
5468
|
+
const exports = parseDataFileExports(found);
|
|
5469
|
+
const arrayCount = Object.values(exports).filter(k => k === "array").length;
|
|
5470
|
+
if (arrayCount > Object.values(bestExports).filter(k => k === "array").length) {
|
|
5471
|
+
bestImportPath = li.importPath;
|
|
5472
|
+
bestSymbols = li.symbols;
|
|
5473
|
+
bestExports = exports;
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
|
|
5477
|
+
// Step 3: Compute import paths relative to STORIES_DIR
|
|
5478
|
+
const storiesRelToSource = path.relative(path.dirname(sourceFile), path.join(PROJECT_ROOT, "src", "stories"));
|
|
5479
|
+
const sourceRelFromStories = path.relative(
|
|
5480
|
+
path.join(PROJECT_ROOT, "src", "stories"),
|
|
5481
|
+
sourceFile.replace(/\.(tsx?|jsx?)$/, "")
|
|
5482
|
+
).replace(/\\/g, "/");
|
|
5483
|
+
const parentImportPath = sourceRelFromStories.startsWith(".") ? sourceRelFromStories : "./" + sourceRelFromStories;
|
|
5484
|
+
|
|
5485
|
+
let dataImportFromStories = null;
|
|
5486
|
+
if (bestImportPath) {
|
|
5487
|
+
const dataAbsolute = path.resolve(path.dirname(sourceFile), bestImportPath);
|
|
5488
|
+
dataImportFromStories = path.relative(
|
|
5489
|
+
path.join(PROJECT_ROOT, "src", "stories"),
|
|
5490
|
+
dataAbsolute
|
|
5491
|
+
).replace(/\\/g, "/").replace(/\.(tsx?|jsx?)$/, "");
|
|
5492
|
+
if (!dataImportFromStories.startsWith(".")) dataImportFromStories = "./" + dataImportFromStories;
|
|
5493
|
+
}
|
|
5494
|
+
|
|
5495
|
+
// Step 4: Generate individual story files
|
|
5496
|
+
for (const name of names) {
|
|
5497
|
+
const storyFile = path.join(STORIES_DIR, `${name}.stories.tsx`);
|
|
5498
|
+
if (fs.existsSync(storyFile)) continue; // Never overwrite existing stories
|
|
5499
|
+
const props = extractComponentProps(sourceContent, name);
|
|
5500
|
+
const content = buildInternalComponentStoryContent(
|
|
5501
|
+
name, parentImportPath, dataImportFromStories, bestSymbols, bestExports, props, comp
|
|
5502
|
+
);
|
|
5503
|
+
fs.writeFileSync(storyFile, content, "utf-8");
|
|
5504
|
+
console.log(`[VDS] Wrote ${path.relative(PROJECT_ROOT, storyFile)} (sub-component of ${comp.name})`);
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5045
5508
|
main();
|
|
5046
5509
|
|