UNPKG

@mmstack/router-core

Version:

Core utilities and Signal-based primitives for enhancing development with `@angular/router`. This library provides helpers for common routing tasks, reactive integration with router state, and intelligent module preloading.

1 lines 75.1 kB
{"version":3,"file":"mmstack-router-core.mjs","sources":["../../../../../packages/router/core/src/lib/breadcrumb/breadcrumb-config.ts","../../../../../packages/router/core/src/lib/url.ts","../../../../../packages/router/core/src/lib/util/leaf.store.ts","../../../../../packages/router/core/src/lib/breadcrumb/breadcrumb.ts","../../../../../packages/router/core/src/lib/breadcrumb/breadcrumb-store.ts","../../../../../packages/router/core/src/lib/util/create-route-predicate.ts","../../../../../packages/router/core/src/lib/util/find-path.ts","../../../../../packages/router/core/src/lib/util/snapshot-path.ts","../../../../../packages/router/core/src/lib/breadcrumb/breadcrumb-resolver.ts","../../../../../packages/router/core/src/lib/preloading/preload-requester.ts","../../../../../packages/router/core/src/lib/preloading/preload-strategy.ts","../../../../../packages/router/core/src/lib/link.ts","../../../../../packages/router/core/src/lib/query-param.ts","../../../../../packages/router/core/src/lib/title/title-config.ts","../../../../../packages/router/core/src/lib/title/title-store.ts","../../../../../packages/router/core/src/mmstack-router-core.ts"],"sourcesContent":["import { inject, InjectionToken } from '@angular/core';\r\nimport { ResolvedLeafRoute } from '../util';\r\n\r\n/**\r\n * A function that returns a custom label generation function.\r\n * The outer function is called in a root injection context\r\n * The returned function takes a `ResolvedLeafRoute` and produces a string label for the breadcrumb.\r\n * As the inner function is wrapped in a computed, changes to signals called within it will update the breadcrumb label reactively.\r\n */\r\ntype GenerateBreadcrumbFn = () => (leaf: ResolvedLeafRoute) => string;\r\n\r\n/**\r\n * Configuration options for the breadcrumb system.\r\n * Use `provideBreadcrumbConfig` to supply these options to your application.\r\n */\r\n\r\nexport type BreadcrumbConfig = {\r\n /**\r\n * Defines how breadcrumb labels are generated.\r\n * - If set to `'manual'`, breadcrumbs will only be displayed if manually registered\r\n * via `createBreadcrumb`. Automatic generation based on routes is disabled.\r\n * - Alternatively provide a custom label generation function\r\n * If left undefined, the system will automatically generate labels based on the route's title, data, or path.\r\n * @see GenerateBreadcrumbFn\r\n * @example\r\n * ```typescript\r\n * // For custom label generation:\r\n * // const myCustomLabelGenerator = () => (leaf: ResolvedLeafRoute) => {\r\n * // return leaf.route.data?.['customTitle'] || leaf.route.routeConfig?.path || 'Default';\r\n * // };\r\n * //\r\n * // config: { generation: myCustomLabelGenerator }\r\n * ```\r\n */\r\n generation?: 'manual' | GenerateBreadcrumbFn;\r\n};\r\n\r\n/**\r\n * @internal\r\n */\r\nconst token = new InjectionToken<BreadcrumbConfig>('MMSTACK_BREADCRUMB_CONFIG');\r\n\r\n/**\r\n * Provides configuration for the breadcrumb system.\r\n * @param config - A partial `BreadcrumbConfig` object with the desired settings. *\r\n * @see BreadcrumbConfig\r\n * @example\r\n * ```typescript\r\n * // In your app.module.ts or a standalone component's providers:\r\n * // import { provideBreadcrumbConfig } from './breadcrumb.config'; // Adjust path\r\n * // import { ResolvedLeafRoute } from './breadcrumb.type'; // Adjust path\r\n *\r\n * // const customLabelStrategy: GenerateBreadcrumbFn = () => {\r\n * // return (leaf: ResolvedLeafRoute): string => {\r\n * // // Example: Prioritize a 'navTitle' data property\r\n * // if (leaf.route.data?.['navTitle']) {\r\n * // return leaf.route.data['navTitle'];\r\n * // }\r\n * // // Fallback to a default mechanism\r\n * // return leaf.route.title || leaf.segment.resolved || 'Unnamed';\r\n * // };\r\n * // };\r\n *\r\n * export const appConfig = [\r\n * // ...rest\r\n * provideBreadcrumbConfig({\r\n * generation: customLabelStrategy, // or 'manual' to disable auto-generation\r\n * }),\r\n * ]\r\n * ```\r\n */\r\nexport function provideBreadcrumbConfig(config: Partial<BreadcrumbConfig>) {\r\n return {\r\n provide: token,\r\n useValue: {\r\n ...config,\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * @internal\r\n */\r\nexport function injectBreadcrumbConfig(): BreadcrumbConfig {\r\n return (\r\n inject(token, {\r\n optional: true,\r\n }) ?? {}\r\n );\r\n}\r\n","import { inject, type Signal } from '@angular/core';\r\nimport { toSignal } from '@angular/core/rxjs-interop';\r\nimport {\r\n type Event,\r\n EventType,\r\n type NavigationEnd,\r\n Router,\r\n} from '@angular/router';\r\nimport { filter, map } from 'rxjs/operators';\r\n\r\n/**\r\n * Type guard to check if a Router Event is a NavigationEnd event.\r\n * @internal\r\n */\r\nfunction isNavigationEnd(e: Event): e is NavigationEnd {\r\n return 'type' in e && e.type === EventType.NavigationEnd;\r\n}\r\n\r\n/**\r\n * Creates a Signal that tracks the current router URL.\r\n *\r\n * The signal emits the URL string reflecting the router state *after* redirects\r\n * have completed for each successful navigation. It initializes with the router's\r\n * current URL state.\r\n *\r\n * @returns {Signal<string>} A Signal emitting the `urlAfterRedirects` upon successful navigation.\r\n *\r\n * @example\r\n * ```ts\r\n * import { Component, effect } from '@angular/core';\r\n * import { url } from '@mmstack/router-core'; // Adjust import path\r\n *\r\n * @Component({\r\n * selector: 'app-root',\r\n * template: `Current URL: {{ currentUrl() }}`\r\n * })\r\n * export class AppComponent {\r\n * currentUrl = url();\r\n *\r\n * constructor() {\r\n * effect(() => {\r\n * console.log('Navigation ended. New URL:', this.currentUrl());\r\n * // e.g., track page view with analytics\r\n * });\r\n * }\r\n * }\r\n * ```\r\n */\r\nexport function url(): Signal<string> {\r\n const router = inject(Router);\r\n\r\n return toSignal(\r\n router.events.pipe(\r\n filter(isNavigationEnd),\r\n map((e) => e.urlAfterRedirects),\r\n ),\r\n {\r\n initialValue: router.url,\r\n },\r\n );\r\n}\r\n","import { computed, inject, Injectable, Signal } from '@angular/core';\r\nimport {\r\n ActivatedRouteSnapshot,\r\n Router,\r\n RouterStateSnapshot,\r\n} from '@angular/router';\r\nimport { url } from '../url';\r\n\r\n/**\r\n * @internal\r\n */\r\nexport type ResolvedLeafRoute = {\r\n route: ActivatedRouteSnapshot;\r\n segment: {\r\n path: string;\r\n resolved: string;\r\n };\r\n path: string;\r\n link: string;\r\n};\r\n\r\nfunction leafRoutes(): Signal<ResolvedLeafRoute[]> {\r\n const router = inject(Router);\r\n\r\n const getLeafRoutes = (\r\n snapshot: RouterStateSnapshot,\r\n ): ResolvedLeafRoute[] => {\r\n const routes: ResolvedLeafRoute[] = [];\r\n let route: ActivatedRouteSnapshot | null = snapshot.root;\r\n const processed = new Set<string>();\r\n\r\n while (route) {\r\n const allSegments = route.pathFromRoot.flatMap(\r\n (snap) => snap.routeConfig?.path ?? [],\r\n );\r\n\r\n const segments = allSegments.filter(Boolean);\r\n\r\n const path = router.serializeUrl(router.parseUrl(segments.join('/')));\r\n\r\n if (processed.has(path)) {\r\n route = route.firstChild;\r\n continue;\r\n }\r\n processed.add(path);\r\n\r\n const parts = route.pathFromRoot\r\n .flatMap((snap) => snap.url ?? [])\r\n .map((u) => u.path)\r\n .filter(Boolean);\r\n\r\n const link = router.serializeUrl(router.parseUrl(parts.join('/')));\r\n\r\n routes.push({\r\n route,\r\n segment: {\r\n path: segments.at(-1) ?? '',\r\n resolved: parts.at(-1) ?? '',\r\n },\r\n path,\r\n link,\r\n });\r\n route = route.firstChild;\r\n }\r\n\r\n return routes;\r\n };\r\n\r\n const currentUrl = url();\r\n\r\n const leafRoutes = computed(() => {\r\n currentUrl();\r\n return getLeafRoutes(router.routerState.snapshot);\r\n });\r\n\r\n return leafRoutes;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root',\r\n})\r\nexport class RouteLeafStore {\r\n readonly leaves = leafRoutes();\r\n}\r\n\r\nexport function injectLeafRoutes() {\r\n const store = inject(RouteLeafStore);\r\n return store.leaves;\r\n}\r\n","import { type Signal } from '@angular/core';\r\n\r\n/**\r\n * Represents a single breadcrumb item within the navigation path.\r\n * All dynamic properties are represented as Angular Signals to enable reactivity.\r\n */\r\nexport type Breadcrumb = {\r\n /**\r\n * A unique identifier for the breadcrumb item. Generally the unresolved path for example `/posts/:id`.\r\n * Useful for `@for` tracking in templates.\r\n */\r\n id: string;\r\n /**\r\n * The visible text for the breadcrumb item.\r\n * Updated reactively as the url/link based on\r\n * either a provided definition, or the current route.\r\n */\r\n label: Signal<string>;\r\n /**\r\n * An accessible label for the breadcrumb item.\r\n * Defaults to the same value as `label` if not provided.\r\n */\r\n ariaLabel: Signal<string>;\r\n /**\r\n * The URL link for the breadcrumb item.\r\n * Updates as the route changes.\r\n */\r\n link: Signal<string>;\r\n};\r\n\r\n/**\r\n * @internal\r\n */\r\nconst INTERNAL_BREADCRUMB_SYMBOL = Symbol.for('MMSTACK_INTERNAL_BREADCRUMB');\r\n\r\n/**\r\n * @internal\r\n */\r\nexport type InternalBreadcrumb = Breadcrumb & {\r\n [INTERNAL_BREADCRUMB_SYMBOL]: {\r\n active: Signal<boolean>;\r\n registered: boolean;\r\n };\r\n};\r\n\r\n/**\r\n * @internal\r\n */\r\nexport function getBreadcrumbInternals(breadcrumb: InternalBreadcrumb) {\r\n return (breadcrumb as InternalBreadcrumb)[INTERNAL_BREADCRUMB_SYMBOL];\r\n}\r\n\r\n/**\r\n * @internal\r\n */\r\nexport function createInternalBreadcrumb(\r\n bc: Breadcrumb,\r\n active: Signal<boolean>,\r\n registered = true,\r\n): InternalBreadcrumb {\r\n return {\r\n ...bc,\r\n [INTERNAL_BREADCRUMB_SYMBOL]: {\r\n active,\r\n registered,\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * @internal\r\n */\r\nexport function isInternalBreadcrumb(\r\n breadcrumb: Breadcrumb | InternalBreadcrumb,\r\n): breadcrumb is InternalBreadcrumb {\r\n return !!(breadcrumb as InternalBreadcrumb)[INTERNAL_BREADCRUMB_SYMBOL];\r\n}\r\n","import { computed, inject, Injectable, Signal } from '@angular/core';\r\nimport { mapArray, mutable } from '@mmstack/primitives';\r\nimport { injectLeafRoutes, ResolvedLeafRoute } from '../util/leaf.store';\r\nimport {\r\n Breadcrumb,\r\n createInternalBreadcrumb,\r\n getBreadcrumbInternals,\r\n InternalBreadcrumb,\r\n isInternalBreadcrumb,\r\n} from './breadcrumb';\r\nimport { injectBreadcrumbConfig } from './breadcrumb-config';\r\n\r\nfunction uppercaseFirst(str: string): string {\r\n const lcs = str.toLowerCase();\r\n return lcs.charAt(0).toUpperCase() + lcs.slice(1);\r\n}\r\n\r\nfunction removeMatrixAndQueryParams(path: string): string {\r\n const [cleanPath] = path.split(';');\r\n return cleanPath.split('?')[0];\r\n}\r\n\r\nfunction parsePathSegment(pathSegment: string): string {\r\n return pathSegment\r\n .split('/')\r\n .flatMap((part) => part.split('.'))\r\n .flatMap((part) => part.split('-'))\r\n .map((part) => uppercaseFirst(removeMatrixAndQueryParams(part)))\r\n .join(' ');\r\n}\r\n\r\nfunction generateLabel(leaf: ResolvedLeafRoute): string {\r\n const title = leaf.route.title ?? leaf.route.data?.['title'];\r\n\r\n if (title && typeof title === 'string') return title;\r\n if (leaf.segment.path.includes(':')) return leaf.segment.resolved;\r\n\r\n return parsePathSegment(leaf.segment.path);\r\n}\r\n\r\nfunction autoGenerateBreadcrumb(\r\n id: string,\r\n leaf: Signal<ResolvedLeafRoute>,\r\n autoGenerateFn: Signal<(leaf: ResolvedLeafRoute) => string>,\r\n): Breadcrumb {\r\n const label = computed(() => autoGenerateFn()(leaf()));\r\n\r\n return createInternalBreadcrumb(\r\n {\r\n id,\r\n label,\r\n ariaLabel: label,\r\n link: computed(() => leaf().link),\r\n },\r\n computed(\r\n () =>\r\n leaf().route.data?.['skipBreadcrumb'] !== true &&\r\n id !== '' &&\r\n id !== '/' &&\r\n leaf().segment.path !== '' &&\r\n leaf().segment.path !== '/' &&\r\n !leaf().segment.path.endsWith('/') &&\r\n !!label(),\r\n ),\r\n );\r\n}\r\n\r\nfunction injectGenerateLabelFn() {\r\n const { generation } = injectBreadcrumbConfig();\r\n\r\n if (typeof generation !== 'function') return computed(() => generateLabel);\r\n\r\n const provided = generation();\r\n return computed(() => provided);\r\n}\r\n\r\nfunction injectIsManual() {\r\n return injectBreadcrumbConfig().generation === 'manual';\r\n}\r\n\r\nfunction exposeActiveSignal(\r\n crumbSignal: Signal<Breadcrumb>,\r\n manual: boolean,\r\n): Signal<Breadcrumb> & {\r\n active: Signal<boolean>;\r\n} {\r\n const active = manual\r\n ? computed(() => {\r\n const crumb = crumbSignal();\r\n\r\n return (\r\n isInternalBreadcrumb(crumb) &&\r\n getBreadcrumbInternals(crumb).registered &&\r\n getBreadcrumbInternals(crumb).active()\r\n );\r\n })\r\n : computed(() => {\r\n const crumb = crumbSignal();\r\n if (!isInternalBreadcrumb(crumb)) return true;\r\n return getBreadcrumbInternals(crumb).active();\r\n });\r\n\r\n const sig = crumbSignal as Signal<Breadcrumb> & {\r\n active: Signal<boolean>;\r\n };\r\n\r\n sig.active = active;\r\n\r\n return sig;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root',\r\n})\r\nexport class BreadcrumbStore {\r\n private readonly map = mutable<Map<string, InternalBreadcrumb>>(new Map());\r\n private readonly isManual = injectIsManual();\r\n private readonly autoGenerateLabelFn = injectGenerateLabelFn();\r\n private readonly leafRoutes = injectLeafRoutes();\r\n\r\n private readonly all = mapArray(\r\n this.leafRoutes,\r\n (leaf) => {\r\n const stableId = computed(() => leaf().path);\r\n\r\n return exposeActiveSignal(\r\n computed(\r\n () => {\r\n const id = stableId();\r\n\r\n const found = this.map().get(id);\r\n\r\n if (!found)\r\n return autoGenerateBreadcrumb(id, leaf, this.autoGenerateLabelFn);\r\n\r\n if (!id.includes(':')) return found;\r\n\r\n return {\r\n ...found,\r\n link: computed(() => leaf().link),\r\n };\r\n },\r\n {\r\n equal: (a, b) => a.id === b.id,\r\n },\r\n ),\r\n this.isManual,\r\n );\r\n },\r\n {\r\n equal: (a, b) => a.link === b.link,\r\n },\r\n );\r\n\r\n private readonly crumbs = computed((): Signal<Breadcrumb>[] =>\r\n this.all().filter((c) => c.active()),\r\n );\r\n\r\n readonly unwrapped = computed(() => this.crumbs().map((c) => c()));\r\n\r\n register(breadcrumb: InternalBreadcrumb) {\r\n this.map.inline((m) => m.set(breadcrumb.id, breadcrumb));\r\n }\r\n}\r\n\r\n/**\r\n * Injects and provides access to a reactive list of breadcrumbs.\r\n *\r\n * The breadcrumbs are ordered and reflect the current active navigation path.\r\n * @see Breadcrumb\r\n * @returns `Signal<Breadcrumb[]>`\r\n *\r\n * @example\r\n * ```typescript\r\n * @Component({\r\n * selector: 'app-breadcrumbs',\r\n * template: `\r\n * <nav aria-label=\"breadcrumb\">\r\n * <ol>\r\n * @for (crumb of breadcrumbs(); track crumb.id) {\r\n * <li>\r\n * <a [href]=\"crumb.link()\" [attr.aria-label]=\"crumb.ariaLabel()\">{{ crumb.label() }}</a>\r\n * </li>\r\n * }\r\n * </ol>\r\n * </nav>\r\n * `\r\n * })\r\n * export class MyBreadcrumbsComponent {\r\n * breadcrumbs = injectBreadcrumbs();\r\n * }\r\n * ```\r\n */\r\nexport function injectBreadcrumbs() {\r\n const store = inject(BreadcrumbStore);\r\n return store.unwrapped;\r\n}\r\n","function parsePathSegment(segmentString: string): {\r\n pathPart: string;\r\n matrixParams: Record<string, string>;\r\n} {\r\n const parts = segmentString.split(';');\r\n const pathPart = parts[0];\r\n const matrixParams: Record<string, string> = {};\r\n for (let i = 1; i < parts.length; i++) {\r\n const [key, value = 'true'] = parts[i].split('=');\r\n if (key) {\r\n matrixParams[key] = value;\r\n }\r\n }\r\n return { pathPart, matrixParams };\r\n}\r\n\r\nfunction createBasePredicate(path: string): (path: string) => boolean {\r\n const partPredicates = path\r\n .split('/')\r\n .filter((part) => !!part.trim())\r\n .map((configSegmentString) => {\r\n const { pathPart: configPathPart, matrixParams: configMatrixParams } =\r\n parsePathSegment(configSegmentString);\r\n\r\n let singlePathPartPredicate: (linkSegmentPathPart: string) => boolean;\r\n if (configPathPart.startsWith(':')) {\r\n singlePathPartPredicate = () => true;\r\n } else {\r\n singlePathPartPredicate = (linkSegmentPathPart: string) =>\r\n linkSegmentPathPart === configPathPart;\r\n }\r\n\r\n const configSegmentHasMatrixParams =\r\n Object.keys(configMatrixParams).length > 0;\r\n\r\n return (linkSegmentString: string) => {\r\n const { pathPart: linkPathPart, matrixParams: linkMatrixParams } =\r\n parsePathSegment(linkSegmentString);\r\n\r\n if (!singlePathPartPredicate(linkPathPart)) {\r\n return false;\r\n }\r\n\r\n if (!configSegmentHasMatrixParams) {\r\n return true;\r\n }\r\n\r\n return Object.entries(configMatrixParams).every(\r\n ([key, value]) =>\r\n Object.prototype.hasOwnProperty.call(linkMatrixParams, key) &&\r\n linkMatrixParams[key] === value,\r\n );\r\n };\r\n });\r\n\r\n return (path: string) => {\r\n const linkPathOnly = path.split(/[?#]/).at(0) ?? '';\r\n if (!linkPathOnly && partPredicates.length > 0) return false;\r\n if (!linkPathOnly && partPredicates.length === 0) return true;\r\n\r\n const parts = linkPathOnly.split('/').filter((part) => !!part.trim());\r\n if (parts.length < partPredicates.length) return false;\r\n\r\n return parts.every((seg, idx) => {\r\n const pred = partPredicates.at(idx);\r\n if (!pred) return true;\r\n return pred(seg);\r\n });\r\n };\r\n}\r\n\r\ntype ParsedSegment = {\r\n pathPart: string;\r\n matrixParams: Record<string, string>;\r\n};\r\n\r\nfunction singleSegmentMatches(\r\n configSegment: ParsedSegment,\r\n linkSegment: ParsedSegment,\r\n): boolean {\r\n if (configSegment.pathPart === ':') {\r\n return true;\r\n } else if (configSegment.pathPart !== linkSegment.pathPart) {\r\n return false;\r\n }\r\n\r\n const configMatrix = configSegment.matrixParams;\r\n const linkMatrix = linkSegment.matrixParams;\r\n for (const key in configMatrix) {\r\n if (\r\n !Object.prototype.hasOwnProperty.call(linkMatrix, key) ||\r\n linkMatrix[key] !== configMatrix[key]\r\n ) {\r\n return false;\r\n }\r\n }\r\n return true;\r\n}\r\n\r\nfunction matchSegmentsRecursive(\r\n configSegments: ParsedSegment[],\r\n linkSegments: ParsedSegment[],\r\n configIdx: number,\r\n linkIdx: number,\r\n): boolean {\r\n if (configIdx === configSegments.length) {\r\n return linkIdx === linkSegments.length;\r\n }\r\n\r\n if (linkIdx === linkSegments.length) {\r\n for (let i = configIdx; i < configSegments.length; i++) {\r\n if (configSegments[i].pathPart !== '**') {\r\n return false;\r\n }\r\n }\r\n return true;\r\n }\r\n\r\n const currentConfigSegment = configSegments[configIdx];\r\n\r\n if (currentConfigSegment.pathPart === '**') {\r\n if (\r\n matchSegmentsRecursive(\r\n configSegments,\r\n linkSegments,\r\n configIdx + 1,\r\n linkIdx,\r\n )\r\n ) {\r\n return true;\r\n }\r\n\r\n if (linkIdx < linkSegments.length) {\r\n if (\r\n matchSegmentsRecursive(\r\n configSegments,\r\n linkSegments,\r\n configIdx,\r\n linkIdx + 1,\r\n )\r\n ) {\r\n return true;\r\n }\r\n }\r\n\r\n return false;\r\n } else {\r\n if (\r\n linkIdx < linkSegments.length &&\r\n singleSegmentMatches(currentConfigSegment, linkSegments[linkIdx])\r\n ) {\r\n return matchSegmentsRecursive(\r\n configSegments,\r\n linkSegments,\r\n configIdx + 1,\r\n linkIdx + 1,\r\n );\r\n }\r\n\r\n return false;\r\n }\r\n}\r\n\r\nfunction createWildcardPredicate(path: string): (linkPath: string) => boolean {\r\n const configSegments = path\r\n .split('/')\r\n .filter((p) => !!p.trim())\r\n .map((segment) => parsePathSegment(segment));\r\n\r\n return (linkPath: string): boolean => {\r\n const linkPathOnly = linkPath.split(/[?#]/).at(0) ?? '';\r\n const linkSegments = linkPathOnly\r\n .split('/')\r\n .filter((p) => !!p.trim())\r\n .map((segment) => parsePathSegment(segment));\r\n\r\n return matchSegmentsRecursive(configSegments, linkSegments, 0, 0);\r\n };\r\n}\r\n\r\nexport function createRoutePredicate(\r\n path: string,\r\n): (linkPath: string) => boolean {\r\n return path.includes('**')\r\n ? createWildcardPredicate(path)\r\n : createBasePredicate(path);\r\n}\r\n","// The following functions are adapted from ngx-quicklink,\r\n// (https://github.com/mgechev/ngx-quicklink)\r\n// Copyright (c) Minko Gechev and contributors, licensed under the MIT License.\r\n\r\nimport { PRIMARY_OUTLET, Route } from '@angular/router';\r\n\r\nfunction isPrimaryRoute(route: Route): boolean {\r\n return route.outlet === PRIMARY_OUTLET || !route.outlet;\r\n}\r\n\r\nexport const findPath = (config: Route[], route: Route): string => {\r\n const configQueue = config.slice();\r\n const parent = new Map<Route, Route>();\r\n const visited = new Set<Route>();\r\n\r\n while (configQueue.length) {\r\n const el = configQueue.shift();\r\n if (!el) {\r\n continue;\r\n }\r\n\r\n visited.add(el);\r\n\r\n if (el === route) {\r\n break;\r\n }\r\n\r\n (el.children || []).forEach((childRoute: Route) => {\r\n if (!visited.has(childRoute)) {\r\n parent.set(childRoute, el);\r\n configQueue.push(childRoute);\r\n }\r\n });\r\n\r\n const lazyRoutes = (el as any)._loadedRoutes || [];\r\n if (Array.isArray(lazyRoutes)) {\r\n lazyRoutes.forEach((lazyRoute: Route) => {\r\n if (lazyRoute && !visited.has(lazyRoute)) {\r\n parent.set(lazyRoute, el);\r\n configQueue.push(lazyRoute);\r\n }\r\n });\r\n }\r\n }\r\n\r\n let path = '';\r\n let currentRoute: Route | undefined = route;\r\n\r\n while (currentRoute) {\r\n const currentPath = currentRoute.path || '';\r\n if (isPrimaryRoute(currentRoute)) {\r\n path = `/${currentPath}${path}`;\r\n } else {\r\n path = `/(${currentRoute.outlet}:${currentPath})${path}`;\r\n }\r\n currentRoute = parent.get(currentRoute);\r\n }\r\n\r\n let normalizedPath = path.replaceAll(/\\/+/g, '/');\r\n\r\n if (normalizedPath !== '/' && normalizedPath.endsWith('/')) {\r\n normalizedPath = normalizedPath.slice(0, -1);\r\n }\r\n\r\n return normalizedPath;\r\n};\r\n","import { inject } from '@angular/core';\r\nimport { ActivatedRouteSnapshot, Router } from '@angular/router';\r\n\r\nexport function injectSnapshotPathResolver() {\r\n const router = inject(Router);\r\n\r\n return (route: ActivatedRouteSnapshot) => {\r\n const segments = route.pathFromRoot.flatMap(\r\n (snap) => snap.routeConfig?.path ?? [],\r\n );\r\n\r\n const joinedSegments = segments.filter(Boolean).join('/');\r\n\r\n return router.serializeUrl(router.parseUrl(joinedSegments));\r\n };\r\n}\r\n","import { computed, inject } from '@angular/core';\r\nimport {\r\n createUrlTreeFromSnapshot,\r\n Router,\r\n type ResolveFn,\r\n} from '@angular/router';\r\nimport { BreadcrumbStore } from './breadcrumb-store';\r\n\r\n/**\r\n * Options for defining a breadcrumb.\r\n *\r\n */\r\ntype CreateBreadcrumbOptions = {\r\n /**\r\n * The visible text for the breadcrumb.\r\n * Can be a static string or a function for dynamic labels.\r\n */\r\n label: string | (() => string);\r\n /**\r\n * An accessible label for the breadcrumb item.\r\n * Defaults to the value of `label` if not provided.\r\n * Can be a static string or a function returning a string for dynamic ARIA labels.\r\n */\r\n ariaLabel?: string | (() => string);\r\n /**\r\n * If `true`, the route resolver will wait until the `label` signal has a value before `resolving`\r\n */\r\n awaitValue?: boolean;\r\n};\r\n\r\nimport { until } from '@mmstack/primitives';\r\nimport { injectSnapshotPathResolver } from '../util';\r\nimport { Breadcrumb, createInternalBreadcrumb } from './breadcrumb';\r\n\r\n/**\r\n * Creates and registers a breadcrumb for a specific route.\r\n * This function is designed to be used as an Angular Route `ResolveFn`.\r\n * It handles the registration of the breadcrumb with the `BreadcrumbStore`\r\n * and ensures automatic deregistration when the route is destroyed.\r\n *\r\n * @param factory A function that returns a `CreateBreadcrumbOptions` object.\r\n * @see CreateBreadcrumbOptions\r\n *\r\n * @example\r\n * ```typescript\r\n * export const appRoutes: Routes = [\r\n * {\r\n * path: 'home',\r\n * component: HomeComponent,\r\n * resolve: {\r\n * breadcrumb: createBreadcrumb(() => ({\r\n * label: 'Home',\r\n * });\r\n * },\r\n * path: 'users/:userId',\r\n * component: UserProfileComponent,\r\n * resolve: {\r\n * breadcrumb: createBreadcrumb(() => {\r\n * const userStore = inject(UserStore);\r\n * return {\r\n * label: () => userStore.user().name ?? 'Loading...\r\n * };\r\n * })\r\n * },\r\n * }\r\n * ];\r\n * ```\r\n */\r\nexport function createBreadcrumb(\r\n factory: () => CreateBreadcrumbOptions,\r\n): ResolveFn<void> {\r\n return async (route) => {\r\n const router = inject(Router);\r\n const store = inject(BreadcrumbStore);\r\n const resolver = injectSnapshotPathResolver();\r\n\r\n const fp = resolver(route);\r\n\r\n const tree = createUrlTreeFromSnapshot(\r\n route,\r\n [],\r\n route.queryParams,\r\n route.fragment,\r\n );\r\n\r\n const provided = factory();\r\n\r\n const link = computed(() => router.serializeUrl(tree));\r\n\r\n const { label, ariaLabel = label } = provided;\r\n\r\n const bc: Breadcrumb = {\r\n id: fp,\r\n ariaLabel:\r\n typeof ariaLabel === 'string'\r\n ? computed(() => ariaLabel)\r\n : computed(ariaLabel),\r\n label:\r\n typeof label === 'string' ? computed(() => label) : computed(label),\r\n link,\r\n };\r\n\r\n store.register(\r\n createInternalBreadcrumb(\r\n bc,\r\n computed(() => route.data?.['skipBreadcrumb'] !== true),\r\n ),\r\n );\r\n\r\n if (provided.awaitValue) await until(bc.label, (v) => !!v);\r\n\r\n return Promise.resolve();\r\n };\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { Subject } from 'rxjs';\r\n\r\n@Injectable({ providedIn: 'root' })\r\nexport class PreloadRequester {\r\n private readonly preloadOnDemand$ = new Subject<string>();\r\n readonly preloadRequested$ = this.preloadOnDemand$.asObservable();\r\n\r\n startPreload(routePath: string) {\r\n this.preloadOnDemand$.next(routePath);\r\n }\r\n}\r\n","import { inject, Injectable } from '@angular/core';\r\nimport { PreloadingStrategy, type Route, Router } from '@angular/router';\r\nimport { EMPTY, filter, finalize, Observable, switchMap, take } from 'rxjs';\r\nimport { createRoutePredicate, findPath } from '../util';\r\nimport { PreloadRequester } from './preload-requester';\r\n\r\nfunction hasSlowConnection() {\r\n if (\r\n globalThis.window &&\r\n 'navigator' in globalThis.window &&\r\n 'connection' in globalThis.window.navigator &&\r\n typeof globalThis.window.navigator.connection === 'object' &&\r\n !!globalThis.window.navigator.connection\r\n ) {\r\n const is2g =\r\n 'effectiveType' in globalThis.window.navigator.connection &&\r\n typeof globalThis.window.navigator.connection.effectiveType ===\r\n 'string' &&\r\n globalThis.window.navigator.connection.effectiveType.endsWith('2g');\r\n if (is2g) return true;\r\n if (\r\n 'saveData' in globalThis.window.navigator.connection &&\r\n typeof globalThis.window.navigator.connection.saveData === 'boolean' &&\r\n globalThis.window.navigator.connection.saveData\r\n )\r\n return true;\r\n }\r\n\r\n return false;\r\n}\r\n\r\nfunction noPreload(route: Route) {\r\n return route.data && route.data['preload'] === false;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root',\r\n})\r\nexport class PreloadStrategy implements PreloadingStrategy {\r\n private readonly loading = new Set<string>();\r\n private readonly router = inject(Router);\r\n private readonly req = inject(PreloadRequester);\r\n\r\n preload(route: Route, load: () => Observable<any>): Observable<any> {\r\n if (noPreload(route) || hasSlowConnection()) return EMPTY;\r\n\r\n const fp = findPath(this.router.config, route);\r\n\r\n if (this.loading.has(fp)) return EMPTY;\r\n\r\n const predicate = createRoutePredicate(fp);\r\n return this.req.preloadRequested$.pipe(\r\n filter((path) => path === fp || predicate(path)),\r\n take(1),\r\n switchMap(() => load()),\r\n finalize(() => this.loading.delete(fp)),\r\n );\r\n }\r\n}\r\n","import {\r\n booleanAttribute,\r\n computed,\r\n Directive,\r\n effect,\r\n HostListener,\r\n inject,\r\n InjectionToken,\r\n input,\r\n output,\r\n Provider,\r\n untracked,\r\n} from '@angular/core';\r\nimport {\r\n type ActivatedRoute,\r\n type Params,\r\n Router,\r\n RouterLink,\r\n RouterLinkWithHref,\r\n UrlTree,\r\n} from '@angular/router';\r\nimport { elementVisibility } from '@mmstack/primitives';\r\nimport { PreloadRequester } from './preloading';\r\n\r\nfunction inputToUrlTree(\r\n router: Router,\r\n link: string | any[] | UrlTree | null,\r\n relativeTo?: ActivatedRoute,\r\n queryParams?: Params,\r\n fragment?: string,\r\n queryParamsHandling?: 'merge' | 'preserve' | '',\r\n routerLinkUrlTree?: UrlTree | null,\r\n): UrlTree | null {\r\n if (!link) return null;\r\n if (routerLinkUrlTree) return routerLinkUrlTree;\r\n\r\n if (link instanceof UrlTree) return link;\r\n\r\n const arr = Array.isArray(link) ? link : [link];\r\n\r\n return router.createUrlTree(arr, {\r\n relativeTo,\r\n queryParams,\r\n fragment,\r\n queryParamsHandling,\r\n });\r\n}\r\n\r\nfunction treeToSerializedUrl(\r\n router: Router,\r\n urlTree: UrlTree | null,\r\n): string | null {\r\n if (!urlTree) return null;\r\n return router.serializeUrl(urlTree);\r\n}\r\n\r\nexport function injectTriggerPreload() {\r\n const req = inject(PreloadRequester);\r\n const router = inject(Router);\r\n\r\n return (\r\n link: string | any[] | UrlTree | null,\r\n relativeTo?: ActivatedRoute,\r\n queryParams?: Params,\r\n fragment?: string,\r\n queryParamsHandling?: 'merge' | 'preserve' | '',\r\n ) => {\r\n const urlTree = inputToUrlTree(\r\n router,\r\n link,\r\n relativeTo,\r\n queryParams,\r\n fragment,\r\n queryParamsHandling,\r\n );\r\n const fullPath = treeToSerializedUrl(router, urlTree);\r\n if (!fullPath) return;\r\n\r\n req.startPreload(fullPath);\r\n };\r\n}\r\n\r\n/**\r\n * Configuration for the `mmLink` directive.\r\n */\r\ntype MMLinkConfig = {\r\n /**\r\n * The default preload behavior for links.\r\n * Can be 'hover', 'visible', or null (no preloading).\r\n * @default 'hover'\r\n */\r\n preloadOn: 'hover' | 'visible' | null;\r\n /**\r\n * Whether to use mouse down events for preloading.\r\n * @default false\r\n */\r\n useMouseDown: boolean;\r\n};\r\n\r\nconst configToken = new InjectionToken<MMLinkConfig>('MMSTACK_LINK_CONFIG');\r\n\r\nexport function provideMMLinkDefaultConfig(\r\n config: Partial<MMLinkConfig>,\r\n): Provider {\r\n const cfg: MMLinkConfig = {\r\n preloadOn: 'hover',\r\n useMouseDown: false,\r\n ...config,\r\n };\r\n\r\n return {\r\n provide: configToken,\r\n useValue: cfg,\r\n };\r\n}\r\n\r\nfunction injectConfig() {\r\n const cfg = inject(configToken, { optional: true });\r\n return {\r\n preloadOn: 'hover' as const,\r\n useMouseDown: false,\r\n ...cfg,\r\n };\r\n}\r\n\r\n@Directive({\r\n selector: '[mmLink]',\r\n exportAs: 'mmLink',\r\n host: {\r\n '(mouseenter)': 'onHover()',\r\n },\r\n hostDirectives: [\r\n {\r\n directive: RouterLink,\r\n inputs: [\r\n 'routerLink: mmLink',\r\n 'target',\r\n 'queryParams',\r\n 'fragment',\r\n 'queryParamsHandling',\r\n 'state',\r\n 'relativeTo',\r\n 'skipLocationChange',\r\n 'replaceUrl',\r\n ],\r\n },\r\n ],\r\n})\r\nexport class Link {\r\n private readonly routerLink =\r\n inject(RouterLink, {\r\n self: true,\r\n optional: true,\r\n }) ?? inject(RouterLinkWithHref, { self: true, optional: true });\r\n\r\n private readonly req = inject(PreloadRequester);\r\n private readonly router = inject(Router);\r\n\r\n readonly target = input<string>();\r\n readonly queryParams = input<Params>();\r\n readonly fragment = input<string>();\r\n readonly queryParamsHandling = input<'merge' | 'preserve' | ''>();\r\n readonly state = input<Record<string, any>>();\r\n readonly info = input<unknown>();\r\n readonly relativeTo = input<ActivatedRoute>();\r\n readonly skipLocationChange = input(false, { transform: booleanAttribute });\r\n readonly replaceUrl = input(false, { transform: booleanAttribute });\r\n readonly mmLink = input<string | any[] | UrlTree | null>(null);\r\n readonly preloadOn = input<'hover' | 'visible' | null>(\r\n injectConfig().preloadOn,\r\n );\r\n readonly useMouseDown = input(injectConfig().useMouseDown, {\r\n transform: booleanAttribute,\r\n });\r\n readonly beforeNavigate = input<() => void>();\r\n\r\n readonly preloading = output<void>();\r\n\r\n private readonly urlTree = computed(() => {\r\n return inputToUrlTree(\r\n this.router,\r\n this.mmLink(),\r\n this.relativeTo(),\r\n this.queryParams(),\r\n this.fragment(),\r\n this.queryParamsHandling(),\r\n this.routerLink?.urlTree,\r\n );\r\n });\r\n\r\n private readonly fullPath = computed(() => {\r\n return treeToSerializedUrl(this.router, this.urlTree());\r\n });\r\n\r\n onHover() {\r\n if (untracked(this.preloadOn) !== 'hover') return;\r\n this.requestPreload();\r\n }\r\n\r\n @HostListener('mousedown', [\r\n '$event.button',\r\n '$event.ctrlKey',\r\n '$event.shiftKey',\r\n '$event.altKey',\r\n '$event.metaKey',\r\n ])\r\n onMouseDown(\r\n button: number,\r\n ctrlKey: boolean,\r\n shiftKey: boolean,\r\n altKey: boolean,\r\n metaKey: boolean,\r\n ) {\r\n if (!untracked(this.useMouseDown)) return;\r\n return this.trigger(button, ctrlKey, shiftKey, altKey, metaKey);\r\n }\r\n\r\n @HostListener('click', [\r\n '$event.button',\r\n '$event.ctrlKey',\r\n '$event.shiftKey',\r\n '$event.altKey',\r\n '$event.metaKey',\r\n ])\r\n onClick(\r\n button: number,\r\n ctrlKey: boolean,\r\n shiftKey: boolean,\r\n altKey: boolean,\r\n metaKey: boolean,\r\n ) {\r\n if (untracked(this.useMouseDown)) return;\r\n return this.trigger(button, ctrlKey, shiftKey, altKey, metaKey);\r\n }\r\n\r\n constructor() {\r\n const intersection = elementVisibility();\r\n\r\n effect(() => {\r\n if (this.preloadOn() !== 'visible') return;\r\n if (intersection.visible()) this.requestPreload();\r\n });\r\n }\r\n\r\n private requestPreload() {\r\n const fp = untracked(this.fullPath);\r\n if (!this.routerLink || !fp) return;\r\n this.req.startPreload(fp);\r\n this.preloading.emit();\r\n }\r\n\r\n private trigger(\r\n button: number,\r\n ctrlKey: boolean,\r\n shiftKey: boolean,\r\n altKey: boolean,\r\n metaKey: boolean,\r\n ) {\r\n untracked(this.beforeNavigate)?.();\r\n return this.routerLink?.onClick(button, ctrlKey, shiftKey, altKey, metaKey);\r\n }\r\n}\r\n","import {\r\n computed,\r\n inject,\r\n isSignal,\r\n untracked,\r\n type WritableSignal,\r\n} from '@angular/core';\r\nimport { toSignal } from '@angular/core/rxjs-interop';\r\nimport { ActivatedRoute, Router } from '@angular/router';\r\nimport { toWritable } from '@mmstack/primitives';\r\n\r\n/**\r\n * Creates a WritableSignal that synchronizes with a specific URL query parameter,\r\n * enabling two-way binding between the signal's state and the URL.\r\n *\r\n * Reading the signal provides the current value of the query parameter (or null if absent).\r\n * Setting the signal updates the URL query parameter using `Router.navigate`, triggering\r\n * navigation and causing the signal to update reactively if the navigation is successful.\r\n *\r\n * @param key The key of the query parameter to synchronize with.\r\n * Can be a static string (e.g., `'search'`) or a function/signal returning a string\r\n * for dynamic keys (e.g., `() => this.userId() + '_filter'` or `computed(() => this.category() + '_sort')`).\r\n * The signal will reactively update if the key returned by the function/signal changes.\r\n * @returns {WritableSignal<string | null>} A signal representing the query parameter's value.\r\n * - Reading returns the current value string, or `null` if the parameter is absent in the URL.\r\n * - Setting the signal to a string updates the query parameter in the URL (e.g., `signal.set('value')` results in `?key=value`).\r\n * - Setting the signal to `null` removes the query parameter from the URL (e.g., `signal.set(null)` results in `?otherParam=...`).\r\n * - Automatically reflects changes if the query parameters update due to external navigation.\r\n * @remarks\r\n * - Requires Angular's `ActivatedRoute` and `Router` to be available in the injection context.\r\n * - Uses `Router.navigate` with `queryParamsHandling: 'merge'` to preserve other existing query parameters during updates.\r\n * - Handles dynamic keys reactively. If the result of the `key` function/signal changes, the signal will start reflecting the value of the *new* query parameter key.\r\n * - During Server-Side Rendering (SSR), it reads the initial value from the route snapshot. Write operations (`set`) might have limited or no effect on the server depending on the platform configuration.\r\n *\r\n * @example\r\n * ```ts\r\n * import { Component, computed, effect, signal } from '@angular/core';\r\n * import { queryParam } from '@mmstack/router-core'; // Adjust import path as needed\r\n * // import { FormsModule } from '@angular/forms'; // If using ngModel\r\n *\r\n * @Component({\r\n * selector: 'app-product-list',\r\n * standalone: true,\r\n * // imports: [FormsModule], // If using ngModel\r\n * template: `\r\n * <div>\r\n * Sort By:\r\n * <select [value]=\"sortSignal() ?? ''\" (change)=\"sortSignal.set($any($event.target).value || null)\">\r\n * <option value=\"\">Default</option>\r\n * <option value=\"price_asc\">Price Asc</option>\r\n * <option value=\"price_desc\">Price Desc</option>\r\n * <option value=\"name\">Name</option>\r\n * </select>\r\n * <button (click)=\"sortSignal.set(null)\" [disabled]=\"!sortSignal()\">Clear Sort</button>\r\n * </div>\r\n * <div>\r\n * Page:\r\n * <input type=\"number\" min=\"1\" [value]=\"pageSignal() ?? '1'\" #p (input)=\"setPage(p.value)\"/>\r\n * </div>\r\n * * `\r\n * })\r\n * export class ProductListComponent {\r\n * // Two-way bind the 'sort' query parameter (?sort=...)\r\n * // Defaults to null if param is missing\r\n * sortSignal = queryParam('sort');\r\n *\r\n * // Example with a different type (needs serialization or separate logic)\r\n * // For simplicity, we treat page as string | null here\r\n * pageSignal = queryParam('page');\r\n *\r\n * constructor() {\r\n * effect(() => {\r\n * const currentSort = this.sortSignal();\r\n * const currentPage = this.pageSignal(); // Read as string | null\r\n * console.log('Sort/Page changed, reloading products for:', { sort: currentSort, page: currentPage });\r\n * // --- Fetch data based on currentSort and currentPage ---\r\n * });\r\n * }\r\n *\r\n * setPage(value: string): void {\r\n * const pageNum = parseInt(value, 10);\r\n * // Set to null if page is 1 (to remove param), otherwise set string value\r\n * this.pageSignal.set(isNaN(pageNum) || pageNum <= 1 ? null : pageNum.toString());\r\n * }\r\n * }\r\n * ```\r\n */\r\nexport function queryParam(\r\n key: string | (() => string),\r\n): WritableSignal<string | null> {\r\n const route = inject(ActivatedRoute);\r\n const router = inject(Router);\r\n\r\n const keySignal =\r\n typeof key === 'string'\r\n ? computed(() => key)\r\n : isSignal(key)\r\n ? key\r\n : computed(key);\r\n\r\n const queryParamMap = toSignal(route.queryParamMap, {\r\n initialValue: route.snapshot.queryParamMap,\r\n });\r\n\r\n const queryParams = toSignal(route.queryParams, {\r\n initialValue: route.snapshot.queryParams,\r\n });\r\n\r\n const queryParam = computed(() => queryParamMap().get(keySignal()));\r\n\r\n const set = (newValue: string | null) => {\r\n const next = {\r\n ...untracked(queryParams),\r\n };\r\n const key = untracked(keySignal);\r\n\r\n if (newValue === null) {\r\n delete next[key];\r\n } else {\r\n next[key] = newValue;\r\n }\r\n\r\n router.navigate([], {\r\n relativeTo: route,\r\n queryParams: next,\r\n queryParamsHandling: 'merge',\r\n });\r\n };\r\n\r\n return toWritable(queryParam, set);\r\n}\r\n","import { inject, InjectionToken, Provider } from '@angular/core';\r\n\r\n/**\r\n * Title configuration interface.\r\n * Defines how createTitle should behave\r\n * @see {createTitle}\r\n */\r\nexport type TitleConfig = {\r\n /**\r\n * The title to be used when no title is set.\r\n * If not provided it defaults to an empty string\r\n * @default ''\r\n */\r\n prefix?: string | ((title: string) => string);\r\n /**\r\n * if false, the title will change to the url, otherwise default to true as that is standard behavior\r\n * @default true\r\n */\r\n keepLastKnownTitle?: boolean;\r\n};\r\n\r\n/**\r\n * @internal\r\n */\r\nexport type InternalTitleConfig = {\r\n parser: (title: string) => string;\r\n keepLastKnown: boolean;\r\n};\r\n\r\nconst token = new InjectionToken<InternalTitleConfig>('MMSTACK_TITLE_CONFIG');\r\n\r\n/**\r\n * used to provide the title configuration, will not be applied unless a `createTitle` resolver is used\r\n */\r\nexport function provideTitleConfig(config?: TitleConfig): Provider {\r\n const prefix = config?.prefix ?? '';\r\n\r\n const prefixFn =\r\n typeof prefix === 'function'\r\n ? prefix\r\n : (title: string) => `${prefix}${title}`;\r\n\r\n return {\r\n provide: token,\r\n useValue: {\r\n parser: prefixFn,\r\n keepLastKnown: config?.keepLastKnownTitle ?? true,\r\n },\r\n };\r\n}\r\n\r\nexport function injectTitleConfig(): InternalTitleConfig {\r\n return inject(token);\r\n}\r\n","import {\r\n computed,\r\n effect,\r\n inject,\r\n Injectable,\r\n linkedSignal,\r\n Signal,\r\n untracked,\r\n} from '@angular/core';\r\nimport { Title } from '@angular/platform-browser';\r\nimport { ResolveFn } from '@angular/router';\r\nimport { mutable, until } from '@mmstack/primitives';\r\nimport { injectLeafRoutes, injectSnapshotPathResolver } from '../util';\r\nimport { injectTitleConfig } from './title-config';\r\n\r\n@Injectable({\r\n providedIn: 'root',\r\n})\r\nexport class TitleStore {\r\n private readonly title = inject(Title);\r\n private readonly map = mutable<Map<string, Signal<string>>>(new Map());\r\n private readonly leafRoutes = injectLeafRoutes();\r\n\r\n constructor() {\r\n const reverseLeaves = computed(() => this.leafRoutes().toReversed());\r\n\r\n const currentResolvedTitles = computed(() => {\r\n const map = this.map();\r\n return reverseLeaves()\r\n .map((leaf) => map.get(leaf.path)?.() ?? leaf.route.title)\r\n .filter((v): v is string => !!v);\r\n });\r\n\r\n const currentTitle = computed(() => currentResolvedTitles().at(0) ?? '');\r\n\r\n const heldTitle = injectTitleConfig().keepLastKnown\r\n ? linkedSignal<string, string>({\r\n source: () => currentTitle(),\r\n computation: (value, prev) => {\r\n if (!value) return prev?.value ?? '';\r\n return value;\r\n },\r\n })\r\n : currentTitle;\r\n\r\n effect(() => {\r\n this.title.setTitle(heldTitle());\r\n });\r\n }\r\n\r\n register(id: string, titleFn: Signal<string>) {\r\n this.map.inline((m) => m.set(id, titleFn));\r\n }\r\n}\r\n\r\n/**\r\n *\r\n * Creates a title resolver function that can be used in Angular's router.\r\n *\r\n * @param fn\r\n * A function that returns a string or a Signal<string> representing the title.\r\n * @param awaitValue\r\n * If `true`, the resolver will wait until the title signal has a value before resolving.\r\n * Defaults to `false`.\r\n */\r\nexport function createTitle(\r\n fn: () => string | (() => string),\r\n awaitValue = false,\r\n): ResolveFn<string> {\r\n return async (route): Promise<string> => {\r\n const store = inject(TitleStore);\r\n const resolver = injectSnapshotPathResolver();\r\n const fp = resolver(route);\r\n\r\n const { parser } = injectTitleConfig();\r\n\r\n const resolved = fn();\r\n\r\n const titleSignal =\r\n typeof resolved === 'string'\r\n ? computed(() => resolved)\r\n : computed(resolved);\r\n\r\n const parsedTitleSignal = computed(() => parser(titleSignal()));\r\n\r\n store.register(fp, parsedTitleSignal);\r\n\r\n if (awaitValue) await until(parsedTitleSignal, (v) => !!v);\r\n\r\n return Promise.resolve(untracked(parsedTitleSignal));\r\n };\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":["token","parsePathSegment","filter"],"mappings":"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