vibe-design-system 2.5.23 → 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.23",
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;
@@ -1183,8 +1183,8 @@ function writeComponentSuggestionsStory(componentSuggestions) {
1183
1183
  "export const Default: Story = {",
1184
1184
  " render: () => (",
1185
1185
  " <div style={{ padding: 24, fontFamily: \"system-ui, sans-serif\" }}>",
1186
- " <h2 style={{ marginBottom: 16 }}>Repeated className patterns (component candidates)</h2>",
1187
- " <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>",
1188
1188
  " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 24 }}>",
1189
1189
  " {suggestions.map((s, i) => (",
1190
1190
  " <div key={i} style={{ border: \"1px solid #333\", borderRadius: 8, padding: 16, background: \"#111\" }}>",