import { DOCUMENT, ViewportScroller } from '@angular/common'; import { afterNextRender, ChangeDetectionStrategy, Component, computed, inject, Injector, signal, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NavigationEnd, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { filter } from 'rxjs'; import { DotBackground } from './components/dot-background/dot-background'; import { SHELL_COPY } from './core/content/shell-copy'; import { SITE_CONFIG } from './core/content/site-config'; import { LOCALE_HTML_LANG, otherLocale } from './core/i18n/locale'; import { LocaleService } from './core/i18n/locale.service'; import { NavigationService } from './core/navigation/navigation.service'; import { isBrowserPlatform, viewportMatches } from './core/platform/browser'; import { TerminalDock } from './shared/terminal/terminal-dock'; /** Matches `md` in `src/_breakpoints.scss` (48rem). */ export const WIDE_NAV_QUERY = '(min-width: 48rem)'; @Component({ selector: 'app-root', changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouterOutlet, RouterLink, RouterLinkActive, DotBackground, TerminalDock], templateUrl: './app.html', styleUrl: './app.scss', }) export class App { protected readonly navigation = inject(NavigationService); protected readonly localeService = inject(LocaleService); private readonly router = inject(Router); private readonly injector = inject(Injector); private readonly document = inject(DOCUMENT); private readonly viewportScroller = inject(ViewportScroller); private readonly isBrowser = isBrowserPlatform(); private readonly wideNav = viewportMatches(WIDE_NAV_QUERY); protected readonly siteConfig = SITE_CONFIG; protected readonly navOpen = signal(true); protected readonly shell = computed(() => SHELL_COPY[this.localeService.locale()]); protected readonly otherLocale = computed(() => otherLocale(this.localeService.locale())); protected readonly otherHtmlLang = computed(() => LOCALE_HTML_LANG[this.otherLocale()]); protected readonly menuLabel = computed(() => this.navOpen() ? this.shell().menuClose : this.shell().menuOpen, ); constructor() { afterNextRender( () => { this.collapseNavIfNarrow(); this.bindHeaderScrollOffset(); }, { injector: this.injector }, ); this.router.events .pipe( filter((event): event is NavigationEnd => event instanceof NavigationEnd), takeUntilDestroyed(), ) .subscribe(() => this.collapseNavIfNarrow()); } protected toggleNav(): void { this.navOpen.update((open) => !open); } private collapseNavIfNarrow(): void { if (!this.isBrowser || this.wideNav) { return; } this.navOpen.set(false); } private bindHeaderScrollOffset(): void { if (!this.isBrowser) { return; } this.viewportScroller.setOffset(() => [0, this.headerOffsetPx()]); } private headerOffsetPx(): number { const view = this.document.defaultView; if (!view) { return 0; } const styles = view.getComputedStyle(this.document.documentElement); const raw = styles.getPropertyValue('--header-offset').trim(); const numeric = Number.parseFloat(raw); if (!Number.isFinite(numeric)) { return 0; } if (raw.endsWith('rem')) { const rootSize = Number.parseFloat(styles.fontSize); return numeric * (Number.isFinite(rootSize) ? rootSize : 16); } return numeric; } }