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 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
- console.warn("⚠️ Bu dizin bir monorepo kök dizini gibi görünüyor.");
677
- console.warn(" VDS'yi her uygulama klasöründe ayrı ayrı kurmanız gerekiyor.\n");
678
- console.warn(" Tespit edilen paketler:");
679
- for (const p of monorepoPackages) {
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.warn(` ${rel}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.8.82",
3
+ "version": "2.9.1",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -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
- results.push({ file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true } : {}) });
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 (VARIANT, WRAPPER, CONFIGURED, SAFE) to prevent vertical stretching
2170
- if (profile !== "SECTION") lines.push(` parameters: { layout: "centered" },`);
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
- // 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
- }
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