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
|
@@ -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 = {};
|
|
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
|
-
|
|
4035
|
-
const
|
|
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);
|
|
4111
|
+
.filter(s => s.hex);
|
|
4059
4112
|
|
|
4060
|
-
|
|
4061
|
-
const
|
|
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:
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
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
|
-
|
|
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
|
|
4140
|
+
const totalComponents = components.length;
|
|
4097
4141
|
const activeComponents = [...usedSet].length;
|
|
4098
|
-
const uniqueTokens
|
|
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
|
-
|
|
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:
|
|
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
|
-
`
|
|
4174
|
-
`
|
|
4175
|
-
`
|
|
4176
|
-
`
|
|
4177
|
-
`
|
|
4178
|
-
`
|
|
4179
|
-
`
|
|
4180
|
-
`
|
|
4181
|
-
`
|
|
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>`,
|