vibe-design-system 2.5.22 → 2.5.24

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": "2.5.22",
3
+ "version": "2.5.24",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,6 +29,7 @@ const cliT = (key, n) => CLI_LOCALES[CLI_LOCALE][key].replace("{n}", String(n));
29
29
  const SRC_DIR = path.join(PROJECT_ROOT, "src");
30
30
  const COMPONENTS_DIR = path.join(PROJECT_ROOT, "src", "components");
31
31
  const PAGES_DIR = path.join(PROJECT_ROOT, "src", "pages");
32
+ const APP_DIR = path.join(PROJECT_ROOT, "src", "app");
32
33
 
33
34
  const IGNORE_DIRS = ["node_modules", "dist", ".next", "build", "vds-generated", ".storybook"];
34
35
  const OUTPUT_FILE = path.join(PROJECT_ROOT, "vds-output.json");
@@ -436,6 +437,47 @@ function suggestNameFromPattern(pattern) {
436
437
  return "Container";
437
438
  }
438
439
 
440
+ /** Count direct child elements in JSX inner content (depth-1 opening tags). */
441
+ function countDirectChildren(innerContent) {
442
+ if (!innerContent || typeof innerContent !== "string") return 0;
443
+ let depth = 0;
444
+ let count = 0;
445
+ const s = innerContent;
446
+ let i = 0;
447
+ while (i < s.length) {
448
+ if (s.slice(i, i + 2) === "</") {
449
+ i += 2;
450
+ depth -= 1;
451
+ while (i < s.length && /[\w-]/.test(s[i])) i += 1;
452
+ continue;
453
+ }
454
+ if (s[i] === "<" && /[\w]/.test(s[i + 1])) {
455
+ depth += 1;
456
+ if (depth === 1) count += 1;
457
+ i += 1;
458
+ while (i < s.length && /[\w-]/.test(s[i])) i += 1;
459
+ continue;
460
+ }
461
+ if (s[i] === "{" || s[i] === '"' || s[i] === "'" || s[i] === "`") {
462
+ const open = s[i];
463
+ const close = open === "{" ? "}" : open;
464
+ i += 1;
465
+ let depthBrace = open === "{" ? 1 : 0;
466
+ while (i < s.length) {
467
+ if (depthBrace === 0 && (s[i] === close || (open === "{" && s[i] === "}"))) break;
468
+ if (s[i] === open) depthBrace += 1;
469
+ else if (s[i] === close) depthBrace -= 1;
470
+ else if ((open === '"' || open === "'") && s[i] === "\\") i += 1;
471
+ i += 1;
472
+ }
473
+ if (s[i] === close) i += 1;
474
+ continue;
475
+ }
476
+ i += 1;
477
+ }
478
+ return count;
479
+ }
480
+
439
481
  /** Extract full JSX element (opening tag to matching closing tag) and inner text. */
440
482
  function extractFullElement(content, classNameMatchIndex, tagName) {
441
483
  const tag = tagName;
@@ -475,17 +517,51 @@ function extractFullElement(content, classNameMatchIndex, tagName) {
475
517
  return null;
476
518
  }
477
519
 
478
- /** Scan src/pages/*.tsx for repeated className clusters; return component suggestions. */
520
+ /** Return set of component names already in src/components (so we don't suggest them again). */
521
+ function getExistingComponentNames() {
522
+ const names = new Set();
523
+ if (!fs.existsSync(COMPONENTS_DIR)) return names;
524
+ const walk = (dir) => {
525
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
526
+ for (const e of entries) {
527
+ const full = path.join(dir, e.name);
528
+ if (e.isDirectory() && !IGNORE_DIRS.includes(e.name)) walk(full);
529
+ else if (e.isFile() && /\.(tsx|jsx)$/i.test(e.name)) {
530
+ const base = e.name.replace(/\.(tsx|jsx)$/i, "");
531
+ if (/^[A-Z]/.test(base)) names.add(base);
532
+ }
533
+ }
534
+ };
535
+ walk(COMPONENTS_DIR);
536
+ return names;
537
+ }
538
+
539
+ /** Scan src/pages and src/app for repeated visual-section patterns (className + 2+ children); return component suggestions. */
479
540
  function extractComponentSuggestions() {
480
- if (!fs.existsSync(PAGES_DIR)) return [];
481
- const pageFiles = getAllComponentFiles(PAGES_DIR);
541
+ const pageDirs = [
542
+ { dir: PAGES_DIR, prefix: "src/pages/" },
543
+ { dir: APP_DIR, prefix: "src/app/" },
544
+ ];
545
+ const allPageFiles = [];
546
+ for (const { dir, prefix } of pageDirs) {
547
+ if (!fs.existsSync(dir)) continue;
548
+ const files = getAllComponentFiles(dir);
549
+ for (const rel of files) {
550
+ allPageFiles.push({ fullPath: path.join(dir, rel), srcRel: prefix + rel.replace(/\\/g, "/") });
551
+ }
552
+ }
553
+ if (allPageFiles.length === 0) return [];
554
+
482
555
  const byPattern = new Map();
483
556
  const re = /className\s*=\s*["']([^"']+)["']|className\s*=\s*\{\s*["']([^"']+)["']/g;
484
557
 
485
- for (const rel of pageFiles) {
486
- const fullPath = path.join(PAGES_DIR, rel);
487
- const content = fs.readFileSync(fullPath, "utf-8");
488
- const srcRel = "src/pages/" + rel.replace(/\\/g, "/");
558
+ for (const { fullPath, srcRel } of allPageFiles) {
559
+ let content;
560
+ try {
561
+ content = fs.readFileSync(fullPath, "utf-8");
562
+ } catch (_) {
563
+ continue;
564
+ }
489
565
  let m;
490
566
  re.lastIndex = 0;
491
567
  while ((m = re.exec(content)) !== null) {
@@ -500,11 +576,14 @@ function extractComponentSuggestions() {
500
576
  const tagName = tagMatch ? tagMatch[1] : "div";
501
577
  let fullJsx = null;
502
578
  let innerText = "";
579
+ let innerContent = "";
503
580
  const extracted = extractFullElement(content, m.index, tagName);
504
581
  if (extracted) {
505
582
  fullJsx = extracted.fullJsx;
506
583
  innerText = extracted.innerText || "";
584
+ innerContent = extracted.innerContent || "";
507
585
  }
586
+ const directChildren = countDirectChildren(innerContent);
508
587
  byPattern.set(raw, {
509
588
  count: 0,
510
589
  files: new Set(),
@@ -512,6 +591,7 @@ function extractComponentSuggestions() {
512
591
  snippet: `<${tagName} className="${raw}">...</${tagName}>`,
513
592
  fullJsx,
514
593
  innerText,
594
+ directChildren,
515
595
  });
516
596
  }
517
597
  const entry = byPattern.get(raw);
@@ -520,19 +600,23 @@ function extractComponentSuggestions() {
520
600
  }
521
601
  }
522
602
 
603
+ const existingNames = getExistingComponentNames();
523
604
  const suggestions = [];
524
- for (const [pattern, { count, files, snippet, tagName, fullJsx, innerText }] of byPattern.entries()) {
605
+ for (const [pattern, { count, files, snippet, tagName, fullJsx, innerText, directChildren }] of byPattern.entries()) {
525
606
  if (count < 2) continue;
526
607
  const suggestedName = suggestNameFromContent(innerText) || suggestNameFromPattern(pattern);
608
+ const name = sanitizeComponentName(suggestedName.replace(/\s+/g, ""));
609
+ if (existingNames.has(name)) continue;
610
+ if (directChildren < 2) continue;
527
611
  suggestions.push({
528
- suggestedName: sanitizeComponentName(suggestedName.replace(/\s+/g, "")),
612
+ suggestedName: name,
529
613
  tagName: tagName || "div",
530
614
  pattern,
531
615
  fullJsx,
532
616
  occurrences: count,
533
617
  foundIn: [...files].sort(),
534
618
  snippet,
535
- reason: `Same className cluster appears ${count} times`,
619
+ reason: `Visual section candidate: same className cluster ${count}×, ${directChildren} child elements`,
536
620
  });
537
621
  }
538
622
  return suggestions;
@@ -478,22 +478,36 @@ const REACTNODE_PLACEHOLDER_TEXT = {
478
478
  children: "Example content",
479
479
  };
480
480
 
481
- /** Recursive list of .tsx file paths under dir (relative to dir). */
482
- function getAllTsxUnderDir(dir) {
481
+ /** Recursive list of .tsx/.jsx file paths under dir (relative to dir). Index.tsx / index.tsx first for deterministic "first usage". */
482
+ function getAllTsxJsxUnderDir(dir) {
483
483
  if (!fs.existsSync(dir)) return [];
484
484
  const out = [];
485
485
  const entries = fs.readdirSync(dir, { withFileTypes: true });
486
486
  for (const e of entries) {
487
487
  const full = path.join(dir, e.name);
488
488
  if (e.isDirectory()) {
489
- out.push(...getAllTsxUnderDir(full).map((r) => path.join(e.name, r)));
490
- } else if (e.isFile() && e.name.endsWith(".tsx")) {
489
+ out.push(...getAllTsxJsxUnderDir(full).map((r) => path.join(e.name, r)));
490
+ } else if (e.isFile() && (e.name.endsWith(".tsx") || e.name.endsWith(".jsx"))) {
491
491
  out.push(e.name);
492
492
  }
493
493
  }
494
+ out.sort((a, b) => {
495
+ const baseA = path.basename(a, path.extname(a));
496
+ const baseB = path.basename(b, path.extname(b));
497
+ const isIndexA = baseA === "Index" || baseA === "index";
498
+ const isIndexB = baseB === "Index" || baseB === "index";
499
+ if (isIndexA && !isIndexB) return -1;
500
+ if (!isIndexA && isIndexB) return 1;
501
+ return a.localeCompare(b);
502
+ });
494
503
  return out;
495
504
  }
496
505
 
506
+ /** @deprecated Use getAllTsxJsxUnderDir. Kept for compatibility. */
507
+ function getAllTsxUnderDir(dir) {
508
+ return getAllTsxJsxUnderDir(dir);
509
+ }
510
+
497
511
  /** Extract prop="value" and prop='value' and prop={"value"} from JSX tag content. Returns { propName: "value", ... }. */
498
512
  function extractPropsFromJsxTagContent(tagContent) {
499
513
  const props = {};
@@ -546,7 +560,7 @@ function buildMockArrayItems(componentSource, itemTypeName, propName) {
546
560
  return items;
547
561
  }
548
562
 
549
- /** Find first real usage of component in src/pages or src/app; return { propName: "literalValue", ... } or null. */
563
+ /** Find first real usage of component in src/pages or src/app (Index.tsx preferred); return { propName: "literalValue", ... } or null. */
550
564
  function findComponentUsageInPages(componentName, projectRoot) {
551
565
  const srcDir = path.join(projectRoot, "src");
552
566
  const pagesDir = path.join(srcDir, "pages");
@@ -557,7 +571,7 @@ function findComponentUsageInPages(componentName, projectRoot) {
557
571
  const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
558
572
  const tagOpenRe = new RegExp(`<${escapedName}\\s+([\\s\\S]*?)\\s*\\/?>`, "m");
559
573
  for (const dir of dirs) {
560
- const files = getAllTsxUnderDir(dir);
574
+ const files = getAllTsxJsxUnderDir(dir);
561
575
  for (const rel of files) {
562
576
  const fullPath = path.join(dir, rel);
563
577
  try {
@@ -596,7 +610,9 @@ function buildDefaultArgsForRequiredProps(props, usageFromPages = null, componen
596
610
  const name = p.name;
597
611
  const required = p.required === true;
598
612
  if (/ReactNode|React\.ReactNode/.test(type)) {
599
- const text = REACTNODE_PLACEHOLDER_TEXT[name] || componentPlaceholders[name] || "Content";
613
+ const text = fromPages[name] != null && typeof fromPages[name] === "string"
614
+ ? fromPages[name]
615
+ : (REACTNODE_PLACEHOLDER_TEXT[name] || componentPlaceholders[name] || "Content");
600
616
  argLines.push(` ${name}: ${JSON.stringify(text)},`);
601
617
  added.add(name);
602
618
  continue;
@@ -1167,8 +1183,8 @@ function writeComponentSuggestionsStory(componentSuggestions) {
1167
1183
  "export const Default: Story = {",
1168
1184
  " render: () => (",
1169
1185
  " <div style={{ padding: 24, fontFamily: \"system-ui, sans-serif\" }}>",
1170
- " <h2 style={{ marginBottom: 16 }}>Repeated className patterns (component candidates)</h2>",
1171
- " <p style={{ color: \"#888\", marginBottom: 24 }}>Patterns with 3+ classes appearing 2+ times across src/pages. Consider extracting as components.</p>",
1186
+ " <h2 style={{ marginBottom: 16 }}>Visual section candidates (component suggestions)</h2>",
1187
+ " <p style={{ color: \"#888\", marginBottom: 24 }}>From src/pages and src/app: own className, 2+ child elements, pattern repeated 2+ times. Not yet in src/components. Consider extracting as components.</p>",
1172
1188
  " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 24 }}>",
1173
1189
  " {suggestions.map((s, i) => (",
1174
1190
  " <div key={i} style={{ border: \"1px solid #333\", borderRadius: 8, padding: 16, background: \"#111\" }}>",