import { computed } from 'vue' import { useRoute } from 'vue-router' import type { ComputedRef } from 'vue' import type { RouteLocationRaw } from 'vue-router' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- /** A single breadcrumb item. `to` is absent for the current (last) item. */ export interface BreadcrumbItem { label: string to?: RouteLocationRaw } /** * Minimal shape we need from a matched route record. * Using a structural type keeps the pure helper testable without * requiring full RouteLocationMatched fixtures in unit tests. * RouteLocationMatched is structurally assignable to this interface so * route.matched (RouteLocationMatched[]) passes the type-checker. */ export interface BreadcrumbRouteRecord { meta?: Record & { breadcrumb?: string title?: string } name?: string | symbol | null path?: string } // --------------------------------------------------------------------------- // Pure helper — testable without a live router // --------------------------------------------------------------------------- /** * Convert an array of matched route records into breadcrumb items. * * Rules: * - Records without `meta.breadcrumb` OR `meta.title` are filtered out. * - Label = `meta.breadcrumb` if set, else `meta.title`. * - The last item is current — no `to` link. * - All preceding items carry a `to` pointing at their `path` (or `name` * when path is absent). */ export function toBreadcrumbItems(matched: readonly BreadcrumbRouteRecord[]): BreadcrumbItem[] { const eligible = matched.filter( r => r.meta?.breadcrumb !== undefined || r.meta?.title !== undefined, ) return eligible.map((record, index): BreadcrumbItem => { const label = (record.meta?.breadcrumb ?? record.meta?.title) as string const isLast = index === eligible.length - 1 if (isLast) return { label } // Prefer path; fall back to name when path is absent/empty. // NOTE: a non-last record whose `path` is a param template (e.g. `/events/:id`) // would yield an unresolved-template `to` — acceptable in foundation scope because // param routes are normally the last (current, no-`to`) segment. // TODO TECH-WS-GUI-REDESIGN: resolve param paths if a non-leaf param route ever needs a crumb link const to: RouteLocationRaw = record.path ? record.path : { name: record.name as string } return { label, to } }) } // --------------------------------------------------------------------------- // Composable — thin wrapper around useRoute() // --------------------------------------------------------------------------- export interface UseBreadcrumbReturn { items: ComputedRef } /** * Composable that derives breadcrumb items from the current route's * `matched` array. Reactive — updates automatically on navigation. * * Composables zone may NOT import from @/navigation — do not add * route-level nav helpers here. */ export function useBreadcrumb(): UseBreadcrumbReturn { const route = useRoute() const items = computed(() => toBreadcrumbItems(route.matched), ) return { items } }