vibe-design-system 2.5.18 → 2.5.21

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.18",
3
+ "version": "2.5.21",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -538,6 +538,19 @@ function extractComponentSuggestions() {
538
538
  return suggestions;
539
539
  }
540
540
 
541
+ /** src/pages/ içinde tanımlı ama src/components'a çıkarılmamış visual section'ları listele (component adayı raporu). */
542
+ function extractUnreleasedSectionCandidates() {
543
+ if (!fs.existsSync(PAGES_DIR)) return [];
544
+ const suggestions = extractComponentSuggestions();
545
+ return suggestions.map((s) => ({
546
+ suggestedName: s.suggestedName,
547
+ file: Array.isArray(s.foundIn) && s.foundIn[0] ? s.foundIn[0] : null,
548
+ files: s.foundIn || [],
549
+ occurrences: s.occurrences || 0,
550
+ snippet: s.snippet,
551
+ }));
552
+ }
553
+
541
554
  const VDS_GENERATED_DIR = path.join(COMPONENTS_DIR, "vds-generated");
542
555
 
543
556
  /** Ensure component name is valid JS: no leading digits/special chars, PascalCase. */
@@ -1145,6 +1158,7 @@ function scan() {
1145
1158
  foundations.icons = extractLucideIconsUsed(SRC_DIR);
1146
1159
  foundations.brand = { assets: extractBrandAssets() };
1147
1160
  const componentSuggestions = extractComponentSuggestions();
1161
+ const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
1148
1162
  const output = {
1149
1163
  branch: getGitBranch(),
1150
1164
  engineer: getGitEngineer(),
@@ -1153,6 +1167,7 @@ function scan() {
1153
1167
  components: results,
1154
1168
  foundations,
1155
1169
  componentSuggestions,
1170
+ unreleasedSectionCandidates,
1156
1171
  };
1157
1172
 
1158
1173
  const prevOutput = readPreviousOutputFull();
@@ -445,7 +445,7 @@ function parsePropsFromSource(source) {
445
445
  /** Default icon for LucideIcon prop when name is generic (icon, leftIcon, etc.). */
446
446
  const LUCIDE_ICON_DEFAULT = "Star";
447
447
 
448
- /** Yaygın required string prop isimleri için anlamlı varsayılan metin (component'in boş/Example yerine görünür render etmesi için). */
448
+ /** Yaygın required string prop isimleri için fallback (sayfada kullanım bulunamazsa). */
449
449
  const DEFAULT_STRINGS_BY_PROP_NAME = {
450
450
  eyebrow: "Label",
451
451
  title: "Section title",
@@ -456,32 +456,186 @@ const DEFAULT_STRINGS_BY_PROP_NAME = {
456
456
  subtitle: "Subtitle text",
457
457
  text: "Sample text",
458
458
  label: "Label",
459
+ status: "Active",
460
+ sector: "Technology",
461
+ step: "01",
462
+ role: "Team Member",
459
463
  };
460
464
 
461
- /** Build default args lines and lucide-react imports for required props (LucideIcon → icon component, X[] → example item). */
462
- function buildDefaultArgsForRequiredProps(props) {
465
+ /** Component adına göre contextual placeholder (kullanım yoksa). */
466
+ const COMPONENT_PLACEHOLDERS = {
467
+ ProcessStep: { step: "01", title: "Discovery Phase", description: "We analyze your current setup and requirements." },
468
+ StatBlock: { title: "Metric", value: "100%", description: "Key performance indicator." },
469
+ BenefitCard: { title: "Benefit", description: "Why this matters for you.", value: "Value proposition." },
470
+ TeamCard: { name: "Jane Doe", role: "Team Member", quote: "Quote text here." },
471
+ ManifestoSection: { title: "Our mission", description: "What we believe in." },
472
+ };
473
+
474
+ /** ReactNode prop adına göre kısa placeholder metin (createElement içinde). */
475
+ const REACTNODE_PLACEHOLDER_TEXT = {
476
+ quote: "Quote text here.",
477
+ title: "Title text",
478
+ children: "Example content",
479
+ };
480
+
481
+ /** Recursive list of .tsx file paths under dir (relative to dir). */
482
+ function getAllTsxUnderDir(dir) {
483
+ if (!fs.existsSync(dir)) return [];
484
+ const out = [];
485
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
486
+ for (const e of entries) {
487
+ const full = path.join(dir, e.name);
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")) {
491
+ out.push(e.name);
492
+ }
493
+ }
494
+ return out;
495
+ }
496
+
497
+ /** Extract prop="value" and prop='value' and prop={"value"} from JSX tag content. Returns { propName: "value", ... }. */
498
+ function extractPropsFromJsxTagContent(tagContent) {
499
+ const props = {};
500
+ let m;
501
+ const doubleQuoted = /\b(\w+)=["]([^"]*)["]/g;
502
+ while ((m = doubleQuoted.exec(tagContent)) !== null) props[m[1]] = m[2];
503
+ const singleQuoted = /\b(\w+)=[']([^']*)[']/g;
504
+ while ((m = singleQuoted.exec(tagContent)) !== null) if (props[m[1]] === undefined) props[m[1]] = m[2];
505
+ const jsxDouble = /\b(\w+)=\{"([^"]*)"\}/g;
506
+ while ((m = jsxDouble.exec(tagContent)) !== null) if (props[m[1]] === undefined) props[m[1]] = m[2];
507
+ const jsxSingle = /\b(\w+)=\{'([^']*)'\}/g;
508
+ while ((m = jsxSingle.exec(tagContent)) !== null) if (props[m[1]] === undefined) props[m[1]] = m[2];
509
+ return props;
510
+ }
511
+
512
+ /** Parse source for interface ItemTypeName { field: type; ... } and return list of field names. */
513
+ function getItemTypeFieldsFromSource(source, itemTypeName) {
514
+ if (!source || !itemTypeName) return [];
515
+ const escaped = itemTypeName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
516
+ let body = null;
517
+ const typeRe = new RegExp(`(?:type|interface)\\s+${escaped}\\s*=\\s*\\{([^}]+)\\}`, "m");
518
+ const ifaceRe = new RegExp(`interface\\s+${escaped}\\s*\\{([^}]+)\\}`, "m");
519
+ const m1 = source.match(typeRe);
520
+ const m2 = source.match(ifaceRe);
521
+ if (m1) body = m1[1];
522
+ else if (m2) body = m2[1];
523
+ if (!body) return [];
524
+ const fieldRe = /(\w+)\s*[:?]/g;
525
+ const fields = [];
526
+ let m;
527
+ while ((m = fieldRe.exec(body)) !== null) fields.push(m[1]);
528
+ return fields;
529
+ }
530
+
531
+ /** Build 2-3 mock items for array prop from component source (item type fields). */
532
+ function buildMockArrayItems(componentSource, itemTypeName, propName) {
533
+ const fields = getItemTypeFieldsFromSource(componentSource, itemTypeName);
534
+ const defaultFields = ["label", "value", "title", "id", "name", "description"];
535
+ const useFields = fields.length ? fields : defaultFields;
536
+ const items = [];
537
+ for (let i = 1; i <= 3; i++) {
538
+ const item = {};
539
+ for (const f of useFields) {
540
+ if (f === "id") item[f] = String(i);
541
+ else if (f === "value") item[f] = String(100 - i * 25);
542
+ else item[f] = `Example ${i}`;
543
+ }
544
+ items.push(item);
545
+ }
546
+ return items;
547
+ }
548
+
549
+ /** Find first real usage of component in src/pages or src/app; return { propName: "literalValue", ... } or null. */
550
+ function findComponentUsageInPages(componentName, projectRoot) {
551
+ const srcDir = path.join(projectRoot, "src");
552
+ const pagesDir = path.join(srcDir, "pages");
553
+ const appDir = path.join(srcDir, "app");
554
+ const dirs = [];
555
+ if (fs.existsSync(pagesDir)) dirs.push(pagesDir);
556
+ if (fs.existsSync(appDir)) dirs.push(appDir);
557
+ const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
558
+ const tagOpenRe = new RegExp(`<${escapedName}\\s+([\\s\\S]*?)\\s*\\/?>`, "m");
559
+ for (const dir of dirs) {
560
+ const files = getAllTsxUnderDir(dir);
561
+ for (const rel of files) {
562
+ const fullPath = path.join(dir, rel);
563
+ try {
564
+ const content = fs.readFileSync(fullPath, "utf-8");
565
+ const match = content.match(tagOpenRe);
566
+ if (match) {
567
+ const props = extractPropsFromJsxTagContent(match[1]);
568
+ if (Object.keys(props).length > 0) return props;
569
+ }
570
+ } catch (_) {}
571
+ }
572
+ }
573
+ return null;
574
+ }
575
+
576
+ /** Build default args lines, lucide imports, argTypes for icon (no raw dump), and needReact for ReactNode. */
577
+ function buildDefaultArgsForRequiredProps(props, usageFromPages = null, componentName = "", componentSource = "") {
463
578
  const argLines = [];
464
579
  const lucideImports = new Set();
465
- if (!Array.isArray(props) || props.length === 0) return { argLines, lucideImports: [] };
466
- for (const p of props) {
467
- if (p.required !== true) continue;
468
- const type = String(p.type || "").trim();
469
- const name = p.name;
470
- if (/LucideIcon|lucide-react/.test(type)) {
471
- const iconName = LUCIDE_ICON_DEFAULT;
472
- lucideImports.add(iconName);
473
- argLines.push(` ${name}: ${iconName},`);
474
- } else if (/\[\]/.test(type)) {
475
- if (/string\s*\[\]/.test(type)) {
476
- argLines.push(` ${name}: ["Example"],`);
477
- } else {
478
- argLines.push(` ${name}: [],`);
580
+ const iconPropNames = [];
581
+ let needReact = false;
582
+ const fromPages = usageFromPages && typeof usageFromPages === "object" ? usageFromPages : {};
583
+ const componentPlaceholders = (componentName && COMPONENT_PLACEHOLDERS[componentName]) || {};
584
+ const added = new Set();
585
+
586
+ const stringFallback = (name) => {
587
+ if (fromPages[name] !== undefined && fromPages[name] !== null) return JSON.stringify(String(fromPages[name]));
588
+ if (componentPlaceholders[name] !== undefined) return JSON.stringify(componentPlaceholders[name]);
589
+ if (DEFAULT_STRINGS_BY_PROP_NAME[name] !== undefined) return JSON.stringify(DEFAULT_STRINGS_BY_PROP_NAME[name]);
590
+ return JSON.stringify("Example");
591
+ };
592
+
593
+ if (Array.isArray(props) && props.length > 0) {
594
+ for (const p of props) {
595
+ const type = String(p.type || "").trim();
596
+ const name = p.name;
597
+ const required = p.required === true;
598
+ if (/ReactNode|React\.ReactNode/.test(type)) {
599
+ const text = REACTNODE_PLACEHOLDER_TEXT[name] || componentPlaceholders[name] || "Content";
600
+ argLines.push(` ${name}: React.createElement('p', null, ${JSON.stringify(text)}),`);
601
+ needReact = true;
602
+ added.add(name);
603
+ continue;
604
+ }
605
+ if (!required) continue;
606
+ if (/LucideIcon|lucide-react/.test(type)) {
607
+ const iconName = LUCIDE_ICON_DEFAULT;
608
+ lucideImports.add(iconName);
609
+ argLines.push(` ${name}: ${iconName},`);
610
+ iconPropNames.push(name);
611
+ added.add(name);
612
+ } else if (/\[\]/.test(type)) {
613
+ if (/string\s*\[\]/.test(type)) {
614
+ argLines.push(` ${name}: ["Example"],`);
615
+ } else {
616
+ const itemTypeMatch = type.match(/([A-Za-z0-9_]+)(?=\s*\[\])/);
617
+ const itemType = itemTypeMatch ? itemTypeMatch[1] : "Item";
618
+ const mockItems = buildMockArrayItems(componentSource, itemType, name);
619
+ argLines.push(` ${name}: ${JSON.stringify(mockItems)},`);
620
+ }
621
+ added.add(name);
622
+ } else if (fromPages[name] !== undefined && fromPages[name] !== null) {
623
+ argLines.push(` ${name}: ${JSON.stringify(String(fromPages[name]))},`);
624
+ added.add(name);
625
+ } else if (/string/.test(type)) {
626
+ argLines.push(` ${name}: ${stringFallback(name)},`);
627
+ added.add(name);
479
628
  }
480
- } else if (/string/.test(type) && DEFAULT_STRINGS_BY_PROP_NAME[name] !== undefined) {
481
- argLines.push(` ${name}: ${JSON.stringify(DEFAULT_STRINGS_BY_PROP_NAME[name])},`);
482
629
  }
483
630
  }
484
- return { argLines, lucideImports: [...lucideImports] };
631
+ for (const name of Object.keys(fromPages)) {
632
+ if (added.has(name)) continue;
633
+ const val = fromPages[name];
634
+ if (val !== undefined && val !== null && typeof val === "string") {
635
+ argLines.push(` ${name}: ${JSON.stringify(val)},`);
636
+ }
637
+ }
638
+ return { argLines, lucideImports: [...lucideImports], iconPropNames, needReact };
485
639
  }
486
640
 
487
641
  function getExampleItemForArrayType(itemType) {
@@ -600,9 +754,12 @@ function buildSpecialStories(componentName, variants) {
600
754
  return "";
601
755
  }
602
756
 
603
- function buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, recipe, defaultArgLines = [], lucideImports = []) {
757
+ function buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, recipe, defaultArgLines = [], lucideImports = [], iconPropNames = [], needReact = false) {
604
758
  const lines = [];
605
759
  lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
760
+ if (needReact) {
761
+ lines.push(`import React from "react";`);
762
+ }
606
763
  if (lucideImports.length > 0) {
607
764
  lines.push(`import { ${lucideImports.join(", ")} } from "lucide-react";`);
608
765
  }
@@ -647,6 +804,13 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
647
804
  lines.push(` title: ${JSON.stringify(title)},`);
648
805
  lines.push(` component: ComponentRef,`);
649
806
  lines.push(` tags: ["autodocs"],`);
807
+ if (iconPropNames && iconPropNames.length > 0) {
808
+ lines.push(` argTypes: {`);
809
+ for (const name of iconPropNames) {
810
+ lines.push(` ${name}: { control: false },`);
811
+ }
812
+ lines.push(` },`);
813
+ }
650
814
  lines.push(`} satisfies Meta<typeof ComponentRef>;`);
651
815
  lines.push("");
652
816
  lines.push(`export default meta;`);
@@ -721,9 +885,10 @@ function buildStoryFileContent(comp) {
721
885
  const omitChildren = componentWrapsVoidElement(source);
722
886
  const isPage = comp.file.startsWith("pages/");
723
887
 
724
- // Props: manifest or parse from source for default args (LucideIcon, array types)
888
+ // Props: manifest or parse from source for default args (LucideIcon, array types, ReactNode, contextual placeholders)
725
889
  const effectiveProps = Array.isArray(comp.props) && comp.props.length > 0 ? comp.props : parsePropsFromSource(source);
726
- const { argLines: defaultArgLines, lucideImports } = buildDefaultArgsForRequiredProps(effectiveProps);
890
+ const usageFromPages = findComponentUsageInPages(componentName, PROJECT_ROOT);
891
+ const { argLines: defaultArgLines, lucideImports, iconPropNames, needReact } = buildDefaultArgsForRequiredProps(effectiveProps, usageFromPages, componentName, source);
727
892
  const useReactNodeChildrenRender = !omitChildren && hasChildrenPropReactNode(effectiveProps);
728
893
 
729
894
  // Skip story only if not a page and no export found
@@ -732,12 +897,12 @@ function buildStoryFileContent(comp) {
732
897
  }
733
898
 
734
899
  if (RECIPES[componentName]) {
735
- return buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, RECIPES[componentName], defaultArgLines, lucideImports);
900
+ return buildRecipeStoryContent(comp, componentName, importPath, title, source, exportStyle, RECIPES[componentName], defaultArgLines, lucideImports, iconPropNames, needReact);
736
901
  }
737
902
 
738
903
  const lines = [];
739
904
  lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
740
- if (useReactNodeChildrenRender) {
905
+ if (useReactNodeChildrenRender || needReact) {
741
906
  lines.push(`import React from "react";`);
742
907
  }
743
908
  if (lucideImports.length > 0) {
@@ -767,6 +932,13 @@ function buildStoryFileContent(comp) {
767
932
  lines.push(` title: ${JSON.stringify(title)},`);
768
933
  lines.push(` component: ComponentRef,`);
769
934
  lines.push(` tags: ["autodocs"],`);
935
+ if (iconPropNames && iconPropNames.length > 0) {
936
+ lines.push(` argTypes: {`);
937
+ for (const name of iconPropNames) {
938
+ lines.push(` ${name}: { control: false },`);
939
+ }
940
+ lines.push(` },`);
941
+ }
770
942
  lines.push(`} satisfies Meta<typeof ComponentRef>;`);
771
943
  lines.push("");
772
944
  lines.push(`export default meta;`);