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>
130 lines
3.5 KiB
TypeScript
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 },
|
|
);
|
|
}
|