/// import { type Cell, cell, Default, derive, handler, lift, recipe, str, } from "commontools"; interface ComponentLibraryCatalogArgs { components: Default; } interface ComponentSeed { id?: string; name?: string; category?: string; description?: string; props?: unknown; } interface ComponentDefinition { id: string; name: string; category: string; categoryKey: string; description: string; props: string[]; } interface RecipeRegistrationEntry { componentId: string; recipe: string; props: string[]; } interface RecipeRegistrationEvent { component?: string; recipe?: string; props?: unknown; } interface ComponentCoverageView { id: string; name: string; category: string; props: string[]; coveredProps: string[]; uncoveredProps: string[]; coveragePercent: number; recipeCount: number; recipes: string[]; } interface PropCoverageView { prop: string; declared: number; covered: number; coveragePercent: number; components: string[]; coveredComponents: string[]; recipes: string[]; } interface CoverageTotals { components: number; fullyCovered: number; partiallyCovered: number; uncovered: number; averageCoverage: number; props: number; propsCovered: number; } interface CategorySummary { key: string; label: string; componentCount: number; } const defaultComponentSeeds: ComponentSeed[] = [ { id: "primary-button", name: "Primary Button", category: "Buttons", description: "Primary action button for critical flows.", props: ["label", "variant", "disabled", "size"], }, { id: "secondary-button", name: "Secondary Button", category: "Buttons", description: "Secondary emphasis button for supporting actions.", props: ["label", "variant", "disabled"], }, { id: "input-field", name: "Input Field", category: "Forms", description: "Form input field supporting helper and error states.", props: [ "label", "value", "placeholder", "error", "helper-text", ], }, ]; function sanitizeText(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function titleCase(value: string): string { const parts = value .toLowerCase() .split(/\s+/) .filter((part) => part.length > 0); if (parts.length === 0) return ""; return parts .map((part) => part[0].toUpperCase() + part.slice(1)) .join(" "); } function slugify(value: string, fallback: string): string { const normalized = value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return normalized.length > 0 ? normalized : fallback; } function sanitizeDescription( value: unknown, name: string, category: string, ): string { const text = sanitizeText(value); if (text) return text; return `${name} component within ${category}.`; } function sanitizeProps(value: unknown): string[] { if (!Array.isArray(value)) return []; const seen = new Set(); const props: string[] = []; for (const entry of value) { const text = sanitizeText(entry); if (!text) continue; const normalized = text .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); if (normalized.length === 0) continue; if (seen.has(normalized)) continue; seen.add(normalized); props.push(normalized); } props.sort((left, right) => left.localeCompare(right)); return props; } function sanitizeComponentDefinitions( seeds: readonly ComponentSeed[] | undefined, ): ComponentDefinition[] { const source = Array.isArray(seeds) && seeds.length > 0 ? seeds : defaultComponentSeeds; const map = new Map(); source.forEach((seed, index) => { const nameSource = sanitizeText(seed?.name) ?? sanitizeText(seed?.id) ?? `Component ${index + 1}`; const name = titleCase(nameSource); const idSource = sanitizeText(seed?.id) ?? name; const id = slugify(idSource, `component-${index + 1}`); const categorySource = sanitizeText(seed?.category) ?? "General"; const category = titleCase(categorySource); const categoryKey = slugify(category, "general"); const description = sanitizeDescription(seed?.description, name, category); const props = sanitizeProps(seed?.props); const finalProps = props.length > 0 ? props : ["label"]; map.set(id, { id, name, category, categoryKey, description, props: finalProps, }); }); const list = Array.from(map.values()); list.sort((left, right) => { const categoryOrder = left.category.localeCompare(right.category); if (categoryOrder !== 0) return categoryOrder; return left.name.localeCompare(right.name); }); return list; } function sanitizeComponentIdFromInput( value: unknown, components: readonly ComponentDefinition[], ): string | null { if (components.length === 0) return null; if (typeof value === "string") { const trimmed = value.trim(); if (trimmed.length > 0) { const slug = slugify(trimmed, trimmed.toLowerCase()); for (const component of components) { if (component.id === slug) return component.id; if (slugify(component.name, component.id) === slug) { return component.id; } } } } return components[0].id; } function sanitizeRecipeLabel( value: unknown, _component: ComponentDefinition, fallback: string, ): string { const text = sanitizeText(value); if (text) return titleCase(text); return fallback; } function rebuildRegistrationMap( entries: unknown, components: readonly ComponentDefinition[], ): Map { const componentMap = new Map(); components.forEach((component) => componentMap.set(component.id, component)); const map = new Map(); if (!Array.isArray(entries)) return map; for (const raw of entries) { if (!raw || typeof raw !== "object") continue; const componentIdValue = (raw as { componentId?: unknown }).componentId; const componentKey = (raw as { component?: unknown }).component; const componentId = typeof componentIdValue === "string" ? componentIdValue : typeof componentKey === "string" ? slugify(componentKey, "component") : null; if (!componentId) continue; const component = componentMap.get(componentId); if (!component) continue; const recipe = sanitizeRecipeLabel( (raw as { recipe?: unknown }).recipe, component, component.name, ); const allowed = new Set(component.props); const props = sanitizeProps((raw as { props?: unknown }).props) .filter((prop) => allowed.has(prop)); const key = `${component.id}#${recipe}`; map.set(key, { componentId: component.id, recipe, props, }); } return map; } function computeCategorySummary( components: readonly ComponentDefinition[], ): CategorySummary[] { const map = new Map(); for (const component of components) { const entry = map.get(component.categoryKey); if (entry) { entry.count += 1; } else { map.set(component.categoryKey, { label: component.category, count: 1, }); } } const categories: CategorySummary[] = []; for (const [key, value] of map.entries()) { categories.push({ key, label: value.label, componentCount: value.count, }); } categories.sort((left, right) => left.label.localeCompare(right.label)); return categories; } function computeComponentCoverage( components: readonly ComponentDefinition[], registrations: readonly RecipeRegistrationEntry[], ): ComponentCoverageView[] { const grouped = new Map(); for (const entry of registrations) { const list = grouped.get(entry.componentId); if (list) { list.push({ componentId: entry.componentId, recipe: entry.recipe, props: [...entry.props], }); } else { grouped.set(entry.componentId, [{ componentId: entry.componentId, recipe: entry.recipe, props: [...entry.props], }]); } } const coverage: ComponentCoverageView[] = []; for (const component of components) { const entries = grouped.get(component.id) ?? []; const allowed = new Set(component.props); const covered = new Set(); const recipeSet = new Set(); for (const entry of entries) { for (const prop of entry.props) { if (allowed.has(prop)) covered.add(prop); } recipeSet.add(entry.recipe); } const coveredProps = Array.from(covered).sort((left, right) => left.localeCompare(right) ); const uncoveredProps = component.props.filter((prop) => !covered.has(prop)); const coveragePercent = component.props.length === 0 ? 100 : Math.round((coveredProps.length / component.props.length) * 100); coverage.push({ id: component.id, name: component.name, category: component.category, props: [...component.props], coveredProps, uncoveredProps, coveragePercent, recipeCount: recipeSet.size, recipes: Array.from(recipeSet).sort((left, right) => left.localeCompare(right) ), }); } return coverage; } function computePropCoverage( components: readonly ComponentDefinition[], registrations: readonly RecipeRegistrationEntry[], ): PropCoverageView[] { const componentMap = new Map(); components.forEach((component) => componentMap.set(component.id, component)); const catalog = new Map< string, { declared: Set; covered: Map>; } >(); for (const component of components) { for (const prop of component.props) { const entry = catalog.get(prop); if (entry) { entry.declared.add(component.id); entry.covered.set(component.id, new Set()); } else { catalog.set(prop, { declared: new Set([component.id]), covered: new Map([[component.id, new Set()]]), }); } } } for (const registration of registrations) { const component = componentMap.get(registration.componentId); if (!component) continue; const allowed = new Set(component.props); for (const prop of registration.props) { if (!allowed.has(prop)) continue; const entry = catalog.get(prop); if (!entry) continue; const record = entry.covered.get(component.id); if (!record) continue; record.add(registration.recipe); } } const coverage: PropCoverageView[] = []; for (const [prop, data] of catalog.entries()) { const declaredComponents = Array.from(data.declared) .map((componentId) => componentMap.get(componentId)?.name ?? componentId) .sort((left, right) => left.localeCompare(right)); const coveredComponents: string[] = []; const recipes = new Set(); for (const [componentId, recipeSet] of data.covered.entries()) { if (recipeSet.size === 0) continue; coveredComponents.push( componentMap.get(componentId)?.name ?? componentId, ); recipeSet.forEach((recipe) => recipes.add(recipe)); } coveredComponents.sort((left, right) => left.localeCompare(right)); const declared = declaredComponents.length; const covered = coveredComponents.length; const coveragePercent = declared === 0 ? 0 : Math.round((covered / declared) * 100); coverage.push({ prop, declared, covered, coveragePercent, components: declaredComponents, coveredComponents, recipes: Array.from(recipes).sort((left, right) => left.localeCompare(right) ), }); } coverage.sort((left, right) => { if (right.coveragePercent !== left.coveragePercent) { return right.coveragePercent - left.coveragePercent; } if (right.covered !== left.covered) { return right.covered - left.covered; } return left.prop.localeCompare(right.prop); }); return coverage; } function summarizeCoverage( componentCoverage: readonly ComponentCoverageView[], propCoverage: readonly PropCoverageView[], ): CoverageTotals { const components = componentCoverage.length; let fullyCovered = 0; let partiallyCovered = 0; for (const entry of componentCoverage) { if (entry.coveragePercent === 100) { fullyCovered += 1; } else if (entry.coveragePercent > 0) { partiallyCovered += 1; } } const uncovered = components - fullyCovered - partiallyCovered; const totalPercent = componentCoverage.reduce( (sum, entry) => sum + entry.coveragePercent, 0, ); const averageCoverage = components === 0 ? 0 : Math.round(totalPercent / components); const props = propCoverage.length; const propsCovered = propCoverage.filter((entry) => entry.covered > 0) .length; return { components, fullyCovered, partiallyCovered, uncovered, averageCoverage, props, propsCovered, }; } function formatRegistrationMessage( component: ComponentDefinition, recipe: string, props: readonly string[], ): string { const propLabel = props.length === component.props.length ? "all props" : `${props.length} props`; return `${component.name}: ${recipe} (${propLabel})`; } function readSequence(value: unknown): number { if (typeof value === "number" && Number.isFinite(value)) { return value; } return 0; } const registerRecipe = handler( ( event: RecipeRegistrationEvent | undefined, context: { components: Cell; registrations: Cell; log: Cell; sequence: Cell; }, ) => { const definitions = sanitizeComponentDefinitions(context.components.get()); if (definitions.length === 0) return; const componentId = sanitizeComponentIdFromInput( event?.component, definitions, ); if (!componentId) return; const component = definitions.find((item) => item.id === componentId); if (!component) return; const allowedProps = new Set(component.props); const normalizedProps = sanitizeProps(event?.props) .filter((prop) => allowedProps.has(prop)); if (normalizedProps.length === 0) return; const sequenceValue = readSequence(context.sequence.get()); const fallbackRecipe = `${component.name} Recipe ${sequenceValue + 1}`; const recipeLabel = sanitizeRecipeLabel( event?.recipe, component, fallbackRecipe, ); context.sequence.set(sequenceValue + 1); const existing = rebuildRegistrationMap( context.registrations.get(), definitions, ); existing.set(`${component.id}#${recipeLabel}`, { componentId: component.id, recipe: recipeLabel, props: normalizedProps, }); const nextEntries = Array.from(existing.values()); nextEntries.sort((left, right) => { if (left.componentId === right.componentId) { return left.recipe.localeCompare(right.recipe); } return left.componentId.localeCompare(right.componentId); }); context.registrations.set(nextEntries.map((entry) => ({ componentId: entry.componentId, recipe: entry.recipe, props: [...entry.props], }))); const previousLog = Array.isArray(context.log.get()) ? (context.log.get() as string[]).filter((value) => typeof value === "string" ) : []; const message = formatRegistrationMessage( component, recipeLabel, normalizedProps, ); const nextLog = [...previousLog.slice(-4), message]; context.log.set(nextLog); }, ); export const componentLibraryCatalog = recipe( "Component Library Catalog", ({ components }) => { const registrations = cell([]); const registrationLog = cell([]); const registrationSequence = cell(0); const componentList = lift((value: ComponentSeed[] | undefined) => sanitizeComponentDefinitions(value) )(components); const registrationList = lift((inputs: { entries: RecipeRegistrationEntry[] | undefined; components: ComponentDefinition[]; }) => Array.isArray(inputs.entries) ? sanitizeRegistrationEntries(inputs.entries, inputs.components) : [] )({ entries: registrations, components: componentList, }); const componentCoverage = lift((inputs: { components: ComponentDefinition[]; registrations: RecipeRegistrationEntry[]; }) => computeComponentCoverage(inputs.components, inputs.registrations))({ components: componentList, registrations: registrationList, }); const propCoverage = lift((inputs: { components: ComponentDefinition[]; registrations: RecipeRegistrationEntry[]; }) => computePropCoverage(inputs.components, inputs.registrations))({ components: componentList, registrations: registrationList, }); const coverageTotals = lift((inputs: { components: ComponentCoverageView[]; props: PropCoverageView[]; }) => summarizeCoverage(inputs.components, inputs.props))({ components: componentCoverage, props: propCoverage, }); const componentCount = derive(coverageTotals, (stats) => stats.components); const fullyCovered = derive(coverageTotals, (stats) => stats.fullyCovered); const partiallyCovered = derive( coverageTotals, (stats) => stats.partiallyCovered, ); const uncovered = derive(coverageTotals, (stats) => stats.uncovered); const averageCoverage = derive( coverageTotals, (stats) => stats.averageCoverage, ); const propCount = derive(coverageTotals, (stats) => stats.props); const propsCovered = derive(coverageTotals, (stats) => stats.propsCovered); const coverageSummary = str`${fullyCovered}/${componentCount} covered | props ${propsCovered}/${propCount}`; const averageCoverageLabel = lift((value: number | undefined) => { if (typeof value === "number") { return `${value}% average coverage`; } return "0% average coverage"; })(averageCoverage); const categorySummary = lift((entries: ComponentDefinition[]) => computeCategorySummary(entries) )(componentList); const registrationTrail = lift((entries: string[] | undefined) => { if (!Array.isArray(entries) || entries.length === 0) { return "No recipes registered yet"; } return entries.join(" | "); })(registrationLog); const lastRegistration = lift((entries: string[] | undefined) => { if (!Array.isArray(entries) || entries.length === 0) { return "none"; } return entries[entries.length - 1]; })(registrationLog); return { components: componentList, categories: categorySummary, registrations: registrationList, componentCoverage, propCoverage, stats: { componentCount, fullyCovered, partiallyCovered, uncovered, averageCoverage, propCount, propsCovered, }, coverageSummary, averageCoverageLabel, registrationTrail, lastRegistration, controls: { register: registerRecipe({ components, registrations, log: registrationLog, sequence: registrationSequence, }), }, }; }, ); function sanitizeRegistrationEntries( entries: readonly RecipeRegistrationEntry[] | undefined, components: readonly ComponentDefinition[], ): RecipeRegistrationEntry[] { if (!Array.isArray(entries) || entries.length === 0) return []; const componentMap = new Map(); components.forEach((component) => componentMap.set(component.id, component)); const map = new Map(); for (const entry of entries) { if (!entry || typeof entry !== "object") continue; const component = componentMap.get(entry.componentId); if (!component) continue; const recipe = sanitizeRecipeLabel(entry.recipe, component, component.name); const allowed = new Set(component.props); const props = entry.props .filter((prop: string) => allowed.has(prop)) .sort((left: string, right: string) => left.localeCompare(right)); const key = `${component.id}#${recipe}`; map.set(key, { componentId: component.id, recipe, props, }); } const list = Array.from(map.values()); list.sort((left, right) => { if (left.componentId === right.componentId) { return left.recipe.localeCompare(right.recipe); } return left.componentId.localeCompare(right.componentId); }); return list; }