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