import metadata from '#docs/metadata'; import type { CategoryMeta, ComponentMeta, DocSection, DocsMetadata, GuideSection, ItemMeta, PackageGroup, PackageMeta, } from '../../modules/extractor/types'; /** A unified, normalised entry for any documented leaf, regardless of kind. */ export type DocEntry = | { kind: 'api'; pkg: PackageMeta; category: CategoryMeta; item: ItemMeta } | { kind: 'components'; pkg: PackageMeta; component: ComponentMeta } | { kind: 'guide'; pkg: PackageMeta; section: GuideSection } | { kind: 'doc'; pkg: PackageMeta; section: DocSection }; export interface SearchResult { pkg: PackageMeta; slug: string; name: string; description: string; /** Display kind for the badge */ badge: string; } const GROUP_LABELS: Record = { core: 'Core', vue: 'Vue', configs: 'Configs', infra: 'Infra', }; const GROUP_ORDER: PackageGroup[] = ['core', 'vue', 'configs', 'infra']; /** Display order for component categories (unlisted categories sort last, A–Z). */ const COMPONENT_CATEGORY_ORDER: string[] = [ 'Forms', 'Selection', 'Color', 'Overlays', 'Menus', 'Disclosure', 'Navigation', 'Display', 'Feedback', 'Canvas & editors', 'Utilities', 'Other', ]; /** A category bucket of components, for grouped rendering. */ export interface ComponentGroup { name: string; components: ComponentMeta[]; } export function useDocs() { const data = metadata as unknown as DocsMetadata; function getPackages(): PackageMeta[] { return data.packages; } /** Packages grouped & ordered for sidebar / landing. */ function getGroupedPackages(): Array<{ group: PackageGroup; label: string; packages: PackageMeta[] }> { return GROUP_ORDER .map(group => ({ group, label: GROUP_LABELS[group], packages: data.packages.filter(p => p.group === group), })) .filter(g => g.packages.length > 0); } function getPackage(slug: string): PackageMeta | undefined { return data.packages.find(p => p.slug === slug); } /** Number of documented leaves in a package, whatever its kind. */ function countEntries(pkg: PackageMeta): number { if (pkg.kind === 'api') return pkg.categories.reduce((s, c) => s + c.items.length, 0); if (pkg.kind === 'components') return pkg.components.length; return pkg.sections.length; } /** The hand-authored intro section for a package, if any. */ function getIntro(pkg: PackageMeta): DocSection | undefined { return pkg.docs.find(s => s.isIntro); } /** Non-intro doc sections (the "Guide" list shown in the sidebar). */ function getDocSections(pkg: PackageMeta): DocSection[] { return pkg.docs.filter(s => !s.isIntro); } /** * A `components`-kind package's components bucketed by `category`, ordered by * {@link COMPONENT_CATEGORY_ORDER} (unlisted categories last, A–Z), with the * components inside each bucket kept in their incoming (alphabetical) order. */ function getComponentGroups(pkg: PackageMeta): ComponentGroup[] { if (pkg.kind !== 'components') return []; const buckets = new Map(); for (const c of pkg.components) { const cat = c.category || 'Other'; const list = buckets.get(cat); if (list) list.push(c); else buckets.set(cat, [c]); } const rank = (name: string) => { const i = COMPONENT_CATEGORY_ORDER.indexOf(name); return i === -1 ? COMPONENT_CATEGORY_ORDER.length : i; }; return [...buckets.entries()] .map(([name, components]) => ({ name, components })) .sort((a, b) => rank(a.name) - rank(b.name) || a.name.localeCompare(b.name)); } /** Resolve any `/:package/:slug` route to a normalised entry. */ function resolveEntry(packageSlug: string, slug: string): DocEntry | undefined { const pkg = getPackage(packageSlug); if (!pkg) return undefined; // Hand-authored doc sections take precedence over auto-generated leaves. const docSection = pkg.docs.find(s => !s.isIntro && s.slug === slug); if (docSection) return { kind: 'doc', pkg, section: docSection }; if (pkg.kind === 'api') { for (const category of pkg.categories) { const item = category.items.find(i => i.slug === slug); if (item) return { kind: 'api', pkg, category, item }; } } else if (pkg.kind === 'components') { const component = pkg.components.find(c => c.slug === slug); if (component) return { kind: 'components', pkg, component }; } else { const section = pkg.sections.find(s => s.slug === slug); if (section) return { kind: 'guide', pkg, section }; } return undefined; } /** The default entry to open when landing on a package, if any. */ function firstEntrySlug(pkg: PackageMeta): string | undefined { if (pkg.kind === 'api') return pkg.categories[0]?.items[0]?.slug; if (pkg.kind === 'components') return pkg.components[0]?.slug; return pkg.sections[0]?.slug; } function search(query: string): SearchResult[] { const q = query.trim().toLowerCase(); if (!q) return []; const results: SearchResult[] = []; for (const pkg of data.packages) { if (pkg.kind === 'api') { for (const category of pkg.categories) { for (const item of category.items) { if (item.name.toLowerCase().includes(q) || item.description.toLowerCase().includes(q)) { results.push({ pkg, slug: item.slug, name: item.name, description: item.description, badge: item.kind }); } } } } else if (pkg.kind === 'components') { for (const c of pkg.components) { if (c.name.toLowerCase().includes(q) || c.description.toLowerCase().includes(q)) { results.push({ pkg, slug: c.slug, name: c.name, description: c.description || `${c.parts.length} parts`, badge: 'component' }); } } } else { for (const s of pkg.sections) { if (s.title.toLowerCase().includes(q) || s.markdown.toLowerCase().includes(q)) { results.push({ pkg, slug: s.slug, name: s.title, description: pkg.name, badge: 'guide' }); } } } } return results; } function getTotalItems(): number { return data.packages.reduce((sum, pkg) => sum + countEntries(pkg), 0); } return { data, getPackages, getGroupedPackages, getPackage, countEntries, resolveEntry, firstEntrySlug, getIntro, getDocSections, getComponentGroups, search, getTotalItems, }; }