vibe-design-system 1.9.5 → 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 +1 -1
- package/vds-core-template/scan.mjs +111 -338
- package/vds-core-template/story-generator.mjs +42 -9
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
|
564
|
-
const
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
669
|
-
path.join(PROJECT_ROOT, "tailwind.config.ts")
|
|
670
|
-
path.join(PROJECT_ROOT, "tailwind.config.js")
|
|
671
|
-
|
|
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
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
.
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
}
|
|
@@ -42,6 +42,12 @@ const SKIP_LIST = [
|
|
|
42
42
|
"InputOtp",
|
|
43
43
|
"Resizable",
|
|
44
44
|
"Sonner",
|
|
45
|
+
"ImageWithFallback",
|
|
46
|
+
"ConnectionLines",
|
|
47
|
+
"ScrollToTop",
|
|
48
|
+
"Form",
|
|
49
|
+
"Toaster",
|
|
50
|
+
"Toast",
|
|
45
51
|
];
|
|
46
52
|
|
|
47
53
|
/** shadcn/ui composite component recipes: component name → imports + render. */
|
|
@@ -215,35 +221,32 @@ const RECIPES = {
|
|
|
215
221
|
)`,
|
|
216
222
|
},
|
|
217
223
|
Checkbox: {
|
|
218
|
-
extraImports: [{ from: "@/components/ui/label", names: ["Label"] }],
|
|
219
224
|
render: `(args) => (
|
|
220
225
|
<div className="flex items-center space-x-2">
|
|
221
226
|
<ComponentRef id="terms" {...args} />
|
|
222
|
-
<
|
|
227
|
+
<label htmlFor="terms" className="text-sm font-medium">Accept terms</label>
|
|
223
228
|
</div>
|
|
224
229
|
)`,
|
|
225
230
|
},
|
|
226
231
|
Switch: {
|
|
227
|
-
extraImports: [{ from: "@/components/ui/label", names: ["Label"] }],
|
|
228
232
|
render: `(args) => (
|
|
229
233
|
<div className="flex items-center space-x-2">
|
|
230
234
|
<ComponentRef id="switch" {...args} />
|
|
231
|
-
<
|
|
235
|
+
<label htmlFor="switch" className="text-sm font-medium">Enable notifications</label>
|
|
232
236
|
</div>
|
|
233
237
|
)`,
|
|
234
238
|
},
|
|
235
239
|
RadioGroup: {
|
|
236
240
|
imports: ["RadioGroupItem"],
|
|
237
|
-
extraImports: [{ from: "@/components/ui/label", names: ["Label"] }],
|
|
238
241
|
render: `(args) => (
|
|
239
242
|
<ComponentRef className="flex flex-col space-y-2" {...args}>
|
|
240
243
|
<div className="flex items-center space-x-2">
|
|
241
244
|
<RadioGroupItem value="comfortable" id="comfortable" />
|
|
242
|
-
<
|
|
245
|
+
<label htmlFor="comfortable" className="text-sm font-medium">Comfortable</label>
|
|
243
246
|
</div>
|
|
244
247
|
<div className="flex items-center space-x-2">
|
|
245
248
|
<RadioGroupItem value="compact" id="compact" />
|
|
246
|
-
<
|
|
249
|
+
<label htmlFor="compact" className="text-sm font-medium">Compact</label>
|
|
247
250
|
</div>
|
|
248
251
|
</ComponentRef>
|
|
249
252
|
)`,
|
|
@@ -276,12 +279,42 @@ const RECIPES = {
|
|
|
276
279
|
</ComponentRef>
|
|
277
280
|
)`,
|
|
278
281
|
},
|
|
282
|
+
Command: {
|
|
283
|
+
imports: ["CommandInput", "CommandList", "CommandEmpty", "CommandGroup", "CommandItem"],
|
|
284
|
+
render: `(args) => (
|
|
285
|
+
<ComponentRef className="rounded-lg border shadow-md" {...args}>
|
|
286
|
+
<CommandInput placeholder="Type a command..." />
|
|
287
|
+
<CommandList>
|
|
288
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
289
|
+
<CommandGroup heading="Suggestions">
|
|
290
|
+
<CommandItem>Calendar</CommandItem>
|
|
291
|
+
<CommandItem>Search</CommandItem>
|
|
292
|
+
<CommandItem>Settings</CommandItem>
|
|
293
|
+
</CommandGroup>
|
|
294
|
+
</CommandList>
|
|
295
|
+
</ComponentRef>
|
|
296
|
+
)`,
|
|
297
|
+
},
|
|
298
|
+
Carousel: {
|
|
299
|
+
imports: ["CarouselContent", "CarouselItem", "CarouselPrevious", "CarouselNext"],
|
|
300
|
+
render: `(args) => (
|
|
301
|
+
<ComponentRef className="w-full max-w-xs" {...args}>
|
|
302
|
+
<CarouselContent>
|
|
303
|
+
<CarouselItem><div className="p-4 text-center border rounded">Slide 1</div></CarouselItem>
|
|
304
|
+
<CarouselItem><div className="p-4 text-center border rounded">Slide 2</div></CarouselItem>
|
|
305
|
+
<CarouselItem><div className="p-4 text-center border rounded">Slide 3</div></CarouselItem>
|
|
306
|
+
</CarouselContent>
|
|
307
|
+
<CarouselPrevious />
|
|
308
|
+
<CarouselNext />
|
|
309
|
+
</ComponentRef>
|
|
310
|
+
)`,
|
|
311
|
+
},
|
|
279
312
|
Collapsible: {
|
|
280
313
|
imports: ["CollapsibleTrigger", "CollapsibleContent"],
|
|
281
314
|
render: `(args) => (
|
|
282
315
|
<ComponentRef {...args}>
|
|
283
|
-
<CollapsibleTrigger asChild><button>Toggle</button></CollapsibleTrigger>
|
|
284
|
-
<CollapsibleContent>
|
|
316
|
+
<CollapsibleTrigger asChild><button className="text-sm font-medium">Toggle content</button></CollapsibleTrigger>
|
|
317
|
+
<CollapsibleContent className="mt-2 p-2 border rounded">Hidden content revealed.</CollapsibleContent>
|
|
285
318
|
</ComponentRef>
|
|
286
319
|
)`,
|
|
287
320
|
},
|