vibe-design-system 1.8.4 → 1.9.2
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/bin/init.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* VDS installer: npx vibe-design-system init
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Single command that:
|
|
6
|
+
* 1. Copies vds-core files (scan.mjs, story-generator.mjs, dashboard-server.mjs)
|
|
7
|
+
* 2. Installs Storybook if not present (npx storybook@latest init --yes)
|
|
8
|
+
* 3. Creates/updates .storybook/preview with index.css import and dark decorator
|
|
9
|
+
* 4. Runs node vds-core/scan.mjs
|
|
10
|
+
* 5. Runs node vds-core/story-generator.mjs
|
|
11
|
+
* 6. Adds all scripts to package.json
|
|
8
12
|
*/
|
|
9
13
|
import fs from "fs";
|
|
10
14
|
import path from "path";
|
|
@@ -24,6 +28,66 @@ function getProjectRoot() {
|
|
|
24
28
|
return cwd;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
function isStorybookInstalled(projectRoot) {
|
|
32
|
+
const storybookDir = path.join(projectRoot, ".storybook");
|
|
33
|
+
if (fs.existsSync(storybookDir)) return true;
|
|
34
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
37
|
+
const dev = pkg.devDependencies || {};
|
|
38
|
+
if (dev["@storybook/react-vite"] || dev["storybook"]) return true;
|
|
39
|
+
} catch (_) {}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function installStorybook(projectRoot) {
|
|
44
|
+
console.log("📚 Storybook kuruluyor...");
|
|
45
|
+
const r = spawnSync("npx", ["storybook@latest", "init", "--yes"], {
|
|
46
|
+
cwd: projectRoot,
|
|
47
|
+
stdio: "inherit",
|
|
48
|
+
shell: true,
|
|
49
|
+
});
|
|
50
|
+
if (r.status !== 0) {
|
|
51
|
+
console.warn("⚠️ Storybook init tamamlanamadı; manuel: npx storybook@latest init");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PREVIEW_CONTENT = `import type { Preview } from "@storybook/react-vite";
|
|
56
|
+
import React from "react";
|
|
57
|
+
import "../src/index.css";
|
|
58
|
+
|
|
59
|
+
const preview: Preview = {
|
|
60
|
+
parameters: {
|
|
61
|
+
controls: {
|
|
62
|
+
matchers: {
|
|
63
|
+
color: /(background|color)$/i,
|
|
64
|
+
date: /Date$/i,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
decorators: [
|
|
69
|
+
(Story) => (
|
|
70
|
+
<div className="dark" style={{ minHeight: "100vh", padding: "1rem" }}>
|
|
71
|
+
<Story />
|
|
72
|
+
</div>
|
|
73
|
+
),
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default preview;
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
function ensureStorybookPreview(projectRoot) {
|
|
81
|
+
const storybookDir = path.join(projectRoot, ".storybook");
|
|
82
|
+
if (!fs.existsSync(storybookDir)) fs.mkdirSync(storybookDir, { recursive: true });
|
|
83
|
+
|
|
84
|
+
const previewTsx = path.join(storybookDir, "preview.tsx");
|
|
85
|
+
const previewTs = path.join(storybookDir, "preview.ts");
|
|
86
|
+
if (fs.existsSync(previewTs)) try { fs.unlinkSync(previewTs); } catch (_) {}
|
|
87
|
+
fs.writeFileSync(previewTsx, PREVIEW_CONTENT, "utf-8");
|
|
88
|
+
console.log("📝 .storybook/preview.tsx güncellendi (index.css + dark decorator).");
|
|
89
|
+
}
|
|
90
|
+
|
|
27
91
|
function addScripts(projectRoot) {
|
|
28
92
|
const pkgPath = path.join(projectRoot, "package.json");
|
|
29
93
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -31,6 +95,9 @@ function addScripts(projectRoot) {
|
|
|
31
95
|
if (!scripts.vds) scripts.vds = "node vds-core/scan.mjs";
|
|
32
96
|
if (!scripts["vds:watch"]) scripts["vds:watch"] = "node vds-core/scan.mjs --watch";
|
|
33
97
|
if (!scripts["vds:dashboard"]) scripts["vds:dashboard"] = "node vds-core/dashboard-server.mjs";
|
|
98
|
+
if (!scripts["vds:stories"]) scripts["vds:stories"] = "node vds-core/story-generator.mjs";
|
|
99
|
+
if (!scripts.storybook) scripts.storybook = "storybook dev -p 6006";
|
|
100
|
+
if (!scripts["build-storybook"]) scripts["build-storybook"] = "storybook build";
|
|
34
101
|
pkg.scripts = scripts;
|
|
35
102
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), "utf-8");
|
|
36
103
|
}
|
|
@@ -40,7 +107,7 @@ function addVdsDependency(projectRoot) {
|
|
|
40
107
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
41
108
|
const dev = pkg.devDependencies || {};
|
|
42
109
|
if (!dev["vibe-design-system"]) {
|
|
43
|
-
dev["vibe-design-system"] = "^1.
|
|
110
|
+
dev["vibe-design-system"] = "^1.9.0";
|
|
44
111
|
pkg.devDependencies = dev;
|
|
45
112
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), "utf-8");
|
|
46
113
|
const r = spawnSync("npm", ["install"], { cwd: projectRoot, stdio: "inherit", shell: true });
|
|
@@ -63,15 +130,18 @@ function ensureVdsCore(projectRoot) {
|
|
|
63
130
|
const serverSrc = path.join(TEMPLATE_DIR, "dashboard-server.mjs");
|
|
64
131
|
if (fs.existsSync(serverSrc)) {
|
|
65
132
|
fs.copyFileSync(serverSrc, path.join(vdsCoreDest, "dashboard-server.mjs"));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const storyGenSrc = path.join(TEMPLATE_DIR, "story-generator.mjs");
|
|
136
|
+
if (fs.existsSync(storyGenSrc)) {
|
|
137
|
+
fs.copyFileSync(storyGenSrc, path.join(vdsCoreDest, "story-generator.mjs"));
|
|
69
138
|
}
|
|
70
139
|
}
|
|
71
140
|
|
|
72
141
|
function runScan(projectRoot) {
|
|
73
142
|
const scanPath = path.join(projectRoot, "vds-core", "scan.mjs");
|
|
74
143
|
if (!fs.existsSync(scanPath)) return;
|
|
144
|
+
console.log("🔍 VDS taraması çalıştırılıyor...");
|
|
75
145
|
const r = spawnSync("node", [scanPath], {
|
|
76
146
|
cwd: projectRoot,
|
|
77
147
|
stdio: "inherit",
|
|
@@ -80,6 +150,18 @@ function runScan(projectRoot) {
|
|
|
80
150
|
if (r.status !== 0) process.exitCode = r.status ?? 1;
|
|
81
151
|
}
|
|
82
152
|
|
|
153
|
+
function runStoryGenerator(projectRoot) {
|
|
154
|
+
const genPath = path.join(projectRoot, "vds-core", "story-generator.mjs");
|
|
155
|
+
if (!fs.existsSync(genPath)) return;
|
|
156
|
+
console.log("📖 Story dosyaları oluşturuluyor...");
|
|
157
|
+
const r = spawnSync("node", [genPath], {
|
|
158
|
+
cwd: projectRoot,
|
|
159
|
+
stdio: "inherit",
|
|
160
|
+
shell: false,
|
|
161
|
+
});
|
|
162
|
+
if (r.status !== 0) process.exitCode = r.status ?? 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
83
165
|
console.log("🎨 VDS kuruluyor...");
|
|
84
166
|
|
|
85
167
|
const projectRoot = getProjectRoot();
|
|
@@ -90,9 +172,29 @@ if (!fs.existsSync(pkgPath)) {
|
|
|
90
172
|
process.exit(1);
|
|
91
173
|
}
|
|
92
174
|
|
|
175
|
+
// 1. Copy vds-core files
|
|
93
176
|
ensureVdsCore(projectRoot);
|
|
177
|
+
|
|
178
|
+
// 2. Install Storybook if not present
|
|
179
|
+
if (!isStorybookInstalled(projectRoot)) {
|
|
180
|
+
installStorybook(projectRoot);
|
|
181
|
+
} else {
|
|
182
|
+
console.log("📚 Storybook zaten mevcut.");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 3. Create/update .storybook/preview with index.css + dark decorator
|
|
186
|
+
ensureStorybookPreview(projectRoot);
|
|
187
|
+
|
|
188
|
+
// 6. Add all scripts to package.json (before scan so scripts are there)
|
|
94
189
|
addScripts(projectRoot);
|
|
95
190
|
addVdsDependency(projectRoot);
|
|
191
|
+
|
|
192
|
+
// 4. Run scan
|
|
96
193
|
runScan(projectRoot);
|
|
97
194
|
|
|
98
|
-
|
|
195
|
+
// 5. Run story generator
|
|
196
|
+
runStoryGenerator(projectRoot);
|
|
197
|
+
|
|
198
|
+
console.log("✅ Kurulum tamamlandı.");
|
|
199
|
+
console.log(" Storybook: npm run storybook");
|
|
200
|
+
console.log(" Dashboard: npm run vds:dashboard");
|
package/package.json
CHANGED
|
@@ -24,13 +24,20 @@ Zero-config, autonomous design system. Drop this folder into any Vite + React pr
|
|
|
24
24
|
|
|
25
25
|
## Live component preview (optional)
|
|
26
26
|
|
|
27
|
-
To see **live renders** of components in the dashboard (
|
|
27
|
+
To see **live renders** of components in the dashboard (isolated, no app chrome):
|
|
28
28
|
|
|
29
|
-
1. Add the preview route
|
|
29
|
+
1. Add the preview route **outside** any layout that has your app header, sidebar, or nav. Otherwise every preview iframe will show your full app shell instead of just the component.
|
|
30
30
|
```tsx
|
|
31
|
-
import VdsPreview from "./vds-core/VdsPreview"; // or
|
|
31
|
+
import VdsPreview from "./vds-core/VdsPreview"; // or from node_modules/vibe-design-system/vds-core-template/
|
|
32
32
|
// ...
|
|
33
|
-
<
|
|
33
|
+
<Routes>
|
|
34
|
+
{/* Preview must NOT be inside a layout with header/sidebar */}
|
|
35
|
+
<Route path="/vds-preview" element={<VdsPreview />} />
|
|
36
|
+
<Route path="/" element={<YourLayoutWithHeader />}>
|
|
37
|
+
<Route index element={<Home />} />
|
|
38
|
+
{/* ... */}
|
|
39
|
+
</Route>
|
|
40
|
+
</Routes>
|
|
34
41
|
```
|
|
35
42
|
|
|
36
43
|
2. If you open the dashboard from the **standalone server** (e.g. `npm run vds:watch` → Dashboard at `http://localhost:3334/vds`), run your app as well (e.g. `npm run dev` on port 5173). The dashboard will load previews from that app. If your app runs on another port (e.g. 3000), set:
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VDS Component Preview — mount at /vds-preview so the dashboard can show live component renders.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Important: Render this route OUTSIDE any layout that contains your app header, sidebar, or nav.
|
|
5
|
+
* Otherwise every component preview iframe will show your full app shell instead of the component alone.
|
|
6
|
+
*
|
|
7
|
+
* Example (React Router):
|
|
8
|
+
* <Route path="/vds-preview" element={<VdsPreview />} /> // at root, not inside <LayoutWithHeader>
|
|
5
9
|
*
|
|
6
10
|
* Requires Vite; uses import.meta.glob. Path alias @/components must point to src/components.
|
|
7
11
|
*/
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VDS Story generator
|
|
4
|
+
*
|
|
5
|
+
* Reads vds-output.json and generates:
|
|
6
|
+
* - Storybook .stories.tsx files under src/stories for each component
|
|
7
|
+
* - Foundations MDX: Colors, Typography, Brand under src/stories/foundations/
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node vds-core/story-generator.mjs # generate for all components
|
|
11
|
+
* node vds-core/story-generator.mjs Button # generate only for "Button"
|
|
12
|
+
*/
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const PROJECT_ROOT = path.join(__dirname, "..");
|
|
19
|
+
const VDS_OUTPUT = path.join(PROJECT_ROOT, "vds-output.json");
|
|
20
|
+
const SRC_DIR = path.join(PROJECT_ROOT, "src");
|
|
21
|
+
const STORIES_DIR = path.join(SRC_DIR, "stories");
|
|
22
|
+
|
|
23
|
+
// Top of every story file for CSS variables
|
|
24
|
+
const STORY_CSS_HEADER = `// @ts-ignore
|
|
25
|
+
import '../../index.css'
|
|
26
|
+
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
// Components we don't want to auto-generate stories for (project-specific dashboards, heavy UIs, etc.)
|
|
30
|
+
const SKIP_LIST = [
|
|
31
|
+
"AnalysisDashboard",
|
|
32
|
+
"ComponentLibrary",
|
|
33
|
+
"EnterprisePushPanel",
|
|
34
|
+
"FigmaLibraryGenerator",
|
|
35
|
+
"IntegrationGuide",
|
|
36
|
+
"ProjectDropzone",
|
|
37
|
+
"RepoConnect",
|
|
38
|
+
"TokensStudioGuide",
|
|
39
|
+
"NavLink",
|
|
40
|
+
"CodeInput",
|
|
41
|
+
"SyncModeSelector",
|
|
42
|
+
"ToggleGroup",
|
|
43
|
+
"Sidebar",
|
|
44
|
+
"TestComponent",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function ensureDir(dir) {
|
|
48
|
+
if (!fs.existsSync(dir)) {
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Detect if component uses router (useLocation, useNavigate, or Link from react-router-dom). */
|
|
54
|
+
function needsRouter(source) {
|
|
55
|
+
if (!source || typeof source !== "string") return false;
|
|
56
|
+
if (/\buseLocation\b|\buseNavigate\b/.test(source)) return true;
|
|
57
|
+
if (/from\s+['"]react-router-dom['"]/.test(source) && /\bLink\b/.test(source)) return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** If component has <img and accepts children prop, we should omit children from story args (void element). */
|
|
62
|
+
function componentHasImgAndChildren(source) {
|
|
63
|
+
if (!source || typeof source !== "string") return false;
|
|
64
|
+
const hasImg = /return\s+<img|<\s*img\s+/.test(source);
|
|
65
|
+
if (!hasImg) return false;
|
|
66
|
+
return /\bchildren\b/.test(source);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toSafeComponentName(name, file) {
|
|
70
|
+
if (!name || typeof name !== "string") {
|
|
71
|
+
const base = (file || "").replace(/\.[^.]+$/, "");
|
|
72
|
+
const parts = base.split(/[\\/]/g);
|
|
73
|
+
const last = parts[parts.length - 1] || "Component";
|
|
74
|
+
return last.charAt(0).toUpperCase() + last.slice(1);
|
|
75
|
+
}
|
|
76
|
+
return name
|
|
77
|
+
.replace(/[^A-Za-z0-9]+/g, " ")
|
|
78
|
+
.trim()
|
|
79
|
+
.replace(/\s+([a-z])/g, (_, c) => c.toUpperCase())
|
|
80
|
+
.replace(/^\w/, (c) => c.toUpperCase())
|
|
81
|
+
.replace(/\s+/g, "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseUnionLiterals(type) {
|
|
85
|
+
if (!type) return [];
|
|
86
|
+
const matches = String(type).match(/"([^"]+)"/g);
|
|
87
|
+
if (!matches) return [];
|
|
88
|
+
return matches.map((s) => s.replace(/"/g, ""));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function capitalize(str) {
|
|
92
|
+
if (!str) return "";
|
|
93
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function detectExportStyle(source, componentName) {
|
|
97
|
+
if (!source) return "unknown";
|
|
98
|
+
const hasDefault = /export\s+default\b/.test(source);
|
|
99
|
+
const namedPatterns = [
|
|
100
|
+
new RegExp(`export\\s+\\{[^}]*\\b${componentName}\\b[^}]*\\}`),
|
|
101
|
+
new RegExp(`export\\s+const\\s+${componentName}\\b`),
|
|
102
|
+
new RegExp(`export\\s+function\\s+${componentName}\\b`),
|
|
103
|
+
new RegExp(`export\\s+class\\s+${componentName}\\b`),
|
|
104
|
+
];
|
|
105
|
+
const hasNamed = namedPatterns.some((re) => re.test(source));
|
|
106
|
+
if (hasDefault && hasNamed) return "both";
|
|
107
|
+
if (hasDefault) return "default";
|
|
108
|
+
if (hasNamed) return "named";
|
|
109
|
+
return "unknown";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildSpecialStories(componentName, variants) {
|
|
113
|
+
const lines = [];
|
|
114
|
+
|
|
115
|
+
// Simple input-like primitives
|
|
116
|
+
if (componentName === "Input") {
|
|
117
|
+
lines.push(`export const Default: Story = {`);
|
|
118
|
+
lines.push(` args: {`);
|
|
119
|
+
lines.push(` placeholder: "Enter your email...",`);
|
|
120
|
+
lines.push(` type: "email",`);
|
|
121
|
+
lines.push(` },`);
|
|
122
|
+
lines.push(`};`);
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (componentName === "Textarea") {
|
|
127
|
+
lines.push(`export const Default: Story = {`);
|
|
128
|
+
lines.push(` args: {`);
|
|
129
|
+
lines.push(` placeholder: "Write your message...",`);
|
|
130
|
+
lines.push(` rows: 4,`);
|
|
131
|
+
lines.push(` },`);
|
|
132
|
+
lines.push(`};`);
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (componentName === "Label") {
|
|
137
|
+
lines.push(`export const Default: Story = {`);
|
|
138
|
+
lines.push(` args: {`);
|
|
139
|
+
lines.push(` children: "Email address",`);
|
|
140
|
+
lines.push(` htmlFor: "email",`);
|
|
141
|
+
lines.push(` },`);
|
|
142
|
+
lines.push(`};`);
|
|
143
|
+
return lines.join("\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (componentName === "Slider") {
|
|
147
|
+
lines.push(`export const Default: Story = {`);
|
|
148
|
+
lines.push(` args: {`);
|
|
149
|
+
lines.push(` defaultValue: [50],`);
|
|
150
|
+
lines.push(` max: 100,`);
|
|
151
|
+
lines.push(` step: 1,`);
|
|
152
|
+
lines.push(` },`);
|
|
153
|
+
lines.push(`};`);
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (componentName === "Calendar") {
|
|
158
|
+
lines.push(`export const Default: Story = {`);
|
|
159
|
+
lines.push(` args: {},`);
|
|
160
|
+
lines.push(`};`);
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (componentName === "InputOtp" || componentName === "InputOTP") {
|
|
165
|
+
lines.push(`export const Default: Story = {`);
|
|
166
|
+
lines.push(` args: {`);
|
|
167
|
+
lines.push(` maxLength: 6,`);
|
|
168
|
+
lines.push(` },`);
|
|
169
|
+
lines.push(`};`);
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (componentName === "Checkbox") {
|
|
174
|
+
lines.push(`export const Default: Story = {`);
|
|
175
|
+
lines.push(` render: (args) => (`);
|
|
176
|
+
lines.push(` <div className="flex items-center space-x-2">`);
|
|
177
|
+
lines.push(` <ComponentRef id="terms" {...args} />`);
|
|
178
|
+
lines.push(` <Label htmlFor="terms">Accept terms</Label>`);
|
|
179
|
+
lines.push(` </div>`);
|
|
180
|
+
lines.push(` ),`);
|
|
181
|
+
lines.push(`};`);
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (componentName === "Switch") {
|
|
186
|
+
lines.push(`export const Default: Story = {`);
|
|
187
|
+
lines.push(` render: (args) => (`);
|
|
188
|
+
lines.push(` <div className="flex items-center space-x-2">`);
|
|
189
|
+
lines.push(` <ComponentRef id="notifications" {...args} />`);
|
|
190
|
+
lines.push(` <Label htmlFor="notifications">Enable notifications</Label>`);
|
|
191
|
+
lines.push(` </div>`);
|
|
192
|
+
lines.push(` ),`);
|
|
193
|
+
lines.push(`};`);
|
|
194
|
+
return lines.join("\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (componentName === "RadioGroup") {
|
|
198
|
+
lines.push(`export const Default: Story = {`);
|
|
199
|
+
lines.push(` render: (args) => (`);
|
|
200
|
+
lines.push(` <ComponentRef className="flex flex-col space-y-2" {...args}>`);
|
|
201
|
+
lines.push(` <div className="flex items-center space-x-2">`);
|
|
202
|
+
lines.push(` <RadioGroupItem value="comfortable" id="comfortable" />`);
|
|
203
|
+
lines.push(` <Label htmlFor="comfortable">Comfortable</Label>`);
|
|
204
|
+
lines.push(` </div>`);
|
|
205
|
+
lines.push(` <div className="flex items-center space-x-2">`);
|
|
206
|
+
lines.push(` <RadioGroupItem value="compact" id="compact" />`);
|
|
207
|
+
lines.push(` <Label htmlFor="compact">Compact</Label>`);
|
|
208
|
+
lines.push(` </div>`);
|
|
209
|
+
lines.push(` </ComponentRef>`);
|
|
210
|
+
lines.push(` ),`);
|
|
211
|
+
lines.push(`};`);
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (componentName === "Select") {
|
|
216
|
+
lines.push(`export const Default: Story = {`);
|
|
217
|
+
lines.push(` args: { defaultValue: "apple" },`);
|
|
218
|
+
lines.push(` render: (args) => (`);
|
|
219
|
+
lines.push(` <ComponentRef {...args}>`);
|
|
220
|
+
lines.push(` <SelectTrigger className="w-[180px]">`);
|
|
221
|
+
lines.push(` <SelectValue placeholder="Select a fruit" />`);
|
|
222
|
+
lines.push(` </SelectTrigger>`);
|
|
223
|
+
lines.push(` <SelectContent>`);
|
|
224
|
+
lines.push(` <SelectItem value="apple">Apple</SelectItem>`);
|
|
225
|
+
lines.push(` <SelectItem value="banana">Banana</SelectItem>`);
|
|
226
|
+
lines.push(` <SelectItem value="orange">Orange</SelectItem>`);
|
|
227
|
+
lines.push(` </SelectContent>`);
|
|
228
|
+
lines.push(` </ComponentRef>`);
|
|
229
|
+
lines.push(` ),`);
|
|
230
|
+
lines.push(`};`);
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (componentName === "Badge") {
|
|
235
|
+
const vs = variants && variants.length ? variants : ["default", "secondary", "destructive", "outline"];
|
|
236
|
+
vs.forEach((v, idx) => {
|
|
237
|
+
const storyName = capitalize(v);
|
|
238
|
+
lines.push(`export const ${storyName}: Story = {`);
|
|
239
|
+
lines.push(` args: {`);
|
|
240
|
+
lines.push(` variant: "${v}",`);
|
|
241
|
+
lines.push(` children: "New",`);
|
|
242
|
+
lines.push(` },`);
|
|
243
|
+
lines.push(`};`);
|
|
244
|
+
if (idx < vs.length - 1) lines.push("");
|
|
245
|
+
});
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (componentName === "Card") {
|
|
250
|
+
lines.push(`export const Default: Story = {`);
|
|
251
|
+
lines.push(` render: (args) => (`);
|
|
252
|
+
lines.push(` <ComponentRef className="w-[340px]" {...args}>`);
|
|
253
|
+
lines.push(` <CardHeader>`);
|
|
254
|
+
lines.push(` <CardTitle>Card title</CardTitle>`);
|
|
255
|
+
lines.push(` <CardDescription>Short description about this card.</CardDescription>`);
|
|
256
|
+
lines.push(` </CardHeader>`);
|
|
257
|
+
lines.push(` <CardContent>`);
|
|
258
|
+
lines.push(` <p>Here is some representative content inside the card body.</p>`);
|
|
259
|
+
lines.push(` </CardContent>`);
|
|
260
|
+
lines.push(` <CardFooter>Footer content</CardFooter>`);
|
|
261
|
+
lines.push(` </ComponentRef>`);
|
|
262
|
+
lines.push(` ),`);
|
|
263
|
+
lines.push(`};`);
|
|
264
|
+
return lines.join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (componentName === "Avatar") {
|
|
268
|
+
lines.push(`export const Default: Story = {`);
|
|
269
|
+
lines.push(` render: (args) => (`);
|
|
270
|
+
lines.push(` <ComponentRef {...args}>`);
|
|
271
|
+
lines.push(` <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />`);
|
|
272
|
+
lines.push(` <AvatarFallback>JD</AvatarFallback>`);
|
|
273
|
+
lines.push(` </ComponentRef>`);
|
|
274
|
+
lines.push(` ),`);
|
|
275
|
+
lines.push(`};`);
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Fallback: let the generic variant-based logic handle it
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildStoryFileContent(comp) {
|
|
284
|
+
const componentName = toSafeComponentName(comp.name, comp.file);
|
|
285
|
+
const fileNoExt = comp.file.replace(/\.(tsx|jsx)$/, "");
|
|
286
|
+
const importPath = `@/components/${fileNoExt}`;
|
|
287
|
+
const group = comp.group || "Components";
|
|
288
|
+
const category = comp.category || null;
|
|
289
|
+
const titleParts = [group, category, componentName].filter(Boolean);
|
|
290
|
+
const title = titleParts.join("/");
|
|
291
|
+
|
|
292
|
+
const props = Array.isArray(comp.props) ? comp.props : [];
|
|
293
|
+
const variantProp = props.find((p) => p.name === "variant");
|
|
294
|
+
let variants = parseUnionLiterals(variantProp && variantProp.type);
|
|
295
|
+
|
|
296
|
+
// Fallback: if manifest doesn't have variant metadata yet, parse cva() directly from component file.
|
|
297
|
+
if (!variants.length) {
|
|
298
|
+
try {
|
|
299
|
+
const srcPath = path.join(SRC_DIR, "components", comp.file);
|
|
300
|
+
if (fs.existsSync(srcPath)) {
|
|
301
|
+
const code = fs.readFileSync(srcPath, "utf-8");
|
|
302
|
+
// Roughly match shadcn-style: variants: { variant: { ... }, size: { ... } }
|
|
303
|
+
const m = code.match(/variant\s*:\s*{([\s\S]*?)}\s*,\s*size\s*:/);
|
|
304
|
+
if (m) {
|
|
305
|
+
const body = m[1];
|
|
306
|
+
const names = [];
|
|
307
|
+
const lineRe = /^\s*([A-Za-z0-9_]+)\s*:/gm;
|
|
308
|
+
let lm;
|
|
309
|
+
while ((lm = lineRe.exec(body))) {
|
|
310
|
+
names.push(lm[1]);
|
|
311
|
+
}
|
|
312
|
+
if (names.length) variants = names;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
// best-effort; ignore parsing errors
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Read component source to detect export style for import
|
|
321
|
+
let source = "";
|
|
322
|
+
try {
|
|
323
|
+
const srcPath = path.join(SRC_DIR, "components", comp.file);
|
|
324
|
+
if (fs.existsSync(srcPath)) {
|
|
325
|
+
source = fs.readFileSync(srcPath, "utf-8");
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// ignore
|
|
329
|
+
}
|
|
330
|
+
const exportStyle = detectExportStyle(source, componentName);
|
|
331
|
+
const omitChildren = componentHasImgAndChildren(source);
|
|
332
|
+
|
|
333
|
+
const lines = [];
|
|
334
|
+
lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
|
|
335
|
+
|
|
336
|
+
if (exportStyle === "default") {
|
|
337
|
+
lines.push(`import ${componentName} from "${importPath}";`);
|
|
338
|
+
lines.push(`const ComponentRef = ${componentName};`);
|
|
339
|
+
} else if (exportStyle === "named") {
|
|
340
|
+
lines.push(`import { ${componentName} } from "${importPath}";`);
|
|
341
|
+
lines.push(`const ComponentRef = ${componentName};`);
|
|
342
|
+
} else {
|
|
343
|
+
const defaultAlias = `${componentName}Default`;
|
|
344
|
+
const namedAlias = `${componentName}Named`;
|
|
345
|
+
lines.push(
|
|
346
|
+
`import ${defaultAlias}, { ${componentName} as ${namedAlias} } from "${importPath}";`,
|
|
347
|
+
);
|
|
348
|
+
lines.push(`const ComponentRef = ${namedAlias} ?? ${defaultAlias};`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Extra imports for composite components
|
|
352
|
+
if (componentName === "Checkbox" || componentName === "Switch" || componentName === "RadioGroup") {
|
|
353
|
+
lines.push(`import { Label } from "@/components/ui/label";`);
|
|
354
|
+
}
|
|
355
|
+
if (componentName === "RadioGroup") {
|
|
356
|
+
lines.push(`import { RadioGroupItem } from "@/components/ui/radio-group";`);
|
|
357
|
+
}
|
|
358
|
+
if (componentName === "Select") {
|
|
359
|
+
lines.push(
|
|
360
|
+
`import { SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (componentName === "Avatar") {
|
|
364
|
+
lines.push(`import { AvatarImage, AvatarFallback } from "@/components/ui/avatar";`);
|
|
365
|
+
}
|
|
366
|
+
if (componentName === "Card") {
|
|
367
|
+
lines.push(
|
|
368
|
+
`import { CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card";`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const useRouterDecorator = needsRouter(source);
|
|
372
|
+
if (useRouterDecorator) {
|
|
373
|
+
lines.push(`import { MemoryRouter } from "react-router-dom";`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
lines.push("");
|
|
377
|
+
lines.push(`const meta = {`);
|
|
378
|
+
lines.push(` title: ${JSON.stringify(title)},`);
|
|
379
|
+
lines.push(` component: ComponentRef,`);
|
|
380
|
+
lines.push(` tags: ["autodocs"],`);
|
|
381
|
+
if (useRouterDecorator) {
|
|
382
|
+
lines.push(` decorators: [(Story) => (`);
|
|
383
|
+
lines.push(` <MemoryRouter>`);
|
|
384
|
+
lines.push(` <Story />`);
|
|
385
|
+
lines.push(` </MemoryRouter>`);
|
|
386
|
+
lines.push(` )],`);
|
|
387
|
+
}
|
|
388
|
+
lines.push(`} satisfies Meta<typeof ComponentRef>;`);
|
|
389
|
+
lines.push("");
|
|
390
|
+
lines.push(`export default meta;`);
|
|
391
|
+
lines.push(`type Story = StoryObj<typeof meta>;`);
|
|
392
|
+
lines.push("");
|
|
393
|
+
|
|
394
|
+
// Component-specific stories (inputs, composite components, etc.)
|
|
395
|
+
const specialStories = buildSpecialStories(componentName, variants);
|
|
396
|
+
if (specialStories) {
|
|
397
|
+
lines.push(specialStories);
|
|
398
|
+
return STORY_CSS_HEADER + lines.join("\n");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Generic variant-based stories (omit children for img/void components)
|
|
402
|
+
if (!variants.length) {
|
|
403
|
+
lines.push(`export const Default: Story = {`);
|
|
404
|
+
lines.push(` args: {`);
|
|
405
|
+
if (!omitChildren) lines.push(` children: "${componentName}",`);
|
|
406
|
+
lines.push(` },`);
|
|
407
|
+
lines.push(`};`);
|
|
408
|
+
} else {
|
|
409
|
+
const defaultVariant = variants[0];
|
|
410
|
+
lines.push(`export const ${capitalize(defaultVariant)}: Story = {`);
|
|
411
|
+
lines.push(` args: {`);
|
|
412
|
+
lines.push(` variant: "${defaultVariant}",`);
|
|
413
|
+
if (!omitChildren) lines.push(` children: "${componentName}",`);
|
|
414
|
+
lines.push(` },`);
|
|
415
|
+
lines.push(`};`);
|
|
416
|
+
lines.push("");
|
|
417
|
+
for (const v of variants.slice(1)) {
|
|
418
|
+
const storyName = capitalize(v);
|
|
419
|
+
lines.push(`export const ${storyName}: Story = {`);
|
|
420
|
+
lines.push(` args: {`);
|
|
421
|
+
lines.push(` variant: "${v}",`);
|
|
422
|
+
if (!omitChildren) lines.push(` children: "${storyName}",`);
|
|
423
|
+
lines.push(` },`);
|
|
424
|
+
lines.push(`};`);
|
|
425
|
+
lines.push("");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return STORY_CSS_HEADER + lines.join("\n");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Build color entries from foundations.colors (skip _dark; flatten to { name, hex }). */
|
|
433
|
+
function getColorEntries(colors) {
|
|
434
|
+
if (!colors || typeof colors !== "object") return [];
|
|
435
|
+
const entries = [];
|
|
436
|
+
for (const [name, v] of Object.entries(colors)) {
|
|
437
|
+
if (name === "_dark") continue;
|
|
438
|
+
const hex = v && (v.hex ?? (typeof v.value === "string" && v.value.startsWith("#") ? v.value : null));
|
|
439
|
+
const value = v && v.value;
|
|
440
|
+
if (hex) entries.push({ name, hex });
|
|
441
|
+
else if (value) entries.push({ name, hex: value });
|
|
442
|
+
}
|
|
443
|
+
return entries;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function writeFoundationsMdx(foundations) {
|
|
447
|
+
const foundationsDir = path.join(STORIES_DIR, "foundations");
|
|
448
|
+
ensureDir(foundationsDir);
|
|
449
|
+
|
|
450
|
+
const colors = foundations?.colors;
|
|
451
|
+
const colorEntries = getColorEntries(colors);
|
|
452
|
+
if (colorEntries.length > 0) {
|
|
453
|
+
const colorLines = [
|
|
454
|
+
"import { Meta } from '@storybook/blocks';",
|
|
455
|
+
"",
|
|
456
|
+
"<Meta title=\"Foundations/Colors\" />",
|
|
457
|
+
"",
|
|
458
|
+
"# Colors",
|
|
459
|
+
"",
|
|
460
|
+
"<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: '1rem' }}>",
|
|
461
|
+
...colorEntries.map(
|
|
462
|
+
(e) =>
|
|
463
|
+
` <div key="${e.name}"><div style={{ backgroundColor: '${e.hex.replace(/'/g, "\\'")}', height: 80, borderRadius: 8, border: '1px solid #333' }} /><p style={{ marginTop: 8, fontSize: 12 }}>${e.name}</p><code style={{ fontSize: 11 }}>${e.hex}</code></div>`,
|
|
464
|
+
),
|
|
465
|
+
"</div>",
|
|
466
|
+
];
|
|
467
|
+
fs.writeFileSync(path.join(foundationsDir, "Colors.stories.mdx"), colorLines.join("\n"), "utf-8");
|
|
468
|
+
console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Colors.stories.mdx")));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const typo = foundations?.typography;
|
|
472
|
+
if (typo && typeof typo === "object" && Object.keys(typo).length > 0) {
|
|
473
|
+
const bodyVal = typo.body || typo.bodyFontFamily || "";
|
|
474
|
+
const monoVal = typo.mono || typo.tailwindMono;
|
|
475
|
+
const monoStr = Array.isArray(monoVal) ? monoVal.join(", ") : String(monoVal || "");
|
|
476
|
+
const typoContent = [
|
|
477
|
+
"import { Meta } from '@storybook/blocks';",
|
|
478
|
+
"",
|
|
479
|
+
"<Meta title=\"Foundations/Typography\" />",
|
|
480
|
+
"",
|
|
481
|
+
"# Typography",
|
|
482
|
+
"",
|
|
483
|
+
"## Font family & scale",
|
|
484
|
+
"",
|
|
485
|
+
"| Token | Value |",
|
|
486
|
+
"| --- | --- |",
|
|
487
|
+
...Object.entries(typo).map(([k, v]) => {
|
|
488
|
+
const val = Array.isArray(v) ? v.join(", ") : String(v);
|
|
489
|
+
return `| ${k} | ${val} |`;
|
|
490
|
+
}),
|
|
491
|
+
"",
|
|
492
|
+
"## Preview",
|
|
493
|
+
"",
|
|
494
|
+
`<p style={{ fontFamily: ${JSON.stringify(bodyVal)}, fontSize: 16 }}>The quick brown fox jumps over the lazy dog.</p>`,
|
|
495
|
+
`<p style={{ fontFamily: ${JSON.stringify(monoStr)}, fontSize: 14 }}>Code: 0123456789</p>`,
|
|
496
|
+
];
|
|
497
|
+
fs.writeFileSync(path.join(foundationsDir, "Typography.stories.mdx"), typoContent.join("\n"), "utf-8");
|
|
498
|
+
console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Typography.stories.mdx")));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const brandAssets = foundations?.brand?.assets;
|
|
502
|
+
if (Array.isArray(brandAssets) && brandAssets.length > 0) {
|
|
503
|
+
const assetRows = brandAssets.map(
|
|
504
|
+
(a) =>
|
|
505
|
+
` <tr key="${(a.path || a.name || "").replace(/"/g, '\\"')}"><td>${a.type || "asset"}</td><td><code>${a.path || a.name || ""}</code></td><td>${a.name || ""}</td></tr>`,
|
|
506
|
+
);
|
|
507
|
+
const brandContent = [
|
|
508
|
+
"import { Meta } from '@storybook/blocks';",
|
|
509
|
+
"",
|
|
510
|
+
"<Meta title=\"Foundations/Brand\" />",
|
|
511
|
+
"",
|
|
512
|
+
"# Brand",
|
|
513
|
+
"",
|
|
514
|
+
"Logo and favicon paths from the project.",
|
|
515
|
+
"",
|
|
516
|
+
"<table><thead><tr><th>Type</th><th>Path</th><th>Name</th></tr></thead><tbody>",
|
|
517
|
+
...assetRows,
|
|
518
|
+
"</tbody></table>",
|
|
519
|
+
];
|
|
520
|
+
fs.writeFileSync(path.join(foundationsDir, "Brand.stories.mdx"), brandContent.join("\n"), "utf-8");
|
|
521
|
+
console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "Brand.stories.mdx")));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function main() {
|
|
526
|
+
if (!fs.existsSync(VDS_OUTPUT)) {
|
|
527
|
+
console.error("[VDS] vds-output.json not found. Run `npm run vds` first.");
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
const raw = fs.readFileSync(VDS_OUTPUT, "utf-8");
|
|
531
|
+
const data = JSON.parse(raw);
|
|
532
|
+
const components = Array.isArray(data.components) ? data.components : [];
|
|
533
|
+
const foundations = data.foundations || null;
|
|
534
|
+
|
|
535
|
+
const onlyName = process.argv[2] || null;
|
|
536
|
+
|
|
537
|
+
ensureDir(STORIES_DIR);
|
|
538
|
+
ensureDir(path.join(STORIES_DIR, "foundations"));
|
|
539
|
+
writeFoundationsMdx(foundations);
|
|
540
|
+
|
|
541
|
+
// Clear existing .stories.* files (except foundations/*.mdx) so only VDS-generated stories remain
|
|
542
|
+
try {
|
|
543
|
+
const existing = fs.readdirSync(STORIES_DIR);
|
|
544
|
+
for (const name of existing) {
|
|
545
|
+
if (name === "foundations") continue;
|
|
546
|
+
if (
|
|
547
|
+
name.endsWith(".stories.tsx") ||
|
|
548
|
+
name.endsWith(".stories.ts") ||
|
|
549
|
+
name.endsWith(".stories.jsx") ||
|
|
550
|
+
name.endsWith(".stories.js")
|
|
551
|
+
) {
|
|
552
|
+
fs.unlinkSync(path.join(STORIES_DIR, name));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} catch {
|
|
556
|
+
// ignore
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (const comp of components) {
|
|
560
|
+
const componentName = toSafeComponentName(comp.name, comp.file);
|
|
561
|
+
if (onlyName && componentName !== onlyName) continue;
|
|
562
|
+
if (SKIP_LIST.includes(componentName)) continue;
|
|
563
|
+
|
|
564
|
+
const storyFileName = `${componentName}.stories.tsx`;
|
|
565
|
+
const storyPath = path.join(STORIES_DIR, storyFileName);
|
|
566
|
+
const content = buildStoryFileContent(comp);
|
|
567
|
+
fs.writeFileSync(storyPath, content, "utf-8");
|
|
568
|
+
console.log(`[VDS] Wrote ${path.relative(PROJECT_ROOT, storyPath)}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
main();
|