vibe-design-system 2.8.75 → 2.8.78

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.75",
3
+ "version": "2.8.78",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -1397,9 +1397,14 @@ function extractFoundations() {
1397
1397
  if (cssChunks.length > 0) {
1398
1398
  const css = cssChunks.join("\n");
1399
1399
 
1400
- const rootMatch = css.match(/:root\s*\{([\s\S]*?)\}/);
1401
- if (rootMatch) {
1402
- const rootVars = parseCssVarBlock(rootMatch[1]);
1400
+ // Collect ALL :root blocks (a file may have multiple, e.g. a small one early
1401
+ // and the main one inside @layer base { :root { } })
1402
+ const rootBlocks = [];
1403
+ const rootGlobalRe = /:root\s*\{([\s\S]*?)\}/g;
1404
+ let rootM;
1405
+ while ((rootM = rootGlobalRe.exec(css)) !== null) { rootBlocks.push(rootM[1]); }
1406
+ if (rootBlocks.length > 0) {
1407
+ const rootVars = parseCssVarBlock(rootBlocks.join("\n"));
1403
1408
  for (const [name, value] of Object.entries(rootVars)) {
1404
1409
  if (/\d+\s+\d+%/.test(value) || /\d+%/.test(value)) {
1405
1410
  const hsl = `hsl(${value})`;
@@ -1424,11 +1429,13 @@ function extractFoundations() {
1424
1429
  }
1425
1430
  }
1426
1431
 
1427
- 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);
1428
1435
  if (bodyMatch) typography.body = bodyMatch[1].trim();
1429
1436
  const monoMatch = css.match(/code,\s*pre,\s*\.font-mono\s*\{[^}]*font-family:\s*([^;]+);/s);
1430
1437
  if (monoMatch) typography.mono = monoMatch[1].trim();
1431
- // 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.)
1432
1439
  const fontVarRe = /--font([\w-]*):\s*([^;\n]+);/g;
1433
1440
  let fvm;
1434
1441
  while ((fvm = fontVarRe.exec(css)) !== null) {
@@ -1436,6 +1443,18 @@ function extractFoundations() {
1436
1443
  const val = fvm[2].trim();
1437
1444
  if (!typography[key]) typography[key] = val;
1438
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);
1439
1458
 
1440
1459
  // ── Type Scale: extract --text-* font sizes + line-heights from compiled CSS
1441
1460
  const textSteps = ["xs","sm","base","lg","xl","2xl","3xl","4xl","5xl","6xl","7xl","8xl","9xl"];
@@ -1910,6 +1929,33 @@ function extractFoundations() {
1910
1929
  }
1911
1930
  }
1912
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
+
1913
1959
  return {
1914
1960
  colors: foundationsColors,
1915
1961
  typography,
@@ -2101,6 +2147,66 @@ function extractButtonUsage() {
2101
2147
  };
2102
2148
  }
2103
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
+
2104
2210
  function scan() {
2105
2211
  const relativeFiles = COMPONENTS_DIR ? getAllComponentFiles(COMPONENTS_DIR) : [];
2106
2212
  if (!COMPONENTS_DIR) {
@@ -2144,6 +2250,9 @@ function scan() {
2144
2250
  });
2145
2251
  }
2146
2252
  }
2253
+ // Attach real-world variant usage (which variant values are actually passed in pages/layouts)
2254
+ extractVariantUsage(results);
2255
+
2147
2256
  const foundations = extractFoundations();
2148
2257
  foundations.icons = extractLucideIconsUsed(SRC_DIR);
2149
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);
@@ -2182,9 +2199,11 @@ function buildStoryFileContent(comp) {
2182
2199
  const resolvedColors = colorRaw.map(token => {
2183
2200
  const m = token.match(/^(?:bg|text|border|ring|from|to|fill|stroke)-(.+)$/);
2184
2201
  const key = m ? m[1] : null;
2185
- const entry = key ? foundColors[key] : null;
2202
+ // Strip opacity modifier (e.g. "muted/20" "muted") as a fallback
2203
+ const baseKey = key ? key.replace(/\/[\d.]+$/, "") : null;
2204
+ const entry = key ? (foundColors[key] || (baseKey !== key ? foundColors[baseKey] : null)) : null;
2186
2205
  const hex = entry?.hex && /^#[0-9a-fA-F]{3,8}$/.test(entry.hex) ? entry.hex : null;
2187
- return { token, hex, label: key };
2206
+ return { token, hex, label: baseKey || key };
2188
2207
  });
2189
2208
 
2190
2209
  const hasContent = resolvedColors.length > 0 || spacingRaw.length > 0 || typographyRaw.length > 0 || radiusRaw.length > 0 || animRaw.length > 0;
@@ -2634,15 +2653,34 @@ function writeFoundationsStories(foundations, components) {
2634
2653
  const usedTextSizes = (foundations?.tokenUsage?.textSizes || []);
2635
2654
  const usedTextMap = Object.fromEntries(usedTextSizes.map(({ token, count }) => [token, count]));
2636
2655
 
2637
- // Font families
2656
+ // Font families — build from known keys, then add arbitrary fonts found in source
2638
2657
  const fontFamilyKeys = ["fontSans", "fontMono", "body", "heading", "tailwindSans", "tailwindMono"];
2639
- const familyRows = Object.entries(typo)
2640
- .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")))
2641
2661
  .map(([k, v]) => {
2642
2662
  const val = typeof v === "object" && v !== null ? (v.family || v.value || "") : Array.isArray(v) ? v.join(", ") : String(v);
2643
2663
  return { token: k, value: val };
2644
2664
  })
2645
- .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
+ });
2646
2684
 
2647
2685
  // Font weights — check fontWeightScale (from twTheme.fontWeight), then legacy specific keys, then Tailwind defaults
2648
2686
  const weightKeys = ["fontWeightNormal", "fontWeightMedium", "fontWeightSemibold", "fontWeightBold"];
@@ -2665,7 +2703,9 @@ function writeFoundationsStories(foundations, components) {
2665
2703
  // Reverse type scale for display (largest first)
2666
2704
  const scaleDesc = [...typeScale].reverse();
2667
2705
 
2668
- 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";
2669
2709
 
2670
2710
  const typoContent = [
2671
2711
  "import React from \"react\";",