vibe-design-system 1.9.9 → 1.9.12

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.9",
3
+ "version": "1.9.12",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -352,6 +352,88 @@ function humanizeName(filePath) {
352
352
  return base.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
353
353
  }
354
354
 
355
+ const COMPONENT_SUGGESTION_KEYWORDS = [
356
+ { re: /\bcard\b/i, name: "Card" },
357
+ { re: /\bsection\b/i, name: "Section" },
358
+ { re: /\bitem\b/i, name: "Item" },
359
+ { re: /\bbadge\b/i, name: "Badge" },
360
+ { re: /\brow\b/i, name: "Row" },
361
+ { re: /\bgrid\b/i, name: "Grid" },
362
+ { re: /\bhero\b/i, name: "Hero" },
363
+ { re: /\bcta\b/i, name: "Cta" },
364
+ { re: /\bstat\b/i, name: "Stat" },
365
+ { re: /\bprice\b/i, name: "Pricing" },
366
+ { re: /\bfeature\b/i, name: "Feature" },
367
+ { re: /\bbutton\b/i, name: "Button" },
368
+ { re: /\btag\b/i, name: "Tag" },
369
+ { re: /\bchip\b/i, name: "Chip" },
370
+ { re: /\btile\b/i, name: "Tile" },
371
+ { re: /\bbanner\b/i, name: "Banner" },
372
+ { re: /\bmodal\b/i, name: "Modal" },
373
+ ];
374
+
375
+ function suggestNameFromPattern(pattern, index) {
376
+ const parts = [];
377
+ for (const { re, name } of COMPONENT_SUGGESTION_KEYWORDS) {
378
+ if (re.test(pattern)) parts.push(name);
379
+ }
380
+ if (parts.length > 0) return parts.join("");
381
+ return "Block" + (index + 1);
382
+ }
383
+
384
+ /** Scan src/pages/*.tsx for repeated className clusters; return component suggestions. */
385
+ function extractComponentSuggestions() {
386
+ if (!fs.existsSync(PAGES_DIR)) return [];
387
+ const pageFiles = getAllComponentFiles(PAGES_DIR);
388
+ const byPattern = new Map();
389
+ const re = /className\s*=\s*["']([^"']+)["']|className\s*=\s*\{\s*["']([^"']+)["']/g;
390
+
391
+ for (const rel of pageFiles) {
392
+ const fullPath = path.join(PAGES_DIR, rel);
393
+ const content = fs.readFileSync(fullPath, "utf-8");
394
+ const srcRel = "src/pages/" + rel.replace(/\\/g, "/");
395
+ let m;
396
+ re.lastIndex = 0;
397
+ while ((m = re.exec(content)) !== null) {
398
+ const raw = (m[1] ?? m[2] ?? "").trim().replace(/\s+/g, " ").trim();
399
+ if (!raw) continue;
400
+ const classCount = raw.split(/\s+/).filter(Boolean).length;
401
+ if (classCount < 3) continue;
402
+ if (!byPattern.has(raw)) {
403
+ const before = content.substring(0, m.index);
404
+ const lastOpen = before.lastIndexOf("<");
405
+ const tagMatch = lastOpen >= 0 ? content.slice(lastOpen).match(/<(\w+)/) : null;
406
+ const tagName = tagMatch ? tagMatch[1] : "div";
407
+ byPattern.set(raw, {
408
+ count: 0,
409
+ files: new Set(),
410
+ snippet: `<${tagName} className="${raw}">...</${tagName}>`,
411
+ });
412
+ }
413
+ const entry = byPattern.get(raw);
414
+ entry.count += 1;
415
+ entry.files.add(srcRel);
416
+ }
417
+ }
418
+
419
+ const suggestions = [];
420
+ let blockIndex = 0;
421
+ for (const [pattern, { count, files, snippet }] of byPattern.entries()) {
422
+ if (count < 2) continue;
423
+ const suggestedName = suggestNameFromPattern(pattern, blockIndex);
424
+ if (suggestedName.startsWith("Block")) blockIndex += 1;
425
+ suggestions.push({
426
+ suggestedName,
427
+ occurrences: count,
428
+ foundIn: [...files].sort(),
429
+ pattern,
430
+ snippet,
431
+ reason: `Same className cluster appears ${count} times`,
432
+ });
433
+ }
434
+ return suggestions;
435
+ }
436
+
355
437
  function extractTailwindTokens(content) {
356
438
  const tokens = new Set();
357
439
  const patterns = [
@@ -667,6 +749,7 @@ function scan() {
667
749
  const foundations = extractFoundations();
668
750
  foundations.icons = extractLucideIconsUsed(SRC_DIR);
669
751
  foundations.brand = { assets: extractBrandAssets() };
752
+ const componentSuggestions = extractComponentSuggestions();
670
753
  const output = {
671
754
  branch: getGitBranch(),
672
755
  engineer: getGitEngineer(),
@@ -674,6 +757,7 @@ function scan() {
674
757
  totalComponents: results.length,
675
758
  components: results,
676
759
  foundations,
760
+ componentSuggestions,
677
761
  };
678
762
 
679
763
  const prevComponents = readPreviousOutput();
@@ -769,6 +769,50 @@ function writeFoundationsStories(foundations) {
769
769
  console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Brand.stories.tsx")));
770
770
  }
771
771
 
772
+ function writeComponentSuggestionsStory(componentSuggestions) {
773
+ if (!Array.isArray(componentSuggestions) || componentSuggestions.length === 0) return;
774
+ const foundationsDir = path.join(STORIES_DIR, "foundations");
775
+ ensureDir(foundationsDir);
776
+ const suggestions = componentSuggestions.map((s) => ({
777
+ suggestedName: s.suggestedName,
778
+ occurrences: s.occurrences,
779
+ foundIn: s.foundIn,
780
+ pattern: s.pattern,
781
+ snippet: s.snippet,
782
+ reason: s.reason,
783
+ }));
784
+ const content =
785
+ [
786
+ "import type { Meta, StoryObj } from \"@storybook/react\";",
787
+ "",
788
+ "const meta = { title: \"Foundations/Component Suggestions\" } satisfies Meta;",
789
+ "export default meta;",
790
+ "type Story = StoryObj;",
791
+ "",
792
+ `const suggestions = ${JSON.stringify(suggestions)};`,
793
+ "",
794
+ "export const Default: Story = {",
795
+ " render: () => (",
796
+ " <div style={{ padding: 24, fontFamily: \"system-ui, sans-serif\" }}>",
797
+ " <h2 style={{ marginBottom: 16 }}>Repeated className patterns (component candidates)</h2>",
798
+ " <p style={{ color: \"#888\", marginBottom: 24 }}>Patterns with 3+ classes appearing 2+ times across src/pages. Consider extracting as components.</p>",
799
+ " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 24 }}>",
800
+ " {suggestions.map((s, i) => (",
801
+ " <div key={i} style={{ border: \"1px solid #333\", borderRadius: 8, padding: 16, background: \"#111\" }}>",
802
+ " <div style={{ fontWeight: 600, marginBottom: 8 }}>{s.suggestedName}</div>",
803
+ " <div style={{ fontSize: 12, color: \"#888\", marginBottom: 8 }}>{s.reason} · {s.foundIn.join(\", \")}</div>",
804
+ " <pre style={{ margin: 0, padding: 12, background: \"#000\", borderRadius: 4, overflow: \"auto\", fontSize: 12 }}><code>{s.snippet}</code></pre>",
805
+ " </div>",
806
+ " ))}",
807
+ " </div>",
808
+ " </div>",
809
+ " ),",
810
+ "};",
811
+ ].join("\n");
812
+ fs.writeFileSync(path.join(foundationsDir, "ComponentSuggestions.stories.tsx"), content, "utf-8");
813
+ console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "ComponentSuggestions.stories.tsx")));
814
+ }
815
+
772
816
  function main() {
773
817
  if (!fs.existsSync(VDS_OUTPUT)) {
774
818
  console.error("[VDS] vds-output.json not found. Run `npm run vds` first.");
@@ -784,6 +828,10 @@ function main() {
784
828
  ensureDir(STORIES_DIR);
785
829
  ensureDir(path.join(STORIES_DIR, "foundations"));
786
830
  writeFoundationsStories(foundations);
831
+ const componentSuggestions = data.componentSuggestions;
832
+ if (componentSuggestions?.length) {
833
+ writeComponentSuggestionsStory(componentSuggestions);
834
+ }
787
835
  try {
788
836
  const fd = path.join(STORIES_DIR, "foundations");
789
837
  if (fs.existsSync(fd)) {