vibe-design-system 2.8.64 → 2.8.66

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.64",
3
+ "version": "2.8.66",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -2525,7 +2525,7 @@ function writeFoundationsStories(foundations) {
2525
2525
  "",
2526
2526
  "export const Default: Story = {",
2527
2527
  " render: () => (",
2528
- " <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, background: \"#fff\", minHeight: 500, color: \"#111\" }}>",
2528
+ " <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, background: \"#fff\", minHeight: \"100vh\", width: \"100%\", color: \"#111\" }}>",
2529
2529
  " <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\", color: \"#111\" }}>Colors</h2>",
2530
2530
  " <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 40px\" }}>Design tokens defined in CSS variables — grouped by role</p>",
2531
2531
  "",
@@ -3815,11 +3815,193 @@ function writeComponentSuggestionsStory(componentSuggestions) {
3815
3815
  console.log("[VDS] Wrote " + path.relative(PROJECT_ROOT, path.join(foundationsDir, "ComponentSuggestions.stories.tsx")));
3816
3816
  }
3817
3817
 
3818
+ /**
3819
+ * Determines the visual preview shape and colors for a component in the inventory.
3820
+ * Returns an HTML string rendered at generation time using the project's resolved colors.
3821
+ */
3822
+ function buildInventoryPreviewHtml(compName, category, colorSwatches, tokens) {
3823
+ const n = compName.toLowerCase().replace(/[\s-]+/g, "");
3824
+
3825
+ // Extract colors from resolved swatches
3826
+ const bgSwatch = colorSwatches.find(s => /^bg-(?!gradient)/.test(s.token));
3827
+ const textSwatch = colorSwatches.find(s => /^text-/.test(s.token));
3828
+ const brdSwatch = colorSwatches.find(s => /^border-/.test(s.token));
3829
+ const accSwatch = colorSwatches.find(s => s.token === "bg-primary" || s.token === "border-primary" || s.token === "ring-primary");
3830
+
3831
+ // Fallback neutral palette (works on white canvas)
3832
+ const bg = bgSwatch?.hex || "#f3f4f6";
3833
+ const txt = textSwatch?.hex || "#374151";
3834
+ const brd = brdSwatch?.hex || "#e5e7eb";
3835
+ const acc = accSwatch?.hex || "#6366f1";
3836
+ // Lighten/darken helpers (inline, no deps)
3837
+ const hex2 = (h) => { // slightly lighter bg for header strips
3838
+ const n = parseInt(h.slice(1), 16);
3839
+ const r = Math.min(255, (n >> 16) + 30), g = Math.min(255, ((n >> 8) & 0xff) + 30), b = Math.min(255, (n & 0xff) + 30);
3840
+ return "#" + [r,g,b].map(x => x.toString(16).padStart(2,"0")).join("");
3841
+ };
3842
+
3843
+ // Border-radius from tokens
3844
+ const radTok = tokens.find(t => /^rounded/.test(t));
3845
+ const rad = radTok === "rounded-none" ? 0 : radTok === "rounded-sm" ? 2 : radTok === "rounded" ? 4
3846
+ : radTok === "rounded-md" ? 6 : radTok === "rounded-lg" ? 8 : radTok === "rounded-xl" ? 12
3847
+ : radTok === "rounded-2xl" ? 16 : radTok === "rounded-3xl" ? 24 : radTok === "rounded-full" ? 9999 : 4;
3848
+
3849
+ const wrap = (inner) => `<div style="width:100%;height:56px;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:0 10px;box-sizing:border-box">${inner}</div>`;
3850
+
3851
+ // ── Specific component shapes ────────────────────────────────────────────
3852
+ if (/^input$/.test(n) || (category === "Forms and Input" && /input/.test(n))) {
3853
+ return wrap(`<div style="width:100%;max-width:200px;background:${bg};border:1px solid ${brd};border-radius:${rad}px;padding:6px 10px;display:flex;align-items:center;gap:6px;box-sizing:border-box"><div style="flex:1;height:8px;background:${txt};opacity:0.3;border-radius:2px"></div></div>`);
3854
+ }
3855
+ if (/^textarea$/.test(n)) {
3856
+ return wrap(`<div style="width:100%;max-width:200px;background:${bg};border:1px solid ${brd};border-radius:${rad}px;padding:6px 10px;height:38px;box-sizing:border-box;display:flex;flex-direction:column;gap:4px"><div style="height:6px;background:${txt};opacity:0.3;border-radius:2px;width:80%"></div><div style="height:6px;background:${txt};opacity:0.2;border-radius:2px;width:50%"></div></div>`);
3857
+ }
3858
+ if (/^(button|submitbutton)$/.test(n) || (category === "Forms and Input" && /^button$/.test(n))) {
3859
+ return wrap(`<div style="display:inline-flex;align-items:center;background:${acc};color:#fff;border:1px solid ${acc};border-radius:${rad}px;padding:6px 18px;font-size:11px;font-weight:600;white-space:nowrap">${compName}</div>`);
3860
+ }
3861
+ if (/^toggle$/.test(n)) {
3862
+ return wrap(`<div style="display:flex;align-items:center;gap:6px"><div style="display:inline-flex;align-items:center;background:${acc};color:#fff;border-radius:${rad}px;padding:5px 12px;font-size:11px;font-weight:600">ON</div><div style="display:inline-flex;align-items:center;background:${bg};color:${txt};border:1px solid ${brd};border-radius:${rad}px;padding:5px 12px;font-size:11px">OFF</div></div>`);
3863
+ }
3864
+ if (/^(badge|chip|tag|lozenge|pill)$/.test(n) || category === "Status Indicators") {
3865
+ const bBg = acc + "20"; const bTxt = acc;
3866
+ return wrap(`<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:center"><span style="background:${bBg};color:${bTxt};border:1px solid ${brd};border-radius:${Math.max(rad,20)}px;padding:3px 10px;font-size:11px;font-weight:600">${compName}</span><span style="background:${brd}40;color:${txt};border:1px solid ${brd};border-radius:${Math.max(rad,20)}px;padding:3px 10px;font-size:11px">Default</span></div>`);
3867
+ }
3868
+ if (/^(checkbox|radio|radiogroup)$/.test(n)) {
3869
+ return wrap(`<div style="display:flex;flex-direction:column;gap:5px"><div style="display:flex;align-items:center;gap:6px"><div style="width:14px;height:14px;border-radius:3px;background:${acc};flex-shrink:0"></div><div style="height:6px;background:${txt};opacity:0.35;border-radius:2px;width:50px"></div></div><div style="display:flex;align-items:center;gap:6px"><div style="width:14px;height:14px;border-radius:3px;background:${bg};border:2px solid ${brd};flex-shrink:0"></div><div style="height:6px;background:${txt};opacity:0.25;border-radius:2px;width:40px"></div></div></div>`);
3870
+ }
3871
+ if (/^switch$/.test(n)) {
3872
+ return wrap(`<div style="display:flex;align-items:center;gap:12px"><div style="width:36px;height:20px;background:${acc};border-radius:9999px;position:relative;flex-shrink:0"><div style="position:absolute;right:2px;top:2px;width:16px;height:16px;background:#fff;border-radius:9999px"></div></div><div style="width:36px;height:20px;background:${brd};border-radius:9999px;position:relative;flex-shrink:0"><div style="position:absolute;left:2px;top:2px;width:16px;height:16px;background:#fff;border-radius:9999px"></div></div></div>`);
3873
+ }
3874
+ if (/^(select|combobox)$/.test(n)) {
3875
+ return wrap(`<div style="width:100%;max-width:190px;background:${bg};border:1px solid ${brd};border-radius:${rad}px;padding:6px 10px;display:flex;align-items:center;gap:4px;box-sizing:border-box"><div style="flex:1;height:8px;background:${txt};opacity:0.3;border-radius:2px"></div><div style="font-size:8px;color:${txt};opacity:0.5;flex-shrink:0">▾</div></div>`);
3876
+ }
3877
+ if (/^slider$/.test(n)) {
3878
+ return wrap(`<div style="width:80%;position:relative;padding:8px 0"><div style="height:4px;background:${brd};border-radius:9999px;position:relative"><div style="position:absolute;left:0;top:0;height:100%;width:60%;background:${acc};border-radius:9999px"></div><div style="position:absolute;left:calc(60% - 7px);top:50%;transform:translateY(-50%);width:14px;height:14px;background:${bg};border:2px solid ${acc};border-radius:9999px"></div></div></div>`);
3879
+ }
3880
+ if (/^skeleton$/.test(n) || category === "Loading") {
3881
+ return wrap(`<div style="display:flex;flex-direction:column;gap:5px;width:80%"><div style="height:10px;background:${bg === "#f3f4f6" ? "#e5e7eb" : hex2(bg)};border-radius:${rad}px;width:100%"></div><div style="height:8px;background:${bg === "#f3f4f6" ? "#e5e7eb" : hex2(bg)};border-radius:${rad}px;width:80%;opacity:0.7"></div><div style="height:8px;background:${bg === "#f3f4f6" ? "#e5e7eb" : hex2(bg)};border-radius:${rad}px;width:60%;opacity:0.5"></div></div>`);
3882
+ }
3883
+ if (/^(spinner|loader)$/.test(n)) {
3884
+ return wrap(`<div style="width:24px;height:24px;border-radius:9999px;border:3px solid ${brd};border-top-color:${acc};"></div>`);
3885
+ }
3886
+ if (/^(alert|banner|notification)$/.test(n) || category === "Messaging") {
3887
+ return wrap(`<div style="display:flex;align-items:center;gap:8px;background:${acc}15;border:1px solid ${acc}40;border-left:3px solid ${acc};border-radius:${rad}px;padding:6px 10px;width:100%;max-width:220px;box-sizing:border-box"><div style="flex:1;height:7px;background:${txt};opacity:0.3;border-radius:2px"></div></div>`);
3888
+ }
3889
+ if (/^toast$/.test(n)) {
3890
+ return wrap(`<div style="background:${bg};border:1px solid ${brd};border-radius:${rad}px;padding:8px 12px;width:100%;max-width:210px;box-sizing:border-box;display:flex;justify-content:space-between;align-items:center"><div style="height:7px;background:${txt};opacity:0.4;border-radius:2px;width:60%"></div><div style="font-size:9px;color:${txt};opacity:0.4">✕</div></div>`);
3891
+ }
3892
+ if (/^avatar$/.test(n) || /^(images and icons)$/i.test(category)) {
3893
+ return wrap(`<div style="display:flex;gap:8px;align-items:center"><div style="width:36px;height:36px;border-radius:9999px;background:${acc};display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:#fff;flex-shrink:0">${compName.slice(0,1).toUpperCase()}</div><div style="width:28px;height:28px;border-radius:9999px;background:${bg};border:1px solid ${brd};flex-shrink:0"></div></div>`);
3894
+ }
3895
+ if (/^(card|tile|panel)$/.test(n) || (category === "Layout and Structure" && /^card$/.test(n))) {
3896
+ return wrap(`<div style="background:${bg};border:1px solid ${brd};border-radius:${rad}px;padding:10px 12px;width:85%;box-sizing:border-box"><div style="height:7px;background:${txt};opacity:0.4;border-radius:2px;width:50%;margin-bottom:5px"></div><div style="height:5px;background:${txt};opacity:0.2;border-radius:2px;width:80%;margin-bottom:4px"></div><div style="height:5px;background:${txt};opacity:0.2;border-radius:2px;width:65%"></div></div>`);
3897
+ }
3898
+ if (/^(accordion|collapsible)$/.test(n)) {
3899
+ return wrap(`<div style="width:85%;box-sizing:border-box"><div style="background:${bg};border:1px solid ${brd};border-radius:${rad}px;padding:7px 12px;display:flex;justify-content:space-between;align-items:center"><div style="height:7px;background:${txt};opacity:0.4;border-radius:2px;width:50%"></div><div style="font-size:10px;color:${txt};opacity:0.5">▾</div></div></div>`);
3900
+ }
3901
+ if (/^(dialog|modal|sheet|alertdialog)$/.test(n) || category === "Overlays and Layering") {
3902
+ return wrap(`<div style="background:${bg};border:1px solid ${brd};border-radius:${rad}px;overflow:hidden;width:85%;box-sizing:border-box"><div style="background:${hex2(bg)};border-bottom:1px solid ${brd};padding:5px 10px;display:flex;justify-content:space-between;align-items:center"><div style="height:6px;background:${txt};opacity:0.5;border-radius:2px;width:50px"></div><div style="font-size:9px;color:${txt};opacity:0.3">✕</div></div><div style="padding:8px 10px;display:flex;flex-direction:column;gap:3px"><div style="height:5px;background:${txt};opacity:0.2;border-radius:2px;width:80%"></div><div style="height:5px;background:${txt};opacity:0.15;border-radius:2px;width:55%"></div></div></div>`);
3903
+ }
3904
+ if (/^drawer$/.test(n)) {
3905
+ return wrap(`<div style="background:${bg};border:1px solid ${brd};border-radius:${rad}px ${rad}px 0 0;width:85%;box-sizing:border-box"><div style="padding:8px 12px;border-bottom:1px solid ${brd};display:flex;align-items:center;gap:6px"><div style="height:6px;background:${txt};opacity:0.4;border-radius:2px;width:60%"></div></div><div style="padding:8px 12px;display:flex;flex-direction:column;gap:3px"><div style="height:5px;background:${txt};opacity:0.2;border-radius:2px;width:100%"></div></div></div>`);
3906
+ }
3907
+ if (/^(tabs|tab)$/.test(n)) {
3908
+ return wrap(`<div style="width:85%;box-sizing:border-box"><div style="display:flex;background:${bg};border-radius:${rad}px;padding:3px;gap:2px"><div style="flex:1;background:${bg === "#f3f4f6" ? "#fff" : hex2(bg)};border-radius:${Math.max(rad-2,2)}px;padding:4px 0;font-size:10px;font-weight:600;color:${txt};text-align:center">Tab 1</div><div style="flex:1;padding:4px 0;font-size:10px;color:${txt};opacity:0.5;text-align:center">Tab 2</div><div style="flex:1;padding:4px 0;font-size:10px;color:${txt};opacity:0.5;text-align:center">Tab 3</div></div></div>`);
3909
+ }
3910
+ if (/^(table|datatable)$/.test(n) || category === "Text and Data Display") {
3911
+ return wrap(`<div style="border:1px solid ${brd};border-radius:${rad}px;overflow:hidden;width:85%;box-sizing:border-box"><div style="display:flex;background:${hex2(bg)};border-bottom:1px solid ${brd};padding:4px 8px;gap:8px">${[1,1,1].map(()=>`<div style="flex:1;height:5px;background:${txt};opacity:0.3;border-radius:1px"></div>`).join("")}</div><div style="display:flex;background:${bg};padding:4px 8px;gap:8px">${[1,1,1].map(()=>`<div style="flex:1;height:5px;background:${txt};opacity:0.15;border-radius:1px"></div>`).join("")}</div><div style="display:flex;background:${bg};padding:4px 8px;gap:8px;opacity:0.7">${[1,1,1].map(()=>`<div style="flex:1;height:5px;background:${txt};opacity:0.15;border-radius:1px"></div>`).join("")}</div></div>`);
3912
+ }
3913
+ if (/^(sidebar|appsidebar)$/.test(n)) {
3914
+ return wrap(`<div style="display:flex;gap:6px;align-items:flex-start"><div style="background:${bg};border:1px solid ${brd};border-radius:${rad}px;width:50px;display:flex;flex-direction:column;gap:4px;padding:6px 4px"><div style="height:4px;background:${acc};border-radius:2px;width:80%;margin:0 auto"></div><div style="height:4px;background:${txt};opacity:0.25;border-radius:2px;width:70%;margin:0 auto"></div><div style="height:4px;background:${txt};opacity:0.25;border-radius:2px;width:60%;margin:0 auto"></div></div><div style="flex:1;background:${bg};border:1px solid ${brd};border-radius:${rad}px;padding:6px"><div style="height:5px;background:${txt};opacity:0.15;border-radius:2px;width:90%"></div></div></div>`);
3915
+ }
3916
+ if (/^(navbar|nav|navigation|breadcrumb|pagination|navigationmenu)$/.test(n) || /^Navigation$/.test(category)) {
3917
+ return wrap(`<div style="display:flex;align-items:center;gap:8px;background:${bg};border-bottom:1px solid ${brd};padding:6px 10px;width:100%;max-width:220px;box-sizing:border-box"><div style="height:6px;background:${acc};border-radius:2px;width:35px"></div><div style="height:6px;background:${txt};opacity:0.25;border-radius:2px;width:28px"></div><div style="height:6px;background:${txt};opacity:0.25;border-radius:2px;width:32px"></div></div>`);
3918
+ }
3919
+ if (/^(progress)$/.test(n)) {
3920
+ return wrap(`<div style="width:80%;box-sizing:border-box"><div style="height:6px;background:${brd};border-radius:9999px;overflow:hidden"><div style="height:100%;width:65%;background:${acc};border-radius:9999px"></div></div></div>`);
3921
+ }
3922
+ if (/^(dropdownmenu|contextmenu|menubar|menu)$/.test(n)) {
3923
+ return wrap(`<div style="background:${bg};border:1px solid ${brd};border-radius:${rad}px;overflow:hidden;width:70%;box-sizing:border-box">${[["60%","#a0a"],["40%",""],["55%",""]].map(([w,a])=>`<div style="padding:4px 10px;display:flex;align-items:center;gap:6px;${a?`background:${acc}20`:``}"><div style="flex:0 0 ${w};height:5px;background:${txt};opacity:${a?0.5:0.25};border-radius:1px"></div></div>`).join("")}</div>`);
3924
+ }
3925
+ if (/^(popover|tooltip|hovercard)$/.test(n)) {
3926
+ return wrap(`<div style="background:${bg};border:1px solid ${brd};border-radius:${rad}px;padding:8px 12px;width:75%;box-sizing:border-box;box-shadow:0 4px 12px rgba(0,0,0,0.1)"><div style="height:6px;background:${txt};opacity:0.3;border-radius:2px;width:60%;margin-bottom:4px"></div><div style="height:5px;background:${txt};opacity:0.2;border-radius:2px;width:80%"></div></div>`);
3927
+ }
3928
+ if (/^(resizable|splitpane)$/.test(n)) {
3929
+ return wrap(`<div style="display:flex;align-items:stretch;gap:0;width:85%;height:40px"><div style="flex:1;background:${bg};border:1px solid ${brd};border-radius:${rad}px 0 0 ${rad}px"></div><div style="width:5px;background:${brd};cursor:col-resize"></div><div style="flex:1;background:${bg};border:1px solid ${brd};border-left:none;border-radius:0 ${rad}px ${rad}px 0"></div></div>`);
3930
+ }
3931
+ if (/^(chart|pricechart|sparkline)$/.test(n)) {
3932
+ const pts = [22,18,28,15,24,12,20,16,26,10,22,18].map((y,x)=>`${x*14},${y}`).join(" ");
3933
+ return wrap(`<div style="width:85%;height:40px;position:relative"><svg viewBox="0 0 168 32" style="width:100%;height:100%"><polyline points="${pts}" fill="none" stroke="${acc}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>`);
3934
+ }
3935
+ if (/^(separator)$/.test(n)) {
3936
+ return wrap(`<div style="width:85%;height:1px;background:${brd}"></div>`);
3937
+ }
3938
+ // Generic page/section components → show token color stripe
3939
+ if (colorSwatches.length > 0) {
3940
+ const stripeW = Math.floor(100 / colorSwatches.length);
3941
+ const stripes = colorSwatches.map(s => `<div style="flex:1;background:${s.hex};height:20px"></div>`).join("");
3942
+ return wrap(`<div style="width:85%;display:flex;flex-direction:column;gap:5px"><div style="display:flex;border-radius:4px;overflow:hidden;height:12px">${stripes}</div><div style="height:5px;background:${txt};opacity:0.2;border-radius:2px;width:60%"></div><div style="height:5px;background:${txt};opacity:0.15;border-radius:2px;width:40%"></div></div>`);
3943
+ }
3944
+ // Absolute fallback: neutral box
3945
+ return wrap(`<div style="width:85%;height:28px;background:${bg};border:1px solid ${brd};border-radius:${rad}px"></div>`);
3946
+ }
3947
+
3818
3948
  /**
3819
3949
  * Generates Foundations/Component Inventory story.
3820
3950
  * Shows all project components classified by Atlassian-style functional category.
3821
3951
  * Every value is derived from vds-output.json — no placeholder content.
3822
3952
  */
3953
+ /**
3954
+ * Single-pass JSX usage scan: returns a Set of component names (PascalCase-normalised)
3955
+ * that actually appear as <Tag in the project's source files (excluding ui/ definitions
3956
+ * and story files). This is used to distinguish "used in project" from "installed only".
3957
+ */
3958
+ function buildComponentUsageSet(componentNames, projectRoot) {
3959
+ // Pre-compute pascal names for each component
3960
+ const patterns = componentNames.map(name => {
3961
+ const pascal = name
3962
+ .replace(/[-\s]+(.)/g, (_, c) => c.toUpperCase())
3963
+ .replace(/^(.)/, c => c.toUpperCase());
3964
+ return { name, pascal };
3965
+ });
3966
+
3967
+ const usedNames = new Set();
3968
+
3969
+ // Resolve the shadcn ui directory path to exclude component definition files
3970
+ // (e.g. dialog.tsx contains <DialogClose> which would falsely register Dialog as "used")
3971
+ const uiDefDir = path.resolve(path.join(projectRoot, COMPONENTS_REL_DIR));
3972
+
3973
+ function walkDir(dir) {
3974
+ let entries;
3975
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
3976
+ for (const entry of entries) {
3977
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
3978
+ const full = path.join(dir, entry.name);
3979
+ if (entry.isDirectory()) {
3980
+ // Skip: story dirs, storybook config, AND the shadcn/ui component definitions dir
3981
+ if (["stories", ".storybook"].includes(entry.name)) continue;
3982
+ if (path.resolve(full) === uiDefDir) continue; // shadcn ui/ — skip definitions
3983
+ walkDir(full);
3984
+ } else if (/\.(tsx|jsx)$/.test(entry.name) && !/\.stories\./.test(entry.name)) {
3985
+ let src;
3986
+ try { src = fs.readFileSync(full, "utf-8"); } catch { continue; }
3987
+ for (const { name, pascal } of patterns) {
3988
+ if (!usedNames.has(name) && src.includes(`<${pascal}`)) {
3989
+ usedNames.add(name);
3990
+ }
3991
+ }
3992
+ }
3993
+ }
3994
+ }
3995
+
3996
+ // Walk common source roots (excluding the component definition directories themselves)
3997
+ const srcRoots = ["src", "client/src", "app", "pages"]
3998
+ .map(d => path.join(projectRoot, d))
3999
+ .filter(d => fs.existsSync(d));
4000
+ for (const root of srcRoots) walkDir(root);
4001
+
4002
+ return usedNames;
4003
+ }
4004
+
3823
4005
  function writeComponentInventoryStory(components, foundations) {
3824
4006
  if (!Array.isArray(components) || components.length === 0) return;
3825
4007
  const foundationsDir = path.join(STORIES_DIR, "foundations");
@@ -3833,6 +4015,10 @@ function writeComponentInventoryStory(components, foundations) {
3833
4015
  const categorized = {}; // category → [{ name, group, tokenCount, propCount, colorSwatches }]
3834
4016
  const foundColors = (foundations && foundations.colors) ? foundations.colors : {};
3835
4017
 
4018
+ // Determine which components are actually used as JSX in the project source
4019
+ const componentNames = components.map(c => c.name);
4020
+ const usedSet = buildComponentUsageSet(componentNames, PROJECT_ROOT);
4021
+
3836
4022
  for (const comp of components) {
3837
4023
  const g = comp.group || "Components";
3838
4024
  const gLower = g.toLowerCase();
@@ -3860,19 +4046,21 @@ function writeComponentInventoryStory(components, foundations) {
3860
4046
  })
3861
4047
  .filter(s => s.hex); // only swatches with resolved hex
3862
4048
 
3863
- // Representative token fingerprint (spacing + typography, max 4, no color)
3864
- const tokenFingerprint = tokens
3865
- .filter(t => !/:/.test(t) && !/^(bg|text|border|ring|from|to|fill|stroke)-/.test(t))
3866
- .slice(0, 4);
4049
+ // Visual preview HTML (computed at generation time using project's resolved colors)
4050
+ const previewHtml = buildInventoryPreviewHtml(comp.name, category, colorSwatches, tokens.filter(t => !/:/.test(t)));
4051
+
4052
+ // Whether this component is actually used as a JSX tag anywhere in the project source
4053
+ const active = usedSet.has(comp.name);
3867
4054
 
3868
4055
  if (!categorized[category]) categorized[category] = [];
3869
4056
  categorized[category].push({
3870
- name: comp.name,
3871
- group: g,
3872
- tokenCount: tokens.filter(t => !/:/.test(t)).length,
3873
- propCount: props.length,
4057
+ name: comp.name,
4058
+ group: g,
4059
+ tokenCount: tokens.filter(t => !/:/.test(t)).length,
4060
+ propCount: props.length,
3874
4061
  colorSwatches,
3875
- tokenFingerprint,
4062
+ previewHtml,
4063
+ active,
3876
4064
  });
3877
4065
  }
3878
4066
 
@@ -3887,10 +4075,15 @@ function writeComponentInventoryStory(components, foundations) {
3887
4075
 
3888
4076
  const inventoryData = sortedCategories.map(cat => ({
3889
4077
  category: cat,
3890
- components: categorized[cat].sort((a, b) => a.name.localeCompare(b.name)),
4078
+ // Sort: active first, then alphabetical within each group
4079
+ components: categorized[cat].sort((a, b) => {
4080
+ if (a.active !== b.active) return a.active ? -1 : 1;
4081
+ return a.name.localeCompare(b.name);
4082
+ }),
3891
4083
  }));
3892
4084
 
3893
4085
  const totalComponents = components.length;
4086
+ const activeComponents = [...usedSet].length;
3894
4087
  const uniqueTokens = [...new Set(
3895
4088
  components.flatMap(c => Array.isArray(c.tokens) ? c.tokens.filter(t => !/:/.test(t)) : [])
3896
4089
  )].length;
@@ -3902,90 +4095,85 @@ function writeComponentInventoryStory(components, foundations) {
3902
4095
  `export default meta;`,
3903
4096
  `type Story = StoryObj;`,
3904
4097
  ``,
3905
- `const inventoryData: { category: string; components: { name: string; group: string; tokenCount: number; propCount: number; colorSwatches: { token: string; hex: string }[]; tokenFingerprint: string[] }[] }[] = ${JSON.stringify(inventoryData)};`,
4098
+ `const inventoryData: { category: string; components: { name: string; group: string; tokenCount: number; propCount: number; colorSwatches: { token: string; hex: string }[]; previewHtml: string; active: boolean }[] }[] = ${JSON.stringify(inventoryData)};`,
3906
4099
  `const totalComponents = ${totalComponents};`,
4100
+ `const activeComponents = ${activeComponents};`,
3907
4101
  `const uniqueTokens = ${uniqueTokens};`,
3908
4102
  ``,
3909
4103
  `export const Default: Story = {`,
3910
- ` render: () => (`,
3911
- ` <div style={{ padding: 32, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh", width: "100%" }}>`,
4104
+ ` render: () => {`,
4105
+ ` const [showUnused, setShowUnused] = (window as any).__vdsShowUnused !== undefined`,
4106
+ ` ? [(window as any).__vdsShowUnused, (v: boolean) => { (window as any).__vdsShowUnused = v; }]`,
4107
+ ` : [false, () => {}];`,
4108
+ ` const [expanded, setExpanded] = React.useState(false);`,
4109
+ ` const Card = ({ comp }: { comp: any }) => (`,
4110
+ ` <div style={{ border: \`1px solid \${comp.active ? "#e5e7eb" : "#f0f0f0"}\`, borderRadius: 10, overflow: "hidden", background: comp.active ? "#fff" : "#fafafa", display: "flex", flexDirection: "column" as any, boxShadow: comp.active ? "0 1px 3px rgba(0,0,0,0.05)" : "none", opacity: comp.active ? 1 : 0.55 }}>`,
4111
+ ` <div style={{ background: comp.active ? "#f8f9fa" : "#f5f5f5", borderBottom: "1px solid #f0f0f0", minHeight: 64, display: "flex", alignItems: "center", justifyContent: "center" }}`,
4112
+ ` dangerouslySetInnerHTML={{ __html: comp.previewHtml }}`,
4113
+ ` />`,
4114
+ ` <div style={{ padding: "10px 12px" }}>`,
4115
+ ` <div style={{ display: "flex", alignItems: "center", gap: 5, marginBottom: 5 }}>`,
4116
+ ` <span style={{ fontWeight: 700, fontSize: 13, color: comp.active ? "#111" : "#9ca3af" }}>{comp.name}</span>`,
4117
+ ` {comp.active && <span style={{ fontSize: 9, background: "#dcfce7", color: "#15803d", padding: "1px 5px", borderRadius: 4, fontWeight: 700, letterSpacing: "0.05em" }}>USED</span>}`,
4118
+ ` </div>`,
4119
+ ` <div style={{ display: "flex", gap: 4, flexWrap: "wrap" as any }}>`,
4120
+ ` {comp.tokenCount > 0 && (`,
4121
+ ` <span style={{ fontSize: 10, background: "#eff6ff", color: "#1d4ed8", padding: "2px 7px", borderRadius: 10, border: "1px solid #dbeafe" }}>{comp.tokenCount} tokens</span>`,
4122
+ ` )}`,
4123
+ ` {comp.colorSwatches.length > 0 && (`,
4124
+ ` <span style={{ fontSize: 10, background: "#fef9c3", color: "#713f12", padding: "2px 7px", borderRadius: 10, border: "1px solid #fde68a" }}>{comp.colorSwatches.length} colors</span>`,
4125
+ ` )}`,
4126
+ ` {!comp.active && (`,
4127
+ ` <span style={{ fontSize: 10, background: "#f3f4f6", color: "#9ca3af", padding: "2px 7px", borderRadius: 10, border: "1px solid #e5e7eb" }}>installed</span>`,
4128
+ ` )}`,
4129
+ ` </div>`,
4130
+ ` </div>`,
4131
+ ` </div>`,
4132
+ ` );`,
4133
+ ` return (`,
4134
+ ` <div style={{ padding: 32, background: "#f8f9fa", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh", width: "100%" }}>`,
3912
4135
  ` {/* Header */}`,
3913
4136
  ` <div style={{ marginBottom: 40 }}>`,
3914
4137
  ` <h2 style={{ fontSize: 28, fontWeight: 700, margin: "0 0 8px" }}>Component Inventory</h2>`,
3915
- ` <p style={{ fontSize: 14, color: "#6b7280", margin: "0 0 20px" }}>All components detected in this project, classified by function.</p>`,
3916
- ` <div style={{ display: "flex", gap: 12, flexWrap: "wrap" as any }}>`,
4138
+ ` <p style={{ fontSize: 14, color: "#6b7280", margin: "0 0 20px" }}>Components actually used in this project. <strong style={{ color: "#111" }}>USED</strong> = appears as a JSX tag in source files.</p>`,
4139
+ ` <div style={{ display: "flex", gap: 12, flexWrap: "wrap" as any, marginBottom: 20 }}>`,
3917
4140
  ` {[`,
3918
- ` { value: totalComponents.toString(), label: "Components" },`,
3919
- ` { value: inventoryData.length.toString(), label: "Categories" },`,
3920
- ` { value: uniqueTokens.toString(), label: "Unique Tokens" },`,
4141
+ ` { value: activeComponents.toString(), label: "Used in Project", bg: "#f0fdf4", fg: "#15803d", border: "#bbf7d0" },`,
4142
+ ` { value: (totalComponents - activeComponents).toString(), label: "Installed Only", bg: "#f9fafb", fg: "#6b7280", border: "#e5e7eb" },`,
4143
+ ` { value: uniqueTokens.toString(), label: "Unique Tokens", bg: "#eff6ff", fg: "#1d4ed8", border: "#dbeafe" },`,
3921
4144
  ` ].map(stat => (`,
3922
- ` <div key={stat.label} style={{ padding: "12px 20px", background: "#f3f4f6", borderRadius: 10, textAlign: "center" as any, minWidth: 90 }}>`,
3923
- ` <div style={{ fontSize: 26, fontWeight: 800, color: "#111", lineHeight: 1 }}>{stat.value}</div>`,
3924
- ` <div style={{ fontSize: 12, color: "#6b7280", marginTop: 4 }}>{stat.label}</div>`,
4145
+ ` <div key={stat.label} style={{ padding: "12px 20px", background: stat.bg, borderRadius: 10, textAlign: "center" as any, minWidth: 110, border: \`1px solid \${stat.border}\` }}>`,
4146
+ ` <div style={{ fontSize: 26, fontWeight: 800, color: stat.fg, lineHeight: 1 }}>{stat.value}</div>`,
4147
+ ` <div style={{ fontSize: 12, color: stat.fg, marginTop: 4, opacity: 0.8 }}>{stat.label}</div>`,
3925
4148
  ` </div>`,
3926
4149
  ` ))}`,
3927
4150
  ` </div>`,
4151
+ ` <button onClick={() => setExpanded(!expanded)} style={{ fontSize: 12, color: "#6b7280", background: "transparent", border: "1px solid #e5e7eb", borderRadius: 6, padding: "5px 12px", cursor: "pointer" }}>`,
4152
+ ` {expanded ? "Hide" : "Show"} installed-only components`,
4153
+ ` </button>`,
3928
4154
  ` </div>`,
3929
- ` {/* Category groups */}`,
3930
- ` {inventoryData.map(group => (`,
3931
- ` <div key={group.category} style={{ marginBottom: 40 }}>`,
3932
- ` <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16, paddingBottom: 10, borderBottom: "2px solid #f3f4f6" }}>`,
4155
+ ` {/* Category groups — active components first */}`,
4156
+ ` {inventoryData.map(group => {`,
4157
+ ` const active = group.components.filter((c: any) => c.active);`,
4158
+ ` const inactive = group.components.filter((c: any) => !c.active);`,
4159
+ ` if (active.length === 0 && !expanded) return null;`,
4160
+ ` return (`,
4161
+ ` <div key={group.category} style={{ marginBottom: 44 }}>`,
4162
+ ` <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16, paddingBottom: 10, borderBottom: "2px solid #e5e7eb" }}>`,
3933
4163
  ` <h3 style={{ margin: 0, fontSize: 15, fontWeight: 700, color: "#111" }}>{group.category}</h3>`,
3934
- ` <span style={{ fontSize: 12, background: "#e0e7ff", color: "#3730a3", padding: "2px 9px", borderRadius: 12, fontWeight: 600 }}>{group.components.length}</span>`,
4164
+ ` <span style={{ fontSize: 12, background: "#dcfce7", color: "#15803d", padding: "2px 9px", borderRadius: 12, fontWeight: 600 }}>{active.length} used</span>`,
4165
+ ` {inactive.length > 0 && <span style={{ fontSize: 12, background: "#f3f4f6", color: "#9ca3af", padding: "2px 9px", borderRadius: 12 }}>{inactive.length} installed</span>}`,
3935
4166
  ` </div>`,
3936
- ` <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(210px, 1fr))", gap: 12 }}>`,
3937
- ` {group.components.map(comp => (`,
3938
- ` <div key={comp.name} style={{ border: "1px solid #e5e7eb", borderRadius: 10, overflow: "hidden", background: "#fff", display: "flex", flexDirection: "column" as any }}>`,
3939
- ` {/* Color swatch strip — project-resolved colors from foundations */}`,
3940
- ` {comp.colorSwatches.length > 0 ? (`,
3941
- ` <div style={{ display: "flex", height: 8 }}>`,
3942
- ` {comp.colorSwatches.map((s: any) => (`,
3943
- ` <div key={s.token} title={s.token} style={{ flex: 1, background: s.hex }} />`,
3944
- ` ))}`,
3945
- ` </div>`,
3946
- ` ) : (`,
3947
- ` <div style={{ height: 4, background: "#f3f4f6" }} />`,
3948
- ` )}`,
3949
- ` <div style={{ padding: "12px 14px", flexGrow: 1 }}>`,
3950
- ` <div style={{ fontWeight: 700, fontSize: 13, color: "#111", marginBottom: 6 }}>{comp.name}</div>`,
3951
- ` {/* Token fingerprint pills */}`,
3952
- ` {comp.tokenFingerprint.length > 0 && (`,
3953
- ` <div style={{ display: "flex", gap: 4, flexWrap: "wrap" as any, marginBottom: 8 }}>`,
3954
- ` {comp.tokenFingerprint.map((t: string) => (`,
3955
- ` <code key={t} style={{ fontSize: 9, background: "#f3f4f6", color: "#6b7280", padding: "1px 5px", borderRadius: 4, border: "1px solid #e5e7eb" }}>{t}</code>`,
3956
- ` ))}`,
3957
- ` </div>`,
3958
- ` )}`,
3959
- ` <div style={{ display: "flex", gap: 5, flexWrap: "wrap" as any }}>`,
3960
- ` {comp.propCount > 0 && (`,
3961
- ` <span style={{ fontSize: 10, background: "#f3f4f6", color: "#6b7280", padding: "2px 7px", borderRadius: 10, border: "1px solid #e5e7eb" }}>`,
3962
- ` {comp.propCount} props`,
3963
- ` </span>`,
3964
- ` )}`,
3965
- ` {comp.tokenCount > 0 && (`,
3966
- ` <span style={{ fontSize: 10, background: "#eff6ff", color: "#1d4ed8", padding: "2px 7px", borderRadius: 10, border: "1px solid #dbeafe" }}>`,
3967
- ` {comp.tokenCount} tokens`,
3968
- ` </span>`,
3969
- ` )}`,
3970
- ` {comp.colorSwatches.length > 0 && (`,
3971
- ` <span style={{ fontSize: 10, background: "#fef9c3", color: "#713f12", padding: "2px 7px", borderRadius: 10, border: "1px solid #fde68a" }}>`,
3972
- ` {comp.colorSwatches.length} colors`,
3973
- ` </span>`,
3974
- ` )}`,
3975
- ` {comp.group && comp.group !== "shadcn" && comp.group !== "UI" && comp.group !== "Components" && (`,
3976
- ` <span style={{ fontSize: 10, background: "#f0fdf4", color: "#166534", padding: "2px 7px", borderRadius: 10, border: "1px solid #bbf7d0" }}>`,
3977
- ` {comp.group}`,
3978
- ` </span>`,
3979
- ` )}`,
3980
- ` </div>`,
3981
- ` </div>`,
3982
- ` </div>`,
3983
- ` ))}`,
4167
+ ` <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 12 }}>`,
4168
+ ` {active.map((comp: any) => <Card key={comp.name} comp={comp} />)}`,
4169
+ ` {expanded && inactive.map((comp: any) => <Card key={comp.name} comp={comp} />)}`,
3984
4170
  ` </div>`,
3985
4171
  ` </div>`,
3986
- ` ))}`,
4172
+ ` );`,
4173
+ ` })}`,
3987
4174
  ` </div>`,
3988
- ` ),`,
4175
+ ` );`,
4176
+ ` },`,
3989
4177
  `};`,
3990
4178
  ].join("\n");
3991
4179
 
@@ -4358,21 +4546,32 @@ function main() {
4358
4546
  }
4359
4547
  }
4360
4548
 
4549
+ // Auto-detect which shadcn/ui components are actually used as JSX in the project source.
4550
+ // This replaces the manual `includeComponents` list — VDS now self-discovers which
4551
+ // shadcn primitives need stories instead of requiring users to maintain a whitelist.
4552
+ const allCompNames = components.map(c => c.name);
4553
+ const autoUsedSet = buildComponentUsageSet(allCompNames, PROJECT_ROOT);
4554
+ console.log(`[VDS] Auto-detected ${autoUsedSet.size} components with active JSX usage`);
4555
+
4361
4556
  let writtenCount = 0;
4362
4557
  for (const comp of components) {
4363
4558
  const componentName = toSafeComponentName(comp.name, comp.file);
4364
- // Skip unclassified and shadcn/ui primitives (UI group) — they're documented at ui.shadcn.com
4365
- // Only project-specific module groups (Circles, Finance, Projects, Time, …) get stories
4366
4559
  const g = comp.group || "Components";
4367
- // Skip shadcn/ui primitives by default — they're documented at ui.shadcn.com
4368
- // Users can override with vds.config.js: includeGroups: ["shadcn"] or includeComponents: ["Button", "Badge"]
4560
+ const gLower = g.toLowerCase();
4561
+
4562
+ // For shadcn/ui primitives: only generate a story if the component is actually
4563
+ // used as a JSX tag (<Button />, <Badge />, etc.) somewhere in the project source.
4564
+ // Manual overrides: includeGroups (all from group) and includeComponents (named list).
4369
4565
  const includeGroups = VDS_CONFIG.includeGroups || [];
4370
4566
  const includeComponents = VDS_CONFIG.includeComponents || [];
4371
- const gLower = g.toLowerCase();
4567
+
4372
4568
  if (g === "Uncategorized" || gLower === "ui" || gLower === "shadcn") {
4373
4569
  const groupIncluded = includeGroups.some((ig) => ig.toLowerCase() === gLower);
4374
4570
  const compIncluded = includeComponents.includes(componentName);
4375
- if (!groupIncluded && !compIncluded) {
4571
+ // Auto-detection: is the component used as a JSX tag in the project?
4572
+ const autoUsed = autoUsedSet.has(comp.name) || autoUsedSet.has(componentName);
4573
+
4574
+ if (!groupIncluded && !compIncluded && !autoUsed) {
4376
4575
  // Clean up leftover story files for components that are now in the skip group
4377
4576
  const oldStory = path.join(STORIES_DIR, `${componentName}.stories.tsx`);
4378
4577
  if (fs.existsSync(oldStory)) {
@@ -4380,12 +4579,15 @@ function main() {
4380
4579
  const firstLine = fs.readFileSync(oldStory, "utf-8").split("\n")[0] || "";
4381
4580
  if (firstLine.includes("@vds-regenerate")) {
4382
4581
  fs.unlinkSync(oldStory);
4383
- console.log(`[VDS] Removed shadcn/ui story: ${componentName}.stories.tsx`);
4582
+ console.log(`[VDS] Removed unused shadcn story: ${componentName}.stories.tsx`);
4384
4583
  }
4385
4584
  } catch { /* ignore */ }
4386
4585
  }
4387
4586
  continue;
4388
4587
  }
4588
+ if (autoUsed && !compIncluded) {
4589
+ console.log(`[VDS] Auto-including ${componentName} (active JSX usage detected)`);
4590
+ }
4389
4591
  }
4390
4592
  if (onlyName && componentName !== onlyName) continue;
4391
4593