Files
task-stream/apps/web/src/app/app.ts
Josep Piguillem 9ccc3b5967 perf(web): the command palette and the bell load once the page is up
They come with @defer (on idle; on interaction): their code, their
stores and the bell's queries and live subscription leave the start,
and main no longer carries anything of the tracker's presenters or
application. Meanwhile the header shows a still copy of each with the
same box, so nothing moves when they arrive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 08:55:19 +02:00

130 lines
3.5 KiB
TypeScript

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import {
NavigationEnd,
Router,
RouterLink,
RouterLinkActive,
RouterOutlet,
} from '@angular/router';
import { filter, map } from 'rxjs';
import { AuthFacade } from '@task-stream/web/identity/application';
import { UserMenuComponent } from '@task-stream/web/identity/presenters';
import { PiIconButtonComponent } from '@pi/ui';
import { AppShellComponent } from '@task-stream/web/shared/ui';
import {
CommandPalette,
NotificationsBell,
} from '@task-stream/web/tracker/presenters';
@Component({
selector: 'ts-root',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
RouterOutlet,
RouterLink,
RouterLinkActive,
AppShellComponent,
UserMenuComponent,
NotificationsBell,
CommandPalette,
PiIconButtonComponent,
],
styles: `
/* The palette's button, still: same box, so nothing moves when it loads. */
.ts-shell-search {
display: inline-flex;
gap: 0.375rem;
align-items: center;
border: 0;
background: none;
font: inherit;
cursor: pointer;
kbd {
font-size: 0.6875rem;
opacity: 0.7;
}
}
`,
template: `
<ts-app-shell>
@if (auth.isAuthenticated()) {
<a
shellNav
class="ts-nav-link"
routerLink="/"
routerLinkActive="is-active"
[routerLinkActiveOptions]="{ exact: true }"
>
Proyectos
</a>
}
@if (auth.isAuthenticated()) {
<a
shellNav
class="ts-nav-link"
routerLink="/portfolio"
routerLinkActive="is-active"
>
Cartera
</a>
}
<a
shellNav
class="ts-nav-link"
routerLink="/explore"
routerLinkActive="is-active"
>
Explorar
</a>
@if (auth.isAuthenticated()) {
<!-- One projectable node per @if branch, or neither reaches the slot. -->
<ng-container shellUser>
<!-- Palette and bell load once the page is up: their code and their
queries stay out of the start. Meanwhile, a still copy. -->
@defer (on idle; on interaction) {
<ts-command-palette />
} @placeholder {
<button
type="button"
class="ts-nav-link ts-shell-search"
tabindex="-1"
aria-hidden="true"
>
Buscar <kbd>⌘K</kbd>
</button>
}
@defer (on idle; on interaction) {
<ts-notifications-bell />
} @placeholder {
<pi-icon-button
icon="bell"
color="grey"
appearance="ghost"
aria-label="Notificaciones"
/>
}
<ts-user-menu />
</ng-container>
} @else if (auth.settled() && !onLogin()) {
<a shellUser class="ts-nav-link ts-nav-link--cta" routerLink="/login"
>Iniciar sesión</a
>
}
<router-outlet />
</ts-app-shell>
`,
})
export class App {
protected readonly auth = inject(AuthFacade);
private readonly router = inject(Router);
/** The login page has its own form: no "Iniciar sesión" button there. */
protected readonly onLogin = toSignal(
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this.router.url.startsWith('/login')),
),
{ initialValue: false },
);
}