vibe-design-system 2.5.24 → 2.5.26
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
|
@@ -205,9 +205,13 @@ function ensureStorybook(projectRoot) {
|
|
|
205
205
|
fs.writeFileSync(mainPath, STORYBOOK_MAIN_TS, "utf-8");
|
|
206
206
|
console.log("📝 .storybook/main.ts yazıldı.");
|
|
207
207
|
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
const hasPreview = ["preview.tsx", "preview.ts", "preview.jsx", "preview.js"].some((name) =>
|
|
209
|
+
fs.existsSync(path.join(storybookDir, name))
|
|
210
|
+
);
|
|
211
|
+
if (!hasPreview) {
|
|
212
|
+
fs.writeFileSync(path.join(storybookDir, "preview.ts"), STORYBOOK_PREVIEW_TS, "utf-8");
|
|
213
|
+
console.log("📝 .storybook/preview.ts yazıldı.");
|
|
214
|
+
}
|
|
211
215
|
}
|
|
212
216
|
|
|
213
217
|
// ADIM 3 — vds-core/ kopyala
|
|
@@ -324,6 +328,19 @@ function runStoryGenerator(projectRoot) {
|
|
|
324
328
|
if (r.status !== 0) process.exitCode = r.status ?? 1;
|
|
325
329
|
}
|
|
326
330
|
|
|
331
|
+
// ADIM 7b — Provider detection & Storybook decorator setup
|
|
332
|
+
function runSetupStorybookProviders(projectRoot) {
|
|
333
|
+
const setupPath = path.join(projectRoot, "vds-core", "setup-storybook-providers.mjs");
|
|
334
|
+
if (!fs.existsSync(setupPath)) return;
|
|
335
|
+
console.log("🔌 Storybook provider decorators ayarlanıyor...");
|
|
336
|
+
const r = spawnSync("node", [setupPath], {
|
|
337
|
+
cwd: projectRoot,
|
|
338
|
+
stdio: "inherit",
|
|
339
|
+
shell: false,
|
|
340
|
+
});
|
|
341
|
+
if (r.status !== 0) process.exitCode = r.status ?? 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
327
344
|
// ADIM 9 — Storybook örnek dosyalarını sil
|
|
328
345
|
function removeStorybookExamples(projectRoot) {
|
|
329
346
|
const storiesDir = path.join(projectRoot, "src", "stories");
|
|
@@ -377,6 +394,7 @@ removeStorybookExamples(projectRoot);
|
|
|
377
394
|
// ADIM 7
|
|
378
395
|
runScan(projectRoot);
|
|
379
396
|
runStoryGenerator(projectRoot);
|
|
397
|
+
runSetupStorybookProviders(projectRoot);
|
|
380
398
|
|
|
381
399
|
// ADIM 8
|
|
382
400
|
console.log("\n✅ VDS kuruldu!");
|
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.
|
|
@@ -316,9 +316,10 @@ function extractBrandAssets() {
|
|
|
316
316
|
return assets;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
/** 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). */
|
|
320
320
|
function extractLucideIconsUsed(srcDir) {
|
|
321
|
-
const
|
|
321
|
+
const allFiles = getAllTsxJsxInDir(srcDir);
|
|
322
|
+
const files = allFiles.filter((rel) => !/^stories[/\\]/.test(rel.replace(/\\/g, "/")));
|
|
322
323
|
const names = new Set();
|
|
323
324
|
const importRe = /import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/g;
|
|
324
325
|
for (const rel of files) {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VDS — Storybook provider detection and decorator setup.
|
|
4
|
+
* Run from project root: node vds-core/setup-storybook-providers.mjs
|
|
5
|
+
*
|
|
6
|
+
* 1. Scans src/ for context hooks (useTimer, useCircles, useSidebar, useAuth).
|
|
7
|
+
* 2. Finds which Provider each hook needs; locates export in project.
|
|
8
|
+
* 3. Injects/merges provider decorators into .storybook/preview.ts or .storybook/preview.tsx.
|
|
9
|
+
* 4. Adds warning comment to story files for components that use a hook whose provider was not found.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const PROJECT_ROOT = path.join(__dirname, "..");
|
|
17
|
+
const SRC_DIR = path.join(PROJECT_ROOT, "src");
|
|
18
|
+
const STORYBOOK_DIR = path.join(PROJECT_ROOT, ".storybook");
|
|
19
|
+
|
|
20
|
+
const HOOK_TO_PROVIDER = {
|
|
21
|
+
useTimer: "TimerProvider",
|
|
22
|
+
useCircles: "CirclesProvider",
|
|
23
|
+
useSidebar: "SidebarProvider",
|
|
24
|
+
useAuth: "AuthProvider",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const IGNORE_DIRS = new Set(["node_modules", "dist", ".next", "build", ".storybook", "stories"]);
|
|
28
|
+
|
|
29
|
+
function getAllSourceFiles(dir, baseDir = dir) {
|
|
30
|
+
if (!fs.existsSync(dir)) return [];
|
|
31
|
+
const out = [];
|
|
32
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
33
|
+
for (const e of entries) {
|
|
34
|
+
const full = path.join(dir, e.name);
|
|
35
|
+
const rel = path.relative(baseDir, full).replace(/\\/g, "/");
|
|
36
|
+
if (e.isDirectory()) {
|
|
37
|
+
if (IGNORE_DIRS.has(e.name)) continue;
|
|
38
|
+
out.push(...getAllSourceFiles(full, baseDir));
|
|
39
|
+
} else if (/\.(tsx?|jsx?)$/i.test(e.name)) {
|
|
40
|
+
out.push(rel);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function detectHooksInFile(content) {
|
|
47
|
+
const found = new Set();
|
|
48
|
+
for (const hook of Object.keys(HOOK_TO_PROVIDER)) {
|
|
49
|
+
if (new RegExp("\\b" + hook + "\\b").test(content)) found.add(hook);
|
|
50
|
+
}
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Map: relative path (src/...) -> Set of hook names */
|
|
55
|
+
function detectHooksUsedInProject(projectRoot) {
|
|
56
|
+
const srcDir = path.join(projectRoot, "src");
|
|
57
|
+
const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join("src", r));
|
|
58
|
+
const byFile = new Map();
|
|
59
|
+
for (const rel of files) {
|
|
60
|
+
const full = path.join(projectRoot, rel);
|
|
61
|
+
try {
|
|
62
|
+
const content = fs.readFileSync(full, "utf-8");
|
|
63
|
+
const hooks = detectHooksInFile(content);
|
|
64
|
+
if (hooks.size) byFile.set(rel, hooks);
|
|
65
|
+
} catch (_) {}
|
|
66
|
+
}
|
|
67
|
+
return byFile;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Find file that exports providerName (e.g. TimerProvider). Returns import path from project root for @/ alias: "contexts/TimerContext" */
|
|
71
|
+
function findProviderExportPath(projectRoot, providerName) {
|
|
72
|
+
const srcDir = path.join(projectRoot, "src");
|
|
73
|
+
const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join("src", r));
|
|
74
|
+
const exportRe = new RegExp(
|
|
75
|
+
"export\\s+(?:default\\s+)?(?:const|function|class)\\s+" + providerName + "\\b|export\\s*\\{[^}]*\\b" + providerName + "\\b[^}]*\\}"
|
|
76
|
+
);
|
|
77
|
+
for (const rel of files) {
|
|
78
|
+
const full = path.join(projectRoot, rel);
|
|
79
|
+
try {
|
|
80
|
+
const content = fs.readFileSync(full, "utf-8");
|
|
81
|
+
if (exportRe.test(content)) {
|
|
82
|
+
const withoutExt = rel.replace(/\.(tsx?|jsx?)$/i, "").replace(/^src\/?/, "");
|
|
83
|
+
return "@/" + withoutExt;
|
|
84
|
+
}
|
|
85
|
+
} catch (_) {}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getPreviewPath(projectRoot) {
|
|
91
|
+
const pTsx = path.join(projectRoot, ".storybook", "preview.tsx");
|
|
92
|
+
const pTs = path.join(projectRoot, ".storybook", "preview.ts");
|
|
93
|
+
const pJsx = path.join(projectRoot, ".storybook", "preview.jsx");
|
|
94
|
+
const pJs = path.join(projectRoot, ".storybook", "preview.js");
|
|
95
|
+
if (fs.existsSync(pTsx)) return pTsx;
|
|
96
|
+
if (fs.existsSync(pTs)) return pTs;
|
|
97
|
+
if (fs.existsSync(pJsx)) return pJsx;
|
|
98
|
+
if (fs.existsSync(pJs)) return pJs;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Collect all hooks used in project; return { providersToAdd: [{ name, importPath }], hooksWithoutProvider: Set, hooksByFile: Map } */
|
|
103
|
+
function collectProvidersAndWarnings(projectRoot) {
|
|
104
|
+
const hooksByFile = detectHooksUsedInProject(projectRoot);
|
|
105
|
+
const allHooksUsed = new Set();
|
|
106
|
+
for (const hooks of hooksByFile.values()) {
|
|
107
|
+
for (const h of hooks) allHooksUsed.add(h);
|
|
108
|
+
}
|
|
109
|
+
const providersToAdd = [];
|
|
110
|
+
const hooksWithoutProvider = new Set();
|
|
111
|
+
for (const hook of allHooksUsed) {
|
|
112
|
+
const providerName = HOOK_TO_PROVIDER[hook];
|
|
113
|
+
const importPath = findProviderExportPath(projectRoot, providerName);
|
|
114
|
+
if (importPath) {
|
|
115
|
+
if (!providersToAdd.some((p) => p.name === providerName)) {
|
|
116
|
+
providersToAdd.push({ name: providerName, importPath });
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
hooksWithoutProvider.add(hook);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { providersToAdd, hooksWithoutProvider, hooksByFile };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Build withProviders decorator using React.createElement (no JSX so preview can stay .ts) */
|
|
126
|
+
function buildWithProvidersCode(providers) {
|
|
127
|
+
if (providers.length === 0) return null;
|
|
128
|
+
let inner = "React.createElement(Story)";
|
|
129
|
+
for (let i = providers.length - 1; i >= 0; i--) {
|
|
130
|
+
inner = `React.createElement(${providers[i].name}, null, ${inner})`;
|
|
131
|
+
}
|
|
132
|
+
return `const withProviders = (Story: any) => ${inner};`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function injectProviderDecorators(projectRoot) {
|
|
136
|
+
const previewPath = getPreviewPath(projectRoot);
|
|
137
|
+
if (!previewPath) {
|
|
138
|
+
console.log("[VDS] .storybook/preview not found; skip provider setup.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { providersToAdd, hooksWithoutProvider, hooksByFile } = collectProvidersAndWarnings(projectRoot);
|
|
143
|
+
if (providersToAdd.length === 0 && hooksWithoutProvider.size === 0) return;
|
|
144
|
+
|
|
145
|
+
if (providersToAdd.length > 0) {
|
|
146
|
+
let content = fs.readFileSync(previewPath, "utf-8");
|
|
147
|
+
const alreadyHas = providersToAdd.every((p) => content.includes(p.name));
|
|
148
|
+
if (!alreadyHas) {
|
|
149
|
+
const importLines = [...new Map(providersToAdd.map((p) => [p.name, p])).values()]
|
|
150
|
+
.map((p) => `import { ${p.name} } from "${p.importPath}";`)
|
|
151
|
+
.join("\n");
|
|
152
|
+
const lastImportIdx = content.search(/\nimport\s+.+?;\s*$/m);
|
|
153
|
+
const insertAt = lastImportIdx >= 0 ? content.indexOf("\n", lastImportIdx) + 1 : content.indexOf("\n") + 1;
|
|
154
|
+
content = content.slice(0, insertAt) + importLines + "\n" + content.slice(insertAt);
|
|
155
|
+
}
|
|
156
|
+
if (!content.includes("withProviders")) {
|
|
157
|
+
if (!content.includes("import React")) {
|
|
158
|
+
const firstImport = content.match(/^import\s+/m);
|
|
159
|
+
const insertAt = firstImport ? content.indexOf(firstImport[0]) : 0;
|
|
160
|
+
content = content.slice(0, insertAt) + "import React from \"react\";\n" + content.slice(insertAt);
|
|
161
|
+
}
|
|
162
|
+
const withProvidersCode = buildWithProvidersCode(providersToAdd);
|
|
163
|
+
content = content.replace(/(\s*)(export default preview;?)/, withProvidersCode + "\n\n$1$2");
|
|
164
|
+
if (content.includes("decorators:")) {
|
|
165
|
+
content = content.replace(/decorators:\s*\[/, "decorators: [withProviders, ");
|
|
166
|
+
} else {
|
|
167
|
+
content = content.replace(/(const preview\s*[^=]*=\s*\{\s*)/, "$1\n decorators: [withProviders],\n ");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
fs.writeFileSync(previewPath, content, "utf-8");
|
|
171
|
+
console.log("[VDS] Storybook preview: added " + providersToAdd.map((p) => p.name).join(", "));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (hooksWithoutProvider.size > 0) {
|
|
175
|
+
const componentNameFromPath = (rel) => path.basename(rel, path.extname(rel));
|
|
176
|
+
const storiesDir = path.join(projectRoot, "src", "stories");
|
|
177
|
+
const storyRels = fs.existsSync(storiesDir) ? getAllSourceFiles(storiesDir, storiesDir) : [];
|
|
178
|
+
const storyByComponent = new Map();
|
|
179
|
+
for (const r of storyRels) {
|
|
180
|
+
if (!r.includes(".stories.")) continue;
|
|
181
|
+
const name = path.basename(r).replace(/\.stories\.(tsx?|jsx?)$/i, "");
|
|
182
|
+
storyByComponent.set(name, path.join(storiesDir, r));
|
|
183
|
+
}
|
|
184
|
+
for (const [fileRel, hooks] of hooksByFile) {
|
|
185
|
+
const missing = [...hooks].filter((h) => hooksWithoutProvider.has(h));
|
|
186
|
+
if (missing.length === 0) continue;
|
|
187
|
+
const compName = componentNameFromPath(fileRel);
|
|
188
|
+
const storyPath = storyByComponent.get(compName);
|
|
189
|
+
if (!storyPath || !fs.existsSync(storyPath)) continue;
|
|
190
|
+
let storyContent = fs.readFileSync(storyPath, "utf-8");
|
|
191
|
+
const comment = "// ⚠️ This component uses " + missing.join(", ") + " but provider not found in project\n";
|
|
192
|
+
if (storyContent.startsWith("// ⚠️")) continue;
|
|
193
|
+
storyContent = comment + storyContent;
|
|
194
|
+
fs.writeFileSync(storyPath, storyContent, "utf-8");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
200
|
+
if (isMain) {
|
|
201
|
+
injectProviderDecorators(PROJECT_ROOT);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { collectProvidersAndWarnings, injectProviderDecorators, getPreviewPath };
|
|
@@ -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
|
"};",
|