vibe-design-system 2.8.67 → 2.8.69

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.67",
3
+ "version": "2.8.69",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -4018,133 +4018,236 @@ function writeComponentInventoryStory(components, foundations) {
4018
4018
  const foundationsDir = path.join(STORIES_DIR, "foundations");
4019
4019
  ensureDir(foundationsDir);
4020
4020
 
4021
- // Build per-component data
4022
4021
  const CATEGORY_ORDER = [
4023
4022
  "Forms and Input", "Status Indicators", "Navigation", "Overlays and Layering",
4024
4023
  "Loading", "Messaging", "Images and Icons", "Layout and Structure", "Text and Data Display",
4025
4024
  ];
4026
- const categorized = {}; // category → [{ name, group, tokenCount, propCount, colorSwatches }]
4025
+ const categorized = {};
4027
4026
  const foundColors = (foundations && foundations.colors) ? foundations.colors : {};
4028
4027
 
4029
- // Determine which components are actually used as JSX in the project source
4030
4028
  const componentNames = components.map(c => c.name);
4031
4029
  const usedSet = buildComponentUsageSet(componentNames, PROJECT_ROOT);
4032
4030
 
4031
+ // ── Build import info for every component ──────────────────────────────────
4032
+ const atRoot = path.dirname(COMPONENTS_REL_DIR); // "client/src" or "src"
4033
+ const compBase = path.basename(COMPONENTS_REL_DIR); // "components"
4034
+
4035
+ function toPascalLocal(name) {
4036
+ return name.replace(/[-\s]+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, c => c.toUpperCase());
4037
+ }
4038
+
4039
+ const importInfoMap = {}; // comp.name → { identifier, importAlias, isDefault }
4040
+ const seenAliases = new Set();
4041
+ const orderedImports = []; // deduplicated
4042
+
4033
4043
  for (const comp of components) {
4034
- const g = comp.group || "Components";
4035
- const gLower = g.toLowerCase();
4044
+ if (!comp.file) continue;
4045
+ const identifier = toPascalLocal(comp.name);
4046
+ const fileNoExt = comp.file.replace(/\.(tsx|ts|jsx|js)$/, '');
4047
+ const posixNoExt = fileNoExt.replace(/\\/g, '/');
4048
+
4049
+ // Derive @/ alias
4050
+ const absFromRoot = path.join(PROJECT_ROOT, comp.file);
4051
+ const isProjectRelative = fs.existsSync(absFromRoot);
4052
+ let importAlias;
4053
+ if (isProjectRelative) {
4054
+ const rel = path.posix.relative(atRoot, posixNoExt);
4055
+ importAlias = rel.startsWith('..') ? `@/${compBase}/${posixNoExt}` : `@/${rel}`;
4056
+ } else {
4057
+ importAlias = `@/${compBase}/${posixNoExt}`;
4058
+ }
4059
+
4060
+ // Detect default vs named export
4061
+ let isDefault = false;
4062
+ try {
4063
+ const srcPath = isProjectRelative
4064
+ ? absFromRoot
4065
+ : path.join(PROJECT_ROOT, COMPONENTS_REL_DIR, comp.file);
4066
+ isDefault = /export\s+default\b/.test(fs.readFileSync(srcPath, 'utf-8'));
4067
+ } catch { /* keep false */ }
4068
+
4069
+ importInfoMap[comp.name] = { identifier, importAlias, isDefault };
4070
+
4071
+ if (!seenAliases.has(importAlias)) {
4072
+ seenAliases.add(importAlias);
4073
+ orderedImports.push({ identifier, importAlias, isDefault });
4074
+ }
4075
+ }
4076
+
4077
+ // Does the project use @tanstack/react-query?
4078
+ let needsQC = false;
4079
+ try {
4080
+ const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf-8'));
4081
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
4082
+ needsQC = '@tanstack/react-query' in deps;
4083
+ } catch { /* ignore */ }
4084
+
4085
+ // Preview scale per category
4086
+ const COMPACT_CATS = new Set(["Status Indicators"]);
4087
+ const MEDIUM_CATS = new Set(["Forms and Input", "Images and Icons", "Loading"]);
4088
+
4089
+ // ── Per-component metadata ─────────────────────────────────────────────────
4090
+ for (const comp of components) {
4091
+ const g = comp.group || "Components";
4036
4092
  const tokens = Array.isArray(comp.tokens) ? comp.tokens : [];
4037
4093
  const props = Array.isArray(comp.props) ? comp.props : [];
4038
4094
  const semantic = classifyComponent(comp.name, tokens, props);
4039
4095
  const category = semantic || g;
4040
4096
 
4041
- // Resolve color tokens to hex swatches from foundations.colors (max 6)
4042
4097
  const colorSwatches = tokens
4043
4098
  .filter(t => !/:/.test(t) &&
4044
4099
  /^(bg|text|border|ring|from|to|fill|stroke)-/.test(t) &&
4045
- // Exclude border-side shorthands (b, t, l, r, x, y) and alignment/size text-* tokens
4046
4100
  !/^border-[btlrxy]$/.test(t) &&
4047
4101
  !/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\d|left|right|center|justify|start|end|wrap|nowrap|balance|pretty|clip|ellipsis)/.test(t))
4048
4102
  .slice(0, 6)
4049
4103
  .map(token => {
4050
4104
  const m = token.match(/^(?:bg|text|border|ring|from|to|fill|stroke)-(.+)$/);
4051
4105
  let key = m ? m[1] : null;
4052
- // Strip opacity modifier: "muted/20" → "muted", "primary/5" → "primary"
4053
4106
  if (key) key = key.replace(/\/\d+$/, "");
4054
4107
  const entry = key ? foundColors[key] : null;
4055
4108
  const hex = entry?.hex && /^#[0-9a-fA-F]{3,8}$/.test(entry.hex) ? entry.hex : null;
4056
4109
  return { token, hex };
4057
4110
  })
4058
- .filter(s => s.hex); // only swatches with resolved hex
4111
+ .filter(s => s.hex);
4059
4112
 
4060
- // Visual preview HTML (computed at generation time using project's resolved colors)
4061
- const previewHtml = buildInventoryPreviewHtml(comp.name, category, colorSwatches, tokens.filter(t => !/:/.test(t)));
4062
-
4063
- // Whether this component is actually used as a JSX tag anywhere in the project source
4064
- const active = usedSet.has(comp.name);
4113
+ const previewHtml = buildInventoryPreviewHtml(comp.name, category, colorSwatches, tokens.filter(t => !/:/.test(t)));
4114
+ const active = usedSet.has(comp.name);
4115
+ const previewScale = COMPACT_CATS.has(category) ? 1.0 : MEDIUM_CATS.has(category) ? 0.65 : 0.3;
4065
4116
 
4066
4117
  if (!categorized[category]) categorized[category] = [];
4067
4118
  categorized[category].push({
4068
- name: comp.name,
4069
- group: g,
4070
- tokenCount: tokens.filter(t => !/:/.test(t)).length,
4071
- propCount: props.length,
4072
- colorSwatches,
4073
- previewHtml,
4074
- active,
4119
+ name: comp.name, group: g,
4120
+ tokenCount: tokens.filter(t => !/:/.test(t)).length,
4121
+ propCount: props.length,
4122
+ colorSwatches, previewHtml, previewScale, active,
4075
4123
  });
4076
4124
  }
4077
4125
 
4078
4126
  const sortedCategories = Object.keys(categorized).sort((a, b) => {
4079
- const ai = CATEGORY_ORDER.indexOf(a);
4080
- const bi = CATEGORY_ORDER.indexOf(b);
4127
+ const ai = CATEGORY_ORDER.indexOf(a), bi = CATEGORY_ORDER.indexOf(b);
4081
4128
  if (ai >= 0 && bi >= 0) return ai - bi;
4082
- if (ai >= 0) return -1;
4083
- if (bi >= 0) return 1;
4084
- return a.localeCompare(b);
4129
+ return (ai >= 0) ? -1 : (bi >= 0) ? 1 : a.localeCompare(b);
4085
4130
  });
4086
4131
 
4087
4132
  const inventoryData = sortedCategories.map(cat => ({
4088
4133
  category: cat,
4089
- // Sort: active first, then alphabetical within each group
4090
4134
  components: categorized[cat].sort((a, b) => {
4091
4135
  if (a.active !== b.active) return a.active ? -1 : 1;
4092
4136
  return a.name.localeCompare(b.name);
4093
4137
  }),
4094
4138
  }));
4095
4139
 
4096
- const totalComponents = components.length;
4140
+ const totalComponents = components.length;
4097
4141
  const activeComponents = [...usedSet].length;
4098
- const uniqueTokens = [...new Set(
4142
+ const uniqueTokens = [...new Set(
4099
4143
  components.flatMap(c => Array.isArray(c.tokens) ? c.tokens.filter(t => !/:/.test(t)) : [])
4100
4144
  )].length;
4101
4145
 
4146
+ // ── Build import lines ─────────────────────────────────────────────────────
4147
+ const importLines = orderedImports.map(imp =>
4148
+ imp.isDefault
4149
+ ? `import ${imp.identifier} from "${imp.importAlias}";`
4150
+ : `import { ${imp.identifier} } from "${imp.importAlias}";`
4151
+ );
4152
+
4153
+ // ── PREVIEWS map entries ───────────────────────────────────────────────────
4154
+ const TEXT_CHILD_NAMES = new Set(['button','badge','toggle','label','link','chip','tag','pill']);
4155
+ const previewEntries = components
4156
+ .filter(comp => importInfoMap[comp.name])
4157
+ .map(comp => {
4158
+ const { identifier } = importInfoMap[comp.name];
4159
+ const hasText = TEXT_CHILD_NAMES.has(identifier.toLowerCase());
4160
+ const call = hasText
4161
+ ? `() => React.createElement(${identifier} as any, null, "${comp.name}")`
4162
+ : `() => React.createElement(${identifier} as any)`;
4163
+ return ` ${JSON.stringify(comp.name)}: ${call}`;
4164
+ });
4165
+
4166
+ // ── Generate story file ────────────────────────────────────────────────────
4102
4167
  const content = [
4103
4168
  `import React from "react";`,
4104
4169
  `import type { Meta, StoryObj } from "@storybook/react";`,
4170
+ ...(needsQC ? [`import { QueryClient, QueryClientProvider } from "@tanstack/react-query";`] : []),
4171
+ ...importLines,
4105
4172
  ``,
4106
4173
  `const meta = { title: "Foundations/Component Inventory", parameters: { layout: "fullscreen" } } satisfies Meta;`,
4107
4174
  `export default meta;`,
4108
4175
  `type Story = StoryObj;`,
4109
4176
  ``,
4110
- `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)};`,
4177
+ // ErrorBoundary catches broken component renders and falls back to shape HTML
4178
+ `class _EB extends React.Component<{ children: React.ReactNode; fallback: React.ReactNode }, { err: boolean }> {`,
4179
+ ` state = { err: false };`,
4180
+ ` static getDerivedStateFromError() { return { err: true }; }`,
4181
+ ` componentDidCatch() {}`,
4182
+ ` render() { return (this.state.err ? this.props.fallback : this.props.children) as React.ReactElement; }`,
4183
+ `}`,
4184
+ // Provider wrapper — QueryClientProvider if project uses react-query, otherwise fragment
4185
+ ...(needsQC ? [
4186
+ `const _qc = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } });`,
4187
+ `const _PW = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: _qc }, children) as React.ReactElement;`,
4188
+ ] : [
4189
+ `const _PW = ({ children }: { children: React.ReactNode }) => React.createElement(React.Fragment, null, children) as React.ReactElement;`,
4190
+ ]),
4191
+ // PREVIEWS: each component renders via its real import; ErrorBoundary catches failures
4192
+ `const PREVIEWS: Record<string, () => React.ReactElement> = {`,
4193
+ ...previewEntries.map(e => e + ','),
4194
+ `};`,
4195
+ ``,
4196
+ // Inventory data (metadata only — visual rendering is done at runtime via PREVIEWS)
4197
+ `const inventoryData: { category: string; components: { name: string; group: string; tokenCount: number; propCount: number; colorSwatches: { token: string; hex: string }[]; previewHtml: string; previewScale: number; active: boolean }[] }[] = ${JSON.stringify(inventoryData)};`,
4111
4198
  `const totalComponents = ${totalComponents};`,
4112
4199
  `const activeComponents = ${activeComponents};`,
4113
4200
  `const uniqueTokens = ${uniqueTokens};`,
4114
4201
  ``,
4202
+ // CardPreview: renders the actual component at the right scale, falls back to shape HTML
4203
+ `const _InvPreview = ({ comp }: { comp: any }) => {`,
4204
+ ` const s: number = comp.previewScale || 0.4;`,
4205
+ ` const centered = s >= 0.9;`,
4206
+ ` const fn = PREVIEWS[comp.name];`,
4207
+ ` const shapeHtml = (`,
4208
+ ` <div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }}`,
4209
+ ` dangerouslySetInnerHTML={{ __html: comp.previewHtml }} />`,
4210
+ ` );`,
4211
+ ` const wPct = (+(100 / s).toFixed(1)) + "%";`,
4212
+ ` const innerStyle: React.CSSProperties = centered`,
4213
+ ` ? { position: "absolute", top: "50%", left: "50%",`,
4214
+ ` transform: "translate(-50%, -50%) scale(" + s + ")",`,
4215
+ ` transformOrigin: "center center", pointerEvents: "none" }`,
4216
+ ` : { position: "absolute", top: 0, left: 0, width: wPct,`,
4217
+ ` transform: "scale(" + s + ")",`,
4218
+ ` transformOrigin: "top left", pointerEvents: "none" };`,
4219
+ ` return (`,
4220
+ ` <div style={{ height: 96, overflow: "hidden", position: "relative", background: "#fff" }}>`,
4221
+ ` <_EB fallback={shapeHtml}>`,
4222
+ ` {fn ? <_PW><div style={innerStyle}>{fn()}</div></_PW> : shapeHtml}`,
4223
+ ` </_EB>`,
4224
+ ` </div>`,
4225
+ ` );`,
4226
+ `};`,
4227
+ `const InventoryCard = ({ comp }: { comp: any }) => (`,
4228
+ ` <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 }}>`,
4229
+ ` <div style={{ background: comp.active ? "#f8f9fa" : "#f5f5f5", borderBottom: "1px solid #f0f0f0" }}>`,
4230
+ ` <_InvPreview comp={comp} />`,
4231
+ ` </div>`,
4232
+ ` <div style={{ padding: "10px 12px" }}>`,
4233
+ ` <div style={{ display: "flex", alignItems: "center", gap: 5, marginBottom: 5 }}>`,
4234
+ ` <span style={{ fontWeight: 700, fontSize: 13, color: comp.active ? "#111" : "#9ca3af" }}>{comp.name}</span>`,
4235
+ ` {comp.active && <span style={{ fontSize: 9, background: "#dcfce7", color: "#15803d", padding: "1px 5px", borderRadius: 4, fontWeight: 700, letterSpacing: "0.05em" }}>USED</span>}`,
4236
+ ` </div>`,
4237
+ ` <div style={{ display: "flex", gap: 4, flexWrap: "wrap" as any }}>`,
4238
+ ` {comp.tokenCount > 0 && <span style={{ fontSize: 10, background: "#eff6ff", color: "#1d4ed8", padding: "2px 7px", borderRadius: 10, border: "1px solid #dbeafe" }}>{comp.tokenCount} tokens</span>}`,
4239
+ ` {comp.colorSwatches.length > 0 && <span style={{ fontSize: 10, background: "#fef9c3", color: "#713f12", padding: "2px 7px", borderRadius: 10, border: "1px solid #fde68a" }}>{comp.colorSwatches.length} colors</span>}`,
4240
+ ` {!comp.active && <span style={{ fontSize: 10, background: "#f3f4f6", color: "#9ca3af", padding: "2px 7px", borderRadius: 10, border: "1px solid #e5e7eb" }}>installed</span>}`,
4241
+ ` </div>`,
4242
+ ` </div>`,
4243
+ ` </div>`,
4244
+ `);`,
4245
+ ``,
4115
4246
  `export const Default: Story = {`,
4116
4247
  ` render: () => {`,
4117
- ` const [showUnused, setShowUnused] = (window as any).__vdsShowUnused !== undefined`,
4118
- ` ? [(window as any).__vdsShowUnused, (v: boolean) => { (window as any).__vdsShowUnused = v; }]`,
4119
- ` : [false, () => {}];`,
4120
4248
  ` const [expanded, setExpanded] = React.useState(false);`,
4121
- ` const Card = ({ comp }: { comp: any }) => (`,
4122
- ` <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 }}>`,
4123
- ` <div style={{ background: comp.active ? "#f8f9fa" : "#f5f5f5", borderBottom: "1px solid #f0f0f0", minHeight: 64, display: "flex", alignItems: "center", justifyContent: "center" }}`,
4124
- ` dangerouslySetInnerHTML={{ __html: comp.previewHtml }}`,
4125
- ` />`,
4126
- ` <div style={{ padding: "10px 12px" }}>`,
4127
- ` <div style={{ display: "flex", alignItems: "center", gap: 5, marginBottom: 5 }}>`,
4128
- ` <span style={{ fontWeight: 700, fontSize: 13, color: comp.active ? "#111" : "#9ca3af" }}>{comp.name}</span>`,
4129
- ` {comp.active && <span style={{ fontSize: 9, background: "#dcfce7", color: "#15803d", padding: "1px 5px", borderRadius: 4, fontWeight: 700, letterSpacing: "0.05em" }}>USED</span>}`,
4130
- ` </div>`,
4131
- ` <div style={{ display: "flex", gap: 4, flexWrap: "wrap" as any }}>`,
4132
- ` {comp.tokenCount > 0 && (`,
4133
- ` <span style={{ fontSize: 10, background: "#eff6ff", color: "#1d4ed8", padding: "2px 7px", borderRadius: 10, border: "1px solid #dbeafe" }}>{comp.tokenCount} tokens</span>`,
4134
- ` )}`,
4135
- ` {comp.colorSwatches.length > 0 && (`,
4136
- ` <span style={{ fontSize: 10, background: "#fef9c3", color: "#713f12", padding: "2px 7px", borderRadius: 10, border: "1px solid #fde68a" }}>{comp.colorSwatches.length} colors</span>`,
4137
- ` )}`,
4138
- ` {!comp.active && (`,
4139
- ` <span style={{ fontSize: 10, background: "#f3f4f6", color: "#9ca3af", padding: "2px 7px", borderRadius: 10, border: "1px solid #e5e7eb" }}>installed</span>`,
4140
- ` )}`,
4141
- ` </div>`,
4142
- ` </div>`,
4143
- ` </div>`,
4144
- ` );`,
4145
4249
  ` return (`,
4146
4250
  ` <div style={{ padding: 32, background: "#f8f9fa", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh", width: "100%" }}>`,
4147
- ` {/* Header */}`,
4148
4251
  ` <div style={{ marginBottom: 40 }}>`,
4149
4252
  ` <h2 style={{ fontSize: 28, fontWeight: 700, margin: "0 0 8px" }}>Component Inventory</h2>`,
4150
4253
  ` <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>`,
@@ -4154,7 +4257,7 @@ function writeComponentInventoryStory(components, foundations) {
4154
4257
  ` { value: (totalComponents - activeComponents).toString(), label: "Installed Only", bg: "#f9fafb", fg: "#6b7280", border: "#e5e7eb" },`,
4155
4258
  ` { value: uniqueTokens.toString(), label: "Unique Tokens", bg: "#eff6ff", fg: "#1d4ed8", border: "#dbeafe" },`,
4156
4259
  ` ].map(stat => (`,
4157
- ` <div key={stat.label} style={{ padding: "12px 20px", background: stat.bg, borderRadius: 10, textAlign: "center" as any, minWidth: 110, border: \`1px solid \${stat.border}\` }}>`,
4260
+ ` <div key={stat.label} style={{ padding: "12px 20px", background: stat.bg, borderRadius: 10, textAlign: "center" as any, minWidth: 110, border: "1px solid " + stat.border }}>`,
4158
4261
  ` <div style={{ fontSize: 26, fontWeight: 800, color: stat.fg, lineHeight: 1 }}>{stat.value}</div>`,
4159
4262
  ` <div style={{ fontSize: 12, color: stat.fg, marginTop: 4, opacity: 0.8 }}>{stat.label}</div>`,
4160
4263
  ` </div>`,
@@ -4164,23 +4267,22 @@ function writeComponentInventoryStory(components, foundations) {
4164
4267
  ` {expanded ? "Hide" : "Show"} installed-only components`,
4165
4268
  ` </button>`,
4166
4269
  ` </div>`,
4167
- ` {/* Category groups — active components first */}`,
4168
4270
  ` {inventoryData.map(group => {`,
4169
4271
  ` const active = group.components.filter((c: any) => c.active);`,
4170
4272
  ` const inactive = group.components.filter((c: any) => !c.active);`,
4171
4273
  ` if (active.length === 0 && !expanded) return null;`,
4172
4274
  ` return (`,
4173
- ` <div key={group.category} style={{ marginBottom: 44 }}>`,
4174
- ` <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16, paddingBottom: 10, borderBottom: "2px solid #e5e7eb" }}>`,
4175
- ` <h3 style={{ margin: 0, fontSize: 15, fontWeight: 700, color: "#111" }}>{group.category}</h3>`,
4176
- ` <span style={{ fontSize: 12, background: "#dcfce7", color: "#15803d", padding: "2px 9px", borderRadius: 12, fontWeight: 600 }}>{active.length} used</span>`,
4177
- ` {inactive.length > 0 && <span style={{ fontSize: 12, background: "#f3f4f6", color: "#9ca3af", padding: "2px 9px", borderRadius: 12 }}>{inactive.length} installed</span>}`,
4178
- ` </div>`,
4179
- ` <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 12 }}>`,
4180
- ` {active.map((comp: any) => <Card key={comp.name} comp={comp} />)}`,
4181
- ` {expanded && inactive.map((comp: any) => <Card key={comp.name} comp={comp} />)}`,
4275
+ ` <div key={group.category} style={{ marginBottom: 44 }}>`,
4276
+ ` <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16, paddingBottom: 10, borderBottom: "2px solid #e5e7eb" }}>`,
4277
+ ` <h3 style={{ margin: 0, fontSize: 15, fontWeight: 700, color: "#111" }}>{group.category}</h3>`,
4278
+ ` <span style={{ fontSize: 12, background: "#dcfce7", color: "#15803d", padding: "2px 9px", borderRadius: 12, fontWeight: 600 }}>{active.length} used</span>`,
4279
+ ` {inactive.length > 0 && <span style={{ fontSize: 12, background: "#f3f4f6", color: "#9ca3af", padding: "2px 9px", borderRadius: 12 }}>{inactive.length} installed</span>}`,
4280
+ ` </div>`,
4281
+ ` <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 12 }}>`,
4282
+ ` {active.map((comp: any) => <InventoryCard key={comp.name} comp={comp} />)}`,
4283
+ ` {expanded && inactive.map((comp: any) => <InventoryCard key={comp.name} comp={comp} />)}`,
4284
+ ` </div>`,
4182
4285
  ` </div>`,
4183
- ` </div>`,
4184
4286
  ` );`,
4185
4287
  ` })}`,
4186
4288
  ` </div>`,