vibe-design-system 2.8.81 → 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.81",
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,11 +1481,33 @@ function extractFoundations() {
1429
1481
  }
1430
1482
  }
1431
1483
 
1432
- // body { font-family } supports nested @layer base { body { } } structure
1433
- const bodyMatch = css.match(/\bbody\s*\{[\s\S]*?font-family:\s*([^;]+);/) ||
1434
- css.match(/body\s*\{[^}]*font-family:\s*([^;]+);/s);
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
+
1500
+ // body { font-family } — Tailwind v4 sets font on html/:host, NOT body
1501
+ // Use [^}]* to stay within block boundaries ([\s\S]*? crosses blocks → mono font false-positive)
1502
+ const bodyMatch =
1503
+ css.match(/\bbody\s*\{[^}]*font-family:\s*([^;]+);/) ||
1504
+ css.match(/html\s*(?:,\s*:host)?\s*\{[^}]*font-family:\s*([^;]+);/); // Tailwind v4
1435
1505
  if (bodyMatch) typography.body = bodyMatch[1].trim();
1436
- const monoMatch = css.match(/code,\s*pre,\s*\.font-mono\s*\{[^}]*font-family:\s*([^;]+);/s);
1506
+ // Mono: code/kbd/samp/pre block (Tailwind v4 uses code,kbd,samp,pre not code,pre,.font-mono)
1507
+ const monoMatch =
1508
+ css.match(/code,\s*pre,\s*\.font-mono\s*\{[^}]*font-family:\s*([^;]+);/) ||
1509
+ css.match(/code,\s*kbd[^{]*\{[^}]*font-family:\s*([^;]+);/) ||
1510
+ css.match(/\bcode\b[^{,]*\{[^}]*font-family:\s*([^;]+);/);
1437
1511
  if (monoMatch) typography.mono = monoMatch[1].trim();
1438
1512
  // CSS custom properties for fonts (--font-sans, --font-mono, --font-display, etc.)
1439
1513
  const fontVarRe = /--font([\w-]*):\s*([^;\n]+);/g;
@@ -1444,14 +1518,27 @@ function extractFoundations() {
1444
1518
  if (!typography[key]) typography[key] = val;
1445
1519
  }
1446
1520
  // Resolve var(--font-*) references in body/mono to actual font names
1447
- // Key construction must match the fontVarRe loop: --font-sans "font-sans" → "fontSans"
1521
+ // Also handles Tailwind v4 --default-font-family / --default-mono-font-family chain
1448
1522
  function resolveTypoVar(val) {
1449
1523
  if (!val || !val.startsWith("var(")) return val;
1524
+ // Direct --font-* reference: var(--font-sans) → typography.fontSans
1450
1525
  const m = val.match(/var\(--font-([\w-]+)\)/);
1451
- if (!m) return val;
1452
- // prepend "-" so camelCase conversion matches: "font-" + "sans" "fontSans"
1453
- const key = `font-${m[1]}`.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
1454
- return (typography[key] && !typography[key].startsWith("var(")) ? typography[key] : val;
1526
+ if (m) {
1527
+ const key = `font-${m[1]}`.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
1528
+ return (typography[key] && !typography[key].startsWith("var(")) ? typography[key] : val;
1529
+ }
1530
+ // Tailwind v4: var(--default-font-family, ...) → resolve via fontSans
1531
+ if (val.includes("--default-font-family") && !val.includes("--default-mono")) {
1532
+ if (typography.fontSans && !typography.fontSans.startsWith("var(")) return typography.fontSans;
1533
+ // Fallback: strip the var() wrapper to get the fallback list
1534
+ return val.replace(/^var\(--default-font-family,\s*/, "").replace(/\)\s*$/, "").trim() || val;
1535
+ }
1536
+ // Tailwind v4: var(--default-mono-font-family, ...) → resolve via fontMono
1537
+ if (val.includes("--default-mono-font-family")) {
1538
+ if (typography.fontMono && !typography.fontMono.startsWith("var(")) return typography.fontMono;
1539
+ return val.replace(/^var\(--default-mono-font-family,\s*/, "").replace(/\)\s*$/, "").trim() || val;
1540
+ }
1541
+ return val;
1455
1542
  }
1456
1543
  if (typography.body) typography.body = resolveTypoVar(typography.body);
1457
1544
  if (typography.mono) typography.mono = resolveTypoVar(typography.mono);
@@ -1956,6 +2043,41 @@ function extractFoundations() {
1956
2043
  if (arbFonts.size > 0) typography.arbitraryFonts = Array.from(arbFonts);
1957
2044
  }
1958
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
+
1959
2081
  return {
1960
2082
  colors: foundationsColors,
1961
2083
  typography,
@@ -2089,6 +2211,88 @@ function extractTokenUsage() {
2089
2211
  };
2090
2212
  }
2091
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
+
2092
2296
  function extractButtonUsage() {
2093
2297
  if (!fs.existsSync(SRC_DIR)) return null;
2094
2298
  const files = getAllTsxJsxInDir(SRC_DIR);
@@ -2147,6 +2351,50 @@ function extractButtonUsage() {
2147
2351
  };
2148
2352
  }
2149
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
+
2150
2398
  /**
2151
2399
  * Scan all non-component source files and count how many times each variant value
2152
2400
  * is explicitly used for each component (e.g. <Button variant="destructive">).
@@ -2247,7 +2495,11 @@ function scan() {
2247
2495
  }
2248
2496
  const tokens = extractTailwindTokens(content);
2249
2497
  const isPageComponent = isComplexPageComponent(content);
2250
- 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);
2251
2503
  }
2252
2504
  if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
2253
2505
  const pageFiles = getAllComponentFiles(PAGES_DIR);
@@ -2271,6 +2523,8 @@ function scan() {
2271
2523
 
2272
2524
  const foundations = extractFoundations();
2273
2525
  foundations.icons = extractLucideIconsUsed(SRC_DIR);
2526
+ // Phase E — Dark mode strategy
2527
+ foundations.darkModeStrategy = detectDarkModeStrategy(PROJECT_ROOT);
2274
2528
  foundations.brand = { assets: extractBrandAssets() };
2275
2529
  const buttonUsage = extractButtonUsage();
2276
2530
  if (buttonUsage) {
@@ -2280,6 +2534,10 @@ function scan() {
2280
2534
  if (tokenUsage) {
2281
2535
  foundations.tokenUsage = tokenUsage;
2282
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;
2283
2541
  const componentSuggestions = extractComponentSuggestions();
2284
2542
  const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
2285
2543
  const output = {
@@ -1792,6 +1792,138 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
1792
1792
  }
1793
1793
  lines.push(`};`);
1794
1794
  }
1795
+
1796
+ // --- Design Tokens story (same block as generateStoryFile) ---
1797
+ {
1798
+ const compTokens = Array.isArray(comp.tokens) ? comp.tokens : [];
1799
+ const foundColors = FOUNDATIONS_DATA?.colors || {};
1800
+ if (compTokens.length >= 3) {
1801
+ const cleanTokens = compTokens.filter(t => !/:/.test(t));
1802
+ const colorRaw = cleanTokens.filter(t =>
1803
+ /^(bg|text|border|ring|from|to|fill|stroke)-/.test(t) &&
1804
+ !/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\d)/.test(t)
1805
+ );
1806
+ const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]?-|m[xylrbt]?-|gap|space-[xy]|w-|h-|min-[wh]|max-[wh]|size-)/.test(t));
1807
+ const typographyRaw = cleanTokens.filter(t => /^(text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)|font-)/.test(t));
1808
+ const radiusRaw = cleanTokens.filter(t => /^rounded/.test(t));
1809
+ const animRaw = cleanTokens.filter(t => /^(transition|duration|animate|ease|delay)-/.test(t));
1810
+ const resolvedColors = colorRaw.map(token => {
1811
+ const m = token.match(/^(?:bg|text|border|ring|from|to|fill|stroke)-(.+)$/);
1812
+ const key = m ? m[1] : null;
1813
+ const baseKey = key ? key.replace(/\/[\d.]+$/, "") : null;
1814
+ const entry = key ? (foundColors[key] || (baseKey !== key ? foundColors[baseKey] : null)) : null;
1815
+ const isValidCssColor = (v) => /^#[0-9a-fA-F]{3,8}$/.test(v) || /^(rgb|rgba|hsl|hsla|oklch|oklab|lch|lab|color)\s*\(/.test(v) || v === 'transparent';
1816
+ const hex = entry?.hex && isValidCssColor(entry.hex) ? entry.hex : null;
1817
+ return { token, hex, label: baseKey || key };
1818
+ });
1819
+ const hasContent = resolvedColors.length > 0 || spacingRaw.length > 0 || typographyRaw.length > 0 || radiusRaw.length > 0 || animRaw.length > 0;
1820
+ if (hasContent) {
1821
+ lines.push("");
1822
+ lines.push(`export const Tokens: Story = {`);
1823
+ lines.push(` name: "Design Tokens",`);
1824
+ lines.push(` parameters: { layout: "fullscreen" },`);
1825
+ lines.push(` render: () => {`);
1826
+ lines.push(` const colorTokens = ${JSON.stringify(resolvedColors)};`);
1827
+ lines.push(` const spacingTokens = ${JSON.stringify(spacingRaw)};`);
1828
+ lines.push(` const typographyTokens = ${JSON.stringify(typographyRaw)};`);
1829
+ lines.push(` const radiusTokens = ${JSON.stringify(radiusRaw)};`);
1830
+ lines.push(` const animationTokens = ${JSON.stringify(animRaw)};`);
1831
+ lines.push(` const chip = (label: string, bg: string, color: string) => (`);
1832
+ lines.push(` <span key={label} style={{ fontFamily: "monospace", fontSize: 11, background: bg, color, padding: "3px 9px", borderRadius: 5, border: \`1px solid \${bg === "#f9fafb" ? "#e5e7eb" : bg}\`, whiteSpace: "nowrap" as any }}>{label}</span>`);
1833
+ lines.push(` );`);
1834
+ lines.push(` const section = (title: string, children: any) => (`);
1835
+ lines.push(` <section style={{ marginBottom: 28 }}>`);
1836
+ lines.push(` <p style={{ margin: "0 0 10px", fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.08em" }}>{title}</p>`);
1837
+ lines.push(` {children}`);
1838
+ lines.push(` </section>`);
1839
+ lines.push(` );`);
1840
+ lines.push(` return (`);
1841
+ lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh", width: "100%" }}>`);
1842
+ lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>Design Tokens</h2>`);
1843
+ lines.push(` <p style={{ fontSize: 13, color: "#6b7280", margin: "0 0 32px" }}>Tailwind utilities used in <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>${componentName}</code> — resolved to project values.</p>`);
1844
+ lines.push(` {colorTokens.length > 0 && section("Color", (`);
1845
+ lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>`);
1846
+ lines.push(` {colorTokens.map(({ token, hex, label }) => (`);
1847
+ lines.push(` <div key={token} style={{ display: "flex", alignItems: "center", gap: 7, padding: "7px 12px", border: "1px solid #e5e7eb", borderRadius: 8, background: "#f9fafb" }}>`);
1848
+ lines.push(` {hex && <span style={{ display: "inline-block", width: 16, height: 16, borderRadius: 4, background: hex, border: "1px solid rgba(0,0,0,0.1)", flexShrink: 0 }} />}`);
1849
+ lines.push(` <code style={{ fontSize: 12, color: "#374151", fontWeight: 600 }}>{token}</code>`);
1850
+ lines.push(` {hex && <span style={{ fontSize: 11, color: "#9ca3af" }}>{hex}</span>}`);
1851
+ lines.push(` </div>`);
1852
+ lines.push(` ))}`);
1853
+ lines.push(` </div>`);
1854
+ lines.push(` ))}`);
1855
+ lines.push(` {spacingTokens.length > 0 && section("Spacing", (`);
1856
+ lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{spacingTokens.map(t => chip(t, "#faf5ff", "#6d28d9"))}</div>`);
1857
+ lines.push(` ))}`);
1858
+ lines.push(` {typographyTokens.length > 0 && section("Typography", (`);
1859
+ lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{typographyTokens.map(t => chip(t, "#fffbeb", "#92400e"))}</div>`);
1860
+ lines.push(` ))}`);
1861
+ lines.push(` {radiusTokens.length > 0 && section("Border Radius", (`);
1862
+ lines.push(` <div style={{ display: "flex", gap: 16, flexWrap: "wrap", alignItems: "flex-end" }}>`);
1863
+ lines.push(` {radiusTokens.map(t => {`);
1864
+ lines.push(` const px = t === "rounded-none" ? 0 : t === "rounded-sm" ? 2 : t === "rounded" ? 4 : t === "rounded-md" ? 6 : t === "rounded-lg" ? 8 : t === "rounded-xl" ? 12 : t === "rounded-2xl" ? 16 : t === "rounded-3xl" ? 24 : t === "rounded-full" ? 9999 : 4;`);
1865
+ lines.push(` return (`);
1866
+ lines.push(` <div key={t} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>`);
1867
+ lines.push(` <div style={{ width: 44, height: 44, background: "#6366f1", borderRadius: px }} />`);
1868
+ lines.push(` <code style={{ fontSize: 10, color: "#6b7280" }}>{t}</code>`);
1869
+ lines.push(` </div>`);
1870
+ lines.push(` );`);
1871
+ lines.push(` })}`);
1872
+ lines.push(` </div>`);
1873
+ lines.push(` ))}`);
1874
+ lines.push(` {animationTokens.length > 0 && section("Motion", (`);
1875
+ lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{animationTokens.map(t => chip(t, "#f0fdf4", "#166534"))}</div>`);
1876
+ lines.push(` ))}`);
1877
+ lines.push(` </div>`);
1878
+ lines.push(` );`);
1879
+ lines.push(` },`);
1880
+ lines.push(`};`);
1881
+ }
1882
+ }
1883
+ // 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
+ }
1925
+ }
1926
+
1795
1927
  return lines.join("\n");
1796
1928
  }
1797
1929
 
@@ -2115,42 +2247,46 @@ function buildStoryFileContent(comp) {
2115
2247
  // Component-specific stories for non-variant components (Input, Textarea, etc.)
2116
2248
  // Skip buildSpecialStories when multi-dimension detection found CVA dimensions —
2117
2249
  // generic detection produces better stories than hardcoded ones.
2250
+ let generatedSpecialStories = false;
2118
2251
  if (!hasDimensions) {
2119
2252
  const specialStories = buildSpecialStories(componentName, variants);
2120
2253
  if (specialStories) {
2121
2254
  lines.push(specialStories);
2122
- return lines.join("\n");
2255
+ generatedSpecialStories = true;
2256
+ // Do NOT return here — fall through to Tokens + Usage stories below
2123
2257
  }
2124
2258
  }
2125
2259
 
2126
- // Profile-driven render + children — single source of truth via getStoryProfile()
2127
- const useSafeWrapper = profile === "SAFE";
2128
- const RenderTarget = useSafeWrapper ? "SafeWrapper" : "ComponentRef";
2129
- const argsFallback = !useSafeWrapper && (componentName && RENDER_ARGS_FALLBACKS[componentName]) || "";
2130
- const renderLine = buildProfileRenderLine(profile, RenderTarget, argsFallback);
2131
- const childrenArgLine = buildProfileChildrenArgLine(profile);
2260
+ if (!generatedSpecialStories) {
2261
+ // Profile-driven render + children — single source of truth via getStoryProfile()
2262
+ const useSafeWrapper = profile === "SAFE";
2263
+ const RenderTarget = useSafeWrapper ? "SafeWrapper" : "ComponentRef";
2264
+ const argsFallback = !useSafeWrapper && (componentName && RENDER_ARGS_FALLBACKS[componentName]) || "";
2265
+ const renderLine = buildProfileRenderLine(profile, RenderTarget, argsFallback);
2266
+ const childrenArgLine = buildProfileChildrenArgLine(profile);
2132
2267
 
2133
- if (hasDimensions) {
2134
- const multiStories = buildMultiDimensionStories(
2135
- variantMap, renderLine, childrenArgLine, defaultArgLines, componentName,
2136
- comp.variantUsage || null
2137
- );
2138
- if (multiStories) {
2139
- lines.push(multiStories);
2140
- }
2141
- } else {
2142
- // No dimensions detected: single Default story
2143
- lines.push(`export const Default: Story = {`);
2144
- lines.push(renderLine);
2145
- const storyArgLines = [];
2146
- if (childrenArgLine(componentName)) storyArgLines.push(childrenArgLine(componentName));
2147
- for (const line of defaultArgLines) storyArgLines.push(line);
2148
- if (storyArgLines.length > 0) {
2149
- lines.push(` args: {`);
2150
- for (const line of storyArgLines) lines.push(line);
2151
- lines.push(` },`);
2268
+ if (hasDimensions) {
2269
+ const multiStories = buildMultiDimensionStories(
2270
+ variantMap, renderLine, childrenArgLine, defaultArgLines, componentName,
2271
+ comp.variantUsage || null
2272
+ );
2273
+ if (multiStories) {
2274
+ lines.push(multiStories);
2275
+ }
2276
+ } else {
2277
+ // No dimensions detected: single Default story
2278
+ lines.push(`export const Default: Story = {`);
2279
+ lines.push(renderLine);
2280
+ const storyArgLines = [];
2281
+ if (childrenArgLine(componentName)) storyArgLines.push(childrenArgLine(componentName));
2282
+ for (const line of defaultArgLines) storyArgLines.push(line);
2283
+ if (storyArgLines.length > 0) {
2284
+ lines.push(` args: {`);
2285
+ for (const line of storyArgLines) lines.push(line);
2286
+ lines.push(` },`);
2287
+ }
2288
+ lines.push(`};`);
2152
2289
  }
2153
- lines.push(`};`);
2154
2290
  }
2155
2291
 
2156
2292
  // --- Project-specific usage stories ---
@@ -2190,7 +2326,7 @@ function buildStoryFileContent(comp) {
2190
2326
  /^(bg|text|border|ring|from|to|fill|stroke)-/.test(t) &&
2191
2327
  !/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\d)/.test(t)
2192
2328
  );
2193
- const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]?|m[xylrbt]?|gap|space-[xy]|w-|h-|min-[wh]|max-[wh]|size-)/.test(t));
2329
+ const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]?-|m[xylrbt]?-|gap|space-[xy]|w-|h-|min-[wh]|max-[wh]|size-)/.test(t));
2194
2330
  const typographyRaw = cleanTokens.filter(t => /^(text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)|font-)/.test(t));
2195
2331
  const radiusRaw = cleanTokens.filter(t => /^rounded/.test(t));
2196
2332
  const animRaw = cleanTokens.filter(t => /^(transition|duration|animate|ease|delay)-/.test(t));
@@ -2271,6 +2407,48 @@ function buildStoryFileContent(comp) {
2271
2407
  lines.push(`};`);
2272
2408
  }
2273
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
+ }
2274
2452
  }
2275
2453
 
2276
2454
  // --- Usage story ---
@@ -2502,7 +2680,8 @@ function writeFoundationsStories(foundations, components) {
2502
2680
  const hex = c?.hex || c?.value || "";
2503
2681
  const u = colorUsage[name] || null;
2504
2682
  return { name, hex, cssVar: `--${name}`, description: DESCRIPTIONS[name] || "", usage: u };
2505
- });
2683
+ })
2684
+ .sort((a, b) => (b.usage?.total || 0) - (a.usage?.total || 0));
2506
2685
  return { key: group.key, label: group.label, colors };
2507
2686
  }).filter((g) => g.colors.length > 0);
2508
2687
 
@@ -2566,6 +2745,11 @@ function writeFoundationsStories(foundations, components) {
2566
2745
  " ×{usage.total}",
2567
2746
  " </span>",
2568
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
+ " )}",
2569
2753
  " </div>",
2570
2754
  " {/* Info */}",
2571
2755
  " <div style={{ padding: \"12px 14px\" }}>",
@@ -2617,6 +2801,74 @@ function writeFoundationsStories(foundations, components) {
2617
2801
  " </div>",
2618
2802
  " ),",
2619
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
+ "};",
2620
2872
  ].join("\n");
2621
2873
 
2622
2874
  fs.writeFileSync(path.join(foundationsDir, "Colors.stories.tsx"), colorsContent, "utf-8");
@@ -2708,6 +2960,9 @@ function writeFoundationsStories(foundations, components) {
2708
2960
  ? `'${typo.arbitraryFonts[0]}', sans-serif` : null;
2709
2961
  const sansFamily = typo.fontSans || typo.body || firstArbitraryFont || typo.tailwindSans || "system-ui, sans-serif";
2710
2962
 
2963
+ const fontFaces = Array.isArray(typo.fontFaces) ? typo.fontFaces : [];
2964
+ const nextFonts = Array.isArray(typo.nextFonts) ? typo.nextFonts : [];
2965
+
2711
2966
  const typoContent = [
2712
2967
  "import React from \"react\";",
2713
2968
  "import type { Meta, StoryObj } from \"@storybook/react\";",
@@ -2723,6 +2978,8 @@ function writeFoundationsStories(foundations, components) {
2723
2978
  `const weightRows: { token: string; value: string }[] = ${JSON.stringify(weightRows)};`,
2724
2979
  `const weightRowsIsDefault: boolean = ${weightRowsIsDefault};`,
2725
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 }))])};`,
2726
2983
  "",
2727
2984
  "export const Default: Story = {",
2728
2985
  " render: () => (",
@@ -2801,6 +3058,26 @@ function writeFoundationsStories(foundations, components) {
2801
3058
  " </>",
2802
3059
  " )}",
2803
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
+ "",
2804
3081
  " {/* ── FONT WEIGHTS ── */}",
2805
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>}",
2806
3083
  " {weightRows.length > 0 && (",
@@ -2819,6 +3096,21 @@ function writeFoundationsStories(foundations, components) {
2819
3096
  " </div>",
2820
3097
  " </>",
2821
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
+ " )}",
2822
3114
  " </div>",
2823
3115
  " ),",
2824
3116
  "};",