vibe-design-system 2.8.76 → 2.8.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.8.76",
3
+ "version": "2.8.79",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -1429,11 +1429,13 @@ function extractFoundations() {
1429
1429
  }
1430
1430
  }
1431
1431
 
1432
- const bodyMatch = css.match(/body\s*\{[^}]*font-family:\s*([^;]+);/s);
1432
+ // body { font-family } — supports nested @layer base { body { } } structure
1433
+ const bodyMatch = css.match(/\bbody\s*\{[\s\S]*?font-family:\s*([^;]+);/) ||
1434
+ css.match(/body\s*\{[^}]*font-family:\s*([^;]+);/s);
1433
1435
  if (bodyMatch) typography.body = bodyMatch[1].trim();
1434
1436
  const monoMatch = css.match(/code,\s*pre,\s*\.font-mono\s*\{[^}]*font-family:\s*([^;]+);/s);
1435
1437
  if (monoMatch) typography.mono = monoMatch[1].trim();
1436
- // Tailwind v4: CSS custom properties for fonts (--font-sans, --font-mono, --font-weight-*)
1438
+ // CSS custom properties for fonts (--font-sans, --font-mono, --font-display, etc.)
1437
1439
  const fontVarRe = /--font([\w-]*):\s*([^;\n]+);/g;
1438
1440
  let fvm;
1439
1441
  while ((fvm = fontVarRe.exec(css)) !== null) {
@@ -1441,6 +1443,18 @@ function extractFoundations() {
1441
1443
  const val = fvm[2].trim();
1442
1444
  if (!typography[key]) typography[key] = val;
1443
1445
  }
1446
+ // Resolve var(--font-*) references in body/mono to actual font names
1447
+ // Key construction must match the fontVarRe loop: --font-sans → "font-sans" → "fontSans"
1448
+ function resolveTypoVar(val) {
1449
+ if (!val || !val.startsWith("var(")) return val;
1450
+ const m = val.match(/var\(--font-([\w-]+)\)/);
1451
+ if (!m) return val;
1452
+ // prepend "-" so camelCase conversion matches: "font-" + "sans" → "fontSans"
1453
+ const key = `font-${m[1]}`.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
1454
+ return (typography[key] && !typography[key].startsWith("var(")) ? typography[key] : val;
1455
+ }
1456
+ if (typography.body) typography.body = resolveTypoVar(typography.body);
1457
+ if (typography.mono) typography.mono = resolveTypoVar(typography.mono);
1444
1458
 
1445
1459
  // ── Type Scale: extract --text-* font sizes + line-heights from compiled CSS
1446
1460
  const textSteps = ["xs","sm","base","lg","xl","2xl","3xl","4xl","5xl","6xl","7xl","8xl","9xl"];
@@ -1915,6 +1929,33 @@ function extractFoundations() {
1915
1929
  }
1916
1930
  }
1917
1931
 
1932
+ // ── Scan source files for arbitrary Tailwind [font-family:'FontName'] usage ──
1933
+ // Covers projects like Blurple where the font is only set via arbitrary Tailwind classes
1934
+ if (SRC_DIR && fs.existsSync(SRC_DIR)) {
1935
+ const arbFonts = new Set();
1936
+ const arbRe = /\[font-family:\s*'([^']+)'/g;
1937
+ function scanSrcForFonts(dir) {
1938
+ if (!fs.existsSync(dir)) return;
1939
+ let entries;
1940
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
1941
+ for (const e of entries) {
1942
+ const full = path.join(dir, e.name);
1943
+ if (e.isDirectory()) {
1944
+ if (!IGNORE_DIRS.includes(e.name)) scanSrcForFonts(full);
1945
+ } else if (/\.(tsx|jsx|ts|js|css)$/i.test(e.name)) {
1946
+ try {
1947
+ const content = fs.readFileSync(full, "utf-8");
1948
+ arbRe.lastIndex = 0;
1949
+ let m;
1950
+ while ((m = arbRe.exec(content)) !== null) arbFonts.add(m[1]);
1951
+ } catch (_) {}
1952
+ }
1953
+ }
1954
+ }
1955
+ scanSrcForFonts(SRC_DIR);
1956
+ if (arbFonts.size > 0) typography.arbitraryFonts = Array.from(arbFonts);
1957
+ }
1958
+
1918
1959
  return {
1919
1960
  colors: foundationsColors,
1920
1961
  typography,
@@ -2106,6 +2147,66 @@ function extractButtonUsage() {
2106
2147
  };
2107
2148
  }
2108
2149
 
2150
+ /**
2151
+ * Scan all non-component source files and count how many times each variant value
2152
+ * is explicitly used for each component (e.g. <Button variant="destructive">).
2153
+ * Returns a map: { [compName]: { [variantValue]: count } }
2154
+ * Only static string literals are counted; dynamic values are ignored.
2155
+ */
2156
+ function extractVariantUsage(componentResults) {
2157
+ if (!SRC_DIR || !fs.existsSync(SRC_DIR)) return;
2158
+ const compDir = COMPONENTS_DIR ? path.resolve(COMPONENTS_DIR) : null;
2159
+
2160
+ // Collect all .tsx/.jsx/.ts/.js files in SRC_DIR that are NOT inside COMPONENTS_DIR
2161
+ function collectNonCompFiles(dir, acc = []) {
2162
+ if (!fs.existsSync(dir)) return acc;
2163
+ let entries;
2164
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return acc; }
2165
+ for (const e of entries) {
2166
+ const full = path.join(dir, e.name);
2167
+ if (e.isDirectory()) {
2168
+ if (IGNORE_DIRS.includes(e.name)) continue;
2169
+ if (compDir && path.resolve(full) === compDir) continue; // skip components dir
2170
+ collectNonCompFiles(full, acc);
2171
+ } else if (e.isFile() && /\.(tsx|jsx|ts|js)$/i.test(e.name) &&
2172
+ !e.name.endsWith(".stories.tsx") && !e.name.endsWith(".stories.ts") &&
2173
+ !e.name.endsWith(".test.tsx") && !e.name.endsWith(".test.ts") &&
2174
+ !e.name.endsWith(".spec.tsx") && !e.name.endsWith(".spec.ts")) {
2175
+ acc.push(full);
2176
+ }
2177
+ }
2178
+ return acc;
2179
+ }
2180
+
2181
+ const nonCompFiles = collectNonCompFiles(SRC_DIR);
2182
+ if (nonCompFiles.length === 0) return;
2183
+
2184
+ for (const comp of componentResults) {
2185
+ const name = comp.name;
2186
+ // Match <ComponentName ...variant="X" or variant='X' anywhere on the same tag
2187
+ // Simple regex: looks for <Name followed (anywhere on the line) by variant="X"
2188
+ const variantRe = new RegExp(
2189
+ `<${name}[\\s\\S]{0,300}?\\bvariant=(?:"([^"]+)"|'([^']+)')`,
2190
+ "g"
2191
+ );
2192
+ const usage = {};
2193
+ for (const file of nonCompFiles) {
2194
+ let content;
2195
+ try { content = fs.readFileSync(file, "utf-8"); } catch (_) { continue; }
2196
+ // Reset lastIndex for global regex per file
2197
+ variantRe.lastIndex = 0;
2198
+ let m;
2199
+ while ((m = variantRe.exec(content)) !== null) {
2200
+ const val = m[1] || m[2];
2201
+ if (val) usage[val] = (usage[val] || 0) + 1;
2202
+ }
2203
+ }
2204
+ if (Object.keys(usage).length > 0) {
2205
+ comp.variantUsage = usage;
2206
+ }
2207
+ }
2208
+ }
2209
+
2109
2210
  function scan() {
2110
2211
  const relativeFiles = COMPONENTS_DIR ? getAllComponentFiles(COMPONENTS_DIR) : [];
2111
2212
  if (!COMPONENTS_DIR) {
@@ -2149,6 +2250,9 @@ function scan() {
2149
2250
  });
2150
2251
  }
2151
2252
  }
2253
+ // Attach real-world variant usage (which variant values are actually passed in pages/layouts)
2254
+ extractVariantUsage(results);
2255
+
2152
2256
  const foundations = extractFoundations();
2153
2257
  foundations.icons = extractLucideIconsUsed(SRC_DIR);
2154
2258
  foundations.brand = { assets: extractBrandAssets() };
@@ -679,7 +679,7 @@ function buildComponentVariantMap(source, effectiveProps) {
679
679
  /**
680
680
  * Generate multi-dimension stories: individual per variant, per size, per state, + AllVariants grid.
681
681
  */
682
- function buildMultiDimensionStories(variantMap, renderLine, childrenArgLine, defaultArgLines, componentName) {
682
+ function buildMultiDimensionStories(variantMap, renderLine, childrenArgLine, defaultArgLines, componentName, variantUsage) {
683
683
  const lines = [];
684
684
  const { dimensions, booleanStates, primaryDimension, secondaryDimensions } = variantMap;
685
685
 
@@ -690,10 +690,26 @@ function buildMultiDimensionStories(variantMap, renderLine, childrenArgLine, def
690
690
  return !match || !allDimNames.has(match[1]);
691
691
  });
692
692
 
693
+ // Pre-compute filtered primary values (used in sections A and D).
694
+ // variantUsage is comp.variantUsage: { [variantValue]: count } (only for "variant" prop)
695
+ // If usage data exists, keep only values that were actually used + the default value.
696
+ // If no usage data exists (old scans / dynamic-only usage) → keep all values.
697
+ let filteredPrimaryValues = null;
698
+ if (primaryDimension && dimensions[primaryDimension]) {
699
+ const _pd = dimensions[primaryDimension];
700
+ const _def = _pd.default || _pd.values[0];
701
+ // Only apply filtering when the primary dimension is "variant" (that's what we scan)
702
+ const _umap = (primaryDimension === "variant" && variantUsage && Object.keys(variantUsage).length > 0)
703
+ ? variantUsage : null;
704
+ filteredPrimaryValues = _umap
705
+ ? _pd.values.filter(v => v === _def || _umap[v])
706
+ : _pd.values;
707
+ }
708
+
693
709
  // --- A. Primary dimension stories (one per value) ---
694
710
  if (primaryDimension && dimensions[primaryDimension]) {
695
- const dim = dimensions[primaryDimension];
696
- const defaultValue = dim.default || dim.values[0];
711
+ const dim = { ...dimensions[primaryDimension], values: filteredPrimaryValues || dimensions[primaryDimension].values };
712
+ const defaultValue = dim.default || dimensions[primaryDimension].values[0];
697
713
 
698
714
  // Default story uses the default value
699
715
  lines.push(`export const Default: Story = {`);
@@ -706,7 +722,7 @@ function buildMultiDimensionStories(variantMap, renderLine, childrenArgLine, def
706
722
  lines.push(`};`);
707
723
  lines.push("");
708
724
 
709
- // Remaining values get their own stories
725
+ // Remaining values get their own stories (filtered by actual project usage)
710
726
  for (const val of dim.values) {
711
727
  if (val === defaultValue) continue;
712
728
  const storyName = capitalize(val);
@@ -778,9 +794,9 @@ function buildMultiDimensionStories(variantMap, renderLine, childrenArgLine, def
778
794
  lines.push("");
779
795
  }
780
796
 
781
- // --- D. AllVariants grid story (only if primary has 2+ values) ---
782
- if (primaryDimension && dimensions[primaryDimension] && dimensions[primaryDimension].values.length >= 2) {
783
- const primaryValues = dimensions[primaryDimension].values;
797
+ // --- D. AllVariants grid story (only if primary has 2+ used values) ---
798
+ if (primaryDimension && filteredPrimaryValues && filteredPrimaryValues.length >= 2) {
799
+ const primaryValues = filteredPrimaryValues;
784
800
  const secondaryDim = secondaryDimensions.find((d) => dimensions[d] && dimensions[d].values.length >= 2);
785
801
 
786
802
  lines.push(`export const AllVariants: Story = {`);
@@ -2116,7 +2132,8 @@ function buildStoryFileContent(comp) {
2116
2132
 
2117
2133
  if (hasDimensions) {
2118
2134
  const multiStories = buildMultiDimensionStories(
2119
- variantMap, renderLine, childrenArgLine, defaultArgLines, componentName
2135
+ variantMap, renderLine, childrenArgLine, defaultArgLines, componentName,
2136
+ comp.variantUsage || null
2120
2137
  );
2121
2138
  if (multiStories) {
2122
2139
  lines.push(multiStories);
@@ -2636,15 +2653,34 @@ function writeFoundationsStories(foundations, components) {
2636
2653
  const usedTextSizes = (foundations?.tokenUsage?.textSizes || []);
2637
2654
  const usedTextMap = Object.fromEntries(usedTextSizes.map(({ token, count }) => [token, count]));
2638
2655
 
2639
- // Font families
2656
+ // Font families — build from known keys, then add arbitrary fonts found in source
2640
2657
  const fontFamilyKeys = ["fontSans", "fontMono", "body", "heading", "tailwindSans", "tailwindMono"];
2641
- const familyRows = Object.entries(typo)
2642
- .filter(([k]) => fontFamilyKeys.includes(k) || k.toLowerCase().includes("font") && !k.toLowerCase().includes("weight") && k !== "typeScale" && k !== "fontSize")
2658
+ const rawFamilyRows = Object.entries(typo)
2659
+ .filter(([k]) => k !== "arbitraryFonts" &&
2660
+ (fontFamilyKeys.includes(k) || (k.toLowerCase().includes("font") && !k.toLowerCase().includes("weight") && k !== "typeScale" && k !== "fontSize")))
2643
2661
  .map(([k, v]) => {
2644
2662
  const val = typeof v === "object" && v !== null ? (v.family || v.value || "") : Array.isArray(v) ? v.join(", ") : String(v);
2645
2663
  return { token: k, value: val };
2646
2664
  })
2647
- .filter((r) => r.value);
2665
+ .filter((r) => r.value && !r.value.startsWith("var(")); // drop unresolved CSS var refs
2666
+
2667
+ // Add arbitrary [font-family:'Name'] fonts found in source files (e.g. Blurple's Urbanist)
2668
+ if (Array.isArray(typo.arbitraryFonts)) {
2669
+ for (const f of typo.arbitraryFonts) {
2670
+ if (!rawFamilyRows.some((r) => r.value.includes(f))) {
2671
+ rawFamilyRows.push({ token: "projectFont", value: `'${f}', sans-serif` });
2672
+ }
2673
+ }
2674
+ }
2675
+
2676
+ // Deduplicate: keep only the first entry per unique primary font name
2677
+ const seenFontNames = new Set();
2678
+ const familyRows = rawFamilyRows.filter((r) => {
2679
+ const primary = r.value.split(",")[0].trim().replace(/['"]/g, "").toLowerCase();
2680
+ if (seenFontNames.has(primary)) return false;
2681
+ seenFontNames.add(primary);
2682
+ return true;
2683
+ });
2648
2684
 
2649
2685
  // Font weights — check fontWeightScale (from twTheme.fontWeight), then legacy specific keys, then Tailwind defaults
2650
2686
  const weightKeys = ["fontWeightNormal", "fontWeightMedium", "fontWeightSemibold", "fontWeightBold"];
@@ -2667,7 +2703,9 @@ function writeFoundationsStories(foundations, components) {
2667
2703
  // Reverse type scale for display (largest first)
2668
2704
  const scaleDesc = [...typeScale].reverse();
2669
2705
 
2670
- const sansFamily = typo.fontSans || typo.tailwindSans || typo.body || "system-ui, sans-serif";
2706
+ const firstArbitraryFont = Array.isArray(typo.arbitraryFonts) && typo.arbitraryFonts.length > 0
2707
+ ? `'${typo.arbitraryFonts[0]}', sans-serif` : null;
2708
+ const sansFamily = typo.fontSans || typo.body || firstArbitraryFont || typo.tailwindSans || "system-ui, sans-serif";
2671
2709
 
2672
2710
  const typoContent = [
2673
2711
  "import React from \"react\";",
@@ -262,6 +262,89 @@ function injectViteConfigAliases(projectRoot) {
262
262
  console.log(`[VDS] Storybook adapt: injected vite aliases: ${missingAliases.map(([k]) => k).join(", ")}`);
263
263
  }
264
264
 
265
+ // ============================================================
266
+ // VERSION-PINNED IMPORT HANDLING (Bolt/Lovable/AI-generated)
267
+ // ============================================================
268
+
269
+ /** Detect if import specifier has a @version suffix (e.g. @radix-ui/react-slot@1.1.2) */
270
+ function isVersionPinned(spec) {
271
+ return /^(?:@[\w.-]+\/)?[\w.-]+@\d+\.\d+/.test(spec) && !spec.startsWith("@/");
272
+ }
273
+
274
+ /** Scan source files for any version-pinned imports */
275
+ function hasVersionPinnedImports() {
276
+ if (!fs.existsSync(SRC_DIR)) return false;
277
+ const files = getAllSourceFiles(SRC_DIR, SRC_DIR).map((r) => path.join(SRC_DIR, r));
278
+ for (const file of files) {
279
+ try {
280
+ const content = fs.readFileSync(file, "utf-8");
281
+ for (const spec of extractImports(content)) {
282
+ if (isVersionPinned(spec)) return true;
283
+ }
284
+ } catch (_) {}
285
+ }
286
+ return false;
287
+ }
288
+
289
+ /**
290
+ * Remove wrong version alias entries previously injected by a broken VDS run.
291
+ * These look like: "@radix-ui/react-slot@1.1.2": path.resolve(process.cwd(), "@radix-ui/react-slot"),
292
+ * They point to non-existent paths and must be replaced by the plugin approach.
293
+ */
294
+ function cleanupWrongVersionAliases(projectRoot) {
295
+ const mainPath = getMainPath(projectRoot);
296
+ if (!mainPath) return;
297
+ let content = fs.readFileSync(mainPath, "utf-8");
298
+ const before = content;
299
+ // Remove any alias line where key has @version suffix
300
+ content = content.replace(
301
+ /\n?\s*"(?:@[\w.-]+\/)?[\w.-]+@[\d.]+"\s*:\s*path\.resolve\(process\.cwd\(\),\s*"[^"]+"\),?/g,
302
+ ""
303
+ );
304
+ if (content !== before) {
305
+ fs.writeFileSync(mainPath, content, "utf-8");
306
+ console.log("[VDS] Storybook adapt: cleaned up incorrect version-pinned aliases from main.ts.");
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Inject a Vite plugin that strips @version suffixes from package import specifiers.
312
+ * e.g. @radix-ui/react-slot@1.1.2 → @radix-ui/react-slot (resolved from node_modules normally).
313
+ * Only added to viteFinal-based configs (not Next.js @storybook/nextjs).
314
+ */
315
+ function injectVersionStripPlugin(projectRoot) {
316
+ const mainPath = getMainPath(projectRoot);
317
+ if (!mainPath) return;
318
+ let content = fs.readFileSync(mainPath, "utf-8");
319
+
320
+ if (content.includes("vds-version-pin-strip")) return; // already injected
321
+ if (!content.includes("viteFinal")) return; // only for Vite-based storybook
322
+
323
+ // The plugin block to inject. Double-escaped backslashes become single in the written file.
324
+ const pluginBlock = ` plugins: [
325
+ {
326
+ name: "vds-version-pin-strip",
327
+ enforce: "pre",
328
+ async resolveId(id) {
329
+ // Strip @version suffix: @radix-ui/react-slot@1.1.2 → @radix-ui/react-slot
330
+ const m = id.match(/^((?:@[\\w.-]+\\/)?[\\w.-]+)@[\\d]+\\.[\\d]/);
331
+ if (m) return this.resolve(m[1], undefined, { skipSelf: true });
332
+ },
333
+ },
334
+ ],
335
+ `;
336
+
337
+ // Insert before the resolve: { block inside viteFinal
338
+ const resolvePattern = /([ \t]+)(resolve\s*:\s*\{)/;
339
+ if (resolvePattern.test(content)) {
340
+ content = content.replace(resolvePattern, (_, indent, resolveKw) =>
341
+ `${indent}${pluginBlock}${resolveKw}`
342
+ );
343
+ fs.writeFileSync(mainPath, content, "utf-8");
344
+ console.log("[VDS] Storybook adapt: injected vds-version-pin-strip Vite plugin.");
345
+ }
346
+ }
347
+
265
348
  function main() {
266
349
  const projectRoot = PROJECT_ROOT;
267
350
  if (!fs.existsSync(path.join(projectRoot, ".storybook"))) {
@@ -272,10 +355,16 @@ function main() {
272
355
  injectViteConfigAliases(projectRoot);
273
356
  reportUnresolvedImports(projectRoot);
274
357
  const problematic = collectProblematicImports(projectRoot);
275
- if (problematic.size === 0) return;
276
- writeMocks(projectRoot, problematic);
277
- injectAliases(projectRoot, problematic);
278
- console.log("[VDS] Storybook adapt: mocked", [...problematic].join(", "));
358
+ if (problematic.size > 0) {
359
+ writeMocks(projectRoot, problematic);
360
+ injectAliases(projectRoot, problematic);
361
+ console.log("[VDS] Storybook adapt: mocked", [...problematic].join(", "));
362
+ }
363
+ // Handle version-pinned imports (AI/Bolt/Lovable-generated projects with @pkg@version syntax)
364
+ if (hasVersionPinnedImports()) {
365
+ cleanupWrongVersionAliases(projectRoot);
366
+ injectVersionStripPlugin(projectRoot);
367
+ }
279
368
  }
280
369
 
281
370
  main();