@mmstack/router-core 21.0.4 → 21.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -407,6 +407,35 @@ createNavItems(() => [
407
407
  ]);
408
408
  ```
409
409
 
410
+ #### Default (fallback) items
411
+
412
+ Routes register items on-demand, so any URL with no registration in its active chain renders an empty menu. `provideNavConfig({ defaults })` declares fallback items rendered on those URLs — handy for landing pages, error routes, or app shells where a few items should always be visible:
413
+
414
+ ```typescript
415
+ provideNavConfig({
416
+ defaults: [
417
+ { label: 'Home', link: '/' },
418
+ { label: 'Docs', link: '/docs' },
419
+ ],
420
+ }),
421
+ ```
422
+
423
+ Relative `link`s on defaults resolve from `/` (the router root), so `link: 'home'` becomes `/home`. Absolute links and `UrlTree`s pass through unchanged.
424
+
425
+ Named scopes are supported via the record form:
426
+
427
+ ```typescript
428
+ provideNavConfig({
429
+ defaults: {
430
+ '': [{ label: 'Home', link: '/' }], // default scope
431
+ main: [{ label: 'Home', link: '/' }], // injectNavItems('main')
432
+ side: () => [{ label: 'Settings', link: '/settings' }], // factory
433
+ },
434
+ }),
435
+ ```
436
+
437
+ Shadowing follows the usual deepest-wins rule — any active route that calls `createNavItems` (including `createNavItems([])` to render an explicitly empty menu) replaces the defaults for that scope.
438
+
410
439
  #### Typed metadata
411
440
 
412
441
  `CreateNavItem` and `NavItem` carry a `TMeta` generic so consumers can attach app-specific fields (icons, badges, etc.) without the library imposing a shape:
@@ -1,7 +1,7 @@
1
1
  import * as i1 from '@angular/router';
2
2
  import { PRIMARY_OUTLET, EventType, Router, createUrlTreeFromSnapshot, UrlTree, RouterLink, RouterLinkWithHref, ActivatedRoute } from '@angular/router';
3
3
  import * as i0 from '@angular/core';
4
- import { inject, computed, Injectable, InjectionToken, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, linkedSignal } from '@angular/core';
4
+ import { inject, computed, Injectable, InjectionToken, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, EnvironmentInjector, runInInjectionContext, linkedSignal } from '@angular/core';
5
5
  import { toSignal } from '@angular/core/rxjs-interop';
6
6
  import { filter, map } from 'rxjs/operators';
7
7
  import { mutable, mapArray, until, elementVisibility, toWritable } from '@mmstack/primitives';
@@ -331,31 +331,33 @@ function isInternalBreadcrumb(breadcrumb) {
331
331
  const token$2 = new InjectionToken('@mmstack/router-core:breadcrumb-config');
332
332
  /**
333
333
  * Provides configuration for the breadcrumb system.
334
- * @param config - A partial `BreadcrumbConfig` object with the desired settings. *
334
+ *
335
+ * @param config A partial {@link BreadcrumbConfig}. The `generation` field controls
336
+ * automatic label generation: `'manual'` disables it (breadcrumbs only show when
337
+ * {@link createBreadcrumb} explicitly registers them); a function provides a
338
+ * custom label generator instead of the default route-title-based one.
339
+ * @returns A `Provider` to add to your app's providers array.
340
+ *
335
341
  * @see BreadcrumbConfig
342
+ *
336
343
  * @example
337
- * ```typescript
338
- * // In your app.module.ts or a standalone component's providers:
339
- * // import { provideBreadcrumbConfig } from './breadcrumb.config'; // Adjust path
340
- * // import { ResolvedLeafRoute } from './breadcrumb.type'; // Adjust path
344
+ * ```ts
345
+ * // Disable automatic generation breadcrumbs only appear when createBreadcrumb is used
346
+ * bootstrapApplication(AppComponent, {
347
+ * providers: [
348
+ * provideRouter(routes),
349
+ * provideBreadcrumbConfig({ generation: 'manual' }),
350
+ * ],
351
+ * });
352
+ * ```
341
353
  *
342
- * // const customLabelStrategy: GenerateBreadcrumbFn = () => {
343
- * // return (leaf: ResolvedLeafRoute): string => {
344
- * // // Example: Prioritize a 'navTitle' data property
345
- * // if (leaf.route.data?.['navTitle']) {
346
- * // return leaf.route.data['navTitle'];
347
- * // }
348
- * // // Fallback to a default mechanism
349
- * // return leaf.route.title || leaf.segment.resolved || 'Unnamed';
350
- * // };
351
- * // };
354
+ * @example
355
+ * ```ts
356
+ * // Custom label strategy outer fn runs in injection context, inner is reactive
357
+ * const customLabelStrategy = () => (leaf: ResolvedLeafRoute) =>
358
+ * leaf.route.data?.['navTitle'] ?? leaf.route.title ?? 'Unnamed';
352
359
  *
353
- * export const appConfig = [
354
- * // ...rest
355
- * provideBreadcrumbConfig({
356
- * generation: customLabelStrategy, // or 'manual' to disable auto-generation
357
- * }),
358
- * ]
360
+ * provideBreadcrumbConfig({ generation: customLabelStrategy });
359
361
  * ```
360
362
  */
361
363
  function provideBreadcrumbConfig(config) {
@@ -514,17 +516,24 @@ function injectBreadcrumbs() {
514
516
  }
515
517
 
516
518
  /**
517
- * Creates and registers a breadcrumb for a specific route.
518
- * This function is designed to be used as an Angular Route `ResolveFn`.
519
+ * Creates and registers a breadcrumb for a specific route. Designed to be used
520
+ * as an Angular Route `ResolveFn` in the route's `resolve` map.
519
521
  *
520
- * Accepts a static label (`string`), a static options object, or a factory returning either —
521
- * use a factory when you need `inject()` for dynamic data.
522
+ * Accepts a static label, a static options object, or a factory returning
523
+ * either — use a factory when you need `inject()` to read dynamic data.
524
+ *
525
+ * @param factoryOrValue One of: a literal label string (shorthand for
526
+ * `{ label: <string> }`), a static {@link CreateBreadcrumbOptions} object,
527
+ * or a factory `() => string | CreateBreadcrumbOptions` invoked inside an
528
+ * injection context (so it can use `inject()`).
529
+ * @returns An Angular `ResolveFn<void>` to wire into a route's `resolve` map.
530
+ * The resolver registers the breadcrumb as a side effect; the resolved value
531
+ * itself is unused.
522
532
  *
523
- * @param factoryOrValue A static label, a static `CreateBreadcrumbOptions`, or a factory returning either.
524
533
  * @see CreateBreadcrumbOptions
525
534
  *
526
535
  * @example
527
- * ```typescript
536
+ * ```ts
528
537
  * export const appRoutes: Routes = [
529
538
  * {
530
539
  * path: 'home',
@@ -541,7 +550,7 @@ function injectBreadcrumbs() {
541
550
  * breadcrumb: createBreadcrumb(() => {
542
551
  * const userStore = inject(UserStore);
543
552
  * return {
544
- * label: () => userStore.user().name ?? 'Loading...',
553
+ * label: () => userStore.user().name ?? 'Loading',
545
554
  * };
546
555
  * }),
547
556
  * },
@@ -618,6 +627,44 @@ function hasSlowConnection() {
618
627
  function noPreload(route) {
619
628
  return route.data && route.data['preload'] === false;
620
629
  }
630
+ /**
631
+ * Demand-driven preloading strategy for Angular's router. Unlike Angular's
632
+ * built-in `PreloadAllModules`, this strategy preloads a lazy route only
633
+ * when something explicitly requests it via {@link PreloadRequester} (e.g.
634
+ * the `mmLink` directive on hover or visibility, or {@link injectTriggerPreload}
635
+ * called imperatively).
636
+ *
637
+ * Skips preloading when:
638
+ * - the route has `data.preload === false`
639
+ * - the network is on `2g` or in `saveData` mode (cheap-data-mode users)
640
+ * - a load for the same path is already in flight
641
+ *
642
+ * Wire this into `provideRouter` to enable the `mmLink` preload pipeline:
643
+ *
644
+ * @example
645
+ * ```ts
646
+ * import { PreloadStrategy } from '@mmstack/router-core';
647
+ * import { provideRouter, withPreloading } from '@angular/router';
648
+ *
649
+ * bootstrapApplication(AppComponent, {
650
+ * providers: [
651
+ * provideRouter(routes, withPreloading(PreloadStrategy)),
652
+ * ],
653
+ * });
654
+ *
655
+ * // Then in templates, `mmLink` (or `injectTriggerPreload`) requests
656
+ * // preloads that this strategy executes:
657
+ * // <a [mmLink]="'/users'">Users</a>
658
+ * ```
659
+ *
660
+ * @example
661
+ * ```ts
662
+ * // Opt a route out of preloading:
663
+ * export const routes: Routes = [
664
+ * { path: 'admin', loadChildren: () => import('./admin'), data: { preload: false } },
665
+ * ];
666
+ * ```
667
+ */
621
668
  class PreloadStrategy {
622
669
  loading = new Set();
623
670
  router = inject(Router);
@@ -705,6 +752,25 @@ function injectTriggerPreload() {
705
752
  };
706
753
  }
707
754
  const configToken = new InjectionToken('MMSTACK_LINK_CONFIG');
755
+ /**
756
+ * Provide application-wide defaults for the `mmLink` directive. Each `[mmLink]`
757
+ * instance can still override per-link via its own `preloadOn` / `useMouseDown`
758
+ * inputs; this just shifts the default.
759
+ *
760
+ * @param config Partial override of `MMLinkConfig`. Unset keys fall back to:
761
+ * - `preloadOn: 'hover'` — preload triggered when the user hovers a link
762
+ * - `useMouseDown: false` — navigation triggered on click (not mousedown)
763
+ * @returns A `Provider` to add to your app's providers array.
764
+ *
765
+ * @example
766
+ * ```ts
767
+ * bootstrapApplication(AppComponent, {
768
+ * providers: [
769
+ * provideMMLinkDefaultConfig({ preloadOn: 'visible', useMouseDown: true }),
770
+ * ],
771
+ * });
772
+ * ```
773
+ */
708
774
  function provideMMLinkDefaultConfig(config) {
709
775
  const cfg = {
710
776
  preloadOn: 'hover',
@@ -724,6 +790,36 @@ function injectConfig() {
724
790
  ...cfg,
725
791
  };
726
792
  }
793
+ /**
794
+ * Drop-in replacement for `[routerLink]` that adds preloading on hover or
795
+ * visibility, optional mousedown-triggered navigation, and a `beforeNavigate`
796
+ * hook. Composes with Angular's `RouterLink` via `hostDirectives`, so every
797
+ * `RouterLink` input (`target`, `queryParams`, `fragment`, etc.) is forwarded.
798
+ *
799
+ * Preload behavior:
800
+ * - `preloadOn: 'hover'` (default) — preload when the user hovers the link
801
+ * - `preloadOn: 'visible'` — preload when the link scrolls into view
802
+ * - `preloadOn: null` — disable preloading on this link
803
+ *
804
+ * Navigation timing:
805
+ * - `useMouseDown: false` (default) — navigate on click
806
+ * - `useMouseDown: true` — navigate on mousedown (shaves ~50ms but breaks if the user
807
+ * moves off the link before mouseup)
808
+ *
809
+ * Requires {@link PreloadStrategy} to be wired via `provideRouter(routes, withComponentInputBinding(), withPreloading(PreloadStrategy))`.
810
+ * Set app-wide defaults with {@link provideMMLinkDefaultConfig}.
811
+ *
812
+ * @example
813
+ * ```html
814
+ * <a [mmLink]="['/users', userId()]">View profile</a>
815
+ *
816
+ * <!-- Override per-link -->
817
+ * <a [mmLink]="'/heavy-page'" preloadOn="visible" useMouseDown>Heavy page</a>
818
+ *
819
+ * <!-- React to the preload starting -->
820
+ * <a [mmLink]="'/checkout'" (preloading)="onPreload()">Checkout</a>
821
+ * ```
822
+ */
727
823
  class Link {
728
824
  routerLink = inject(RouterLink, {
729
825
  self: true,
@@ -934,19 +1030,50 @@ function createInternalNavItem(input, router, relativeTo, configActiveMatch, par
934
1030
  /** @internal */
935
1031
  const token$1 = new InjectionToken('@mmstack/router-core:nav-config');
936
1032
  /**
937
- * Provides global configuration for the nav system.
1033
+ * Provides global configuration for the nav system. The factory form runs in
1034
+ * an injection context, so it can use `inject()` to build defaults from app
1035
+ * state.
1036
+ *
1037
+ * @param config Either a literal {@link NavConfig} object or a factory
1038
+ * `() => NavConfig`. Optional — without it, the nav system uses Angular's
1039
+ * default `activeMatch` options and renders nothing in scopes that have no
1040
+ * registered items.
1041
+ * @returns A `Provider` to add to your app's providers array.
938
1042
  *
939
1043
  * @example
940
- * ```typescript
941
- * provideNavConfig({
942
- * activeMatch: { queryParams: 'ignored' },
943
- * }),
1044
+ * ```ts
1045
+ * bootstrapApplication(AppComponent, {
1046
+ * providers: [
1047
+ * provideRouter(routes),
1048
+ * provideNavConfig({
1049
+ * activeMatch: { queryParams: 'ignored' },
1050
+ * defaults: [
1051
+ * { label: 'Home', link: '/' },
1052
+ * { label: 'Docs', link: '/docs' },
1053
+ * ],
1054
+ * }),
1055
+ * ],
1056
+ * });
1057
+ * ```
1058
+ *
1059
+ * @example
1060
+ * ```ts
1061
+ * // Factory form — read defaults from a service
1062
+ * provideNavConfig(() => {
1063
+ * const role = inject(AuthStore).role();
1064
+ * return {
1065
+ * defaults: {
1066
+ * main: role === 'admin' ? adminNav : guestNav,
1067
+ * },
1068
+ * };
1069
+ * });
944
1070
  * ```
945
1071
  */
946
1072
  function provideNavConfig(config) {
1073
+ const fn = typeof config === 'function' ? config : () => ({ ...config });
947
1074
  return {
948
1075
  provide: token$1,
949
- useValue: { ...config },
1076
+ useFactory: fn,
950
1077
  };
951
1078
  }
952
1079
  /** @internal */
@@ -959,6 +1086,10 @@ const DEFAULT_NAV_SCOPE = Symbol('mmstack.nav.default');
959
1086
  class NavStore {
960
1087
  map = mutable(new Map());
961
1088
  leafRoutes = injectLeafRoutes();
1089
+ router = inject(Router);
1090
+ config = injectNavConfig();
1091
+ injector = inject(EnvironmentInjector);
1092
+ defaultsCache = new Map();
962
1093
  /** @internal */
963
1094
  register(scope, routePath, items) {
964
1095
  this.map.inline((m) => {
@@ -974,18 +1105,55 @@ class NavStore {
974
1105
  scope(name) {
975
1106
  return computed(() => {
976
1107
  const scopeMap = this.map().get(name);
977
- if (!scopeMap)
978
- return [];
979
- const leaves = this.leafRoutes();
980
- for (let i = leaves.length - 1; i >= 0; i--) {
981
- const items = scopeMap.get(leaves[i].path);
982
- if (items) {
983
- return items.filter((it) => !it.hidden());
1108
+ if (scopeMap) {
1109
+ const leaves = this.leafRoutes();
1110
+ for (let i = leaves.length - 1; i >= 0; i--) {
1111
+ const items = scopeMap.get(leaves[i].path);
1112
+ if (items) {
1113
+ return items.filter((it) => !it.hidden());
1114
+ }
984
1115
  }
985
1116
  }
1117
+ const defaults = this.getDefaultItems(name);
1118
+ if (defaults) {
1119
+ return defaults.filter((it) => !it.hidden());
1120
+ }
986
1121
  return [];
987
1122
  });
988
1123
  }
1124
+ getDefaultItems(scope) {
1125
+ const cached = this.defaultsCache.get(scope);
1126
+ if (cached !== undefined)
1127
+ return cached;
1128
+ const built = this.buildDefaultItems(scope);
1129
+ this.defaultsCache.set(scope, built);
1130
+ return built;
1131
+ }
1132
+ buildDefaultItems(scope) {
1133
+ const defaults = this.config.defaults;
1134
+ if (!defaults)
1135
+ return null;
1136
+ let entry;
1137
+ if (Array.isArray(defaults) || typeof defaults === 'function') {
1138
+ if (scope === DEFAULT_NAV_SCOPE)
1139
+ entry = defaults;
1140
+ }
1141
+ else {
1142
+ const key = scope === DEFAULT_NAV_SCOPE ? '' : scope;
1143
+ entry = defaults[key];
1144
+ }
1145
+ if (!entry)
1146
+ return null;
1147
+ const resolved = entry;
1148
+ return untracked(() => runInInjectionContext(this.injector, () => {
1149
+ const inputs = typeof resolved === 'function' ? resolved() : resolved;
1150
+ const rootSnapshot = this.router.routerState.snapshot.root;
1151
+ const prefix = scope === DEFAULT_NAV_SCOPE
1152
+ ? '__defaults__'
1153
+ : `__defaults__:${scope}`;
1154
+ return inputs.map((input, i) => createInternalNavItem(input, this.router, rootSnapshot, this.config.activeMatch, NEVER_TRUE, NEVER_TRUE, `${prefix}#${i}`));
1155
+ }));
1156
+ }
989
1157
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
990
1158
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, providedIn: 'root' });
991
1159
  }
@@ -997,7 +1165,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
997
1165
  * Returns a reactive list of nav items for the requested scope.
998
1166
  *
999
1167
  * The returned signal reflects the nearest active ancestor route that registered items
1000
- * for `name` via `createNavItems`. Hidden items are filtered out.
1168
+ * for `name` via `createNavItems`. If no active route has registered items for the
1169
+ * scope, falls back to `NavConfig.defaults` (when provided via `provideNavConfig`).
1170
+ * Hidden items are filtered out.
1001
1171
  *
1002
1172
  * @typeParam TMeta The shape of `NavItem.meta` for the consuming code. Untyped at the
1003
1173
  * registration site — this is a consumer-side assertion.
@@ -1023,22 +1193,54 @@ function injectNavItems(name) {
1023
1193
 
1024
1194
  /**
1025
1195
  * Registers a set of nav items for the activating route under the given scope.
1026
- * Mirrors `createBreadcrumb` / `createTitle` — designed to be used in a route's
1027
- * `resolve` map.
1196
+ * Mirrors {@link createBreadcrumb} / {@link createTitle} — designed to be used
1197
+ * in a route's `resolve` map.
1028
1198
  *
1029
- * Multiple scopes can be registered on a single route by giving each its own `name`
1030
- * (and a unique key in the `resolve` map):
1199
+ * Scope override semantics: when multiple routes in the active chain register
1200
+ * items under the same scope, the deepest active registration wins. Navigating
1201
+ * away restores the shallower registration. To explicitly render an empty nav
1202
+ * (shadowing a default), pass `[]`.
1031
1203
  *
1032
- * ```typescript
1033
- * resolve: {
1034
- * mainNav: createNavItems([...], { name: 'main' }),
1035
- * sideNav: createNavItems([...], { name: 'side' }),
1204
+ * @typeParam TMeta Optional per-item metadata type — flows through the
1205
+ * registered items so consumers reading via {@link injectNavItems} get
1206
+ * typed access to `item.meta`.
1207
+ * @param itemsOrFactory Either a static array of {@link CreateNavItem} or a
1208
+ * factory `() => CreateNavItem<TMeta>[]` invoked inside an injection
1209
+ * context (so it can use `inject()` for dynamic items).
1210
+ * @param options Optional `{ name }` for registering multiple scopes on a
1211
+ * single route. Omit to target the default (unnamed) scope.
1212
+ * @returns An Angular `ResolveFn<void>` to wire into a route's `resolve` map.
1213
+ * The resolver registers items as a side effect; the resolved value itself
1214
+ * is unused.
1215
+ *
1216
+ * @example
1217
+ * ```ts
1218
+ * // Single default-scope nav
1219
+ * {
1220
+ * path: 'app',
1221
+ * resolve: {
1222
+ * _nav: createNavItems([
1223
+ * { label: 'Dashboard', link: 'dashboard' },
1224
+ * { label: 'Reports', link: 'reports' },
1225
+ * ]),
1226
+ * },
1227
+ * }
1228
+ *
1229
+ * // Multiple scopes
1230
+ * {
1231
+ * path: 'app',
1232
+ * resolve: {
1233
+ * mainNav: createNavItems([...], { name: 'main' }),
1234
+ * sideNav: createNavItems([...], { name: 'side' }),
1235
+ * },
1036
1236
  * }
1037
- * ```
1038
1237
  *
1039
- * Scope override semantics: when multiple routes in the active chain register items
1040
- * under the same scope, the deepest active registration wins. Navigating away restores
1041
- * the shallower registration.
1238
+ * // Factory using inject()
1239
+ * createNavItems(() => {
1240
+ * const auth = inject(AuthStore);
1241
+ * return auth.canAdmin() ? adminItems : userItems;
1242
+ * });
1243
+ * ```
1042
1244
  */
1043
1245
  function createNavItems(itemsOrFactory, options) {
1044
1246
  const factory = typeof itemsOrFactory === 'function'
@@ -1168,7 +1370,29 @@ function queryParam(key, route = inject(ActivatedRoute)) {
1168
1370
 
1169
1371
  const token = new InjectionToken('@mmstack/router-core:title-config');
1170
1372
  /**
1171
- * used to provide the title configuration, will not be applied unless a `createTitle` resolver is used
1373
+ * Provide application-wide configuration for the title subsystem. The config
1374
+ * is only consumed when at least one route uses a {@link createTitle} resolver;
1375
+ * routes without `createTitle` are unaffected.
1376
+ *
1377
+ * @param config Optional {@link TitleConfig}. All fields are optional — pass
1378
+ * `prefix` to namespace titles (e.g. `"My App – "`), `initialTitle` to
1379
+ * override the fallback (defaults to the `<title>` from `index.html`), and
1380
+ * `keepLastKnownTitle: false` to clear the title on navigations to routes
1381
+ * without a title (the default keeps the previous one).
1382
+ * @returns A `Provider` to add to your app's providers array.
1383
+ *
1384
+ * @example
1385
+ * ```ts
1386
+ * bootstrapApplication(AppComponent, {
1387
+ * providers: [
1388
+ * provideRouter(routes),
1389
+ * provideTitleConfig({
1390
+ * prefix: (title) => `${title} • My App`,
1391
+ * keepLastKnownTitle: true,
1392
+ * }),
1393
+ * ],
1394
+ * });
1395
+ * ```
1172
1396
  */
1173
1397
  function provideTitleConfig(config) {
1174
1398
  const prefix = config?.prefix ?? '';
@@ -1239,14 +1463,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1239
1463
  }]
1240
1464
  }], ctorParameters: () => [] });
1241
1465
  /**
1466
+ * Creates an Angular router `ResolveFn<string>` that registers a title for the
1467
+ * route it's attached to. Titles can be static strings, factory functions
1468
+ * (called in an injection context, so they can use `inject()`), or signal
1469
+ * factories (for reactive titles that change when underlying data does).
1242
1470
  *
1243
- * Creates a title resolver function that can be used in Angular's router.
1471
+ * The resolved title flows through any `prefix` configured via
1472
+ * {@link provideTitleConfig}, and is wired into Angular's `Title` service
1473
+ * via an effect. Nested routes pick the most-specific leaf's title; if a
1474
+ * deeper route has no title and `keepLastKnownTitle` is `true` (default),
1475
+ * the previous title is preserved.
1244
1476
  *
1245
- * @param factoryOrValue
1246
- * A function that returns a string or a Signal<string> representing the title or just the string directly.
1247
- * @param awaitValue
1248
- * If `true`, the resolver will wait until the title signal has a value before resolving.
1249
- * Defaults to `false`.
1477
+ * @param factoryOrValue Either a literal string title, a `() => string`
1478
+ * factory, or a `() => Signal<string>` factory for reactive titles. Factory
1479
+ * callbacks run inside an injection context, so they can use `inject()`.
1480
+ * @param awaitValue When `true`, the resolver waits until the title signal
1481
+ * emits a truthy value before resolving — useful for SSR/SEO where the
1482
+ * resolved title should not be empty. Defaults to `false`.
1483
+ * @returns An Angular `ResolveFn<string>` to wire into a route's `title` field
1484
+ * (or any other `resolve` slot — the return value isn't usually consumed).
1485
+ *
1486
+ * @example
1487
+ * ```ts
1488
+ * // Static title
1489
+ * { path: 'about', component: AboutComponent, title: createTitle('About us') }
1490
+ *
1491
+ * // Factory using inject()
1492
+ * {
1493
+ * path: 'users/:id',
1494
+ * component: UserComponent,
1495
+ * title: createTitle(() => inject(ActivatedRoute).snapshot.params['id']),
1496
+ * }
1497
+ *
1498
+ * // Reactive title from a signal store
1499
+ * {
1500
+ * path: 'dashboard',
1501
+ * component: DashboardComponent,
1502
+ * title: createTitle(() => {
1503
+ * const user = inject(UserStore).current;
1504
+ * return computed(() => `Dashboard – ${user()?.name ?? 'Guest'}`);
1505
+ * }),
1506
+ * }
1507
+ * ```
1250
1508
  */
1251
1509
  function createTitle(factoryOrValue, awaitValue = false) {
1252
1510
  const factory = typeof factoryOrValue === 'string' ? () => factoryOrValue : factoryOrValue;