vibe-design-system 1.9.6 → 1.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "1.9.6",
3
+ "version": "1.9.8",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,22 +14,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  const PROJECT_ROOT = path.join(__dirname, "..");
15
15
  const projectRequire = createRequire(path.join(PROJECT_ROOT, "package.json"));
16
16
 
17
- function getVdsPackageVersion() {
18
- try {
19
- const fromNodeModules = path.join(PROJECT_ROOT, "node_modules", "vibe-design-system", "package.json");
20
- if (fs.existsSync(fromNodeModules)) {
21
- const pkg = JSON.parse(fs.readFileSync(fromNodeModules, "utf-8"));
22
- return pkg.version || null;
23
- }
24
- const nextToScript = path.join(__dirname, "..", "package.json");
25
- if (fs.existsSync(nextToScript)) {
26
- const pkg = JSON.parse(fs.readFileSync(nextToScript, "utf-8"));
27
- if (pkg.name === "vibe-design-system") return pkg.version || null;
28
- }
29
- } catch (_) {}
30
- return null;
31
- }
32
-
33
17
  const CLI_LOCALES = {
34
18
  en: {
35
19
  componentsNotFound: "src/components not found. VDS scan skipped.",
@@ -266,7 +250,7 @@ function getAllTsxJsxInDir(dir) {
266
250
  }
267
251
 
268
252
  const BRAND_KEYWORDS = /logo|brand|icon|favicon|emblem|mark/i;
269
- const BRAND_EXTENSIONS = /\.(svg|png|ico|jpg|jpeg|webp|gif)$/i;
253
+ const BRAND_EXTENSIONS = /\.(svg|png|ico)$/i;
270
254
 
271
255
  function getFilesByExtension(dir, extRe, baseDir = dir) {
272
256
  if (!fs.existsSync(dir)) return [];
@@ -283,7 +267,7 @@ function getFilesByExtension(dir, extRe, baseDir = dir) {
283
267
  return files;
284
268
  }
285
269
 
286
- /** Scan public/ and src/assets/ for all images; include logos/brand by keyword, rest as "asset". Preview URL is relative for dashboard /api/vds-asset. */
270
+ /** Scan public/, src/assets/, src/images/ for .svg, .png, .ico; filter by brand keywords. */
287
271
  function extractBrandAssets() {
288
272
  const assets = [];
289
273
  const dirs = [
@@ -292,12 +276,26 @@ function extractBrandAssets() {
292
276
  path.join(PROJECT_ROOT, "src", "images"),
293
277
  ];
294
278
  for (const dir of dirs) {
295
- if (!fs.existsSync(dir)) continue;
279
+ const relDir = path.relative(PROJECT_ROOT, dir).replace(/\\/g, "/");
296
280
  const files = getFilesByExtension(dir, BRAND_EXTENSIONS, PROJECT_ROOT);
297
281
  for (const filePath of files) {
298
282
  const baseName = path.basename(filePath);
299
- const type = /favicon|\.ico$/i.test(baseName) ? "favicon" : BRAND_KEYWORDS.test(baseName) ? (baseName.match(/logo|brand|emblem|mark/i) ? "logo" : "icon") : "asset";
300
- assets.push({ path: filePath, name: baseName, type, previewUrl: `/api/vds-asset?path=${encodeURIComponent(filePath)}` });
283
+ if (!BRAND_KEYWORDS.test(baseName)) continue;
284
+ const type = /favicon|\.ico$/i.test(baseName) ? "favicon" : /logo|brand|emblem|mark/i.test(baseName) ? "logo" : "icon";
285
+ assets.push({ path: filePath, name: baseName, type });
286
+ }
287
+ }
288
+ // Fallback: if no branded assets found by keyword, include all image files from src/assets
289
+ if (assets.length === 0 || assets.every((r) => r.type === "asset")) {
290
+ const assetsDir = path.join(SRC_DIR, "assets");
291
+ if (fs.existsSync(assetsDir)) {
292
+ const imgExtRe = /\.(png|jpg|jpeg|svg|gif|webp|ico)$/i;
293
+ const allImages = fs.readdirSync(assetsDir).filter((f) => imgExtRe.test(f));
294
+ for (const img of allImages) {
295
+ if (!assets.some((r) => r.path === "src/assets/" + img)) {
296
+ assets.push({ type: "asset", path: "src/assets/" + img, name: img });
297
+ }
298
+ }
301
299
  }
302
300
  }
303
301
  return assets;
@@ -353,14 +351,6 @@ function humanizeName(filePath) {
353
351
  return base.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
354
352
  }
355
353
 
356
- /** Normalize component source for storage: consistent newlines, collapse excess blank lines, trim. */
357
- function cleanRawCode(raw) {
358
- if (typeof raw !== "string") return "";
359
- let s = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
360
- s = s.replace(/\n{3,}/g, "\n\n");
361
- return s;
362
- }
363
-
364
354
  function extractTailwindTokens(content) {
365
355
  const tokens = new Set();
366
356
  const patterns = [
@@ -385,42 +375,6 @@ function extractTailwindTokens(content) {
385
375
  return [...tokens].sort();
386
376
  }
387
377
 
388
- const TAILWIND_CATEGORIES = [
389
- { tag: "Spacing", test: (c) => /^(p|m|gap|space|w|h|min-|max-|size)-|padding|margin/.test(c) || /^inset-|top-|left-|right-|bottom-/.test(c) },
390
- { tag: "Colors", test: (c) => /^(bg|text|border|ring|from|to|via|fill|stroke|decoration|divide|placeholder)-/.test(c) || /^outline-/.test(c) },
391
- { tag: "Layout", test: (c) => /^(flex|grid|block|inline|hidden|visible|overflow|float|clear|object-|aspect-)/.test(c) || /^container|box-/.test(c) },
392
- { tag: "Typography", test: (c) => /^(text|font|leading|tracking|italic|not-italic|antialiased|subpixel)/.test(c) || /^truncate|break-|whitespace-|underline|line-through/.test(c) },
393
- { tag: "Effects", test: (c) => /^(shadow|opacity|mix-blend|bg-blend)/.test(c) },
394
- { tag: "Borders", test: (c) => /^(border|rounded|ring-|outline)/.test(c) },
395
- { tag: "Transitions", test: (c) => /^(transition|duration|ease|animate|delay)/.test(c) },
396
- { tag: "Interactivity", test: (c) => /^(cursor|pointer-events|resize|scroll|snap)/.test(c) || /^hover:|focus:|active:|disabled:/.test(c) },
397
- { tag: "Responsive", test: (c) => /^(sm|md|lg|xl|2xl):/.test(c) },
398
- { tag: "Position", test: (c) => /^(relative|absolute|fixed|sticky)/.test(c) },
399
- ];
400
-
401
- function categorizeTailwindTokens(tokens) {
402
- const byCategory = {};
403
- const tags = new Set();
404
- for (const token of tokens) {
405
- let matched = false;
406
- for (const { tag, test } of TAILWIND_CATEGORIES) {
407
- if (test(token)) {
408
- tags.add(tag);
409
- if (!byCategory[tag]) byCategory[tag] = [];
410
- byCategory[tag].push(token);
411
- matched = true;
412
- break;
413
- }
414
- }
415
- if (!matched) {
416
- tags.add("Other");
417
- if (!byCategory.Other) byCategory.Other = [];
418
- byCategory.Other.push(token);
419
- }
420
- }
421
- return { tags: [...tags].sort(), byCategory };
422
- }
423
-
424
378
  /** Parse HSL string "0 0% 0%" or "hsl(0 0% 0%)" and return hex. */
425
379
  function hslToHex(hslStr) {
426
380
  const match = hslStr.match(/hsl\s*\(\s*([\d.]+)\s*[, ]\s*([\d.]+)%\s*[, ]\s*([\d.]+)%\s*\)/) ||
@@ -466,83 +420,6 @@ function parseCssVarBlock(block) {
466
420
  return out;
467
421
  }
468
422
 
469
- /** Default Tailwind theme values when project has none (no placeholders). */
470
- const DEFAULT_TAILWIND = {
471
- spacing: {
472
- 0: "0", 0.5: "0.125rem", 1: "0.25rem", 1.5: "0.375rem", 2: "0.5rem", 2.5: "0.625rem",
473
- 3: "0.75rem", 3.5: "0.875rem", 4: "1rem", 5: "1.25rem", 6: "1.5rem", 7: "1.75rem",
474
- 8: "2rem", 9: "2.25rem", 10: "2.5rem", 11: "2.75rem", 12: "3rem", 14: "3.5rem",
475
- 16: "4rem", 20: "5rem", 24: "6rem", 28: "7rem", 32: "8rem", 36: "9rem", 40: "10rem",
476
- 44: "11rem", 48: "12rem", 52: "13rem", 56: "14rem", 60: "15rem", 64: "16rem",
477
- 72: "18rem", 80: "20rem", 96: "24rem", px: "1px",
478
- },
479
- borderRadius: {
480
- none: "0", sm: "0.125rem", DEFAULT: "0.25rem", md: "0.375rem", lg: "0.5rem",
481
- xl: "0.75rem", "2xl": "1rem", "3xl": "1.5rem", full: "9999px",
482
- },
483
- boxShadow: {
484
- sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
485
- DEFAULT: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
486
- md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
487
- lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
488
- xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
489
- "2xl": "0 25px 50px -12px rgb(0 0 0 / 0.25)",
490
- },
491
- transitionDuration: { DEFAULT: "150ms", 75: "75ms", 100: "100ms", 150: "150ms", 200: "200ms", 300: "300ms", 500: "500ms", 700: "700ms", 1000: "1000ms" },
492
- transitionTimingFunction: { DEFAULT: "cubic-bezier(0.4, 0, 0.2, 1)", linear: "linear", in: "cubic-bezier(0.4, 0, 1, 1)", out: "cubic-bezier(0, 0, 0.2, 1)", "in-out": "cubic-bezier(0.4, 0, 0.2, 1)" },
493
- animation: { DEFAULT: "none" },
494
- };
495
-
496
- /** Extract a single theme block (e.g. borderRadius: { lg: "..." }) from config text. Handles nested braces. */
497
- function extractThemeBlock(content, key) {
498
- const keyRe = new RegExp(`${key}\\s*:\\s*\\{`, "g");
499
- const m = keyRe.exec(content);
500
- if (!m) return null;
501
- let start = m.index + m[0].length;
502
- let depth = 1;
503
- let i = start;
504
- while (i < content.length && depth > 0) {
505
- if (content[i] === "{") depth++;
506
- else if (content[i] === "}") depth--;
507
- i++;
508
- }
509
- const block = content.slice(start, i - 1);
510
- const out = {};
511
- const pairRe = /(\w+):\s*["']([^"']*)["']/g;
512
- let pm;
513
- while ((pm = pairRe.exec(block)) !== null) out[pm[1]] = pm[2].trim();
514
- if (Object.keys(out).length === 0) {
515
- const fallbackRe = /(\w+):\s*([^,}\n]+)/g;
516
- while ((pm = fallbackRe.exec(block)) !== null) out[pm[1]] = pm[2].trim().replace(/^["']|["']$/g, "");
517
- }
518
- return Object.keys(out).length ? out : null;
519
- }
520
-
521
- /** Parse tailwind.config.js/ts/mjs as raw text to extract theme.extend (and theme) for radius, shadows, motion. */
522
- function parseTailwindConfigRaw(content) {
523
- const out = { borderRadius: {}, boxShadow: {}, transitionDuration: {}, transitionTimingFunction: {}, animation: {}, spacing: {}, screens: {} };
524
- const normalized = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
525
- const extendMatch = normalized.match(/extend\s*:\s*\{/);
526
- const themeMatch = normalized.match(/theme\s*:\s*\{/);
527
- const searchStart = extendMatch ? normalized.indexOf("extend") : themeMatch ? normalized.indexOf("theme") : 0;
528
- const slice = searchStart ? normalized.slice(searchStart) : normalized;
529
- const br = extractThemeBlock(slice, "borderRadius");
530
- if (br) out.borderRadius = br;
531
- const bs = extractThemeBlock(slice, "boxShadow");
532
- if (bs) out.boxShadow = bs;
533
- const td = extractThemeBlock(slice, "transitionDuration");
534
- if (td) out.transitionDuration = td;
535
- const ttf = extractThemeBlock(slice, "transitionTimingFunction");
536
- if (ttf) out.transitionTimingFunction = ttf;
537
- const anim = extractThemeBlock(slice, "animation");
538
- if (anim) out.animation = anim;
539
- const sp = extractThemeBlock(slice, "spacing");
540
- if (sp) out.spacing = sp;
541
- const scr = extractThemeBlock(slice, "screens");
542
- if (scr) out.screens = scr;
543
- return out;
544
- }
545
-
546
423
  /** Resolve Tailwind theme for boxShadow, spacing, screens, zIndex, motion. Uses resolveConfig when available. */
547
424
  function getTailwindTheme() {
548
425
  const empty = { shadows: {}, spacing: {}, breakpoints: {}, zIndex: {}, transitionDuration: {}, transitionTimingFunction: {}, animation: {} };
@@ -560,14 +437,13 @@ function getTailwindTheme() {
560
437
  }
561
438
  const resolved = resolveConfig(config);
562
439
  const theme = resolved.theme || {};
563
- const extend = theme.extend || {};
564
- const boxShadow = extend.boxShadow ?? theme.boxShadow;
565
- const spacing = extend.spacing ?? theme.spacing;
566
- const screens = extend.screens ?? theme.screens;
567
- const zIndex = extend.zIndex ?? theme.zIndex;
568
- const transitionDuration = extend.transitionDuration ?? theme.transitionDuration;
569
- const transitionTimingFunction = extend.transitionTimingFunction ?? theme.transitionTimingFunction;
570
- const animation = extend.animation ?? theme.animation;
440
+ const boxShadow = theme.boxShadow;
441
+ const spacing = theme.spacing;
442
+ const screens = theme.screens;
443
+ const zIndex = theme.zIndex;
444
+ const transitionDuration = theme.transitionDuration;
445
+ const transitionTimingFunction = theme.transitionTimingFunction;
446
+ const animation = theme.animation;
571
447
  const toObj = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
572
448
  return {
573
449
  shadows: toObj(boxShadow),
@@ -589,7 +465,6 @@ function extractFoundations() {
589
465
  const typography = {};
590
466
  const cssRadiusVars = {};
591
467
  const borderRadiusScale = {};
592
- const cssByPrefix = { radius: {}, spacing: {}, border: {}, shadows: {}, motion: {} };
593
468
  const cssPath = path.join(PROJECT_ROOT, "src", "index.css");
594
469
  const globalsCss = path.join(PROJECT_ROOT, "src", "globals.css");
595
470
  const appGlobals = path.join(PROJECT_ROOT, "app", "globals.css");
@@ -603,30 +478,10 @@ function extractFoundations() {
603
478
  if (rootMatch) {
604
479
  const rootVars = parseCssVarBlock(rootMatch[1]);
605
480
  for (const [name, value] of Object.entries(rootVars)) {
606
- const norm = name.toLowerCase();
607
- if (norm.startsWith("radius") || norm === "radius") {
608
- cssByPrefix.radius[name] = value;
609
- } else if (norm.startsWith("spacing") || norm.startsWith("space")) {
610
- cssByPrefix.spacing[name] = value;
611
- } else if (norm.startsWith("border")) {
612
- cssByPrefix.border[name] = value;
613
- } else if (norm.startsWith("shadow")) {
614
- cssByPrefix.shadows[name] = value;
615
- } else if (norm.startsWith("font") || norm.startsWith("line-height") || norm.startsWith("font-weight") || norm.startsWith("letter-spacing")) {
616
- typography[name] = value;
617
- } else if (norm.startsWith("color")) {
618
- if (/\d+\s+\d+%/.test(value) || /\d+%/.test(value)) {
619
- const hsl = `hsl(${value})`;
620
- colors[name] = { value: hsl, hex: hslToHex(hsl) || hsl };
621
- } else {
622
- colors[name] = { value, hex: value };
623
- }
624
- } else if (/\d+\s+\d+%/.test(value) || /\d+%/.test(value)) {
481
+ if (/\d+\s+\d+%/.test(value) || /\d+%/.test(value)) {
625
482
  const hsl = `hsl(${value})`;
626
483
  colors[name] = { value: hsl, hex: hslToHex(hsl) || hsl };
627
- } else if (/^\d+(\.\d+)?(ms|s)$/.test(value) || /ease|linear|cubic/.test(value)) {
628
- cssByPrefix.motion[name] = value;
629
- } else if (/rem|px|em|calc/.test(value)) {
484
+ } else if (/rem|px/.test(value)) {
630
485
  cssRadiusVars[name] = value;
631
486
  } else {
632
487
  colors[name] = { value, hex: value };
@@ -646,190 +501,125 @@ function extractFoundations() {
646
501
  }
647
502
  }
648
503
 
649
- const bodyBlock = css.match(/body\s*\{([^}]*)\}/);
650
- if (bodyBlock) {
651
- const bodyProps = bodyBlock[1];
652
- const ff = bodyProps.match(/font-family:\s*([^;]+);/);
653
- if (ff) typography.bodyFontFamily = ff[1].trim();
654
- const lh = bodyProps.match(/line-height:\s*([^;]+);/);
655
- if (lh) typography.bodyLineHeight = lh[1].trim();
656
- const fw = bodyProps.match(/font-weight:\s*([^;]+);/);
657
- if (fw) typography.bodyFontWeight = fw[1].trim();
658
- const ls = bodyProps.match(/letter-spacing:\s*([^;]+);/);
659
- if (ls) typography.bodyLetterSpacing = ls[1].trim();
660
- }
661
504
  const bodyMatch = css.match(/body\s*\{[^}]*font-family:\s*([^;]+);/);
662
- if (bodyMatch) typography.body = typography.body || bodyMatch[1].trim();
505
+ if (bodyMatch) typography.body = bodyMatch[1].trim();
663
506
  const monoMatch = css.match(/code,\s*pre,\s*\.font-mono\s*\{[^}]*font-family:\s*([^;]+);/);
664
507
  if (monoMatch) typography.mono = monoMatch[1].trim();
665
508
  }
666
509
  } catch (_) {}
667
510
 
668
- const twPaths = [
669
- path.join(PROJECT_ROOT, "tailwind.config.ts"),
670
- path.join(PROJECT_ROOT, "tailwind.config.js"),
671
- path.join(PROJECT_ROOT, "tailwind.config.mjs"),
672
- ];
673
- let rawParsed = null;
674
- for (const twFile of twPaths) {
511
+ try {
512
+ const twPath = path.join(PROJECT_ROOT, "tailwind.config.ts");
513
+ const twPathJs = path.join(PROJECT_ROOT, "tailwind.config.js");
514
+ const twFile = fs.existsSync(twPath) ? twPath : twPathJs;
675
515
  if (fs.existsSync(twFile)) {
516
+ const tw = fs.readFileSync(twFile, "utf-8");
517
+ const sansMatch = tw.match(/sans:\s*\[([^\]]+)\]/);
518
+ if (sansMatch) {
519
+ typography.tailwindSans = sansMatch[1]
520
+ .split(",")
521
+ .map((s) => s.trim().replace(/^['"`]|['"`]$/g, ""))
522
+ .filter(Boolean);
523
+ }
524
+ const monoMatch2 = tw.match(/mono:\s*\[([^\]]+)\]/);
525
+ if (monoMatch2) {
526
+ typography.tailwindMono = monoMatch2[1]
527
+ .split(",")
528
+ .map((s) => s.trim().replace(/^['"`]|['"`]$/g, ""))
529
+ .filter(Boolean);
530
+ }
531
+ const brMatch = tw.match(/borderRadius:\s*\{([\s\S]*?)\}/);
532
+ if (brMatch) {
533
+ const body = brMatch[1];
534
+ const brRe = /(\w+):\s*"([^"]+)"/g;
535
+ let m2;
536
+ while ((m2 = brRe.exec(body)) !== null) borderRadiusScale[m2[1]] = m2[2];
537
+ }
538
+ }
539
+ } catch (_) {}
540
+
541
+ // Fallback: if no CSS variable colors found, extract hardcoded colors from component files
542
+ if (Object.keys(colors).length === 0) {
543
+ const hardcodedColors = new Map();
544
+ const srcFiles = getAllTsxJsxInDir(SRC_DIR);
545
+ for (const f of srcFiles) {
676
546
  try {
677
- const tw = fs.readFileSync(twFile, "utf-8");
678
- const sansMatch = tw.match(/sans:\s*\[([^\]]+)\]/);
679
- if (sansMatch) {
680
- typography.tailwindSans = sansMatch[1]
681
- .split(",")
682
- .map((s) => s.trim().replace(/^['"`]|['"`]$/g, ""))
683
- .filter(Boolean);
684
- }
685
- const monoMatch2 = tw.match(/mono:\s*\[([^\]]+)\]/);
686
- if (monoMatch2) {
687
- typography.tailwindMono = monoMatch2[1]
688
- .split(",")
689
- .map((s) => s.trim().replace(/^['"`]|['"`]$/g, ""))
690
- .filter(Boolean);
691
- }
692
- const brMatch = tw.match(/borderRadius:\s*\{([\s\S]*?)\}/);
693
- if (brMatch) {
694
- const body = brMatch[1];
695
- const brRe = /(\w+):\s*["']?([^"',}\n]+)["']?/g;
696
- let m2;
697
- while ((m2 = brRe.exec(body)) !== null) borderRadiusScale[m2[1]] = m2[2].trim();
547
+ const fullPath = path.join(SRC_DIR, f);
548
+ const content = fs.readFileSync(fullPath, "utf-8");
549
+ const matches = content.matchAll(/(bg|text|border|from|to|via|ring|fill|stroke|shadow|outline|accent|divide|decoration|placeholder)-\[#([0-9a-fA-F]{3,8})\]/g);
550
+ for (const m of matches) {
551
+ const hex = "#" + m[2].toLowerCase();
552
+ if (!hardcodedColors.has(hex)) {
553
+ hardcodedColors.set(hex, { usages: new Set(), count: 0 });
554
+ }
555
+ hardcodedColors.get(hex).usages.add(m[1]);
556
+ hardcodedColors.get(hex).count++;
698
557
  }
699
- rawParsed = parseTailwindConfigRaw(tw);
700
- break;
701
558
  } catch (_) {}
702
559
  }
560
+ const sorted = [...hardcodedColors.entries()].sort((a, b) => b[1].count - a[1].count);
561
+ for (const [hex, info] of sorted) {
562
+ const primaryUsage = [...info.usages][0];
563
+ const name = `color-${primaryUsage}-${hex.slice(1)}`;
564
+ colors[name] = { value: hex, hex };
565
+ }
703
566
  }
704
567
 
705
568
  const radius = {};
706
569
  if (cssRadiusVars.radius) radius.base = cssRadiusVars.radius;
707
- for (const [k, v] of Object.entries(cssRadiusVars)) {
708
- if (k !== "_motion" && k !== "radius" && /radius|rounded/.test(k)) radius[k] = v;
709
- }
710
- for (const [k, v] of Object.entries(cssByPrefix.radius)) radius[k] = v;
711
570
  if (Object.keys(borderRadiusScale).length > 0) radius.borderRadius = borderRadiusScale;
712
- if (rawParsed && Object.keys(rawParsed.borderRadius || {}).length > 0) {
713
- radius.borderRadius = { ...radius.borderRadius, ...rawParsed.borderRadius };
714
- }
715
-
716
571
  const foundationsColors = { ...colors };
717
572
  if (Object.keys(colorsDark).length > 0) foundationsColors._dark = colorsDark;
718
573
 
574
+ // Fallback: if no typography tokens from config, extract from Google Fonts @import
575
+ if (Object.keys(typography).length === 0 || (!typography.body && !typography.bodyFontFamily)) {
576
+ for (const cssFile of ["src/index.css", "src/App.css", "src/globals.css", "src/styles/globals.css"]) {
577
+ const cssPath = path.join(PROJECT_ROOT, cssFile);
578
+ if (!fs.existsSync(cssPath)) continue;
579
+ try {
580
+ const cssContent = fs.readFileSync(cssPath, "utf-8");
581
+ const fontImports = cssContent.matchAll(/@import\s+url\(['"]?https:\/\/fonts\.googleapis\.com\/css2\?family=([^&'"]+)/g);
582
+ const families = [];
583
+ for (const m of fontImports) {
584
+ const family = decodeURIComponent(m[1]).replace(/\+/g, " ").split(":")[0];
585
+ families.push(family);
586
+ }
587
+ if (families.length > 0) {
588
+ typography.body = families[0] + ", sans-serif";
589
+ typography.fontFamilies = families;
590
+ if (families.length > 1) typography.headingFont = families.find((f) => f !== families[0]) || families[1];
591
+ break;
592
+ }
593
+ } catch (_) {}
594
+ }
595
+ }
596
+
719
597
  const twTheme = getTailwindTheme();
720
598
  const normalizeThemeObj = (obj) => {
721
599
  if (!obj || typeof obj !== "object") return {};
722
600
  const out = {};
723
601
  for (const [k, v] of Object.entries(obj)) {
724
- if (v !== undefined && v !== null && k !== "_placeholder") out[k] = typeof v === "string" ? v : String(v);
602
+ if (v !== undefined && v !== null) out[k] = typeof v === "string" ? v : String(v);
725
603
  }
726
604
  return out;
727
605
  };
728
-
729
- let shadows = {
730
- ...normalizeThemeObj(rawParsed?.boxShadow),
731
- ...normalizeThemeObj(twTheme.shadows),
732
- ...cssByPrefix.shadows,
733
- };
734
- if (Object.keys(shadows).length === 0) shadows = DEFAULT_TAILWIND.boxShadow;
735
-
736
- const motionFromCss = cssByPrefix.motion || {};
737
- let transitionDuration = {
738
- ...normalizeThemeObj(rawParsed?.transitionDuration),
739
- ...normalizeThemeObj(twTheme.transitionDuration),
740
- ...(Object.keys(motionFromCss).length ? { _cssVars: motionFromCss } : {}),
741
- };
742
- if (Object.keys(transitionDuration).length === 0) transitionDuration = DEFAULT_TAILWIND.transitionDuration;
743
- let transitionTimingFunction = {
744
- ...normalizeThemeObj(rawParsed?.transitionTimingFunction),
745
- ...normalizeThemeObj(twTheme.transitionTimingFunction),
746
- };
747
- if (Object.keys(transitionTimingFunction).length === 0) transitionTimingFunction = DEFAULT_TAILWIND.transitionTimingFunction;
748
- let animation = {
749
- ...normalizeThemeObj(rawParsed?.animation),
750
- ...normalizeThemeObj(twTheme.animation),
751
- };
752
- if (Object.keys(animation).length === 0) animation = DEFAULT_TAILWIND.animation;
753
-
754
- let spacing = Object.keys(normalizeThemeObj(rawParsed?.spacing)).length
755
- ? { ...normalizeThemeObj(rawParsed.spacing), ...normalizeThemeObj(twTheme.spacing) }
756
- : normalizeThemeObj(twTheme.spacing);
757
- if (Object.keys(spacing).length === 0) spacing = DEFAULT_TAILWIND.spacing;
758
- spacing = { ...spacing, ...cssByPrefix.spacing };
759
-
760
- const border = Object.keys(cssByPrefix.border).length ? cssByPrefix.border : null;
761
-
762
606
  return {
763
607
  colors: foundationsColors,
764
608
  typography,
765
609
  radius,
766
- shadows,
767
- spacing,
768
- breakpoints: Object.keys(normalizeThemeObj(rawParsed?.screens)).length
769
- ? { ...normalizeThemeObj(rawParsed.screens), ...normalizeThemeObj(twTheme.breakpoints) }
770
- : normalizeThemeObj(twTheme.breakpoints),
610
+ shadows: normalizeThemeObj(twTheme.shadows),
611
+ spacing: normalizeThemeObj(twTheme.spacing),
612
+ breakpoints: normalizeThemeObj(twTheme.breakpoints),
771
613
  zIndex: normalizeThemeObj(twTheme.zIndex),
772
- ...(border ? { border } : {}),
773
614
  motion: {
774
- transitionDuration,
775
- transitionTimingFunction,
776
- animation,
615
+ transitionDuration: normalizeThemeObj(twTheme.transitionDuration),
616
+ transitionTimingFunction: normalizeThemeObj(twTheme.transitionTimingFunction),
617
+ animation: normalizeThemeObj(twTheme.animation),
777
618
  },
778
619
  };
779
620
  }
780
621
 
781
- /** Resolve a Tailwind utility class to its theme value (e.g. rounded-md → 0.375rem). */
782
- function resolveClassToValue(className, foundations) {
783
- if (!foundations || typeof className !== "string") return null;
784
- const parts = className.split("-");
785
- if (parts.length < 2) return null;
786
- const spacingKeys = ["p", "m", "px", "py", "pt", "pr", "pb", "pl", "gap", "w", "h", "min-w", "min-h", "max-w", "max-h", "top", "right", "bottom", "left", "space-x", "space-y"];
787
- const spacing = foundations.spacing || DEFAULT_TAILWIND.spacing;
788
- const radius = foundations.radius?.borderRadius || foundations.radius || DEFAULT_TAILWIND.borderRadius;
789
- const shadows = foundations.shadows || DEFAULT_TAILWIND.boxShadow;
790
- const motionDuration = foundations.motion?.transitionDuration || DEFAULT_TAILWIND.transitionDuration;
791
- const motionEasing = foundations.motion?.transitionTimingFunction || DEFAULT_TAILWIND.transitionTimingFunction;
792
-
793
- if (parts[0] === "rounded") {
794
- const key = parts.length >= 2 ? parts.slice(1).join("-") : "DEFAULT";
795
- const v = radius[key];
796
- return v != null ? v : null;
797
- }
798
- if (parts[0] === "shadow") {
799
- const key = parts.length === 2 ? "DEFAULT" : parts[1];
800
- const v = shadows[key];
801
- return v != null ? v : null;
802
- }
803
- if (spacingKeys.some((pre) => className === pre || className.startsWith(pre + "-"))) {
804
- const last = parts[parts.length - 1];
805
- const v = spacing[last];
806
- if (v != null) return v;
807
- return null;
808
- }
809
- if (parts[0] === "duration") {
810
- const key = parts[1] || "DEFAULT";
811
- return motionDuration[key] ?? null;
812
- }
813
- if (parts[0] === "ease") {
814
- const key = parts[1] || "DEFAULT";
815
- return motionEasing[key] ?? null;
816
- }
817
- return null;
818
- }
819
-
820
- function buildResolvedSpecs(tokens, foundations) {
821
- if (!Array.isArray(tokens) || !foundations) return [];
822
- const out = [];
823
- for (const t of tokens) {
824
- const value = resolveClassToValue(t, foundations);
825
- if (value != null) out.push({ class: t, value });
826
- }
827
- return out;
828
- }
829
-
830
622
  function scan() {
831
- const pkgVersion = getVdsPackageVersion();
832
- if (pkgVersion) console.log("[VDS] vibe-design-system v" + pkgVersion);
833
623
  if (!fs.existsSync(COMPONENTS_DIR)) {
834
624
  console.error(CLI_LOCALES[CLI_LOCALE].componentsNotFound);
835
625
  process.exit(1);
@@ -854,26 +644,11 @@ function scan() {
854
644
  description = "";
855
645
  }
856
646
  const tokens = extractTailwindTokens(content);
857
- const { tags: tokenTags, byCategory: tokenCategories } = categorizeTailwindTokens(tokens);
858
- results.push({
859
- file: rel,
860
- name,
861
- group,
862
- category,
863
- description,
864
- tokens,
865
- tokenTags,
866
- tokenCategories,
867
- componentRender: true,
868
- rawCode: cleanRawCode(content),
869
- });
647
+ results.push({ file: rel, name, group, category, description, tokens });
870
648
  }
871
649
  const foundations = extractFoundations();
872
650
  foundations.icons = extractLucideIconsUsed(SRC_DIR);
873
651
  foundations.brand = { assets: extractBrandAssets() };
874
- for (const r of results) {
875
- r.resolvedSpecs = buildResolvedSpecs(r.tokens, foundations);
876
- }
877
652
  const output = {
878
653
  branch: getGitBranch(),
879
654
  engineer: getGitEngineer(),
@@ -923,7 +698,7 @@ function loadVdsOutputFromBranch(branch) {
923
698
  }
924
699
 
925
700
  function componentSignature(c) {
926
- return JSON.stringify({ name: c.name, group: c.group, category: c.category, tokenTags: (c.tokenTags || []).slice().sort(), tokens: (c.tokens || []).slice().sort() });
701
+ return JSON.stringify({ name: c.name, group: c.group, category: c.category, tokens: (c.tokens || []).slice().sort() });
927
702
  }
928
703
 
929
704
  function runCompare() {
@@ -1008,8 +783,6 @@ if (isCompare) {
1008
783
  }
1009
784
  scan();
1010
785
  console.log("[VDS] Watch: src/components (.tsx, .jsx). Scan complete.");
1011
- const v = getVdsPackageVersion();
1012
- if (v) console.log("[VDS] vibe-design-system v" + v);
1013
786
  const url = vdsDashboardUrl();
1014
787
  console.log("[VDS] Dashboard: " + formatClickableLink(url) + "\n");
1015
788
  }