#!/usr/bin/env tsx /** * Generate Catalog MDX Pages + Index * * Walks registry/blocks/ and registry/components/, reads each item's * registry-item.json, and emits: * * docs/catalog/blocks/.mdx — per-block detail page * docs/catalog/components/.mdx — per-component detail page * docs/public/catalog-index.json — flat manifest for the grid page * * Run before building docs (e.g., in a Mintlify pre-build script): * npx tsx scripts/generate-catalog-pages.ts */ import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { join, resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; // Import from source — bun workspace linking doesn't resolve for scripts outside packages/. import { type RegistryItem, isBlockItem, ITEM_TYPE_DIRS, } from "../packages/core/src/registry/types.js"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, ".."); const registryDir = resolve(repoRoot, "registry"); const docsDir = resolve(repoRoot, "docs"); const catalogImageBase = "https://static.heygen.ai/hyperframes-oss/docs/images/catalog"; // ── Types ────────────────────────────────────────────────────────────────── type ItemKind = "block" | "component"; interface SourceMetadata { authorUrl?: string; sourcePrompt?: string; } interface TextureGroup { title: string; items: string[]; } interface CatalogEntry { name: string; type: ItemKind; title: string; description: string; tags: string[]; /** Relative href within the docs site. */ href: string; /** Preview poster image path (relative to docs root). */ preview?: string; } // ── Discovery ────────────────────────────────────────────────────────────── function discoverItems(): { kind: ItemKind; manifest: RegistryItem }[] { const items: { kind: ItemKind; manifest: RegistryItem }[] = []; const registryManifest = JSON.parse( readFileSync(join(registryDir, "registry.json"), "utf-8"), ) as { items?: { name: string; type: string }[] }; for (const item of registryManifest.items ?? []) { const kind = item.type === "hyperframes:block" ? "block" : item.type === "hyperframes:component" ? "component" : null; if (!kind) continue; const manifestPath = join(registryDir, typeDir(kind), item.name, "registry-item.json"); if (!existsSync(manifestPath)) { console.warn(` ⚠ Skipping ${item.name}: missing ${manifestPath}`); continue; } let manifest: RegistryItem; try { manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as RegistryItem; } catch (err) { console.warn(` ⚠ Skipping ${manifestPath}: ${(err as Error).message}`); continue; } items.push({ kind, manifest }); } return items.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name)); } // ── MDX generation ───────────────────────────────────────────────────────── function typeLabel(kind: ItemKind): string { return kind === "block" ? "Block" : "Component"; } function typeDir(kind: ItemKind): string { return ITEM_TYPE_DIRS[kind === "block" ? "hyperframes:block" : "hyperframes:component"]; } function textureGroupsFor(manifest: RegistryItem): TextureGroup[] { if (!("textureGroups" in manifest)) return []; const value = manifest.textureGroups; if (!Array.isArray(value)) return []; return value.filter((group): group is TextureGroup => { if (!group || typeof group !== "object") return false; if (!("title" in group) || typeof group.title !== "string") return false; if (!("items" in group) || !Array.isArray(group.items)) return false; return group.items.every((item) => typeof item === "string"); }); } function textureLabel(slug: string): string { return slug .split("-") .map((part) => part.length === 1 ? part.toUpperCase() : part[0]!.toUpperCase() + part.slice(1), ) .join(" "); } function textureSampleWord(slug: string): string { if (slug.includes("brick")) return "BRICK"; if (slug.includes("concrete")) return "CONCRETE"; if (slug.includes("plaster")) return "PLASTER"; if (slug.includes("rock")) return "ROCK"; if (slug.includes("onyx")) return "ONYX"; if (slug.includes("marble")) return "MARBLE"; if (slug.includes("travertine")) return "STONE"; if (slug.includes("paving")) return "STONE"; if (slug.includes("tiles")) return "TILE"; if (slug.includes("ground")) return "GROUND"; if (slug.includes("road")) return "ROAD"; if (slug.includes("asphalt")) return "ASPHALT"; if (slug.includes("wood-floor")) return "FLOOR"; if (slug.includes("wood")) return "WOOD"; if (slug.includes("bark")) return "BARK"; if (slug.includes("diamond")) return "PLATE"; if (slug.includes("metal")) return "METAL"; if (slug.includes("lava")) return "LAVA"; if (slug.includes("grass")) return "GRASS"; if (slug.includes("carpet")) return "WOVEN"; if (slug.includes("fabric")) return "FABRIC"; if (slug.includes("snow")) return "SNOW"; if (slug.includes("leather")) return "LEATHER"; return slug.toUpperCase(); } function textureMaskUrlFor(manifest: RegistryItem, texture: string): string { return `${catalogImageBase}/components/${manifest.name}/masks/${texture}.png`; } function generateTextureExamples(manifest: RegistryItem, textureGroups: TextureGroup[]): string[] { const lines: string[] = [ "## Texture Examples", "", '
', ]; for (const group of textureGroups) { lines.push( "
", `

${group.title}

`, '
', ); for (const item of group.items) { const maskPath = textureMaskUrlFor(manifest, item); const textureClass = `hf-texture-${item}`; lines.push( `
`, `
${textureLabel(item)}
${textureClass}
`, `
${textureSampleWord(item)}
`, `
Use hf-texture-text ${textureClass}
`, "
", ); } lines.push("
", "
"); } lines.push("
", ""); return lines; } function generateTextureAgentUsage( manifest: RegistryItem, textureGroups: TextureGroup[], ): string[] { const firstTexture = textureGroups[0]?.items[0] ?? "brick"; const firstClass = `hf-texture-${firstTexture}`; const installedSnippet = `compositions/components/${manifest.name}/${manifest.name}.html`; return [ "## Agent Usage", "", "Use this wording when asking an agent to apply a texture:", "", "```text", `Use the ${manifest.title} catalog component.`, "", "1. From the project root, run:", ` npx hyperframes add ${manifest.name}`, "2. That command creates this installed snippet:", ` ${installedSnippet}`, "3. Open that file and paste the real