vibe-design-system 2.8.82 → 2.9.0

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
@@ -241,12 +241,22 @@ function detectFramework(pkg) {
241
241
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
242
242
  if (deps["next"]) return "nextjs";
243
243
  if (deps["@remix-run/react"] || deps["@remix-run/node"]) return "remix";
244
+ if (deps["react-scripts"]) return "cra"; // Phase E2 — Create React App
245
+ // Phase F — Vue/Svelte/Angular detection
246
+ if (deps["vue"] || deps["nuxt"]) return "vue";
247
+ if (deps["svelte"] || deps["@sveltejs/kit"]) return "svelte";
248
+ if (deps["@angular/core"]) return "angular";
244
249
  return "vite";
245
250
  }
246
251
 
247
252
  /** Framework'e göre Storybook framework paket adı */
248
253
  function storybookFrameworkPackage(framework) {
249
254
  if (framework === "nextjs") return "@storybook/nextjs";
255
+ if (framework === "cra") return "@storybook/react-webpack5"; // Phase E2 — CRA uses webpack5
256
+ // Phase F — Cross-framework support
257
+ if (framework === "vue") return "@storybook/vue3-vite";
258
+ if (framework === "svelte") return "@storybook/svelte-vite";
259
+ if (framework === "angular") return "@storybook/angular";
250
260
  return "@storybook/react-vite"; // vite & remix
251
261
  }
252
262
 
@@ -257,6 +267,48 @@ function buildStorybookMainTs(framework, srcPrefix) {
257
267
  ? `\n "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",`
258
268
  : "";
259
269
 
270
+ // Phase F — Vue framework
271
+ if (framework === "vue") {
272
+ return `import type { StorybookConfig } from "@storybook/vue3-vite";
273
+
274
+ const config: StorybookConfig = {
275
+ stories: ["../${srcPrefix}/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
276
+ addons: ["@storybook/addon-essentials"],
277
+ framework: { name: "@storybook/vue3-vite", options: {} },
278
+ };
279
+
280
+ export default config;
281
+ `;
282
+ }
283
+
284
+ // Phase F — Svelte framework
285
+ if (framework === "svelte") {
286
+ return `import type { StorybookConfig } from "@storybook/svelte-vite";
287
+
288
+ const config: StorybookConfig = {
289
+ stories: ["../${srcPrefix}/**/*.stories.@(js|ts|svelte)"],
290
+ addons: ["@storybook/addon-essentials"],
291
+ framework: { name: "@storybook/svelte-vite", options: {} },
292
+ };
293
+
294
+ export default config;
295
+ `;
296
+ }
297
+
298
+ // Phase F — Angular framework
299
+ if (framework === "angular") {
300
+ return `import type { StorybookConfig } from "@storybook/angular";
301
+
302
+ const config: StorybookConfig = {
303
+ stories: ["../${srcPrefix}/**/*.stories.@(js|ts)"],
304
+ addons: ["@storybook/addon-essentials"],
305
+ framework: { name: "@storybook/angular", options: {} },
306
+ };
307
+
308
+ export default config;
309
+ `;
310
+ }
311
+
260
312
  if (framework === "nextjs") {
261
313
  return `import type { StorybookConfig } from "@storybook/nextjs";
262
314
 
@@ -297,10 +349,36 @@ const config: StorybookConfig = {
297
349
  options: {},
298
350
  },
299
351
  async viteFinal(config) {
352
+ // Phase C2 — read tsconfig.json paths and inject as Vite aliases
353
+ const extraAliases = (() => {
354
+ try {
355
+ const tsConfigPaths = [
356
+ path.resolve(process.cwd(), "tsconfig.json"),
357
+ path.resolve(process.cwd(), "tsconfig.app.json"),
358
+ path.resolve(process.cwd(), "${srcPrefix}", "..", "tsconfig.json"),
359
+ ];
360
+ for (const tcp of tsConfigPaths) {
361
+ if (!require("fs").existsSync(tcp)) continue;
362
+ const raw = JSON.parse(require("fs").readFileSync(tcp, "utf-8").replace(/\/\/[^\n]*/g, "").replace(/,(\s*[}\]])/g, "$1"));
363
+ const paths = raw?.compilerOptions?.paths || {};
364
+ const aliases: Record<string, string> = {};
365
+ for (const [alias, targets] of Object.entries(paths) as [string, string[]][]) {
366
+ const cleanAlias = alias.replace(/\/\*$/, "");
367
+ const target = targets[0]?.replace(/\/\*$/, "") || "";
368
+ if (cleanAlias && target && cleanAlias !== "@") {
369
+ aliases[cleanAlias] = path.resolve(process.cwd(), target);
370
+ }
371
+ }
372
+ if (Object.keys(aliases).length > 0) return aliases;
373
+ }
374
+ } catch (_) {}
375
+ return {};
376
+ })();
300
377
  return mergeConfig(config, {
301
378
  resolve: {
302
379
  alias: {
303
380
  "@": path.resolve(process.cwd(), "${srcPrefix}"),
381
+ ...extraAliases,
304
382
  },
305
383
  },
306
384
  });
@@ -673,18 +751,42 @@ if (!pkg) {
673
751
  // Monorepo kök dizininde mi?
674
752
  const monorepoPackages = detectMonorepoPackages(projectRoot);
675
753
  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) {
754
+ // Phase E3 Monorepo guidance: show all workspaces with React deps, auto-install in first one
755
+ const reactPackages = monorepoPackages.filter(p => {
756
+ try {
757
+ const pkgPath = path.join(p, "package.json");
758
+ if (!fs.existsSync(pkgPath)) return false;
759
+ const wpkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
760
+ const deps = { ...wpkg.dependencies, ...wpkg.devDependencies };
761
+ return !!deps["react"];
762
+ } catch (_) { return false; }
763
+ });
764
+ const targetPackages = reactPackages.length > 0 ? reactPackages : monorepoPackages;
765
+
766
+ console.log("📦 Monorepo tespit edildi — React içeren workspace'ler:");
767
+ for (const p of targetPackages) {
680
768
  const rel = path.relative(projectRoot, p);
681
- console.warn(` → ${rel}`);
769
+ console.log(` → ${rel}`);
770
+ }
771
+ console.log("");
772
+ console.log("💡 Her workspace için ayrı kurulum komutları:");
773
+ for (const p of targetPackages) {
774
+ const rel = path.relative(projectRoot, p);
775
+ console.log(` cd ${rel} && npx vibe-design-system init`);
776
+ }
777
+ console.log("");
778
+ if (!process.env.VDS_COMPONENTS_DIR) {
779
+ // Auto-select first React workspace and continue installation there
780
+ const firstReact = targetPackages[0];
781
+ if (firstReact) {
782
+ const rel = path.relative(projectRoot, firstReact);
783
+ console.log(`🚀 İlk React workspace'e otomatik kurulum yapılıyor: ${rel}`);
784
+ console.log(" (VDS_COMPONENTS_DIR env değişkeni ile farklı bir workspace seçebilirsiniz)\n");
785
+ process.chdir(firstReact);
786
+ // Update projectRoot for remaining steps
787
+ Object.assign(global, { __vdsProjectRoot: firstReact });
788
+ }
682
789
  }
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
790
  }
689
791
 
690
792
  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.0",
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,
@@ -1393,6 +1393,58 @@ function extractFoundations() {
1393
1393
  // Also use first found as primary (for legacy logic below)
1394
1394
  const cssToRead = allCssCandidates.find((p) => fs.existsSync(p)) || "";
1395
1395
 
1396
+ // Phase C — JSON token file reader (W3C DTCG + Style Dictionary)
1397
+ {
1398
+ const tokenFileCandidates = [
1399
+ path.join(PROJECT_ROOT, "tokens.json"),
1400
+ path.join(PROJECT_ROOT, "design-tokens.json"),
1401
+ path.join(PROJECT_ROOT, "src", "tokens.json"),
1402
+ path.join(PROJECT_ROOT, "src", "design-tokens.json"),
1403
+ path.join(PROJECT_ROOT, "tokens", "tokens.json"),
1404
+ path.join(PROJECT_ROOT, "tokens", "design-tokens.json"),
1405
+ ];
1406
+ // Also scan tokens/ directory for any .json files
1407
+ const tokensDir = path.join(PROJECT_ROOT, "tokens");
1408
+ if (fs.existsSync(tokensDir)) {
1409
+ try {
1410
+ for (const f of fs.readdirSync(tokensDir)) {
1411
+ if (f.endsWith(".json")) tokenFileCandidates.push(path.join(tokensDir, f));
1412
+ }
1413
+ } catch (_) {}
1414
+ }
1415
+ for (const tf of tokenFileCandidates) {
1416
+ if (!fs.existsSync(tf)) continue;
1417
+ try {
1418
+ const raw = JSON.parse(fs.readFileSync(tf, "utf-8"));
1419
+ // W3C DTCG: { "color": { "primary": { "$value": "#...", "$type": "color" } } }
1420
+ // Style Dictionary: { "color-primary": { "value": "#..." } }
1421
+ const flatTokens = {};
1422
+ function flattenDTCG(obj, prefix) {
1423
+ for (const [k, v] of Object.entries(obj)) {
1424
+ if (v && typeof v === "object") {
1425
+ if ("$value" in v) {
1426
+ if (!v.$type || v.$type === "color") {
1427
+ flatTokens[(prefix ? prefix + "-" : "") + k] = v.$value;
1428
+ }
1429
+ } else if ("value" in v) {
1430
+ flatTokens[(prefix ? prefix + "-" : "") + k] = v.value;
1431
+ } else {
1432
+ flattenDTCG(v, (prefix ? prefix + "-" : "") + k);
1433
+ }
1434
+ }
1435
+ }
1436
+ }
1437
+ flattenDTCG(raw, "");
1438
+ for (const [name, value] of Object.entries(flatTokens)) {
1439
+ const cleanName = name.replace(/^color[-_]/i, "").replace(/^-/, "");
1440
+ if (!colors[cleanName] && (typeof value === "string") && (value.startsWith("#") || /^(rgb|hsl|oklch|transparent)/.test(value))) {
1441
+ colors[cleanName] = { value, hex: value };
1442
+ }
1443
+ }
1444
+ } catch (_) {}
1445
+ }
1446
+ }
1447
+
1396
1448
  try {
1397
1449
  if (cssChunks.length > 0) {
1398
1450
  const css = cssChunks.join("\n");
@@ -1429,6 +1481,22 @@ function extractFoundations() {
1429
1481
  }
1430
1482
  }
1431
1483
 
1484
+ // Phase B — @font-face declarations (variable fonts, custom webfonts)
1485
+ const fontFaceDecls = [];
1486
+ const fontFaceRe = /@font-face\s*\{([^}]+)\}/g;
1487
+ let ffm;
1488
+ while ((ffm = fontFaceRe.exec(css)) !== null) {
1489
+ const block = ffm[1];
1490
+ const fam = block.match(/font-family:\s*['"]?([^;'"]+)['"]?;/)?.[1]?.trim();
1491
+ const wgt = block.match(/font-weight:\s*([^;]+);/)?.[1]?.trim();
1492
+ const sty = block.match(/font-style:\s*([^;]+);/)?.[1]?.trim() || "normal";
1493
+ if (fam) {
1494
+ const isVariable = wgt ? /^\d+\s+\d+$/.test(wgt.trim()) : false;
1495
+ fontFaceDecls.push({ family: fam, weight: wgt || null, style: sty, isVariable });
1496
+ }
1497
+ }
1498
+ if (fontFaceDecls.length > 0) typography.fontFaces = fontFaceDecls;
1499
+
1432
1500
  // body { font-family } — Tailwind v4 sets font on html/:host, NOT body
1433
1501
  // Use [^}]* to stay within block boundaries ([\s\S]*? crosses blocks → mono font false-positive)
1434
1502
  const bodyMatch =
@@ -1975,6 +2043,41 @@ function extractFoundations() {
1975
2043
  if (arbFonts.size > 0) typography.arbitraryFonts = Array.from(arbFonts);
1976
2044
  }
1977
2045
 
2046
+ // Phase B — next/font detection
2047
+ {
2048
+ const nextFonts = [];
2049
+ const allTsxForFonts = getAllTsxJsxInDir(SRC_DIR);
2050
+ const nextFontRe = /import\s*\{\s*([\w,\s]+)\s*\}\s*from\s*['"]next\/font\/(google|local)['"]/g;
2051
+ const fontCallRe = /const\s+(\w+)\s*=\s*(\w+)\s*\(\s*\{([^}]+)\}\s*\)/g;
2052
+ for (const file of allTsxForFonts) {
2053
+ let src;
2054
+ try { src = fs.readFileSync(file, "utf-8"); } catch (_) { continue; }
2055
+ nextFontRe.lastIndex = 0;
2056
+ let nm;
2057
+ while ((nm = nextFontRe.exec(src)) !== null) {
2058
+ const source = `next/font/${nm[2]}`;
2059
+ const imported = nm[1].split(",").map(s => s.trim()).filter(Boolean);
2060
+ for (const fontName of imported) {
2061
+ fontCallRe.lastIndex = 0;
2062
+ let cm;
2063
+ while ((cm = fontCallRe.exec(src)) !== null) {
2064
+ if (cm[2] === fontName) {
2065
+ const varM = cm[3].match(/variable:\s*['"]([^'"]+)['"]/);
2066
+ const subM = cm[3].match(/subsets?:\s*\[([^\]]+)\]/);
2067
+ nextFonts.push({
2068
+ source,
2069
+ family: fontName,
2070
+ variable: varM ? varM[1] : null,
2071
+ subsets: subM ? subM[1].replace(/['"]/g, "").split(",").map(s=>s.trim()) : [],
2072
+ });
2073
+ }
2074
+ }
2075
+ }
2076
+ }
2077
+ }
2078
+ if (nextFonts.length > 0) typography.nextFonts = nextFonts;
2079
+ }
2080
+
1978
2081
  return {
1979
2082
  colors: foundationsColors,
1980
2083
  typography,
@@ -2108,6 +2211,88 @@ function extractTokenUsage() {
2108
2211
  };
2109
2212
  }
2110
2213
 
2214
+ /**
2215
+ * Phase A — Color Intelligence v2.9.0
2216
+ * Counts how many times each color token is used in src/ files.
2217
+ * Tracks bg/text/border/ring/fill/stroke/other categories and dark: usage separately.
2218
+ * O(files) complexity — single readFileSync per file, one combined regex for all color names.
2219
+ */
2220
+ function extractColorUsage(colorTokenNames) {
2221
+ if (!fs.existsSync(SRC_DIR) || !colorTokenNames || colorTokenNames.length === 0) return null;
2222
+ const allSrcFiles = getAllTsxJsxInDir(SRC_DIR).filter(f => !f.includes("stories"));
2223
+ if (allSrcFiles.length === 0) return null;
2224
+ const prefixes = ["bg","text","border","ring","fill","stroke","from","to","via","shadow","outline","decoration","placeholder","accent"];
2225
+ const escapedNames = colorTokenNames.map(n => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
2226
+ const combined = new RegExp(
2227
+ `\\b(dark:)?(${prefixes.join("|")})-(${escapedNames.join("|")})(?:\\/\\d+)?\\b`,
2228
+ "g"
2229
+ );
2230
+ const usage = {};
2231
+ const fileMap = {};
2232
+ for (const file of allSrcFiles) {
2233
+ const compName = path.basename(file, path.extname(file));
2234
+ let content;
2235
+ try { content = fs.readFileSync(path.join(SRC_DIR, file), "utf-8"); } catch (_) { continue; }
2236
+ combined.lastIndex = 0;
2237
+ let m;
2238
+ while ((m = combined.exec(content)) !== null) {
2239
+ const isDark = !!m[1];
2240
+ const prefix = m[2];
2241
+ const colorName = m[3];
2242
+ if (!usage[colorName]) {
2243
+ usage[colorName] = { bg:0, text:0, border:0, ring:0, fill:0, stroke:0, other:0, total:0, dark:0 };
2244
+ }
2245
+ if (isDark) { usage[colorName].dark++; continue; }
2246
+ const cat = prefix === "bg" ? "bg" : prefix === "text" ? "text" : prefix === "border" ? "border"
2247
+ : prefix === "ring" ? "ring" : prefix === "fill" ? "fill" : prefix === "stroke" ? "stroke" : "other";
2248
+ usage[colorName][cat]++;
2249
+ usage[colorName].total++;
2250
+ if (!fileMap[colorName]) fileMap[colorName] = new Map();
2251
+ fileMap[colorName].set(compName, (fileMap[colorName].get(compName) || 0) + 1);
2252
+ }
2253
+ }
2254
+ for (const [name, u] of Object.entries(usage)) {
2255
+ const sorted = [...(fileMap[name]?.entries() || [])].sort((a,b) => b[1]-a[1]).slice(0, 5);
2256
+ u.topFiles = sorted.map(([f]) => f);
2257
+ }
2258
+ return Object.keys(usage).length > 0 ? usage : null;
2259
+ }
2260
+
2261
+ /**
2262
+ * Phase E — Dark mode strategy detection v2.13.0
2263
+ * Detects whether the project uses class-based, media-query, or data-attribute dark mode.
2264
+ */
2265
+ function detectDarkModeStrategy(projectRoot) {
2266
+ // 1. Check tailwind.config.js/ts
2267
+ const twConfigs = ["tailwind.config.js","tailwind.config.ts","tailwind.config.mjs","tailwind.config.cjs"];
2268
+ for (const cfg of twConfigs) {
2269
+ const p = path.join(projectRoot, cfg);
2270
+ if (!fs.existsSync(p)) continue;
2271
+ try {
2272
+ const src = fs.readFileSync(p, "utf-8");
2273
+ const m = src.match(/darkMode\s*:\s*['"]?(class|media|selector)['"]?/);
2274
+ if (m) return m[1] === "selector" ? "data-attribute" : m[1];
2275
+ } catch (_) {}
2276
+ }
2277
+ // 2. Check CSS files for .dark {} or [data-theme="dark"] or @media (prefers-color-scheme)
2278
+ const cssCandidates = [
2279
+ path.join(projectRoot, "src", "index.css"),
2280
+ path.join(projectRoot, "src", "globals.css"),
2281
+ path.join(projectRoot, "src", "styles", "globals.css"),
2282
+ path.join(projectRoot, "app", "globals.css"),
2283
+ ];
2284
+ for (const cp of cssCandidates) {
2285
+ if (!fs.existsSync(cp)) continue;
2286
+ try {
2287
+ const css = fs.readFileSync(cp, "utf-8");
2288
+ if (/\[data-theme\s*=\s*['"]dark['"]\]/.test(css)) return "data-attribute";
2289
+ if (/\.dark\s*[{,]/.test(css)) return "class";
2290
+ if (/@media\s*\(prefers-color-scheme\s*:\s*dark\)/.test(css)) return "media";
2291
+ } catch (_) {}
2292
+ }
2293
+ return "unknown";
2294
+ }
2295
+
2111
2296
  function extractButtonUsage() {
2112
2297
  if (!fs.existsSync(SRC_DIR)) return null;
2113
2298
  const files = getAllTsxJsxInDir(SRC_DIR);
@@ -2166,6 +2351,50 @@ function extractButtonUsage() {
2166
2351
  };
2167
2352
  }
2168
2353
 
2354
+ /**
2355
+ * Phase D — CSS Modules Support v2.12.0
2356
+ * Reads .module.css files imported by a component and extracts design-relevant CSS properties.
2357
+ */
2358
+ function extractCssModuleTokens(compFilePath) {
2359
+ if (!compFilePath || !fs.existsSync(compFilePath)) return null;
2360
+ let src;
2361
+ try { src = fs.readFileSync(compFilePath, "utf-8"); } catch (_) { return null; }
2362
+ // Find `import styles from './Foo.module.css'` or named imports
2363
+ const moduleImportRe = /import\s+(?:[\w{},\s]+\s+from\s+)?['"]([^'"]+\.module\.css)['"]/g;
2364
+ const dir = path.dirname(compFilePath);
2365
+ const classNames = [];
2366
+ const properties = {};
2367
+ let found = false;
2368
+ let m;
2369
+ while ((m = moduleImportRe.exec(src)) !== null) {
2370
+ const cssPath = path.resolve(dir, m[1]);
2371
+ if (!fs.existsSync(cssPath)) continue;
2372
+ let css;
2373
+ try { css = fs.readFileSync(cssPath, "utf-8"); } catch (_) { continue; }
2374
+ found = true;
2375
+ // Extract class names and design-relevant properties
2376
+ const classBlockRe = /\.(\w[\w-]*)\s*\{([^}]+)\}/g;
2377
+ let cb;
2378
+ while ((cb = classBlockRe.exec(css)) !== null) {
2379
+ const className = cb[1];
2380
+ if (!classNames.includes(className)) classNames.push(className);
2381
+ const block = cb[2];
2382
+ 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"];
2383
+ for (const prop of DESIGN_PROPS) {
2384
+ const re = new RegExp(`${prop}:\\s*([^;]+);`);
2385
+ const pm = block.match(re);
2386
+ if (pm) {
2387
+ if (!properties[prop]) properties[prop] = [];
2388
+ const val = pm[1].trim();
2389
+ if (!properties[prop].includes(val)) properties[prop].push(val);
2390
+ }
2391
+ }
2392
+ }
2393
+ }
2394
+ if (!found || classNames.length === 0) return null;
2395
+ return { classNames, properties };
2396
+ }
2397
+
2169
2398
  /**
2170
2399
  * Scan all non-component source files and count how many times each variant value
2171
2400
  * is explicitly used for each component (e.g. <Button variant="destructive">).
@@ -2266,7 +2495,11 @@ function scan() {
2266
2495
  }
2267
2496
  const tokens = extractTailwindTokens(content);
2268
2497
  const isPageComponent = isComplexPageComponent(content);
2269
- results.push({ file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true } : {}) });
2498
+ const comp = { file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true } : {}) };
2499
+ // Phase D — CSS Module tokens
2500
+ const cssModuleTokens = extractCssModuleTokens(COMPONENTS_DIR ? path.join(COMPONENTS_DIR, rel) : null);
2501
+ if (cssModuleTokens) comp.cssModuleTokens = cssModuleTokens;
2502
+ results.push(comp);
2270
2503
  }
2271
2504
  if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
2272
2505
  const pageFiles = getAllComponentFiles(PAGES_DIR);
@@ -2290,6 +2523,8 @@ function scan() {
2290
2523
 
2291
2524
  const foundations = extractFoundations();
2292
2525
  foundations.icons = extractLucideIconsUsed(SRC_DIR);
2526
+ // Phase E — Dark mode strategy
2527
+ foundations.darkModeStrategy = detectDarkModeStrategy(PROJECT_ROOT);
2293
2528
  foundations.brand = { assets: extractBrandAssets() };
2294
2529
  const buttonUsage = extractButtonUsage();
2295
2530
  if (buttonUsage) {
@@ -2299,6 +2534,10 @@ function scan() {
2299
2534
  if (tokenUsage) {
2300
2535
  foundations.tokenUsage = tokenUsage;
2301
2536
  }
2537
+ // Phase A — Color Intelligence
2538
+ const colorTokenNames = Object.keys(foundations.colors || {}).filter(k => k !== "_dark");
2539
+ const colorUsage = extractColorUsage(colorTokenNames);
2540
+ if (colorUsage) foundations.colorUsage = colorUsage;
2302
2541
  const componentSuggestions = extractComponentSuggestions();
2303
2542
  const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
2304
2543
  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");
@@ -2365,6 +2407,48 @@ function buildStoryFileContent(comp) {
2365
2407
  lines.push(`};`);
2366
2408
  }
2367
2409
  }
2410
+ // Phase D2 — CSS Module Classes section
2411
+ const cssModTokens = comp.cssModuleTokens || null;
2412
+ if (cssModTokens && Array.isArray(cssModTokens.classNames) && cssModTokens.classNames.length > 0) {
2413
+ lines.push("");
2414
+ lines.push(`export const CssModuleClasses: Story = {`);
2415
+ lines.push(` name: "CSS Module Classes",`);
2416
+ lines.push(` parameters: { layout: "fullscreen" },`);
2417
+ lines.push(` render: () => {`);
2418
+ lines.push(` const classNames = ${JSON.stringify(cssModTokens.classNames)};`);
2419
+ lines.push(` const properties = ${JSON.stringify(cssModTokens.properties)};`);
2420
+ lines.push(` return (`);
2421
+ lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh" }}>`);
2422
+ lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>CSS Module Classes</h2>`);
2423
+ 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>`);
2424
+ lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 32 }}>`);
2425
+ lines.push(` {classNames.map((cls: string) => (`);
2426
+ 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>`);
2427
+ lines.push(` ))}`);
2428
+ lines.push(` {classNames.map((cls: string) => (`);
2429
+ lines.push(` <code key={cls} style={{ fontSize: 12, background: "#f9fafb", color: "#374151", padding: "4px 10px", borderRadius: 6, border: "1px solid #e5e7eb" }}>.{cls}</code>`);
2430
+ lines.push(` ))}`);
2431
+ lines.push(` </div>`);
2432
+ lines.push(` {Object.entries(properties).length > 0 && (`);
2433
+ lines.push(` <div>`);
2434
+ lines.push(` <p style={{ fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 12 }}>Extracted Properties</p>`);
2435
+ lines.push(` <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>`);
2436
+ lines.push(` {Object.entries(properties).map(([prop, values]: [string, string[]]) => (`);
2437
+ lines.push(` <div key={prop} style={{ display: "flex", gap: 8, alignItems: "flex-start", padding: "8px 12px", border: "1px solid #e5e7eb", borderRadius: 6, background: "#f9fafb" }}>`);
2438
+ lines.push(` <code style={{ fontSize: 11, color: "#6b7280", minWidth: 140, flexShrink: 0 }}>{prop}</code>`);
2439
+ lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>`);
2440
+ 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>)}`);
2441
+ lines.push(` </div>`);
2442
+ lines.push(` </div>`);
2443
+ lines.push(` ))}`);
2444
+ lines.push(` </div>`);
2445
+ lines.push(` </div>`);
2446
+ lines.push(` )}`);
2447
+ lines.push(` </div>`);
2448
+ lines.push(` );`);
2449
+ lines.push(` },`);
2450
+ lines.push(`};`);
2451
+ }
2368
2452
  }
2369
2453
 
2370
2454
  // --- Usage story ---
@@ -2596,7 +2680,8 @@ function writeFoundationsStories(foundations, components) {
2596
2680
  const hex = c?.hex || c?.value || "";
2597
2681
  const u = colorUsage[name] || null;
2598
2682
  return { name, hex, cssVar: `--${name}`, description: DESCRIPTIONS[name] || "", usage: u };
2599
- });
2683
+ })
2684
+ .sort((a, b) => (b.usage?.total || 0) - (a.usage?.total || 0));
2600
2685
  return { key: group.key, label: group.label, colors };
2601
2686
  }).filter((g) => g.colors.length > 0);
2602
2687
 
@@ -2660,6 +2745,11 @@ function writeFoundationsStories(foundations, components) {
2660
2745
  " ×{usage.total}",
2661
2746
  " </span>",
2662
2747
  " )}",
2748
+ " {usage && usage.dark > 0 && (",
2749
+ " <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)\" }}>",
2750
+ " dark ×{usage.dark}",
2751
+ " </span>",
2752
+ " )}",
2663
2753
  " </div>",
2664
2754
  " {/* Info */}",
2665
2755
  " <div style={{ padding: \"12px 14px\" }}>",
@@ -2711,6 +2801,74 @@ function writeFoundationsStories(foundations, components) {
2711
2801
  " </div>",
2712
2802
  " ),",
2713
2803
  "};",
2804
+ "",
2805
+ "export const UsedOnly: Story = {",
2806
+ " name: \"Used Colors\",",
2807
+ " render: () => {",
2808
+ ` const usedGroups = colorGroups.map(g => ({ ...g, colors: g.colors.filter(c => c.usage && c.usage.total > 0) })).filter(g => g.colors.length > 0);`,
2809
+ " return (",
2810
+ " <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, background: \"#fff\", minHeight: \"100vh\", width: \"100%\", color: \"#111\" }}>",
2811
+ " <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Used Colors</h2>",
2812
+ " <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 40px\" }}>Only colors actually used in source — sorted by usage frequency</p>",
2813
+ " {usedGroups.map(group => (",
2814
+ " <div key={group.key} style={{ marginBottom: 40 }}>",
2815
+ " <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>",
2816
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 12 }}>",
2817
+ " {group.colors.map(({ name, hex, usage }) => (",
2818
+ " <div key={name} style={{ display: \"flex\", flexDirection: \"column\", gap: 4, width: 80 }}>",
2819
+ " <div style={{ width: 80, height: 48, borderRadius: 8, background: hex, border: \"1px solid #e5e7eb\" }} />",
2820
+ " <code style={{ fontSize: 10, color: \"#374151\", wordBreak: \"break-all\" as any }}>{name}</code>",
2821
+ " <span style={{ fontSize: 10, color: \"#6b7280\" }}>×{usage?.total}</span>",
2822
+ " </div>",
2823
+ " ))}",
2824
+ " </div>",
2825
+ " </div>",
2826
+ " ))}",
2827
+ " {usedGroups.length === 0 && <p style={{ color: \"#9ca3af\", fontSize: 14 }}>No colorUsage data — run scan.mjs to generate.</p>}",
2828
+ " </div>",
2829
+ " );",
2830
+ " },",
2831
+ "};",
2832
+ "",
2833
+ "export const ColorUsageByComponent: Story = {",
2834
+ " name: \"Component → Color\",",
2835
+ " render: () => {",
2836
+ " // Build reverse map: component → colors used",
2837
+ " const compMap: Record<string, {name:string;hex:string;total:number}[]> = {};",
2838
+ " for (const group of colorGroups) {",
2839
+ " for (const { name, hex, usage } of group.colors) {",
2840
+ " if (!usage || usage.total === 0) continue;",
2841
+ " for (const comp of (usage.topFiles || [])) {",
2842
+ " if (!compMap[comp]) compMap[comp] = [];",
2843
+ " if (!compMap[comp].find(c => c.name === name)) compMap[comp].push({ name, hex, total: usage.total });",
2844
+ " }",
2845
+ " }",
2846
+ " }",
2847
+ " const compEntries = Object.entries(compMap).sort((a,b) => b[1].length - a[1].length).slice(0, 30);",
2848
+ " return (",
2849
+ " <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, background: \"#fff\", minHeight: \"100vh\", width: \"100%\", color: \"#111\" }}>",
2850
+ " <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Component → Color</h2>",
2851
+ " <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 32px\" }}>Which components use which color tokens</p>",
2852
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>",
2853
+ " {compEntries.map(([comp, colors]) => (",
2854
+ " <div key={comp} style={{ padding: \"14px 18px\", border: \"1px solid #e5e7eb\", borderRadius: 10, background: \"#f9fafb\" }}>",
2855
+ " <div style={{ fontSize: 13, fontWeight: 700, color: \"#111\", marginBottom: 10, fontFamily: \"monospace\" }}>{comp}</div>",
2856
+ " <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6 }}>",
2857
+ " {colors.map(({ name, hex }) => (",
2858
+ " <div key={name} style={{ display: \"flex\", alignItems: \"center\", gap: 5, background: \"#fff\", border: \"1px solid #e5e7eb\", borderRadius: 6, padding: \"3px 9px\" }}>",
2859
+ " <span style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 2, background: hex, border: \"1px solid rgba(0,0,0,0.1)\", flexShrink: 0 }} />",
2860
+ " <code style={{ fontSize: 11, color: \"#374151\" }}>{name}</code>",
2861
+ " </div>",
2862
+ " ))}",
2863
+ " </div>",
2864
+ " </div>",
2865
+ " ))}",
2866
+ " {compEntries.length === 0 && <p style={{ color: \"#9ca3af\", fontSize: 14 }}>No colorUsage data — run scan.mjs to generate.</p>}",
2867
+ " </div>",
2868
+ " </div>",
2869
+ " );",
2870
+ " },",
2871
+ "};",
2714
2872
  ].join("\n");
2715
2873
 
2716
2874
  fs.writeFileSync(path.join(foundationsDir, "Colors.stories.tsx"), colorsContent, "utf-8");
@@ -2802,6 +2960,9 @@ function writeFoundationsStories(foundations, components) {
2802
2960
  ? `'${typo.arbitraryFonts[0]}', sans-serif` : null;
2803
2961
  const sansFamily = typo.fontSans || typo.body || firstArbitraryFont || typo.tailwindSans || "system-ui, sans-serif";
2804
2962
 
2963
+ const fontFaces = Array.isArray(typo.fontFaces) ? typo.fontFaces : [];
2964
+ const nextFonts = Array.isArray(typo.nextFonts) ? typo.nextFonts : [];
2965
+
2805
2966
  const typoContent = [
2806
2967
  "import React from \"react\";",
2807
2968
  "import type { Meta, StoryObj } from \"@storybook/react\";",
@@ -2817,6 +2978,8 @@ function writeFoundationsStories(foundations, components) {
2817
2978
  `const weightRows: { token: string; value: string }[] = ${JSON.stringify(weightRows)};`,
2818
2979
  `const weightRowsIsDefault: boolean = ${weightRowsIsDefault};`,
2819
2980
  `const sansFamily = ${JSON.stringify(sansFamily)};`,
2981
+ `const fontFamilyRows: { token: string; value: string }[] = ${JSON.stringify(familyRows)};`,
2982
+ `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
2983
  "",
2821
2984
  "export const Default: Story = {",
2822
2985
  " render: () => (",
@@ -2895,6 +3058,26 @@ function writeFoundationsStories(foundations, components) {
2895
3058
  " </>",
2896
3059
  " )}",
2897
3060
  "",
3061
+ " {/* Phase B4 — Letter Specimens */}",
3062
+ " {fontFamilyRows.length > 0 && (",
3063
+ " <div style={{ marginBottom: 48 }}>",
3064
+ " <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>",
3065
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 32 }}>",
3066
+ " {fontFamilyRows.map(({ token, value }) => {",
3067
+ " const family = value.split(',')[0].trim().replace(/['\\\"/]/g, '');",
3068
+ " return (",
3069
+ " <div key={token} style={{ padding: 24, border: \"1px solid #e5e7eb\", borderRadius: 12, background: \"#fafafa\" }}>",
3070
+ " <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>",
3071
+ " <div style={{ fontFamily: value, fontSize: 48, fontWeight: 700, color: \"#111\", lineHeight: 1.1, marginBottom: 8 }}>Aa Bb Cc</div>",
3072
+ " <div style={{ fontFamily: value, fontSize: 18, color: \"#374151\", lineHeight: 1.6, marginBottom: 8 }}>The quick brown fox jumps over the lazy dog</div>",
3073
+ " <div style={{ fontFamily: value, fontSize: 14, color: \"#6b7280\", letterSpacing: \"0.1em\" }}>0123456789 ! @ # $ %</div>",
3074
+ " </div>",
3075
+ " );",
3076
+ " })}",
3077
+ " </div>",
3078
+ " </div>",
3079
+ " )}",
3080
+ "",
2898
3081
  " {/* ── FONT WEIGHTS ── */}",
2899
3082
  " {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
3083
  " {weightRows.length > 0 && (",
@@ -2913,6 +3096,21 @@ function writeFoundationsStories(foundations, components) {
2913
3096
  " </div>",
2914
3097
  " </>",
2915
3098
  " )}",
3099
+ " {fontSourceRows.length > 0 && (",
3100
+ " <div style={{ marginBottom: 48 }}>",
3101
+ " <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>",
3102
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>",
3103
+ " {fontSourceRows.map((f, i) => (",
3104
+ " <div key={i} style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: \"10px 16px\", border: \"1px solid #e5e7eb\", borderRadius: 8, background: \"#f9fafb\" }}>",
3105
+ " <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>",
3106
+ " <code style={{ fontSize: 12, fontWeight: 600, color: \"#111\" }}>{f.family}</code>",
3107
+ " {f.isVariable && <span style={{ fontSize: 10, padding: \"2px 7px\", borderRadius: 4, background: \"#fef9c3\", color: \"#854d0e\" }}>variable {f.weight}</span>}",
3108
+ " {f.variable && <code style={{ fontSize: 10, color: \"#9ca3af\" }}>{f.variable}</code>}",
3109
+ " </div>",
3110
+ " ))}",
3111
+ " </div>",
3112
+ " </div>",
3113
+ " )}",
2916
3114
  " </div>",
2917
3115
  " ),",
2918
3116
  "};",