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
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
481
|
-
|
|
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
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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:
|
|
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: `
|
|
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 }}>
|
|
1187
|
-
" <p style={{ color: \"#888\", marginBottom: 24 }}>
|
|
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\" }}>",
|