vibe-design-system 2.9.0 → 2.9.1

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
@@ -24,6 +24,7 @@ const TEMPLATE_DIR = path.join(INSTALLER_ROOT, "vds-core-template");
24
24
  const STORYBOOK_MAIN_TS = `import type { StorybookConfig } from "@storybook/react-vite";
25
25
  import { mergeConfig } from "vite";
26
26
  import path from "path";
27
+ import fs from "fs";
27
28
 
28
29
  const config: StorybookConfig = {
29
30
  stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
@@ -33,10 +34,48 @@ const config: StorybookConfig = {
33
34
  options: {},
34
35
  },
35
36
  async viteFinal(config) {
37
+ // Phase C2 — read tsconfig.json paths and inject as Vite aliases
38
+ const extraAliases = (() => {
39
+ try {
40
+ const tsConfigPaths = [
41
+ path.resolve(process.cwd(), "tsconfig.json"),
42
+ path.resolve(process.cwd(), "tsconfig.app.json"),
43
+ ];
44
+ for (const tcp of tsConfigPaths) {
45
+ if (!fs.existsSync(tcp)) continue;
46
+ const raw = JSON.parse(fs.readFileSync(tcp, "utf-8").replace(/\\/\\/[^\\n]*/g, "").replace(/,(\\s*[}\\]])/g, "$1"));
47
+ const paths = raw?.compilerOptions?.paths || {};
48
+ const aliases: Record<string, string> = {};
49
+ for (const [alias, targets] of Object.entries(paths) as [string, string[]][]) {
50
+ const cleanAlias = alias.replace(/\\/\\*$/, "");
51
+ const target = targets[0]?.replace(/\\/\\*$/, "") || "";
52
+ if (cleanAlias && target && cleanAlias !== "@") {
53
+ aliases[cleanAlias] = path.resolve(process.cwd(), target);
54
+ }
55
+ }
56
+ if (Object.keys(aliases).length > 0) return aliases;
57
+ }
58
+ } catch (_) {}
59
+ return {};
60
+ })();
36
61
  return mergeConfig(config, {
62
+ plugins: [
63
+ {
64
+ // Mock figma:asset/* imports — returns empty string so components render without crashing in Storybook
65
+ name: "vds-figma-asset-mock",
66
+ enforce: "pre" as const,
67
+ resolveId(id: string) {
68
+ if (id.startsWith("figma:asset")) return "\\0vds-figma-asset-mock";
69
+ },
70
+ load(id: string) {
71
+ if (id === "\\0vds-figma-asset-mock") return "export default '';";
72
+ },
73
+ },
74
+ ],
37
75
  resolve: {
38
76
  alias: {
39
77
  "@": path.resolve(process.cwd(), "src"),
78
+ ...extraAliases,
40
79
  },
41
80
  },
42
81
  });
@@ -338,6 +377,7 @@ export default config;
338
377
  return `import type { StorybookConfig } from "@storybook/react-vite";
339
378
  import { mergeConfig } from "vite";
340
379
  import path from "path";
380
+ import fs from "fs";
341
381
 
342
382
  const config: StorybookConfig = {
343
383
  stories: [
@@ -358,8 +398,8 @@ const config: StorybookConfig = {
358
398
  path.resolve(process.cwd(), "${srcPrefix}", "..", "tsconfig.json"),
359
399
  ];
360
400
  for (const tcp of tsConfigPaths) {
361
- if (!require("fs").existsSync(tcp)) continue;
362
- const raw = JSON.parse(require("fs").readFileSync(tcp, "utf-8").replace(/\/\/[^\n]*/g, "").replace(/,(\s*[}\]])/g, "$1"));
401
+ if (!fs.existsSync(tcp)) continue;
402
+ const raw = JSON.parse(fs.readFileSync(tcp, "utf-8").replace(/\/\/[^\n]*/g, "").replace(/,(\s*[}\]])/g, "$1"));
363
403
  const paths = raw?.compilerOptions?.paths || {};
364
404
  const aliases: Record<string, string> = {};
365
405
  for (const [alias, targets] of Object.entries(paths) as [string, string[]][]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.9.0",
3
+ "version": "2.9.1",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -1145,6 +1145,8 @@ function extractTailwindTokens(content) {
1145
1145
  /className\s*=\s*\{\s*["'`]([^"'`]+)["'`]/g,
1146
1146
  /cn\s*\(\s*["'`]([^"'`]+)["'`]/g,
1147
1147
  /cva\s*\(\s*["'`]([^"'`]+)["'`]/g,
1148
+ // CVA variant values: captures `default: "bg-x text-y"`, `destructive: "border-x..."` etc.
1149
+ /\b\w+\s*:\s*["'`]([a-zA-Z0-9_\-\/\s\[\]&>:%.]+)["'`]/g,
1148
1150
  /["'`]([a-zA-Z0-9_\-\/\s\[\]&:%.]+(?:hover|focus|active|disabled|sm|md|lg|xl|2xl|dark:)[a-zA-Z0-9_\-\/\s\[\]&:%.]*)["'`]/g,
1149
1151
  ];
1150
1152
  for (const re of patterns) {
@@ -2470,6 +2472,26 @@ function isComplexPageComponent(content) {
2470
2472
  return true;
2471
2473
  }
2472
2474
 
2475
+ /**
2476
+ * Extract top-level (no indentation) PascalCase component names from a source file.
2477
+ * Used for isPageComponent files to find internal sub-components that can be auto-exported.
2478
+ * Only matches declarations at column 0 (^) to exclude nested arrow functions.
2479
+ */
2480
+ function extractTopLevelInternalComponentNames(content) {
2481
+ // Match only unindented `const ComponentName = (` or `const ComponentName: React.FC = (`
2482
+ const re = /^(?:export\s+)?const\s+([A-Z][A-Za-z0-9]+)\s*[:=]\s*(?:\(|\bReact\.)/gm;
2483
+ const names = [];
2484
+ const seen = new Set();
2485
+ let m;
2486
+ while ((m = re.exec(content)) !== null) {
2487
+ if (!seen.has(m[1])) {
2488
+ seen.add(m[1]);
2489
+ names.push(m[1]);
2490
+ }
2491
+ }
2492
+ return names;
2493
+ }
2494
+
2473
2495
  function scan() {
2474
2496
  const relativeFiles = COMPONENTS_DIR ? getAllComponentFiles(COMPONENTS_DIR) : [];
2475
2497
  if (!COMPONENTS_DIR) {
@@ -2495,7 +2517,10 @@ function scan() {
2495
2517
  }
2496
2518
  const tokens = extractTailwindTokens(content);
2497
2519
  const isPageComponent = isComplexPageComponent(content);
2498
- const comp = { file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true } : {}) };
2520
+ const internalComponentNames = isPageComponent
2521
+ ? extractTopLevelInternalComponentNames(content).filter(n => n !== name)
2522
+ : undefined;
2523
+ const comp = { file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true, internalComponentNames } : {}) };
2499
2524
  // Phase D — CSS Module tokens
2500
2525
  const cssModuleTokens = extractCssModuleTokens(COMPONENTS_DIR ? path.join(COMPONENTS_DIR, rel) : null);
2501
2526
  if (cssModuleTokens) comp.cssModuleTokens = cssModuleTokens;
@@ -2208,8 +2208,11 @@ function buildStoryFileContent(comp) {
2208
2208
  lines.push(` component: ComponentRef,`);
2209
2209
  // SECTION: no props/args → autodocs tries to render React.lazy without Suspense → useRef crash
2210
2210
  if (profile !== "SECTION") lines.push(` tags: ["autodocs"],`);
2211
- // Center small components (VARIANT, WRAPPER, CONFIGURED, SAFE) to prevent vertical stretching
2212
- if (profile !== "SECTION") lines.push(` parameters: { layout: "centered" },`);
2211
+ // Center small components; fullscreen for complex page-level components
2212
+ if (profile !== "SECTION") {
2213
+ const layout = comp.isPageComponent ? "fullscreen" : "centered";
2214
+ lines.push(` parameters: { layout: "${layout}" },`);
2215
+ }
2213
2216
  // Wrap with QueryClientProvider for components that use @tanstack/react-query hooks
2214
2217
  if (needsQueryClient) {
2215
2218
  lines.push(` decorators: [(Story: any) => React.createElement(QueryClientProvider, { client: _queryClient }, React.createElement(Story))],`);
@@ -5200,11 +5203,8 @@ function main() {
5200
5203
  const storyFileName = `${componentName}.stories.tsx`;
5201
5204
  const storyPath = path.join(STORIES_DIR, storyFileName);
5202
5205
  if (SKIP_LIST.includes(componentName)) continue;
5203
- // Skip complex page-level components detected by scan (500+ lines, 4+ inline sub-components)
5204
- if (comp.isPageComponent) {
5205
- console.log(`[VDS] ${componentName} → skipped (complex page component — add to extraSkipList to suppress this message)`);
5206
- continue;
5207
- }
5206
+ // Complex page-level components (500+ lines, 4+ inline sub-components) — generate fullscreen story instead of skipping
5207
+ // (Previously skipped; now a simplified fullscreen story is generated so they appear in Storybook)
5208
5208
  const requiredCount = Array.isArray(comp.props) ? comp.props.filter((p) => p.required === true).length : 0;
5209
5209
  if (requiredCount > 3) {
5210
5210
  console.log(`[VDS] ${componentName} → skipped (${requiredCount} required props — too complex to auto-generate)`);
@@ -5224,6 +5224,12 @@ function main() {
5224
5224
  writtenCount++;
5225
5225
  }
5226
5226
 
5227
+ // Phase I — Internal component sub-stories for isPageComponent files
5228
+ for (const comp of components) {
5229
+ if (!comp.isPageComponent || !Array.isArray(comp.internalComponentNames) || comp.internalComponentNames.length === 0) continue;
5230
+ generateInternalComponentStories(comp);
5231
+ }
5232
+
5227
5233
  // Summary
5228
5234
  if (writtenCount === 0 && components.length > 0) {
5229
5235
  const hasShadcnGroup = components.some(c => {
@@ -5240,5 +5246,264 @@ function main() {
5240
5246
  }
5241
5247
  }
5242
5248
 
5249
+ // ─── Phase I: Internal Component Sub-Story Engine ─────────────────────────────
5250
+ /**
5251
+ * Adds `export` to top-level internal component declarations in a source file.
5252
+ * Non-destructive: only adds `export` keyword, no logic changes.
5253
+ * Returns the list of names that were newly exported.
5254
+ */
5255
+ function autoExportInternalComponents(sourceFilePath, names) {
5256
+ let src = fs.readFileSync(sourceFilePath, "utf-8");
5257
+ const newly = [];
5258
+ for (const n of names) {
5259
+ // Already exported? skip
5260
+ if (new RegExp(`^export\\s+const\\s+${n}\\b`, "m").test(src)) continue;
5261
+ // Exists as unexported top-level const?
5262
+ const re = new RegExp(`^(const\\s+${n}\\b)`, "m");
5263
+ if (!re.test(src)) continue;
5264
+ src = src.replace(re, "export $1");
5265
+ newly.push(n);
5266
+ }
5267
+ if (newly.length > 0) fs.writeFileSync(sourceFilePath, src, "utf-8");
5268
+ return newly;
5269
+ }
5270
+
5271
+ /**
5272
+ * Parse top-level relative import statements (local files only).
5273
+ * Returns [{importPath, symbols}]
5274
+ */
5275
+ function parseLocalDataImports(content) {
5276
+ const re = /^import\s*\{([^}]+)\}\s*from\s*["'](\.[^"']+)["']/gm;
5277
+ const results = [];
5278
+ let m;
5279
+ while ((m = re.exec(content)) !== null) {
5280
+ const symbols = m[1].split(",").map(s => s.trim().replace(/\s+as\s+\w+$/, "").trim()).filter(Boolean);
5281
+ results.push({ importPath: m[2], symbols });
5282
+ }
5283
+ return results;
5284
+ }
5285
+
5286
+ /**
5287
+ * Detect named exports from a data file: returns { exportName → 'array' | 'object' | 'scalar' }
5288
+ */
5289
+ function parseDataFileExports(filePath) {
5290
+ if (!fs.existsSync(filePath)) return {};
5291
+ const src = fs.readFileSync(filePath, "utf-8");
5292
+ const result = {};
5293
+ const re = /^export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=:]/gm;
5294
+ let m;
5295
+ while ((m = re.exec(src)) !== null) {
5296
+ const name = m[1];
5297
+ const afterIdx = src.indexOf("=", m.index) + 1;
5298
+ const afterSlice = src.slice(afterIdx, afterIdx + 10).trimStart();
5299
+ result[name] = afterSlice.startsWith("[") ? "array" : afterSlice.startsWith("{") ? "object" : "scalar";
5300
+ }
5301
+ return result;
5302
+ }
5303
+
5304
+ /**
5305
+ * Extract inline TypeScript prop types for a named component from source content.
5306
+ * Handles: `const Foo = ({ a, b }: { a: boolean; b: string }) =>`
5307
+ * Handles nested object types (e.g., `data: { subStation: SubStation; } | null`)
5308
+ * Returns [{name, type}]
5309
+ */
5310
+ function extractComponentProps(content, componentName) {
5311
+ const esc = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5312
+ const declRe = new RegExp(`(?:export\\s+)?const\\s+${esc}\\s*[:=]\\s*\\(`, "g");
5313
+ const declMatch = declRe.exec(content);
5314
+ if (!declMatch) return [];
5315
+ const slice = content.slice(declMatch.index, declMatch.index + 5000);
5316
+ // Find `}: {` — start of inline type annotation
5317
+ const typeHeaderIdx = slice.search(/\}\s*:\s*\{/);
5318
+ if (typeHeaderIdx === -1) return [];
5319
+ const braceStart = slice.indexOf("{", typeHeaderIdx);
5320
+ // Walk forward tracking brace depth to find the matching closing brace
5321
+ let depth = 0, braceEnd = -1;
5322
+ for (let i = braceStart; i < slice.length; i++) {
5323
+ if (slice[i] === "{") depth++;
5324
+ else if (slice[i] === "}") { depth--; if (depth === 0) { braceEnd = i; break; } }
5325
+ }
5326
+ if (braceEnd === -1) return [];
5327
+ const typeBody = slice.slice(braceStart + 1, braceEnd);
5328
+ // Smart prop parser: tracks depth so `;` inside nested types is skipped
5329
+ const props = [];
5330
+ let i = 0;
5331
+ while (i < typeBody.length) {
5332
+ while (i < typeBody.length && /\s/.test(typeBody[i])) i++;
5333
+ if (i >= typeBody.length) break;
5334
+ const nameMatch = typeBody.slice(i).match(/^(\w+)\??(\s*:\s*)/);
5335
+ if (!nameMatch) { i++; continue; }
5336
+ const propName = nameMatch[1];
5337
+ i += nameMatch[0].length;
5338
+ let typeDepth = 0;
5339
+ const typeStartIdx = i;
5340
+ while (i < typeBody.length) {
5341
+ const ch = typeBody[i];
5342
+ if ("{([<".includes(ch)) typeDepth++;
5343
+ // `>` only closes angle brackets (generics), not `=>` arrow functions
5344
+ else if ("})]".includes(ch) || (ch === ">" && typeDepth > 0)) typeDepth--;
5345
+ else if (ch === ";" && typeDepth === 0) break;
5346
+ i++;
5347
+ }
5348
+ const type = typeBody.slice(typeStartIdx, i).trim();
5349
+ if (propName && type && !propName.startsWith("//") && propName !== "readonly") {
5350
+ props.push({ name: propName, type });
5351
+ }
5352
+ i++; // skip `;`
5353
+ }
5354
+ return props;
5355
+ }
5356
+
5357
+ /**
5358
+ * Generate a mock value string for a TypeScript type, using available data exports.
5359
+ */
5360
+ function mockValueForType(type, localExports) {
5361
+ const t = type.trim();
5362
+ if (t === "boolean") return "false";
5363
+ if (t === "string") return '""';
5364
+ if (t === "number") return "0";
5365
+ if (t === "null" || t === "undefined") return "null";
5366
+ if (t.startsWith("() =>") || t === "VoidFunction") return "() => {}";
5367
+ if (t.startsWith("(") && t.includes("=>")) return "() => {}";
5368
+ // Inline object type → null (complex; user fills it in)
5369
+ if (t.startsWith("{")) return "null";
5370
+ // Union ending in null/undefined → null
5371
+ if (t.endsWith("| null") || t.endsWith("| undefined") || t.startsWith("null |")) return "null";
5372
+ // String literal union → pick first quoted value
5373
+ const firstLiteral = t.match(/["']([^"']+)["']/);
5374
+ if (firstLiteral && (t.startsWith('"') || t.startsWith("'"))) return `"${firstLiteral[1]}"`;
5375
+
5376
+ // Try matching named type to available array exports
5377
+ // Strip common suffixes to get the "base" name (e.g., RouteDef→route, SubStation→substation)
5378
+ const typeLower = t.toLowerCase().replace(/def$|id$|key$|type$|interface$|kind$/i, "");
5379
+ if (typeLower.length >= 3) {
5380
+ for (const [expName, expKind] of Object.entries(localExports)) {
5381
+ if (expKind !== "array") continue;
5382
+ const expBase = expName.toLowerCase().replace(/_/g, "").replace(/s$/, "").replace(/data$/, "");
5383
+ // Direct match: RouteDef → ROUTES (routedef→rout vs routes→rout)
5384
+ if (expBase.slice(0, 4) === typeLower.slice(0, 4) || typeLower.slice(0, 4) === expBase.slice(0, 4)) {
5385
+ return `${expName}[0]`;
5386
+ }
5387
+ }
5388
+ }
5389
+ return "undefined as any";
5390
+ }
5391
+
5392
+ /**
5393
+ * Build the story file content for a single internal component.
5394
+ */
5395
+ function buildInternalComponentStoryContent(compName, parentImportPath, dataImportPath, dataSymbols, localExports, props, parentComp) {
5396
+ const lines = [];
5397
+ lines.push(`// @vds-regenerate — VDS auto-generated. Remove this line to prevent overwrite.`);
5398
+ lines.push(`import React from "react";`);
5399
+ lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
5400
+ lines.push(`import { ${compName} } from "${parentImportPath}";`);
5401
+ if (dataImportPath && dataSymbols.length > 0) {
5402
+ lines.push(`import { ${dataSymbols.join(", ")} } from "${dataImportPath}";`);
5403
+ }
5404
+ lines.push(``);
5405
+ lines.push(`/**`);
5406
+ lines.push(` * **${compName}** — sub-component of ${parentComp.name}.`);
5407
+ lines.push(` * Auto-exported by VDS for isolated Storybook documentation.`);
5408
+ lines.push(` */`);
5409
+ lines.push(`const meta: Meta<typeof ${compName}> = {`);
5410
+ lines.push(` title: "${parentComp.group || "Screens"}/${parentComp.name}/${compName}",`);
5411
+ lines.push(` component: ${compName},`);
5412
+ lines.push(` parameters: {`);
5413
+ lines.push(` layout: "centered",`);
5414
+ lines.push(` backgrounds: { default: "dark" },`);
5415
+ lines.push(` },`);
5416
+ lines.push(`};`);
5417
+ lines.push(`export default meta;`);
5418
+ lines.push(`type Story = StoryObj<typeof meta>;`);
5419
+ lines.push(``);
5420
+
5421
+ if (props.length > 0) {
5422
+ const argLines = props.map(p => ` ${p.name}: ${mockValueForType(p.type, localExports)},`).join("\n");
5423
+ lines.push(`export const Default: Story = {`);
5424
+ lines.push(` args: {`);
5425
+ lines.push(argLines);
5426
+ lines.push(` },`);
5427
+ lines.push(`};`);
5428
+ } else {
5429
+ lines.push(`export const Default: Story = {};`);
5430
+ }
5431
+ return lines.join("\n") + "\n";
5432
+ }
5433
+
5434
+ /**
5435
+ * Main orchestrator: auto-exports internal components + generates individual story files.
5436
+ * Called for each comp with isPageComponent: true.
5437
+ */
5438
+ function generateInternalComponentStories(comp) {
5439
+ const names = comp.internalComponentNames || [];
5440
+ if (names.length === 0) return;
5441
+
5442
+ // Locate the source file (COMPONENTS_REL_DIR is a module-level variable set in main())
5443
+ const sourceFile = path.join(PROJECT_ROOT, COMPONENTS_REL_DIR, comp.file);
5444
+ if (!fs.existsSync(sourceFile)) return;
5445
+
5446
+ const sourceContent = fs.readFileSync(sourceFile, "utf-8");
5447
+
5448
+ // Step 1: Auto-export internal components
5449
+ const newly = autoExportInternalComponents(sourceFile, names);
5450
+ if (newly.length > 0) {
5451
+ console.log(`[VDS] ${comp.name} → auto-exported ${newly.length} sub-components: ${newly.join(", ")}`);
5452
+ }
5453
+
5454
+ // Step 2: Find best local data import (most array-typed exports)
5455
+ const localImports = parseLocalDataImports(sourceContent);
5456
+ let bestImportPath = null;
5457
+ let bestSymbols = [];
5458
+ let bestExports = {};
5459
+ for (const li of localImports) {
5460
+ const resolved = path.resolve(path.dirname(sourceFile), li.importPath);
5461
+ const exts = ["", ".ts", ".tsx", ".js", ".jsx"];
5462
+ let found = null;
5463
+ for (const ext of exts) {
5464
+ const p = resolved + ext;
5465
+ if (fs.existsSync(p)) { found = p; break; }
5466
+ }
5467
+ if (!found) continue;
5468
+ const exports = parseDataFileExports(found);
5469
+ const arrayCount = Object.values(exports).filter(k => k === "array").length;
5470
+ if (arrayCount > Object.values(bestExports).filter(k => k === "array").length) {
5471
+ bestImportPath = li.importPath;
5472
+ bestSymbols = li.symbols;
5473
+ bestExports = exports;
5474
+ }
5475
+ }
5476
+
5477
+ // Step 3: Compute import paths relative to STORIES_DIR
5478
+ const storiesRelToSource = path.relative(path.dirname(sourceFile), path.join(PROJECT_ROOT, "src", "stories"));
5479
+ const sourceRelFromStories = path.relative(
5480
+ path.join(PROJECT_ROOT, "src", "stories"),
5481
+ sourceFile.replace(/\.(tsx?|jsx?)$/, "")
5482
+ ).replace(/\\/g, "/");
5483
+ const parentImportPath = sourceRelFromStories.startsWith(".") ? sourceRelFromStories : "./" + sourceRelFromStories;
5484
+
5485
+ let dataImportFromStories = null;
5486
+ if (bestImportPath) {
5487
+ const dataAbsolute = path.resolve(path.dirname(sourceFile), bestImportPath);
5488
+ dataImportFromStories = path.relative(
5489
+ path.join(PROJECT_ROOT, "src", "stories"),
5490
+ dataAbsolute
5491
+ ).replace(/\\/g, "/").replace(/\.(tsx?|jsx?)$/, "");
5492
+ if (!dataImportFromStories.startsWith(".")) dataImportFromStories = "./" + dataImportFromStories;
5493
+ }
5494
+
5495
+ // Step 4: Generate individual story files
5496
+ for (const name of names) {
5497
+ const storyFile = path.join(STORIES_DIR, `${name}.stories.tsx`);
5498
+ if (fs.existsSync(storyFile)) continue; // Never overwrite existing stories
5499
+ const props = extractComponentProps(sourceContent, name);
5500
+ const content = buildInternalComponentStoryContent(
5501
+ name, parentImportPath, dataImportFromStories, bestSymbols, bestExports, props, comp
5502
+ );
5503
+ fs.writeFileSync(storyFile, content, "utf-8");
5504
+ console.log(`[VDS] Wrote ${path.relative(PROJECT_ROOT, storyFile)} (sub-component of ${comp.name})`);
5505
+ }
5506
+ }
5507
+
5243
5508
  main();
5244
5509