import { ChangeDetectionStrategy, Component, computed, effect, inject, input, } from '@angular/core'; import { Router, RouterLink, RouterLinkActive, RouterOutlet, } from '@angular/router'; import { AuthFacade } from '@task-stream/web/identity/application'; import { PiNoteComponent, PiSpinnerComponent } from '@pi/ui'; import { ProjectFacade, provideProjectFacade, } from '@task-stream/web/tracker/application'; import { AvatarsComponent } from '../ui/avatars'; import { projectTone, PUBLIC_TONE, ROLE } from '../ui/copy'; /** /p/:key — loads the project once for all its tabs (its store and facade are scoped here). */ @Component({ selector: 'ts-project-shell', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ RouterOutlet, RouterLink, RouterLinkActive, AvatarsComponent, PiSpinnerComponent, PiNoteComponent, ], providers: [provideProjectFacade()], template: ` @if (facade.project(); as project) {

@if (auth.isAuthenticated()) { Proyectos / } @else { Explorar / } {{ project.key }} @if (facade.myRole(); as role) { · {{ roleLabel(role) }} } @if (project.isPublic) { Público }

{{ project.key }} {{ project.name }}

@if (project.description) {

{{ project.description }}

} @if (project.isPublic && project.acceptsProposals) { ¿Has visto un error o tienes una idea? Cuéntanoslo → }
{{ project.members.length }} en el equipo
@if (!facade.myRole()) {

Proyecto público: lo estás viendo en modo lectura. @if (!auth.isAuthenticated()) { Inicia sesión si eres del equipo. }

} @if (project.archivedAt) {

Proyecto archivado Se puede ver todo, pero es de solo lectura. @if (facade.isOwner()) { }

} } @else if (facade.error(); as error) { @if (!auth.isAuthenticated()) {

Iniciar sesión · Ver proyectos públicos

} } @else {
} `, styles: ` .ts-archived { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 1rem; padding: 0.625rem 0.875rem; border: 1px solid var(--ts-line); border-radius: 0.625rem; background: var(--ts-sunken); color: var(--ts-ink-soft); font-size: 0.875rem; } .ts-eyebrow .ts-pill { margin-left: 0.5rem; letter-spacing: 0; text-transform: none; vertical-align: middle; } .ts-readonly { margin: 1rem 0 0; padding: 0.625rem 0.875rem; border-radius: 0.625rem; background: color-mix(in srgb, var(--ts-c-teal) 8%, var(--ts-surface)); border: 1px solid color-mix(in srgb, var(--ts-c-teal) 25%, transparent); font-size: 0.875rem; color: var(--ts-ink-soft); a { font-weight: 600; color: var(--ts-brand); } } .ts-center-actions { margin: 1rem 0 0; font-size: 0.875rem; a { font-weight: 600; color: var(--ts-brand); } } .ts-project-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap; h1 { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.25rem 0.875rem; margin: 0.5rem 0 0; font-size: clamp(1.75rem, 1.2rem + 1.8vw, 2.75rem); font-weight: 700; line-height: 1.05; } } .ts-project-header__main { min-width: 0; } .ts-project-header__key { font-family: var(--ts-font-mono); font-size: 0.55em; font-weight: 600; letter-spacing: -0.03em; color: var(--tone); } .ts-project-header__description { max-width: 48rem; margin: 0.5rem 0 0; color: var(--ts-muted); } .ts-project-header__propose { display: inline-block; margin-top: 0.625rem; color: var(--ts-brand); font-weight: 600; text-decoration: none; &:hover { text-decoration: underline; } } .ts-project-header__team { display: flex; align-items: center; gap: 0.75rem; } .ts-project-header__people { font-size: 0.75rem; color: var(--ts-muted); text-decoration: none; &:hover { color: var(--ts-ink); } } `, }) export class ProjectShellPage { /** Route param (`withComponentInputBinding`). */ readonly key = input.required(); protected readonly facade = inject(ProjectFacade); protected readonly auth = inject(AuthFacade); private readonly router = inject(Router); protected readonly publicTone = PUBLIC_TONE; /** Where to come back after signing in. */ protected readonly url = () => this.router.url; protected readonly roleLabel = (role: keyof typeof ROLE) => ROLE[role]; protected readonly tone = projectTone; protected readonly memberNames = computed(() => this.facade.members().map((m) => m.user.name), ); constructor() { effect(() => this.facade.open(this.key())); } }