@mixin external-link-with-icon() {
  &::after {
    display: inline-block;
    content: '\e89e'; // codepoint for "open_in_new"
    font-family: 'Material Symbols Outlined';
    margin-left: 0.2rem;
    vertical-align: middle;
  }
}


import {ChangeDetectionStrategy, Component, inject, input, OnInit, signal} from '@angular/core';
import {ExternalLink} from '../../directives';
import {LOCAL_STORAGE} from '../../providers';
import {IconComponent} from '../icon/icon.component';

export const STORAGE_KEY_PREFIX = 'docs-was-closed-top-banner-';

@Component({
  selector: 'docs-top-level-banner',
  standalone: true,
  imports: [ExternalLink, IconComponent],
  templateUrl: './top-level-banner.component.html',
  styleUrl: './top-level-banner.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TopLevelBannerComponent implements OnInit {
  private readonly localStorage = inject(LOCAL_STORAGE);

  /**
   * Unique identifier for the banner. This ID is required to ensure that
   * the state of the banner (e.g., whether it has been closed) is tracked
   * separately for different events or instances. Without a unique ID,
   * closing one banner could inadvertently hide other banners for different events.
   */
  id = input.required<string>();
  // Optional URL link that the banner should navigate to when clicked.
  link = input<string>();
  // Text content to be displayed in the banner.
  text = input.required<string>();
  // Optional expiry date. Setting the default expiry as a future date so we
  // don't have to deal with undefined signal values.
  expiry = input(new Date('3000-01-01'), {transform: parseDate});
  // Whether the user has closed the banner or the survey has expired.
  hasClosed = signal<boolean>(false);

  ngOnInit(): void {
    const expired = Date.now() > this.expiry().getTime();

    // Needs to be in a try/catch, because some browsers will
    // throw when using `localStorage` in private mode.
    try {
      this.hasClosed.set(
        this.localStorage?.getItem(this.getBannerStorageKey()) === 'true' || expired,
      );
    } catch {
      this.hasClosed.set(false);
    }
  }

  close(): void {
    this.localStorage?.setItem(this.getBannerStorageKey(), 'true');
    this.hasClosed.set(true);
  }

  private getBannerStorageKey(): string {
    return `${STORAGE_KEY_PREFIX}${this.id()}`;
  }
}

const parseDate = (inputDate: string | Date): Date => {
  if (inputDate instanceof Date) {
    return inputDate;
  }
  const outputDate = new Date(inputDate);
  if (isNaN(outputDate.getTime())) {
    throw new Error(`Invalid date string: ${inputDate}`);
  }
  return outputDate;
};


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {mergeApplicationConfig, ApplicationConfig} from '@angular/core';
import {provideServerRendering} from '@angular/platform-server';
import {provideServerRoutesConfig, RenderMode} from '@angular/ssr';
import {appConfig} from './app.config';

const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(),
    provideServerRoutesConfig([{path: '**', renderMode: RenderMode.Prerender}]),
  ],
};

export const config = mergeApplicationConfig(appConfig, serverConfig);


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {DOCUMENT, isPlatformBrowser} from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  OnInit,
  PLATFORM_ID,
  signal,
  WritableSignal,
} from '@angular/core';
import {NavigationEnd, NavigationSkipped, Router, RouterLink, RouterOutlet} from '@angular/router';
import {filter, map, skip} from 'rxjs/operators';
import {
  CookiePopup,
  getActivatedRouteSnapshotFromRouter,
  IS_SEARCH_DIALOG_OPEN,
  SearchDialog,
  TopLevelBannerComponent,
} from '@angular/docs';
import {Footer} from './core/layout/footer/footer.component';
import {Navigation} from './core/layout/navigation/navigation.component';
import {SecondaryNavigation} from './core/layout/secondary-navigation/secondary-navigation.component';
import {ProgressBarComponent} from './core/layout/progress-bar/progress-bar.component';
import {ESCAPE, SEARCH_TRIGGER_KEY} from './core/constants/keys';
import {HeaderService} from './core/services/header.service';

@Component({
  selector: 'adev-root',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [
    CookiePopup,
    Navigation,
    Footer,
    SecondaryNavigation,
    RouterOutlet,
    SearchDialog,
    ProgressBarComponent,
    TopLevelBannerComponent,
  ],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  host: {
    '(window:keydown)': 'setSearchDialogVisibilityOnKeyPress($event)',
  },
})
export class AppComponent implements OnInit {
  private readonly document = inject(DOCUMENT);
  private readonly router = inject(Router);
  private readonly headerService = inject(HeaderService);

  currentUrl = signal('');
  displayFooter = signal(false);
  displaySecondaryNav = signal(false);
  displaySearchDialog: WritableSignal<boolean> = inject(IS_SEARCH_DIALOG_OPEN);

  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  ngOnInit(): void {
    this.closeSearchDialogOnNavigationSkipped();
    this.router.events
      .pipe(
        filter((e): e is NavigationEnd => e instanceof NavigationEnd),
        map((event) => event.urlAfterRedirects),
      )
      .subscribe((url) => {
        this.currentUrl.set(url);
        this.setComponentsVisibility();
        this.displaySearchDialog.set(false);

        this.updateCanonicalLink(url);
      });
  }

  focusFirstHeading(): void {
    if (!this.isBrowser) {
      return;
    }

    const h1 = this.document.querySelector<HTMLHeadingElement>('h1');
    h1?.focus();
  }

  private updateCanonicalLink(absoluteUrl: string) {
    this.headerService.setCanonical(absoluteUrl);
  }

  private setComponentsVisibility(): void {
    const activatedRoute = getActivatedRouteSnapshotFromRouter(this.router as any);

    this.displaySecondaryNav.set(activatedRoute.data['displaySecondaryNav']);
    this.displayFooter.set(!activatedRoute.data['hideFooter']);
  }

  private setSearchDialogVisibilityOnKeyPress(event: KeyboardEvent): void {
    if (event.key === SEARCH_TRIGGER_KEY && (event.metaKey || event.ctrlKey)) {
      event.preventDefault();
      this.displaySearchDialog.update((display) => !display);
    }

    if (event.key === ESCAPE && this.displaySearchDialog()) {
      event.preventDefault();
      this.displaySearchDialog.set(false);
    }
  }

  private closeSearchDialogOnNavigationSkipped(): void {
    this.router.events.pipe(filter((event) => event instanceof NavigationSkipped)).subscribe(() => {
      this.displaySearchDialog.set(false);
    });
  }
}


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  OnInit,
  PLATFORM_ID,
  computed,
  inject,
  signal,
} from '@angular/core';
import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop';
import {
  ClickOutside,
  NavigationItem,
  NavigationList,
  NavigationState,
  WINDOW,
  findNavigationItem,
  getBaseUrlAfterRedirects,
  getNavigationItemsTree,
  markExternalLinks,
  shouldReduceMotion,
} from '@angular/docs';
import {distinctUntilChanged, filter, map, skip, startWith} from 'rxjs/operators';
import {SUB_NAVIGATION_DATA} from '../../../sub-navigation-data';
import {PagePrefix} from '../../enums/pages';
import {ActivatedRouteSnapshot, NavigationEnd, Router, RouterStateSnapshot} from '@angular/router';
import {isPlatformBrowser} from '@angular/common';
import {trigger, transition, style, animate} from '@angular/animations';
import {PRIMARY_NAV_ID, SECONDARY_NAV_ID} from '../../constants/element-ids';

export const ANIMATION_DURATION = 500;

@Component({
  selector: 'adev-secondary-navigation',
  standalone: true,
  imports: [NavigationList, ClickOutside],
  templateUrl: './secondary-navigation.component.html',
  styleUrls: ['./secondary-navigation.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  animations: [
    trigger('leaveAnimation', [
      transition(':leave', [
        style({transform: 'translateX(0%)'}),
        animate(
          `${ANIMATION_DURATION}ms ${ANIMATION_DURATION}ms ease-out`,
          style({transform: 'translateX(100%)'}),
        ),
      ]),
    ]),
  ],
})
export class SecondaryNavigation implements OnInit {
  private readonly destroyRef = inject(DestroyRef);
  private readonly navigationState = inject(NavigationState);
  private readonly platformId = inject(PLATFORM_ID);
  private readonly router = inject(Router);
  private readonly window = inject(WINDOW);

  readonly isSecondaryNavVisible = this.navigationState.isMobileNavVisible;
  readonly primaryActiveRouteItem = this.navigationState.primaryActiveRouteItem;
  readonly maxVisibleLevelsOnSecondaryNav = computed(() =>
    this.primaryActiveRouteItem() === PagePrefix.REFERENCE ? 1 : 2,
  );
  readonly navigationItemsSlides = this.navigationState.expandedItems;
  navigationItems: NavigationItem[] | undefined;

  translateX = computed(() => {
    const level = this.navigationState.expandedItems()?.length ?? 0;
    return `translateX(${-level * 100}%)`;
  });
  transition = signal('0ms');

  readonly PRIMARY_NAV_ID = PRIMARY_NAV_ID;
  readonly SECONDARY_NAV_ID = SECONDARY_NAV_ID;

  private readonly routeMap: Record<string, NavigationItem[]> = {
    [PagePrefix.REFERENCE]: getNavigationItemsTree(SUB_NAVIGATION_DATA.reference, (tree) =>
      markExternalLinks(tree, this.window.origin),
    ),
    [PagePrefix.DOCS]: getNavigationItemsTree(SUB_NAVIGATION_DATA.docs, (tree) =>
      markExternalLinks(tree, this.window.origin),
    ),
  };

  private readonly primaryActiveRouteChanged$ = toObservable(this.primaryActiveRouteItem).pipe(
    distinctUntilChanged(),
    takeUntilDestroyed(this.destroyRef),
  );

  private readonly urlAfterRedirects$ = this.router.events.pipe(
    filter((event) => event instanceof NavigationEnd),
    map((event) => (event as NavigationEnd).urlAfterRedirects),
    filter((url): url is string => url !== undefined),
    startWith(this.getInitialPath(this.router.routerState.snapshot)),
    takeUntilDestroyed(this.destroyRef),
  );

  ngOnInit(): void {
    this.navigationState.cleanExpandedState();
    this.listenToPrimaryRouteChange();
    this.setActiveRouteOnNavigationEnd();

    if (isPlatformBrowser(this.platformId)) {
      this.initSlideAnimation();
    }
  }

  close(): void {
    this.navigationState.setMobileNavigationListVisibility(false);
  }

  private setActiveRouteOnNavigationEnd(): void {
    this.urlAfterRedirects$.subscribe((url) => {
      const activeNavigationItem = this.getActiveNavigationItem(url);
      if (
        activeNavigationItem?.level &&
        activeNavigationItem.level <= this.maxVisibleLevelsOnSecondaryNav()
      ) {
        this.navigationState.cleanExpandedState();
      } else if (activeNavigationItem) {
        /**
         * For the `Docs`, we don't expand the "level === 1" items because they are already displayed in the main navigation list.
         * Example:
         * In-depth Guides (level == 0)
         * Components (level == 1) -> Selectors, Styling, etc (level == 2)
         * Template Syntax (level == 1) -> Text interpolation, etc (level == 2)
         *
         * For the `Tutorials`, we display the navigation in the dropdown and it has flat structure (all items are displayed as items with level === 0).
         *
         * For the `Reference` we would like to give possibility to expand the "level === 1" items cause they are not visible in the first slide of navigation list.
         * Example:
         * API Reference (level == 0) -> Overview, Animations, common, etc (level == 1) -> API Package exports (level == 2)
         */
        const shouldExpandItem = (node: NavigationItem): boolean =>
          !!node.level &&
          (this.primaryActiveRouteItem() === PagePrefix.REFERENCE
            ? node.level > 0
            : node.level > 1);

        // Skip expand when active item is API Reference homepage - `/api`.
        // It protect us from displaying second level of the navigation when user clicks on `Reference`,
        // Because in this situation we want to display the first level, which contains, in addition to the API Reference, also the CLI Reference, Error Encyclopedia etc.
        const skipExpandPredicateFn = (node: NavigationItem): boolean =>
          node.path === PagePrefix.API;

        this.navigationState.expandItemHierarchy(
          activeNavigationItem,
          shouldExpandItem,
          skipExpandPredicateFn,
        );
      }
    });
  }

  private getActiveNavigationItem(url: string): NavigationItem | null {
    // set visible navigation items if not present
    this.setVisibleNavigationItems();

    const activeNavigationItem = findNavigationItem(
      this.navigationItems!,
      (item) =>
        !!item.path &&
        getBaseUrlAfterRedirects(item.path, this.router) ===
          getBaseUrlAfterRedirects(url, this.router),
    );

    this.navigationState.setActiveNavigationItem(activeNavigationItem);

    return activeNavigationItem;
  }

  private initSlideAnimation(): void {
    if (shouldReduceMotion()) {
      return;
    }
    setTimeout(() => {
      this.transition.set(`${ANIMATION_DURATION}ms`);
    }, ANIMATION_DURATION);
  }

  private setVisibleNavigationItems(): void {
    const routeMap = this.routeMap[this.primaryActiveRouteItem()!];
    this.navigationItems = routeMap
      ? getNavigationItemsTree(routeMap, (item) => {
          item.isExpanded = this.primaryActiveRouteItem() === PagePrefix.DOCS && item.level === 1;
        })
      : [];
  }

  private listenToPrimaryRouteChange(): void {
    // Fix: flicker of sub-navigation on init
    this.primaryActiveRouteChanged$.pipe(skip(1)).subscribe(() => {
      this.navigationState.cleanExpandedState();
    });
  }

  private getInitialPath(routerState: RouterStateSnapshot): string {
    let route: ActivatedRouteSnapshot = routerState.root;

    while (route.firstChild) {
      route = route.firstChild;
    }

    return route.routeConfig?.path ?? '';
  }
}


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  ChangeDetectionStrategy,
  Component,
  inject,
  OnInit,
  PLATFORM_ID,
  ViewChild,
} from '@angular/core';
import {isPlatformBrowser} from '@angular/common';
import {NgProgressbar} from 'ngx-progressbar';
import {
  NavigationCancel,
  NavigationEnd,
  NavigationError,
  NavigationSkipped,
  NavigationStart,
  Router,
} from '@angular/router';
import {filter, map, switchMap, take} from 'rxjs/operators';

/** Time to wait after navigation starts before showing the progress bar. This delay allows a small amount of time to skip showing the progress bar when a navigation is effectively immediate. 30ms is approximately the amount of time we can wait before a delay is perceptible.*/
export const PROGRESS_BAR_DELAY = 30;

@Component({
  selector: 'adev-progress-bar',
  standalone: true,
  imports: [NgProgressbar],
  template: `
    <ng-progress aria-label="Page load progress" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProgressBarComponent implements OnInit {
  private readonly router = inject(Router);

  @ViewChild(NgProgressbar, {static: true}) progressBar!: NgProgressbar;

  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  ngOnInit() {
    this.setupPageNavigationDimming();
  }

  /**
   * Dims the main router-outlet content when navigating to a new page.
   */
  private setupPageNavigationDimming() {
    if (!this.isBrowser) {
      return;
    }
    this.router.events
      .pipe(
        filter((e) => e instanceof NavigationStart),
        map(() => {
          // Only apply set the property if the navigation is not "immediate"
          return setTimeout(() => {
            this.progressBar.start();
          }, PROGRESS_BAR_DELAY);
        }),
        switchMap((timeoutId) => {
          return this.router.events.pipe(
            filter((e) => {
              return (
                e instanceof NavigationEnd ||
                e instanceof NavigationCancel ||
                e instanceof NavigationSkipped ||
                e instanceof NavigationError
              );
            }),
            take(1),
            map(() => timeoutId),
          );
        }),
      )
      .subscribe((timeoutId) => {
        // When the navigation finishes, prevent the navigating class from being applied in the timeout.
        clearTimeout(timeoutId);
        this.progressBar.complete();
      });
  }
}


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu';
import {DOCUMENT, Location, isPlatformBrowser} from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  OnInit,
  PLATFORM_ID,
  inject,
} from '@angular/core';
import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop';
import {
  ClickOutside,
  NavigationState,
  WINDOW,
  IconComponent,
  getBaseUrlAfterRedirects,
  isApple,
  IS_SEARCH_DIALOG_OPEN,
} from '@angular/docs';
import {NavigationEnd, Router, RouterLink} from '@angular/router';
import {filter, map, startWith} from 'rxjs/operators';
import {DOCS_ROUTES, REFERENCE_ROUTES, TUTORIALS_ROUTES} from '../../../routes';
import {GITHUB, MEDIUM, X, YOUTUBE, DISCORD} from '../../constants/links';
import {PagePrefix} from '../../enums/pages';
import {Theme, ThemeManager} from '../../services/theme-manager.service';
import {VersionManager} from '../../services/version-manager.service';
import {PRIMARY_NAV_ID, SECONDARY_NAV_ID} from '../../constants/element-ids';
import {ConnectionPositionPair} from '@angular/cdk/overlay';
import {COMMAND, CONTROL, SEARCH_TRIGGER_KEY} from '../../constants/keys';

type MenuType = 'social' | 'theme-picker' | 'version-picker';

@Component({
  selector: 'div.adev-nav',
  standalone: true,
  imports: [RouterLink, ClickOutside, CdkMenu, CdkMenuItem, CdkMenuTrigger, IconComponent],
  templateUrl: './navigation.component.html',
  styleUrls: ['./navigation.component.scss', './mini-menu.scss', './nav-item.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Navigation implements OnInit {
  private readonly destroyRef = inject(DestroyRef);
  private readonly document = inject(DOCUMENT);
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
  private readonly navigationState = inject(NavigationState);
  private readonly router = inject(Router);
  private readonly location = inject(Location);
  private readonly themeManager = inject(ThemeManager);
  private readonly isSearchDialogOpen = inject(IS_SEARCH_DIALOG_OPEN);
  private readonly window = inject(WINDOW);
  private readonly versionManager = inject(VersionManager);

  readonly DOCS_ROUTE = PagePrefix.DOCS;
  readonly HOME_ROUTE = PagePrefix.HOME;
  readonly PLAYGROUND_ROUTE = PagePrefix.PLAYGROUND;
  readonly REFERENCE_ROUTE = PagePrefix.REFERENCE;
  readonly TUTORIALS_ROUTE = PagePrefix.TUTORIALS;

  readonly GITHUB = GITHUB;
  readonly X = X;
  readonly MEDIUM = MEDIUM;
  readonly YOUTUBE = YOUTUBE;
  readonly DISCORD = DISCORD;

  readonly PRIMARY_NAV_ID = PRIMARY_NAV_ID;
  readonly SECONDARY_NAV_ID = SECONDARY_NAV_ID;

  // We can't use the ActivatedRouter queryParams as we're outside the router outlet
  readonly isUwu = 'location' in globalThis ? location.search.includes('uwu') : false;

  miniMenuPositions = [
    new ConnectionPositionPair(
      {originX: 'end', originY: 'center'},
      {overlayX: 'start', overlayY: 'center'},
    ),
    new ConnectionPositionPair(
      {originX: 'end', originY: 'top'},
      {overlayX: 'start', overlayY: 'top'},
    ),
  ];

  readonly APPLE_SEARCH_LABEL = `⌘`;
  readonly DEFAULT_SEARCH_LABEL = `ctrl`;

  activeRouteItem = this.navigationState.primaryActiveRouteItem;
  theme = this.themeManager.theme;
  openedMenu: MenuType | null = null;

  currentDocsVersion = this.versionManager.currentDocsVersion;
  currentDocsVersionMode = this.versionManager.currentDocsVersion()?.version;

  // Set the values of the search label and title only on the client, because the label is user-agent specific.
  searchLabel = this.isBrowser
    ? isApple
      ? this.APPLE_SEARCH_LABEL
      : this.DEFAULT_SEARCH_LABEL
    : '';
  searchTitle = this.isBrowser
    ? isApple
      ? `${COMMAND} ${SEARCH_TRIGGER_KEY.toUpperCase()}`
      : `${CONTROL} ${SEARCH_TRIGGER_KEY.toUpperCase()}`
    : '';
  versions = this.versionManager.versions;

  isMobileNavigationOpened = this.navigationState.isMobileNavVisible;
  isMobileNavigationOpened$ = toObservable(this.isMobileNavigationOpened);
  primaryRouteChanged$ = toObservable(this.activeRouteItem);

  ngOnInit(): void {
    this.listenToRouteChange();
    this.preventToScrollContentWhenSecondaryNavIsOpened();
    this.closeMobileNavOnPrimaryRouteChange();
  }

  setTheme(theme: Theme): void {
    this.themeManager.setTheme(theme);
  }

  openVersionMenu($event: MouseEvent): void {
    // It's required to avoid redirection to `home`
    $event.stopImmediatePropagation();
    $event.preventDefault();
    this.openMenu('version-picker');
  }

  openMenu(menuType: MenuType): void {
    this.openedMenu = menuType;
  }

  closeMenu(): void {
    this.openedMenu = null;
  }

  openMobileNav($event: MouseEvent): void {
    $event.stopPropagation();
    this.navigationState.setMobileNavigationListVisibility(true);
  }

  closeMobileNav(): void {
    this.navigationState.setMobileNavigationListVisibility(false);
  }

  toggleSearchDialog(event: MouseEvent): void {
    event.stopPropagation();
    this.isSearchDialogOpen.update((isOpen) => !isOpen);
  }

  private closeMobileNavOnPrimaryRouteChange(): void {
    this.primaryRouteChanged$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
      this.closeMobileNav();
    });
  }

  private listenToRouteChange(): void {
    this.router.events
      .pipe(
        filter((event) => event instanceof NavigationEnd),
        map((event) => (event as NavigationEnd).urlAfterRedirects),
      )
      .pipe(
        takeUntilDestroyed(this.destroyRef),
        //using location because router.url will only return "/" here
        startWith(this.location.path()),
      )
      .subscribe((url) => {
        this.setActivePrimaryRoute(getBaseUrlAfterRedirects(url, this.router));
      });
  }

  // Set active route item, based on urlAfterRedirects.
  // First check if url starts with the main prefixes (docs, reference, tutorials).
  // (*) Docs navigation tree contains items which will navigate to /tutorials.
  // In that case after click on such link we should mark as active item, and display tutorials navigation tree.
  // If it's not starting with prefix then check if specific path exist in the array of defined routes
  // (*) Reference navigation tree contains items which are not start with prefix like /migrations or /errors.
  private setActivePrimaryRoute(urlAfterRedirects: string): void {
    if (urlAfterRedirects === '') {
      this.activeRouteItem.set(PagePrefix.HOME);
    } else if (urlAfterRedirects.startsWith(PagePrefix.DOCS)) {
      this.activeRouteItem.set(PagePrefix.DOCS);
    } else if (
      urlAfterRedirects.startsWith(PagePrefix.REFERENCE) ||
      urlAfterRedirects.startsWith(PagePrefix.API) ||
      urlAfterRedirects.startsWith(PagePrefix.UPDATE)
    ) {
      this.activeRouteItem.set(PagePrefix.REFERENCE);
    } else if (urlAfterRedirects === PagePrefix.PLAYGROUND) {
      this.activeRouteItem.set(PagePrefix.PLAYGROUND);
    } else if (urlAfterRedirects.startsWith(PagePrefix.TUTORIALS)) {
      this.activeRouteItem.set(PagePrefix.TUTORIALS);
    } else if (DOCS_ROUTES.some((route) => route.path === urlAfterRedirects)) {
      this.activeRouteItem.set(PagePrefix.DOCS);
    } else if (REFERENCE_ROUTES.some((route) => route.path === urlAfterRedirects)) {
      this.activeRouteItem.set(PagePrefix.REFERENCE);
    } else if (TUTORIALS_ROUTES.some((route) => route.path === urlAfterRedirects)) {
      this.activeRouteItem.set(PagePrefix.TUTORIALS);
    } else {
      // Reset if no active route item could be found
      this.activeRouteItem.set(null);
    }
  }

  private preventToScrollContentWhenSecondaryNavIsOpened(): void {
    this.isMobileNavigationOpened$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((opened) => {
      if (opened) {
        this.document.body.style.overflowY = 'hidden';
      } else {
        this.document.body.style.removeProperty('overflow-y');
      }
    });
  }
}


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {ComponentFixture, TestBed} from '@angular/core/testing';

import {Navigation} from './navigation.component';
import {RouterTestingModule} from '@angular/router/testing';
import {By} from '@angular/platform-browser';
import {PagePrefix} from '../../enums/pages';
import {Theme, ThemeManager} from '../../services/theme-manager.service';
import {Version, signal} from '@angular/core';
import {of} from 'rxjs';
import {VersionManager} from '../../services/version-manager.service';
import {Search, WINDOW} from '@angular/docs';

describe('Navigation', () => {
  let component: Navigation;
  let fixture: ComponentFixture<Navigation>;

  const fakeThemeManager = {
    theme: signal<Theme>('dark'),
    setTheme: (theme: Theme) => {},
    themeChanged$: of(),
  };

  const fakeVersionManager = {
    currentDocsVersion: signal('v17'),
    versions: signal<Version[]>([]),
  };

  const fakeWindow = {};
  const fakeSearch = {};

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [Navigation, RouterTestingModule],
      providers: [
        {
          provide: WINDOW,
          useValue: fakeWindow,
        },
        {
          provide: Search,
          useValue: fakeSearch,
        },
      ],
    }).compileComponents();

    TestBed.overrideProvider(ThemeManager, {useValue: fakeThemeManager});
    TestBed.overrideProvider(VersionManager, {useValue: fakeVersionManager});

    fixture = TestBed.createComponent(Navigation);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should append active class to DOCS_ROUTE when DOCS_ROUTE is active', () => {
    component.activeRouteItem.set(PagePrefix.DOCS);

    fixture.detectChanges();

    const docsLink = fixture.debugElement.query(By.css('a[href="/docs"]')).parent?.nativeElement;

    expect(docsLink).toHaveClass('adev-nav-item--active');
  });

  it('should not have active class when activeRouteItem is null', () => {
    component.activeRouteItem.set(null);

    fixture.detectChanges();

    const docsLink = fixture.debugElement.query(By.css('a[href="/docs"]')).nativeElement;
    const referenceLink = fixture.debugElement.query(By.css('a[href="/reference"]')).nativeElement;

    expect(docsLink).not.toHaveClass('adev-nav-item--active');
    expect(referenceLink).not.toHaveClass('adev-nav-item--active');
  });

  it('should call themeManager.setTheme(dark) when user tries to set dark theme', () => {
    const openThemePickerButton = fixture.debugElement.query(
      By.css('button[aria-label^="Open theme picker"]'),
    ).nativeElement;
    const setThemeSpy = spyOn(fakeThemeManager, 'setTheme');

    openThemePickerButton.click();
    fixture.detectChanges();

    const setDarkModeButton = fixture.debugElement.query(
      By.css('button[aria-label="Set dark theme"]'),
    ).nativeElement;

    setDarkModeButton.click();

    expect(setThemeSpy).toHaveBeenCalledOnceWith('dark');
  });
});


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {DOCUMENT, isPlatformBrowser} from '@angular/common';
import {Injectable, PLATFORM_ID, inject, signal} from '@angular/core';
import {LOCAL_STORAGE} from '@angular/docs';
import {Subject} from 'rxjs';

// Keep these constants in sync with the code in index.html

export const THEME_PREFERENCE_LOCAL_STORAGE_KEY = 'themePreference';
export const DARK_MODE_CLASS_NAME = 'docs-dark-mode';
export const LIGHT_MODE_CLASS_NAME = 'docs-light-mode';
export const PREFERS_COLOR_SCHEME_DARK = '(prefers-color-scheme: dark)';

export type Theme = 'dark' | 'light' | 'auto';

@Injectable({
  providedIn: 'root',
})
export class ThemeManager {
  private readonly document = inject(DOCUMENT);
  private readonly localStorage = inject(LOCAL_STORAGE);
  private readonly platformId = inject(PLATFORM_ID);

  readonly theme = signal<Theme | null>(this.getThemeFromLocalStorageValue());
  // Zoneless - it's required to notify that theme was changed. It could be removed when signal-based components will be available.
  readonly themeChanged$ = new Subject<void>();

  constructor() {
    if (!isPlatformBrowser(this.platformId)) {
      return;
    }

    this.loadThemePreference();
    this.watchPreferredColorScheme();
  }

  setTheme(theme: Theme): void {
    this.theme.set(theme);
    this.setThemeInLocalStorage();
    this.setThemeBodyClasses(theme === 'auto' ? preferredScheme() : theme);
  }

  // 1. Read theme preferences stored in localStorage
  // 2. In case when there are no stored user preferences, then read them from device preferences.
  private loadThemePreference(): void {
    const savedUserPreference = this.getThemeFromLocalStorageValue();
    const useTheme = savedUserPreference ?? 'auto';

    this.theme.set(useTheme);
    this.setThemeBodyClasses(useTheme === 'auto' ? preferredScheme() : useTheme);
  }

  // Set theme classes on the body element
  private setThemeBodyClasses(theme: 'dark' | 'light'): void {
    const documentClassList = this.document.documentElement.classList;
    if (theme === 'dark') {
      documentClassList.add(DARK_MODE_CLASS_NAME);
      documentClassList.remove(LIGHT_MODE_CLASS_NAME);
    } else {
      documentClassList.add(LIGHT_MODE_CLASS_NAME);
      documentClassList.remove(DARK_MODE_CLASS_NAME);
    }
    this.themeChanged$.next();
  }

  private getThemeFromLocalStorageValue(): Theme | null {
    const theme = this.localStorage?.getItem(THEME_PREFERENCE_LOCAL_STORAGE_KEY) as Theme | null;
    return theme ?? null;
  }

  private setThemeInLocalStorage(): void {
    if (this.theme()) {
      this.localStorage?.setItem(THEME_PREFERENCE_LOCAL_STORAGE_KEY, this.theme()!);
    }
  }

  private watchPreferredColorScheme() {
    window.matchMedia(PREFERS_COLOR_SCHEME_DARK).addEventListener('change', (event) => {
      const preferredScheme = event.matches ? 'dark' : 'light';
      this.setThemeBodyClasses(preferredScheme);
    });
  }
}

function preferredScheme(): 'dark' | 'light' {
  return window.matchMedia(PREFERS_COLOR_SCHEME_DARK).matches ? 'dark' : 'light';
}


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {DOCUMENT, isPlatformBrowser} from '@angular/common';
import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  Injector,
  OnDestroy,
  OnInit,
  PLATFORM_ID,
  ViewChild,
  inject,
} from '@angular/core';
import {WINDOW, shouldReduceMotion, isIos} from '@angular/docs';
import {ActivatedRoute, RouterLink} from '@angular/router';

import {injectAsync} from '../../core/services/inject-async';

import {CodeEditorComponent} from './components/home-editor.component';

import {HEADER_CLASS_NAME} from './home-animation-constants';
import type {HomeAnimation} from './services/home-animation.service';

export const TUTORIALS_HOMEPAGE_DIRECTORY = 'homepage';

@Component({
  standalone: true,
  selector: 'adev-home',
  imports: [RouterLink, CodeEditorComponent],
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class Home implements OnInit, AfterViewInit, OnDestroy {
  @ViewChild('home') home!: ElementRef<HTMLDivElement>;

  private readonly document = inject(DOCUMENT);
  private readonly injector = inject(Injector);
  private readonly platformId = inject(PLATFORM_ID);
  private readonly window = inject(WINDOW);
  private readonly activatedRoute = inject(ActivatedRoute);

  protected readonly tutorialFiles = TUTORIALS_HOMEPAGE_DIRECTORY;
  protected readonly isUwu = 'uwu' in this.activatedRoute.snapshot.queryParams;
  private element!: HTMLDivElement;
  private homeAnimation?: HomeAnimation;
  private intersectionObserver: IntersectionObserver | undefined;

  ctaLink = 'tutorials/learn-angular';
  ctaIosLink = 'overview';

  ngOnInit(): void {
    if (isIos) {
      this.ctaLink = this.ctaIosLink;
    }
  }

  ngAfterViewInit() {
    this.element = this.home.nativeElement;

    if (isPlatformBrowser(this.platformId)) {
      // Always scroll to top on home page (even for navigating back)
      this.window.scrollTo({top: 0, left: 0, behavior: 'instant'});

      // Create a single intersection observer used for disabling the animation
      // at the end of the page, and to load the embedded editor.
      this.initIntersectionObserver();

      if (this.isWebGLAvailable() && !shouldReduceMotion() && !this.isUwu) {
        this.loadHomeAnimation();
      }
    }
  }

  ngOnDestroy(): void {
    if (isPlatformBrowser(this.platformId)) {
      // Stop observing and disconnect
      this.intersectionObserver?.disconnect();

      if (this.homeAnimation) {
        this.homeAnimation.destroy();
      }
    }
  }

  private initIntersectionObserver(): void {
    const header = this.document.querySelector('.adev-top');
    const footer = this.document.querySelector('footer');

    this.intersectionObserver = new IntersectionObserver((entries) => {
      const headerEntry = entries.find((entry) => entry.target === header);
      const footerEntry = entries.find((entry) => entry.target === footer);

      // CTA and arrow animation
      this.headerTop(headerEntry);

      // Disable animation at end of page
      if (this.homeAnimation) {
        this.homeAnimation.disableEnd(footerEntry);
      }
    });

    // Start observing
    this.intersectionObserver.observe(header!);
    this.intersectionObserver.observe(footer!);
  }

  private headerTop(headerEntry: IntersectionObserverEntry | undefined): void {
    if (!headerEntry) {
      return;
    }

    if (headerEntry.isIntersecting) {
      this.element.classList.add(HEADER_CLASS_NAME);
    } else {
      this.element.classList.remove(HEADER_CLASS_NAME);
    }
  }

  private async loadHomeAnimation() {
    this.homeAnimation = await injectAsync(this.injector, () =>
      import('./services/home-animation.service').then((c) => c.HomeAnimation),
    );

    await this.homeAnimation.init(this.element);
  }

  private isWebGLAvailable() {
    try {
      return !!document
        .createElement('canvas')
        .getContext('webgl', {failIfMajorPerformanceCaveat: true});
    } catch (e) {
      return false;
    }
  }
}


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

// import {ComponentFixture, TestBed} from '@angular/core/testing';

// import Home from './home.component';
// import {HomeAnimation} from './services/home-animation.service';

// TODO: refactor for lazy-loading when both conditions are met
// 1. WebGL is available with isWebGLAvailable()
// 2. When a user's device settings are not set to reduced motion with !shouldReduceMotion()
/* describe('Home', () => {
  let component: Home;
  let fixture: ComponentFixture<Home>;

  let fakeHomeAnimation = {
    init: () => Promise.resolve(),
    destroy: () => {},
  };

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [Home],
    }).compileComponents();

    TestBed.overrideProvider(HomeAnimation, {useValue: fakeHomeAnimation});

    fixture = TestBed.createComponent(Home);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should call homeAnimation.destroy() on ngOnDestroy', () => {
    const destroySpy = spyOn(fakeHomeAnimation, 'destroy');

    component.ngOnDestroy();

    expect(destroySpy).toHaveBeenCalled();
  });
}); */


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {DOCS_VIEWER_SELECTOR, DocViewer, WINDOW} from '@angular/docs';

import {Component, Input, signal} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {RouterTestingModule} from '@angular/router/testing';
import {of} from 'rxjs';

import {
  EMBEDDED_EDITOR_SELECTOR,
  EmbeddedEditor,
  EmbeddedTutorialManager,
  NodeRuntimeSandbox,
} from '../../editor';

import {mockAsyncProvider} from '../../core/services/inject-async';
import Tutorial from './tutorial.component';
import {TutorialConfig, TutorialType} from '@angular/docs';

@Component({
  selector: EMBEDDED_EDITOR_SELECTOR,
  template: '
FakeEmbeddedEditor
',
  standalone: true,
})
class FakeEmbeddedEditor {}

@Component({
  selector: DOCS_VIEWER_SELECTOR,
  template: '
FakeDocsViewer
',
  standalone: true,
})
class FakeDocViewer {
  @Input('documentFilePath') documentFilePath: string | undefined;
}

// TODO: export this class, it's a helpful mock we could you on other tests.
class FakeNodeRuntimeSandbox {
  loadingStep = signal(0);
  previewUrl$ = of();
  writeFile(path: string, content: string) {
    return Promise.resolve();
  }
  init() {
    return Promise.resolve();
  }
}

describe('Tutorial', () => {
  let component: Tutorial;
  let fixture: ComponentFixture<Tutorial>;
  const fakeWindow = {
    addEventListener: () => {},
    removeEventListener: () => {},
  };

  const fakeEmbeddedTutorialManager: Partial<EmbeddedTutorialManager> = {
    tutorialFiles: signal({'app.component.ts': 'original'}),
    answerFiles: signal({'app.component.ts': 'answer'}),
    type: signal(TutorialType.EDITOR),
    revealAnswer: () => {},
    resetRevealAnswer: () => {},
    tutorialChanged$: of(false),
    openFiles: signal<NonNullable<TutorialConfig['openFiles']>>(['app.component.ts']),
  };

  function setupRevealAnswerValues() {
    component['shouldRenderRevealAnswer'].set(true);
    component['canRevealAnswer'] = signal(true);
    component['embeddedTutorialManager'].answerFiles.set({'app.component.ts': 'answer'});
  }

  function setupDisabledRevealAnswerValues() {
    component['shouldRenderRevealAnswer'].set(true);
    component['canRevealAnswer'] = signal(false);
    component['embeddedTutorialManager'].answerFiles.set({'app.component.ts': 'answer'});
  }

  function setupNoRevealAnswerValues() {
    component['shouldRenderRevealAnswer'].set(false);
    component['canRevealAnswer'] = signal(true);
    component['embeddedTutorialManager'].answerFiles.set({});
  }

  function setupResetRevealAnswerValues() {
    setupRevealAnswerValues();
    component['answerRevealed'].set(true);
  }

  beforeEach(async () => {
    TestBed.configureTestingModule({
      imports: [Tutorial, RouterTestingModule, EmbeddedEditor, DocViewer, NoopAnimationsModule],
      providers: [
        {
          provide: WINDOW,
          useValue: fakeWindow,
        },
        {
          provide: EmbeddedTutorialManager,
          useValue: fakeEmbeddedTutorialManager,
        },
        mockAsyncProvider(NodeRuntimeSandbox, FakeNodeRuntimeSandbox),
      ],
    });
    TestBed.overrideComponent(Tutorial, {
      remove: {
        imports: [DocViewer],
      },
      add: {
        imports: [FakeDocViewer],
      },
    });

    await TestBed.compileComponents();

    fixture = TestBed.createComponent(Tutorial);
    component = fixture.componentInstance;

    // Replace EmbeddedEditor with FakeEmbeddedEditor
    spyOn(component as any, 'loadEmbeddedEditorComponent').and.resolveTo(FakeEmbeddedEditor);

    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  // TODO: Add tests in a future PR
  // it('should render the embedded editor based on the tutorial config', () => {});
  // it('should not render the embedded editor based on the tutorial config', () => {});
  // it('should load the tutorial', () => {});

  it('should reset the reveal answer', async () => {
    setupResetRevealAnswerValues();
    fixture.detectChanges();

    if (!component.revealAnswerButton) throw new Error('revealAnswerButton is undefined');

    const revealAnswerSpy = spyOn(component['embeddedTutorialManager'], 'revealAnswer');
    const resetRevealAnswerSpy = spyOn(component['embeddedTutorialManager'], 'resetRevealAnswer');

    component.revealAnswerButton.nativeElement.click();

    expect(revealAnswerSpy).not.toHaveBeenCalled();
    expect(resetRevealAnswerSpy).toHaveBeenCalled();
  });

  it('should reveal the answer on button click', async () => {
    setupRevealAnswerValues();
    fixture.detectChanges();

    if (!component.revealAnswerButton) throw new Error('revealAnswerButton is undefined');

    const embeddedTutorialManagerRevealAnswerSpy = spyOn(
      component['embeddedTutorialManager'],
      'revealAnswer',
    );
    component.revealAnswerButton.nativeElement.click();

    expect(embeddedTutorialManagerRevealAnswerSpy).toHaveBeenCalled();

    await fixture.whenStable();
    fixture.detectChanges();

    expect(component.revealAnswerButton.nativeElement.textContent?.trim()).toBe('Reset');
  });

  it('should not reveal the answer when button is disabled', async () => {
    setupDisabledRevealAnswerValues();
    fixture.detectChanges();

    if (!component.revealAnswerButton) throw new Error('revealAnswerButton is undefined');

    spyOn(component, 'canRevealAnswer').and.returnValue(false);

    const handleRevealAnswerSpy = spyOn(component, 'handleRevealAnswer');

    component.revealAnswerButton.nativeElement.click();

    expect(component.revealAnswerButton.nativeElement.getAttribute('disabled')).toBeDefined();
    expect(handleRevealAnswerSpy).not.toHaveBeenCalled();
  });

  it('should not render the reveal answer button when there are no answers', () => {
    setupNoRevealAnswerValues();
    fixture.detectChanges();

    expect(component.revealAnswerButton).toBe(undefined);
  });
});


possibleIn: 900,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'debug',
    action:
      'With Angular 9 Ivy is now the default rendering engine, for any compatibility problems that might arise, read the [Ivy compatibility guide](https://v9.angular.io/guide/ivy-compatibility).',
  },
  {
    possibleIn: 900,
    necessaryAsOf: 900,
    level: ApplicationComplexity.Advanced,
    step: 'express-universal-server',
    action:
      'If you use Angular Universal with  `@nguniversal/express-engine` or `@nguniversal/hapi-engine`, several backup files will be created. One of them for `server.ts`. If this file defers from the default one, you may need to copy some changes from the `server.ts.bak` to `server.ts` manually.',
  },
  {
    possibleIn: 900,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Basic,
    step: 'ivy i18n',
    action:
      "Angular 9 introduced a global `$localize()` function that needs to be loaded if you depend on Angular's internationalization (i18n). Run `ng add @angular/localize` to add the necessary packages and code modifications. Consult the [$localize Global Import Migration guide](https://v9.angular.io/guide/migration-localize) to learn more about the changes.",
  },
  {
    possibleIn: 900,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'entryComponents',
    action:
      'In your application projects, you can remove `entryComponents` NgModules and any uses of `ANALYZE_FOR_ENTRY_COMPONENTS`. They are no longer required with the Ivy compiler and runtime. You may need to keep these if building a library that will be consumed by a View Engine application.',
  },
  {
    possibleIn: 900,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'testbed-get',
    action:
      'If you use `TestBed.get`, you should instead use `TestBed.inject`. This new method has the same behavior, but is type safe.',
  },
  {
    possibleIn: 900,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: '$localize',
    action:
      "If you use [Angular's i18n support](http://angular.io/guide/i18n), you will need to begin using `@angular/localize`. Learn more about the [$localize Global Import Migration](https://v9.angular.io/guide/migration-localize).",
  },

  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Basic,
    step: 'v10 NodeJS 12',
    action:
      'Make sure you are using Node 12 or later.',
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Basic,
    step: 'ng update v10',
    action:
      'Run `npx @angular/cli@10 update @angular/core@10 @angular/cli@10` which should bring you to version 10 of Angular.',
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Basic,
    material: true,
    step: 'update @angular/material',
    action: 'Run `npx @angular/cli@10 update @angular/material@10`.',
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Basic,
    step: 'browserlist',
    action:
      'New projects use the filename `.browserslistrc` instead of `browserslist`. `ng update` will migrate you automatically.',
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'v10-versions',
    action:
      'Angular now requires `tslint` v6, `tslib` v2, and [TypeScript 3.9](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html). `ng update` will migrate you automatically.',
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Advanced,
    step: 'styleext',
    action:
      'Stop using `styleext` or `spec` in your Angular schematics. `ng update` will migrate you automatically.',
  },
  {
    possibleIn: 900,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'classes-without-decorators',
    action:
      'In version 10, classes that use Angular features and do not have an Angular decorator are no longer supported.  [Read more](https://v10.angular.io/guide/migration-undecorated-classes).  `ng update` will migrate you automatically.',
  },
  {
    possibleIn: 900,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'injectable-definitions',
    action:
      'As of Angular 9, enforcement of @Injectable decorators for DI is stricter and incomplete provider definitions behave differently. [Read more](https://v9.angular.io/guide/migration-injectable). `ng update` will migrate you automatically.',
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Advanced,
    step: 'closure-jsdoc-comments',
    action:
      "Angular's NPM packages no longer contain jsdoc comments, which are necessary for use with closure compiler (extremely uncommon). This support was experimental and only worked in some use cases. There will be an alternative recommended path announced shortly.",
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'forms-number-input',
    action:
      'If you use Angular forms, inputs of type `number` no longer listen to [change events](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event) (this events are not necessarily fired for each alteration the value), instead listen for an [input events](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event). ',
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'forms-length-input',
    action:
      "For Angular forms validation, the `minLength` and `maxLength` validators now verify that the form control's value has a numeric length property, and only validate for length if that's the case.",
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'esm5-bundles',
    action:
      "The [Angular Package Format](https://g.co/ng/apf) has been updated to remove `esm5` and `fesm5` formats. These are no longer distributed in our npm packages. If you don't use the CLI, you may need to downlevel Angular code to ES5 yourself.",
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Medium,
    step: 'console-errors',
    action:
      "Warnings about unknown elements are now logged as errors. This won't break your app, but it may trip up tools that expect nothing to be logged via `console.error`.",
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Advanced,
    step: 'router-resolver-empty',
    action:
      'Any resolver which returns `EMPTY` will cancel navigation. If you want to allow navigation to continue, you will need to update the resolvers to emit some value, (i.e. `defaultIfEmpty(...)`, `of(...)`, etc).',
  },
  {
    possibleIn: 1000,
    necessaryAsOf: 1000,
    level: ApplicationComplexity.Advanced,
    step: 'sw-vary-headers',
    action:
      'If you use the Angular service worker and rely on resources with [Vary](https://developer.mozilla.org/docs/Web/HTTP/Headers/Vary) headers, these headers are now ignored to avoid unpredictable behavior across browsers. To avoid this, [configure](https://angular.io/guide/service-worker-config) your service worker to avoid caching these resources.',
  },


possibleIn: 1500,
    necessaryAsOf: 1500,
    level: ApplicationComplexity.Medium,
    step: 'v15 iframe',
    action:
      "Existing `<iframe>` instances might have security-sensitive attributes applied to them as an attribute or property binding. These security-sensitive attributes can occur in a template or in a directive's host bindings. Such occurrences require an update to ensure compliance with the new and stricter rules about `<iframe>` bindings. For more information, see [the error page](https://v15.angular.io/errors/NG0910).",
  },
  {
    possibleIn: 1500,
    necessaryAsOf: 1500,
    level: ApplicationComplexity.Medium,
    step: 'v15 Injector.get',
    action:
      'Update instances of `Injector.get()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `Injector.get()` is deprecated in v15. Read further',
  },
  {
    possibleIn: 1500,
    necessaryAsOf: 1500,
    level: ApplicationComplexity.Basic,
    step: 'v15 TestBed.inject',
    action:
      'Update instances of `TestBed.inject()` that use an `InjectFlags` parameter to use an `InjectOptions` parameter. The `InjectFlags` parameter of `TestBed.inject()` is deprecated in v15. Read further',
  },
  {
    possibleIn: 1500,
    necessaryAsOf: 1500,
    level: ApplicationComplexity.Medium,
    step: 'v15 ngModule in providedIn',
    action:
      'Using `providedIn: ngModule` for an `@Injectable` and `InjectionToken` is deprecated in v15. Read further',
  },
  {
    possibleIn: 1500,
    necessaryAsOf: 1500,
    level: ApplicationComplexity.Basic,
    step: 'v15 providedIn any',
    action:
      'Using `providedIn: \'any\'` for an `@Injectable` or `InjectionToken` is deprecated in v15. Read further',
  },
  {
    possibleIn: 1500,
    necessaryAsOf: 1500,
    level: ApplicationComplexity.Medium,
    step: 'v15 RouterLinkWithHref',
    action:
      'Update instances of the `RouterLinkWithHref`directive to use the `RouterLink` directive. The `RouterLinkWithHref` directive is deprecated in v15. Read further',
  },
  {
    possibleIn: 1500,
    necessaryAsOf: 1500,
    level: ApplicationComplexity.Basic,
    material: true,
    step: 'v15 mat refactor',
    action:
      'In Angular Material v15, many of the components have been refactored to be based on the official Material Design Components for Web (MDC). This change affected the DOM and CSS classes of many components. Read further',
  },
  {
    possibleIn: 1500,
    necessaryAsOf: 1500,
    level: ApplicationComplexity.Basic,
    step: 'v15 visual review',
    action:
      'After you update your application to v15, visually review your application and its interactions to ensure everything is working as it should.',
  },

  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Basic,
    step: 'v16 node support',
    action:
      'Make sure that you are using a supported version of node.js before you upgrade your application. Angular v16 supports node.js versions: v16 and v18.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Basic,
    step: 'v16 ts support',
    action:
      'Make sure that you are using a supported version of TypeScript before you upgrade your application. Angular v16 supports TypeScript version 4.9.3 or later.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Basic,
    step: 'v16 ng update',
    action:
      "In the application's project directory, run `ng update @angular/core@16 @angular/cli@16` to update your application to Angular v16.",
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Basic,
    material: true,
    step: 'update @angular/material',
    action: 'Run `ng update @angular/material@16`.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Basic,
    step: 'v16 zone.js support',
    action:
      'Make sure that you are using a supported version of Zone.js before you upgrade your application. Angular v16 supports Zone.js version 0.13.x or later.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 RouterEvent',
    action:
      "The Event union no longer contains `RouterEvent`, which means that if you're using the Event type you may have to change the type definition from `(e: Event)` to `(e: Event|RouterEvent)`",
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 routerEvent prop type',
    action:
      'In addition to `NavigationEnd` the `routerEvent` property now also accepts type `NavigationSkipped`',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 RendererType2',
    action:
      'Pass only flat arrays to `RendererType2.styles` because it no longer accepts nested arrays',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 BrowserPlatformLocation',
    action:
      'You may have to update tests that use `BrowserPlatformLocation` because `MockPlatformLocation` is now provided by default in tests. [Read further](https://github.com/angular/angular/blob/main/CHANGELOG.md#common-9).',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Basic,
    step: 'v16 ngcc',
    action:
      'Due to the removal of the Angular Compatibility Compiler (ngcc) in v16, projects on v16 and later no longer support View Engine libraries.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 createUrlTree',
    action:
      'After bug fixes in `Router.createUrlTree` you may have to readjust tests which mock `ActiveRoute`. [Read further](https://github.com/angular/angular/blob/main/CHANGELOG.md#1600-next1-2023-03-01)',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 ApplicationConfig imports',
    action: 'Change imports of `ApplicationConfig` to be from `@angular/core`.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 renderModule',
    action:
      'Revise your code to use `renderModule` instead of `renderModuleFactory` because it has been deleted.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 XhrFactory',
    action:
      'Revise your code to use `XhrFactory` from `@angular/common` instead of `XhrFactory` export from `@angular/common/http`.',
  },


possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 withServerTransition',
    action:
      "If you're running multiple Angular apps on the same page and you're using `BrowserModule.withServerTransition({ appId: 'serverApp' })` make sure you set the `APP_ID` instead since `withServerTransition` is now deprecated. [Read further](https://github.com/angular/angular/blob/main/CHANGELOG.md#platform-browser-4)",
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 EnvironmentInjector',
    action:
      'Change `EnvironmentInjector.runInContext` to `runInInjectionContext` and pass the environment injector as the first parameter.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 ViewContainerRef.createComponent',
    action:
      'Update your code to use `ViewContainerRef.createComponent` without the factory resolver. `ComponentFactoryResolver` has been removed from Router APIs.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 APP_ID',
    action: 'If you bootstrap multiple apps on the same page, make sure you set unique `APP_IDs`.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 server renderApplication',
    action:
      'Update your code to revise `renderApplication` method as it no longer accepts a root component as first argument, but instead a callback that should bootstrap your app. [Read further](https://github.com/angular/angular/blob/main/CHANGELOG.md#platform-server-3)',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 PlatformConfig.baseUrl',
    action:
      'Update your code to remove any reference to `PlatformConfig.baseUrl` and `PlatformConfig.useAbsoluteUrl` platform-server config options as it has been deprecated.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Basic,
    step: 'v16 moduleid',
    action:
      'Update your code to remove any reference to `@Directive`/`@Component` `moduleId` property as it does not have any effect and will be removed in v17.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 transfer state imports',
    action:
      "Update imports from `import {makeStateKey, StateKey, TransferState} from '@angular/platform-browser'` to `import {makeStateKey, StateKey, TransferState} from '@angular/core'`",
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 ComponentRef',
    action:
      "If you rely on `ComponentRef.setInput` to set the component input even if it's the same based on `Object.is` equality check, make sure you copy its value.",
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 ANALYZE_FOR_ENTRY_COMPONENTS',
    action:
      'Update your code to remove any reference to `ANALYZE_FOR_ENTRY_COMPONENTS` injection token as it has been deleted.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Basic,
    step: 'v16 entry components',
    action:
      '`entryComponents` is no longer available and any reference to it can be removed from the `@NgModule` and `@Component` public APIs.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 ngTemplateOutletContext',
    action:
      'ngTemplateOutletContext has stricter type checking which requires you to declare all the properties in the corresponding object. [Read further](https://github.com/angular/angular/blob/main/CHANGELOG.md#common-1).',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 APF',
    action:
      'Angular packages no longer include FESM2015 and the distributed ECMScript has been updated from 2020 to 2022.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Advanced,
    step: 'v16 EventManager',
    action:
      'The deprecated `EventManager` method `addGlobalEventListener` has been removed as it is not used by Ivy.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 BrowserTransferStateModule',
    action:
      '`BrowserTransferStateModule` is no longer available and any reference to it can be removed from your applications.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Medium,
    step: 'v16 ReflectiveInjector',
    action:
      'Update your code to use `Injector.create` rather than `ReflectiveInjector` since `ReflectiveInjector` is removed.',
  },
  {
    possibleIn: 1600,
    necessaryAsOf: 1600,
    level: ApplicationComplexity.Basic,
    step: 'v16 QueryList',
    action:
      '`QueryList.filter` now supports type guard functions. Since the type will be narrowed, you may have to update your application code that relies on the old behavior.',
  },

  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Basic,
    step: 'v17 node support',
    action:
      'Make sure that you are using a supported version of node.js before you upgrade your application. Angular v17 supports node.js versions: v18.13.0 and newer',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Basic,
    step: 'v17 ts support',
    action:
      'Make sure that you are using a supported version of TypeScript before you upgrade your application. Angular v17 supports TypeScript version 5.2 or later.',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Basic,
    step: 'v17 zone.js support',
    action:
      'Make sure that you are using a supported version of Zone.js before you upgrade your application. Angular v17 supports Zone.js version 0.14.x or later.',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Basic,
    step: 'v17 ng update',
    action:
      "In the application's project directory, run `ng update @angular/core@17 @angular/cli@17` to update your application to Angular v17.",
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Basic,
    material: true,
    step: 'update @angular/material',
    action: 'Run `ng update @angular/material@17`.',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Medium,
    step: 'v17 style removal',
    action:
      'Angular now automatically removes styles of destroyed components, which may impact your existing apps in cases you rely on leaked styles. To change this update the value of the `REMOVE_STYLES_ON_COMPONENT_DESTROY` provider to `false`.',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Basic,
    step: 'v17 router removals',
    action:
      "Make sure you configure `setupTestingRouter`, `canceledNavigationResolution`, `paramsInheritanceStrategy`, `titleStrategy`, `urlUpdateStrategy`, `urlHandlingStrategy`, and `malformedUriErrorHandler` in `provideRouter` or `RouterModule.forRoot` since these properties are now not part of the `Router`'s public API",
  },


possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Advanced,
    step: 'v17 ngDoCheck dynamic components',
    action:
      'For dynamically instantiated components we now execute `ngDoCheck` during change detection if the component is marked as dirty. You may need to update your tests or logic within `ngDoCheck` for dynamically instantiated components.',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Medium,
    step: 'v17 malformedUriErrorHandler',
    action:
      "Handle URL parsing errors in the `UrlSerializer.parse` instead of `malformedUriErrorHandler` because it's now part of the public API surface.",
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Medium,
    step: 'v17 zone deep imports',
    action:
      'Change Zone.js deep imports like `zone.js/bundles/zone-testing.js` and `zone.js/dist/zone` to `zone.js` and `zone.js/testing`.',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Advanced,
    step: 'v17 absolute redirects',
    action:
      'You may need to adjust your router configuration to prevent infinite redirects after absolute redirects. In v17 we no longer prevent additional redirects after absolute redirects.',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Medium,
    step: 'v17 AnimationDriver',
    action:
      'Change references to `AnimationDriver.NOOP` to use `NoopAnimationDriver` because `AnimationDriver.NOOP` is now deprecated.',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Basic,
    step: 'v17 switch strictness',
    action:
      "You may need to adjust the equality check for `NgSwitch` because now it defaults to stricter check with `===` instead of `==`. Angular will log a warning message for the usages where you'd need to provide an adjustment.",
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Advanced,
    step: 'v17 mutate in signals',
    action:
      'Use `update` instead of `mutate` in Angular Signals. For example `items.mutate(itemsArray => itemsArray.push(newItem));` will now be `items.update(itemsArray => [itemsArray, …newItem]);`',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Medium,
    step: 'v17 withNoDomReuse',
    action:
      'To disable hydration use `ngSkipHydration` or remove the `provideClientHydration` call from the provider list since `withNoDomReuse` is no longer part of the public API.',
  },
  {
    possibleIn: 1700,
    necessaryAsOf: 1700,
    level: ApplicationComplexity.Basic,
    step: 'v17 paramsInheritanceStrategy',
    action:
      'If you want the child routes of `loadComponent` routes to inherit data from their parent specify the `paramsInheritanceStrategy` to `always`, which in v17 is now set to `emptyOnly`.',
  },

  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Basic,
    step: 'v18 node support',
    action:
      'Make sure that you are using a supported version of node.js before you upgrade your application. Angular v18 supports node.js versions: v18.19.0 and newer',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Basic,
    step: 'v18 ng update',
    action:
      "In the application's project directory, run `ng update @angular/core@18 @angular/cli@18` to update your application to Angular v18.",
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Basic,
    material: true,
    step: 'update @angular/material',
    action: 'Run `ng update @angular/material@18`.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Basic,
    step: '18.0.0 Upgrade TypeScript',
    action: 'Update TypeScript to versions 5.4 or newer.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Advanced,
    step: '18.0.0: async has been removed, use `waitForAsync` instead',
    action: 'Replace `async` from `@angular/core` with `waitForAsync`.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Advanced,
    step: '18.0.0: Deprecated matchesElement method removed from AnimationDriver',
    action: "Remove calls to `matchesElement` because it's now not part of `AnimationDriver`.",
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Medium,
    step: '18.0.0. Use `@angular/core` StateKey and TransferState',
    action:
      'Import `StateKey` and `TransferState` from `@angular/core` instead of `@angular/platform-browser`.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Medium,
    step: '18.0.0. Opt-in of caching for HTTP requests with auth headers',
    action:
      'Use `includeRequestsWithAuthHeaders: true` in `withHttpTransferCache` to opt-in of caching for HTTP requests that require authorization.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Advanced,
    step: '18.0.0.REMOVE_OBSOLETE_IS_WORKER',
    action:
      'Update the application to remove `isPlatformWorkerUi` and `isPlatformWorkerApp` since they were part of platform WebWorker which is now not part of Angular.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Medium,
    step: '18.0.0.FORCE_ZONE_CHANGE_DETECTION',
    action:
      'Tests may run additional rounds of change detection to fully reflect test state in the DOM. As a last resort, revert to the old behavior by adding `provideZoneChangeDetection({ignoreChangesOutsideZone: true})` to the TestBed providers.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Medium,
    step: '18.0.0: Remove two-way binding expressions in writable bindings',
    action: 'Remove expressions that write to properties in templates that use `[(ngModel)]`',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Advanced,
    step: '18.0.0: Use zones to track pending requests',
    action:
      'Remove calls to `Testability` methods `increasePendingRequestCount`, `decreasePendingRequestCount`, and `getPendingRequestCount`. This information is tracked by ZoneJS.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Medium,
    step: '18.0.0: Move shared providers to the routed component',
    action:
      'Move any environment providers that should be available to routed components from the component that defines the `RouterOutlet` to the providers of `bootstrapApplication` or the `Route` config.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Advanced,
    step: '18.0.0 Use RedirectCommand or new NavigationBehaviorOptions',
    action:
      'When a guard returns a `UrlTree` as a redirect, the redirecting navigation will now use `replaceUrl` if the initial navigation was also using the `replaceUrl` option. If you prefer the previous behavior, configure the redirect using the new `NavigationBehaviorOptions` by returning a `RedirectCommand` with the desired options instead of `UrlTree`.',
  },
  {
    possibleIn: 1800,
    necessaryAsOf: 1800,
    level: ApplicationComplexity.Advanced,
    step: '18.0.0: Remove deprecated resource cache providers',
    action:
      "Remove dependencies of `RESOURCE_CACHE_PROVIDER` since it's no longer part of the Angular runtime.",
  },


/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {DOCUMENT, isPlatformBrowser} from '@angular/common';
import {
  DestroyRef,
  EnvironmentInjector,
  Injectable,
  OnDestroy,
  PLATFORM_ID,
  afterNextRender,
  inject,
  signal,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {fromEvent} from 'rxjs';
import {auditTime} from 'rxjs/operators';
import {
  API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME,
  API_REFERENCE_MEMBER_CARD_CLASS_NAME,
  API_TAB_ACTIVE_CODE_LINE,
  MEMBER_ID_ATTRIBUTE,
} from '../constants/api-reference-prerender.constants';
import {WINDOW} from '@angular/docs';
import {Router} from '@angular/router';
import {AppScroller} from '../../../app-scroller';

export const SCROLL_EVENT_DELAY = 20;
export const SCROLL_THRESHOLD = 20;

@Injectable()
export class ReferenceScrollHandler implements OnDestroy {
  private readonly destroyRef = inject(DestroyRef);
  private readonly document = inject(DOCUMENT);
  private readonly injector = inject(EnvironmentInjector);
  private readonly window = inject(WINDOW);
  private readonly router = inject(Router);
  private readonly appScroller = inject(AppScroller);
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  private readonly cardOffsetTop = new Map<string, number>();
  private resizeObserver: ResizeObserver | null = null;

  membersMarginTopInPx = signal<number>(0);

  ngOnDestroy(): void {
    this.resizeObserver?.disconnect();
  }

  setupListeners(tocSelector: string): void {
    if (!this.isBrowser) {
      return;
    }

    this.setupCodeToCListeners(tocSelector);
    this.setupMemberCardListeners();
    this.setScrollEventHandlers();
    this.listenToResizeCardContainer();
    this.setupFragmentChangeListener();
  }

  private setupFragmentChangeListener() {
    this.router.routerState.root.fragment
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((fragment) => {
        // If there is no fragment or the scroll event has a position (traversing through history),
        // allow the scroller to handler scrolling instead of going to the fragment
        if (!fragment || this.appScroller.lastScrollEvent?.position) {
          this.appScroller.scroll();
          return;
        }

        const card = this.document.getElementById(fragment) as HTMLDivElement | null;
        this.scrollToCard(card);
      });
  }

  updateMembersMarginTop(selectorOfTheElementToAlign: string): void {
    if (!this.isBrowser) {
      return;
    }

    const elementToAlign = this.document.querySelector<HTMLElement>(selectorOfTheElementToAlign);

    if (elementToAlign) {
      this.updateMarginTopWhenTabBodyIsResized(elementToAlign);
    }
  }

  private setupCodeToCListeners(tocSelector: string): void {
    const tocContainer = this.document.querySelector<HTMLDivElement>(tocSelector);

    if (!tocContainer) {
      return;
    }

    fromEvent(tocContainer, 'click')
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((event) => {
        if (event.target instanceof HTMLAnchorElement) {
          event.stopPropagation();
          return;
        }

        // Get the card member ID from the attributes
        const target =
          event.target instanceof HTMLButtonElement
            ? event.target
            : this.findButtonElement(event.target as HTMLElement);
        const memberId = this.getMemberId(target);

        if (memberId) {
          this.router.navigate([], {fragment: memberId, replaceUrl: true});
        }
      });
  }

  private setupMemberCardListeners(): void {
    this.getAllMemberCards().forEach((card) => {
      this.cardOffsetTop.set(card.id, card.offsetTop);
      const header = card.querySelector('header');

      if (!header) {
        return;
      }
      fromEvent(header, 'click')
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe((event) => {
          const target = event.target as HTMLElement;
          if (target instanceof HTMLAnchorElement) {
            return;
          }

          this.router.navigate([], {fragment: card.id, replaceUrl: true});
        });
    });
  }

  private setScrollEventHandlers(): void {
    const scroll$ = fromEvent(this.document, 'scroll').pipe(
      auditTime(SCROLL_EVENT_DELAY),
      takeUntilDestroyed(this.destroyRef),
    );

    scroll$.subscribe(() => this.setActiveCodeLine());
  }

  private listenToResizeCardContainer(): void {
    const membersCardContainer = this.document.querySelector(
      API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME,
    );
    if (membersCardContainer) {
      afterNextRender(
        () => {
          const resizeObserver = new ResizeObserver(() => {
            this.updateCardsOffsetTop();
            this.setActiveCodeLine();
          });
          resizeObserver.observe(membersCardContainer);
          this.destroyRef.onDestroy(() => resizeObserver.disconnect());
        },
        {injector: this.injector},
      );
    }
  }

  private setActiveCodeLine(): void {
    const activeCard = Array.from(this.cardOffsetTop)
      .filter(([_, offsetTop]) => {
        return offsetTop < this.window.scrollY + this.membersMarginTopInPx() + SCROLL_THRESHOLD;
      })
      .pop();

    if (!activeCard) {
      return;
    }

    const activeLines = this.document.querySelectorAll<HTMLButtonElement>(
      `button.${API_TAB_ACTIVE_CODE_LINE}`,
    );

    const activeLine = activeLines.length > 0 ? activeLines.item(0) : null;
    const previousActiveMemberId = this.getMemberId(activeLine);
    const currentActiveMemberId = activeCard[0];

    if (previousActiveMemberId && previousActiveMemberId !== currentActiveMemberId) {
      for (const line of Array.from(activeLines)) {
        line.classList.remove(API_TAB_ACTIVE_CODE_LINE);
      }
    } else {
      const lines = this.document.querySelectorAll<HTMLButtonElement>(
        `button[${MEMBER_ID_ATTRIBUTE}="${currentActiveMemberId}"]`,
      );
      for (const line of Array.from(lines)) {
        line.classList.add(API_TAB_ACTIVE_CODE_LINE);
      }
      this.document.getElementById(`${currentActiveMemberId}`)?.focus({preventScroll: true});
    }
  }

  private scrollToCard(card: HTMLDivElement | null): void {
    if (!card) {
      return;
    }

    if (card !== <HTMLElement>document.activeElement) {
      (<HTMLElement>document.activeElement).blur();
    }

    this.window.scrollTo({
      top: card!.offsetTop - this.membersMarginTopInPx(),
      behavior: 'smooth',
    });
  }

  private updateCardsOffsetTop(): void {
    this.getAllMemberCards().forEach((card) => {
      this.cardOffsetTop.set(card.id, card.offsetTop);
    });
  }

  private getAllMemberCards(): NodeListOf<HTMLDivElement> {
    return this.document.querySelectorAll<HTMLDivElement>(
      `${API_REFERENCE_MEMBER_CARD_CLASS_NAME}`,
    );
  }

  private getMemberId(lineButton: HTMLButtonElement | null): string | undefined {
    if (!lineButton) {
      return undefined;
    }
    return lineButton.attributes.getNamedItem(MEMBER_ID_ATTRIBUTE)?.value;
  }

  private updateMarginTopWhenTabBodyIsResized(tabBody: HTMLElement): void {
    this.resizeObserver?.disconnect();

    this.resizeObserver = new ResizeObserver((_) => {
      const offsetTop = tabBody.getBoundingClientRect().top;
      if (offsetTop) {
        this.membersMarginTopInPx.set(offsetTop);
      }
    });

    this.resizeObserver.observe(tabBody);
  }

  private findButtonElement(element: HTMLElement) {
    let parent = element.parentElement;

    while (parent) {
      if (parent instanceof HTMLButtonElement) {
        return parent;
      }

      parent = parent.parentElement;
    }

    return null;
  }
}


# Deferrable Views

## Overview

Deferrable views can be used in component template to defer the loading of select dependencies within that template. Those dependencies include components, directives, and pipes, and any associated CSS. To use this feature, you can declaratively wrap a section of your template in a `@defer` block which specifies the loading conditions.

Deferrable views support a series of [triggers](guide/defer#triggers), [prefetching](guide/defer#prefetching), and several sub blocks used for [placeholder](guide/defer#placeholder), [loading](guide/defer#loading), and [error](guide/defer#error) state management. You can also create custom conditions with [`when`](guide/defer#when) and [`prefetch when`](guide/defer#prefetching).

```angular-html
@defer {
  <large-component />
}
```

## Why use Deferrable Views?

Deferrable views, also known as `@defer` blocks, are a powerful tool that can be used to reduce the initial bundle size of your application or defer heavy components that may not ever be loaded until a later time. This should result in a faster initial load and an improvement in your Core Web Vitals (CWV) results. Deferring some of your components until later should specifically improve Largest Contentful Paint (LCP) and Time to First Byte (TTFB).

Note: It is highly recommended that any defer loaded component that might result in layout shift once the dependencies have loaded be below the fold or otherwise not yet visible to the user.

## Which dependencies are defer-loadable?

In order for dependencies within a `@defer` block to be deferred, they need to meet two conditions:

1. They must be standalone. Non-standalone dependencies cannot be deferred and will still be eagerly loaded, even inside of `@defer` blocks.

2. They must not be directly referenced from the same file, outside of `@defer` blocks; this includes ViewChild queries.

Transitive dependencies of the components, directives, and pipes used in the defer block can be standalone or NgModule based and will still be deferred.

## Blocks

`@defer` blocks have several sub blocks to allow you to gracefully handle different stages in the deferred loading process.

### `@defer`

The content of the main `@defer` block is the section of content that is lazily loaded. It will not be rendered initially, and all of the content will appear once the specified [trigger](guide/defer#triggers) or `when` condition is met and the dependencies have been fetched. By default, a `@defer` block is triggered when the browser state becomes [idle](guide/defer#on-idle).

### `@placeholder`

By default, defer blocks do not render any content before they are triggered. The `@placeholder` is an optional block that declares content to show before the defer block is triggered. This placeholder content is replaced with the main content once the loading is complete. You can use any content in the placeholder section including plain HTML, components, directives, and pipes; however keep in mind the dependencies of the placeholder block are eagerly loaded.

Note: For the best user experience, you should always specify a `@placeholder` block.

The `@placeholder` block accepts an optional parameter to specify the `minimum` amount of time that this placeholder should be shown. This `minimum` parameter is specified in time increments of milliseconds (ms) or seconds (s). This parameter exists to prevent fast flickering of placeholder content in the case that the deferred dependencies are fetched quickly. The `minimum` timer for the `@placeholder` block begins after the initial render of this `@placeholder` block completes.

```angular-html
@defer {
  <large-component />
} @placeholder (minimum 500ms) {
  <p>Placeholder content</p>
}
```

Note: Certain triggers may require the presence of either a `@placeholder` or a [template reference variable](guide/templates/variables#template-reference-variables) to function. See the [Triggers](guide/defer#triggers) section for more details.

### `@loading`

The `@loading` block is an optional block that allows you to declare content that will be shown during the loading of any deferred dependencies. Its dependences are eagerly loaded (similar to `@placeholder`).

For example, you could show a loading spinner. Once loading has been triggered, the `@loading` block replaces the `@placeholder` block.

The `@loading` block accepts two optional parameters to specify the `minimum` amount of time that this placeholder should be shown and amount of time to wait `after` loading begins before showing the loading template. `minimum` and `after` parameters are specified in time increments of milliseconds (ms) or seconds (s). Just like `@placeholder`, these parameters exist to prevent fast flickering of content in the case that the deferred dependencies are fetched quickly. Both the `minimum` and `after` timers for the `@loading` block begins immediately after the loading has been triggered.

```angular-html
@defer {
  <large-component />
} @loading (after 100ms; minimum 1s) {
  <img alt="loading..." src="loading.gif" />
}
```

### `@error`

The `@error` block allows you to declare content that will be shown if deferred loading fails. Similar to `@placeholder` and `@loading`, the dependencies of the `@error` block are eagerly loaded. The `@error` block is optional.

```angular-html
@defer {
  <calendar-cmp />
} @error {
  <p>Failed to load the calendar</p>
}
```


## Triggers

When a `@defer` block is triggered, it replaces placeholder content with lazily loaded content. There are two options for configuring when this swap is triggered: `on` and `when`.

`on` specifies a trigger condition using a trigger from the list of available triggers below. An example would be on interaction or on viewport.

Multiple event triggers can be defined at once. For example: `on interaction; on timer(5s)` means that the defer block will be triggered if the user interacts with the placeholder, or after 5 seconds.

Note: Multiple `on` triggers are always OR conditions. Similarly, `on` mixed with `when` conditions are also OR conditions.

```angular-html
@defer (on viewport; on timer(5s)) {
  <calendar-cmp />
} @placeholder {
  <img src="placeholder.png" />
}
```

`when` specifies a condition as an expression that returns a boolean. When this expression becomes truthy, the placeholder is swapped with the lazily loaded content (which may be an asynchronous operation if the dependencies need to be fetched).

Note: if the `when` condition switches back to `false`, the defer block is not reverted back to the placeholder. The swap is a one-time operation. If the content within the block should be conditionally rendered, an `if` condition can be used within the block itself.

```angular-html
@defer (when cond) {
  <calendar-cmp />
}
```

You could also use both `when` and `on` together in one statement, and the swap will be triggered if either condition is met.

```angular-html
@defer (on viewport; when cond) {
  <calendar-cmp />
} @placeholder {
  <img src="placeholder.png" />
}
```

### on idle

`idle` will trigger the deferred loading once the browser has reached an idle state (detected using the `requestIdleCallback` API under the hood). This is the default behavior with a defer block.

### on viewport

`viewport` would trigger the deferred block when the specified content enters the viewport using the [`IntersectionObserver` API](https://developer.mozilla.org/docs/Web/API/Intersection_Observer_API). This could be the placeholder content or an element reference.

By default, the placeholder will act as the element watched for entering viewport as long as it is a single root element node.

```angular-html
@defer (on viewport) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}
```

Alternatively, you can specify a [template reference variable](guide/templates/variables#template-reference-variables) in the same template as the `@defer` block as the element that is watched to enter the viewport. This variable is passed in as a parameter on the viewport trigger.

```angular-html
<div #greeting>Hello!</div>

@defer (on viewport(greeting)) {
  <greetings-cmp />
}
```

### on interaction

`interaction` will trigger the deferred block when the user interacts with the specified element through `click` or `keydown` events.

By default, the placeholder will act as the interaction element as long as it is a single root element node.

```angular-html
@defer (on interaction) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}
```

Alternatively, you can specify a [template reference variable](guide/templates/variables#template-reference-variables) as the element that triggers interaction. This variable is passed in as a parameter on the interaction trigger.

```angular-html
<button type="button" #greeting>Hello!</button>

@defer (on interaction(greeting)) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}
```

### on hover

`hover` triggers deferred loading when the mouse has hovered over the trigger area. Events used for this are `mouseenter` and `focusin`.

By default, the placeholder will act as the hover element as long as it is a single root element node.

```angular-html
@defer (on hover) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}
```

Alternatively, you can specify a [template reference variable](guide/templates/variables#template-reference-variables) as the hover element. This variable is passed in as a parameter on the hover trigger.

```angular-html
<div #greeting>Hello!</div>

@defer (on hover(greeting)) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}
```

### on immediate

`immediate` triggers the deferred load immediately, meaning once the client has finished rendering, the defer chunk would then start fetching right away.

```angular-html
@defer (on immediate) {
  <calendar-cmp />
} @placeholder {
  <div>Calendar placeholder</div>
}
```

### on timer

`timer(x)` would trigger after a specified duration. The duration is required and can be specified in `ms` or `s`.

```angular-html
@defer (on timer(500ms)) {
  <calendar-cmp />
}
```

## Prefetching

`@defer` allows to specify conditions when prefetching of the dependencies should be triggered. You can use a special `prefetch` keyword. `prefetch` syntax works similarly to the main defer conditions, and accepts `when` and/or `on` to declare the trigger.

In this case, `when` and `on` associated with defer controls when to render, and `prefetch when` and `prefetch on` controls when to fetch the resources. This enables more advanced behaviors, such as letting you start to prefetch resources before a user has actually seen or interacted with a defer block, but might interact with it soon, making the resources available faster.

In the example below, the prefetching starts when a browser becomes idle and the contents of the block is rendered on interaction.

```angular-html
@defer (on interaction; prefetch on idle) {
  <calendar-cmp />
} @placeholder {
  <img src="placeholder.png" />
}
```

## Testing

Angular provides TestBed APIs to simplify the process of testing `@defer` blocks and triggering different states during testing. By default, `@defer` blocks in tests will play through like a defer block would behave in a real application. If you want to manually step through states, you can switch the defer block behavior to `Manual` in the TestBed configuration.

```typescript
it('should render a defer block in different states', async () => {
  // configures the defer block behavior to start in "paused" state for manual control.
  TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Manual});

  @Component({
    // ...
    template: `
      @defer {
        <large-component />
      } @placeholder {
        Placeholder
      } @loading {
        Loading...
      }
    `
  })
  class ComponentA {}

  // Create component fixture.
  const componentFixture = TestBed.createComponent(ComponentA);

  // Retrieve the list of all defer block fixtures and get the first block.
  const deferBlockFixture = (await componentFixture.getDeferBlocks())[0];

  // Renders placeholder state by default.
  expect(componentFixture.nativeElement.innerHTML).toContain('Placeholder');

  // Render loading state and verify rendered output.
  await deferBlockFixture.render(DeferBlockState.Loading);
  expect(componentFixture.nativeElement.innerHTML).toContain('Loading');

  // Render final state and verify the output.
  await deferBlockFixture.render(DeferBlockState.Complete);
  expect(componentFixture.nativeElement.innerHTML).toContain('large works!');
});
```

## Behavior with Server-side rendering (SSR) and Static site generation (SSG)

When rendering an application on the server (either using SSR or SSG), defer blocks always render their `@placeholder` (or nothing if a placeholder is not specified). Triggers are ignored on the server.

## Behavior with `NgModule`

`@defer` blocks can be used in both standalone and NgModule-based components, directives and pipes. You can use standalone and NgModule-based dependencies inside of a `@defer` block, however **only standalone components, directives, and pipes can be deferred**. The NgModule-based dependencies would be included into the eagerly loaded bundle.

## Nested `@defer` blocks and avoiding cascading loads

There are cases where nesting multiple `@defer` blocks may cause cascading requests. An example of this would be when a `@defer` block with an immediate trigger has a nested `@defer` block with another immediate trigger. When you have nested `@defer` blocks, make sure that an inner one has a different set of conditions, so that they don't trigger at the same time, causing cascading requests.

## Avoiding Layout Shifts

It is a recommended best practice to not defer components that will be visible in the user's viewport on initial load. This will negatively affect Core Web Vitals by causing an increase in cumulative layout shift (CLS). If you choose to defer components in this area, it's best to avoid `immediate`, `timer`, `viewport`, and custom `when` conditions that would cause the content to be loaded during the initial render of the page.