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
- * - Kullanıcı projesine dashboard dosyası kopyalanmaz.
5
- * - Sadece vds-core/scan.mjs ve vds-core/dashboard-server.mjs projeye kopyalanır.
6
- * - package.json'a vds, vds:watch, vds:dashboard scriptleri eklenir.
7
- * - Kurulum sonrası otomatik tarama çalıştırılır.
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.4.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
- } else {
67
- console.error("vds-core-template/dashboard-server.mjs bulunamadı.");
68
- process.exit(1);
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
- console.log("✅ Kurulum tamamlandı. Dashboard: npm run vds:dashboard ile başlatın; terminalde adres gösterilir.");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "1.8.4",
3
+ "version": "1.9.2",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 (instead of a placeholder):
27
+ To see **live renders** of components in the dashboard (isolated, no app chrome):
28
28
 
29
- 1. Add the preview route to your app (e.g. in the same router):
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 copy VdsPreview.tsx from node_modules/vibe-design-system/vds-core-template/
31
+ import VdsPreview from "./vds-core/VdsPreview"; // or from node_modules/vibe-design-system/vds-core-template/
32
32
  // ...
33
- <Route path="/vds-preview" element={<VdsPreview />} />
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
- * Add to your router, e.g.:
4
- * <Route path="/vds-preview" element={<VdsPreview />} />
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();