Files
tools/docs/app/composables/useDocs.ts
robonen 8adc2522c6 docs: site WIP, extractor type cleanup, tests preset; add broadcastedRef
Type the docs extractor's package.json parsing as unknown; comment the Vite
plugin version-skew cast; wire the tests preset; site/architecture WIP.
2026-06-15 16:55:22 +07:00

210 lines
6.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PackageGroup, string> = {
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, AZ). */
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, AZ), 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<string, ComponentMeta[]>();
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,
};
}