vibe-design-system 2.5.23 → 2.5.25

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.25",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,8 +45,24 @@ To see **live renders** of components in the dashboard (isolated, no app chrome)
45
45
  VDS_PREVIEW_URL=http://localhost:3000 node vds-core/dashboard-server.mjs
46
46
  ```
47
47
 
48
+ ## Storybook
49
+
50
+ 1. **Generate stories** (after `npm run vds`):
51
+ ```bash
52
+ npm run vds:stories
53
+ ```
54
+ Add to `package.json` if missing:
55
+ ```json
56
+ "vds:stories": "node vds-core/story-generator.mjs"
57
+ ```
58
+
59
+ 2. **Context providers:** If components use React context (e.g. `useTimer`, `useSidebar`, `useCircles`, drag-drop context), they will error in Storybook unless wrapped with the right providers. In your project, add decorators in `.storybook/preview.tsx` to wrap all stories (or per-story) with your app’s providers (e.g. `TimerProvider`, `SidebarProvider`, `CirclesProvider`). See [Storybook decorators](https://storybook.js.org/docs/writing-stories/decorators).
60
+
61
+ 3. **Icons:** The Foundations/Icons story lists only icons that are imported from `lucide-react` in your app code (src/, excluding `src/stories`), so it reflects real usage.
62
+
48
63
  ## Layout
49
64
 
50
65
  - `vds-core/scan.mjs` — Node script; scans `src/components`, CSS, Tailwind config. Writes `vds-output.json` and `public/vds-output.json`. Paths are relative to **project root** (parent of `vds-core`).
66
+ - `vds-core/story-generator.mjs` — Reads `vds-output.json`, writes Storybook stories under `src/stories`.
51
67
  - `vds-core/VdsPreview.tsx` — Optional; mount at `/vds-preview` so the dashboard can show live component renders.
52
68
  - Dashboard UI reads `/vds-output.json`, renders Foundations + Component Library.
@@ -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");
@@ -315,9 +316,10 @@ function extractBrandAssets() {
315
316
  return assets;
316
317
  }
317
318
 
318
- /** Extract icon names from `import { A, B, C } from "lucide-react"` (or 'lucide-react'). Returns unique list. */
319
+ /** Extract icon names from `import { A, B, C } from "lucide-react"` in app code only (exclude stories so generated Star/defaults don't pollute). */
319
320
  function extractLucideIconsUsed(srcDir) {
320
- const files = getAllTsxJsxInDir(srcDir);
321
+ const allFiles = getAllTsxJsxInDir(srcDir);
322
+ const files = allFiles.filter((rel) => !/^stories[/\\]/.test(rel.replace(/\\/g, "/")));
321
323
  const names = new Set();
322
324
  const importRe = /import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/g;
323
325
  for (const rel of files) {
@@ -436,6 +438,47 @@ function suggestNameFromPattern(pattern) {
436
438
  return "Container";
437
439
  }
438
440
 
441
+ /** Count direct child elements in JSX inner content (depth-1 opening tags). */
442
+ function countDirectChildren(innerContent) {
443
+ if (!innerContent || typeof innerContent !== "string") return 0;
444
+ let depth = 0;
445
+ let count = 0;
446
+ const s = innerContent;
447
+ let i = 0;
448
+ while (i < s.length) {
449
+ if (s.slice(i, i + 2) === "</") {
450
+ i += 2;
451
+ depth -= 1;
452
+ while (i < s.length && /[\w-]/.test(s[i])) i += 1;
453
+ continue;
454
+ }
455
+ if (s[i] === "<" && /[\w]/.test(s[i + 1])) {
456
+ depth += 1;
457
+ if (depth === 1) count += 1;
458
+ i += 1;
459
+ while (i < s.length && /[\w-]/.test(s[i])) i += 1;
460
+ continue;
461
+ }
462
+ if (s[i] === "{" || s[i] === '"' || s[i] === "'" || s[i] === "`") {
463
+ const open = s[i];
464
+ const close = open === "{" ? "}" : open;
465
+ i += 1;
466
+ let depthBrace = open === "{" ? 1 : 0;
467
+ while (i < s.length) {
468
+ if (depthBrace === 0 && (s[i] === close || (open === "{" && s[i] === "}"))) break;
469
+ if (s[i] === open) depthBrace += 1;
470
+ else if (s[i] === close) depthBrace -= 1;
471
+ else if ((open === '"' || open === "'") && s[i] === "\\") i += 1;
472
+ i += 1;
473
+ }
474
+ if (s[i] === close) i += 1;
475
+ continue;
476
+ }
477
+ i += 1;
478
+ }
479
+ return count;
480
+ }
481
+
439
482
  /** Extract full JSX element (opening tag to matching closing tag) and inner text. */
440
483
  function extractFullElement(content, classNameMatchIndex, tagName) {
441
484
  const tag = tagName;
@@ -475,17 +518,51 @@ function extractFullElement(content, classNameMatchIndex, tagName) {
475
518
  return null;
476
519
  }
477
520
 
478
- /** Scan src/pages/*.tsx for repeated className clusters; return component suggestions. */
521
+ /** Return set of component names already in src/components (so we don't suggest them again). */
522
+ function getExistingComponentNames() {
523
+ const names = new Set();
524
+ if (!fs.existsSync(COMPONENTS_DIR)) return names;
525
+ const walk = (dir) => {
526
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
527
+ for (const e of entries) {
528
+ const full = path.join(dir, e.name);
529
+ if (e.isDirectory() && !IGNORE_DIRS.includes(e.name)) walk(full);
530
+ else if (e.isFile() && /\.(tsx|jsx)$/i.test(e.name)) {
531
+ const base = e.name.replace(/\.(tsx|jsx)$/i, "");
532
+ if (/^[A-Z]/.test(base)) names.add(base);
533
+ }
534
+ }
535
+ };
536
+ walk(COMPONENTS_DIR);
537
+ return names;
538
+ }
539
+
540
+ /** Scan src/pages and src/app for repeated visual-section patterns (className + 2+ children); return component suggestions. */
479
541
  function extractComponentSuggestions() {
480
- if (!fs.existsSync(PAGES_DIR)) return [];
481
- const pageFiles = getAllComponentFiles(PAGES_DIR);
542
+ const pageDirs = [
543
+ { dir: PAGES_DIR, prefix: "src/pages/" },
544
+ { dir: APP_DIR, prefix: "src/app/" },
545
+ ];
546
+ const allPageFiles = [];
547
+ for (const { dir, prefix } of pageDirs) {
548
+ if (!fs.existsSync(dir)) continue;
549
+ const files = getAllComponentFiles(dir);
550
+ for (const rel of files) {
551
+ allPageFiles.push({ fullPath: path.join(dir, rel), srcRel: prefix + rel.replace(/\\/g, "/") });
552
+ }
553
+ }
554
+ if (allPageFiles.length === 0) return [];
555
+
482
556
  const byPattern = new Map();
483
557
  const re = /className\s*=\s*["']([^"']+)["']|className\s*=\s*\{\s*["']([^"']+)["']/g;
484
558
 
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, "/");
559
+ for (const { fullPath, srcRel } of allPageFiles) {
560
+ let content;
561
+ try {
562
+ content = fs.readFileSync(fullPath, "utf-8");
563
+ } catch (_) {
564
+ continue;
565
+ }
489
566
  let m;
490
567
  re.lastIndex = 0;
491
568
  while ((m = re.exec(content)) !== null) {
@@ -500,11 +577,14 @@ function extractComponentSuggestions() {
500
577
  const tagName = tagMatch ? tagMatch[1] : "div";
501
578
  let fullJsx = null;
502
579
  let innerText = "";
580
+ let innerContent = "";
503
581
  const extracted = extractFullElement(content, m.index, tagName);
504
582
  if (extracted) {
505
583
  fullJsx = extracted.fullJsx;
506
584
  innerText = extracted.innerText || "";
585
+ innerContent = extracted.innerContent || "";
507
586
  }
587
+ const directChildren = countDirectChildren(innerContent);
508
588
  byPattern.set(raw, {
509
589
  count: 0,
510
590
  files: new Set(),
@@ -512,6 +592,7 @@ function extractComponentSuggestions() {
512
592
  snippet: `<${tagName} className="${raw}">...</${tagName}>`,
513
593
  fullJsx,
514
594
  innerText,
595
+ directChildren,
515
596
  });
516
597
  }
517
598
  const entry = byPattern.get(raw);
@@ -520,19 +601,23 @@ function extractComponentSuggestions() {
520
601
  }
521
602
  }
522
603
 
604
+ const existingNames = getExistingComponentNames();
523
605
  const suggestions = [];
524
- for (const [pattern, { count, files, snippet, tagName, fullJsx, innerText }] of byPattern.entries()) {
606
+ for (const [pattern, { count, files, snippet, tagName, fullJsx, innerText, directChildren }] of byPattern.entries()) {
525
607
  if (count < 2) continue;
526
608
  const suggestedName = suggestNameFromContent(innerText) || suggestNameFromPattern(pattern);
609
+ const name = sanitizeComponentName(suggestedName.replace(/\s+/g, ""));
610
+ if (existingNames.has(name)) continue;
611
+ if (directChildren < 2) continue;
527
612
  suggestions.push({
528
- suggestedName: sanitizeComponentName(suggestedName.replace(/\s+/g, "")),
613
+ suggestedName: name,
529
614
  tagName: tagName || "div",
530
615
  pattern,
531
616
  fullJsx,
532
617
  occurrences: count,
533
618
  foundIn: [...files].sort(),
534
619
  snippet,
535
- reason: `Same className cluster appears ${count} times`,
620
+ reason: `Visual section candidate: same className cluster ${count}×, ${directChildren} child elements`,
536
621
  });
537
622
  }
538
623
  return suggestions;
@@ -542,17 +542,26 @@ function getItemTypeFieldsFromSource(source, itemTypeName) {
542
542
  return fields;
543
543
  }
544
544
 
545
- /** Build 2-3 mock items for array prop from component source (item type fields). */
545
+ /** Whether a field name is date/time-like (for safe mock values and avoiding "Invalid time value"). */
546
+ function isDateLikeField(name) {
547
+ if (!name || typeof name !== "string") return false;
548
+ const n = name.toLowerCase();
549
+ return /date|time|start|end|created|updated|timeline/.test(n);
550
+ }
551
+
552
+ /** Build 2-3 mock items for array prop from component source (item type fields). Uses valid ISO date strings for date-like fields to avoid "Invalid time value". */
546
553
  function buildMockArrayItems(componentSource, itemTypeName, propName) {
547
554
  const fields = getItemTypeFieldsFromSource(componentSource, itemTypeName);
548
555
  const defaultFields = ["label", "value", "title", "id", "name", "description"];
549
556
  const useFields = fields.length ? fields : defaultFields;
550
557
  const items = [];
558
+ const baseDate = new Date("2024-01-15T12:00:00.000Z").getTime();
551
559
  for (let i = 1; i <= 3; i++) {
552
560
  const item = {};
553
561
  for (const f of useFields) {
554
562
  if (f === "id") item[f] = String(i);
555
563
  else if (f === "value") item[f] = String(100 - i * 25);
564
+ else if (isDateLikeField(f)) item[f] = new Date(baseDate + i * 86400000).toISOString();
556
565
  else item[f] = `Example ${i}`;
557
566
  }
558
567
  items.push(item);
@@ -637,6 +646,9 @@ function buildDefaultArgsForRequiredProps(props, usageFromPages = null, componen
637
646
  } else if (fromPages[name] !== undefined && fromPages[name] !== null) {
638
647
  argLines.push(` ${name}: ${JSON.stringify(String(fromPages[name]))},`);
639
648
  added.add(name);
649
+ } else if (/Date\b/.test(type) || /^(currentDate|startDate|endDate|date)$/.test(name)) {
650
+ argLines.push(` ${name}: ${JSON.stringify(new Date().toISOString())},`);
651
+ added.add(name);
640
652
  } else if (/string/.test(type)) {
641
653
  argLines.push(` ${name}: ${stringFallback(name)},`);
642
654
  added.add(name);
@@ -1138,7 +1150,9 @@ function writeFoundationsStories(foundations) {
1138
1150
  "",
1139
1151
  "export const Default: Story = {",
1140
1152
  " render: () => (",
1141
- " <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(100px, 1fr))\", gap: 16, padding: 24 }}>",
1153
+ " <div style={{ padding: 24 }}>",
1154
+ " <p style={{ marginBottom: 16, color: \"#888\", fontSize: 14 }}>Icons imported from lucide-react in app code (src/, excluding stories).</p>",
1155
+ " <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(auto-fill, minmax(100px, 1fr))\", gap: 16 }}>",
1142
1156
  " {iconNames.map((name) => {",
1143
1157
  " const Icon = Lucide[name];",
1144
1158
  " if (!Icon) return null;",
@@ -1149,6 +1163,7 @@ function writeFoundationsStories(foundations) {
1149
1163
  " </div>",
1150
1164
  " );",
1151
1165
  " })}",
1166
+ " </div>",
1152
1167
  " </div>",
1153
1168
  " ),",
1154
1169
  "};",
@@ -1183,8 +1198,8 @@ function writeComponentSuggestionsStory(componentSuggestions) {
1183
1198
  "export const Default: Story = {",
1184
1199
  " render: () => (",
1185
1200
  " <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>",
1201
+ " <h2 style={{ marginBottom: 16 }}>Visual section candidates (component suggestions)</h2>",
1202
+ " <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
1203
  " <div style={{ display: \"flex\", flexDirection: \"column\", gap: 24 }}>",
1189
1204
  " {suggestions.map((s, i) => (",
1190
1205
  " <div key={i} style={{ border: \"1px solid #333\", borderRadius: 8, padding: 16, background: \"#111\" }}>",