first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { PiEmptyStateComponent } from '@pi/ui';
import { ProjectFacade } from '@task-stream/web/tracker/application';
import { clock, initials, timeAgo } from '../ui/copy';
/** Live feed of everything that happens in the project, as a log. */
@Component({
selector: 'ts-activity-page',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterLink, PiEmptyStateComponent],
template: `
<div class="ts-log-head">
<p class="ts-eyebrow"><span class="ts-live"></span> En directo</p>
<span class="ts-muted ts-mono"
>{{ facade.activity().length }} eventos</span
>
</div>
@if (facade.activity().length) {
<ol class="ts-log ts-panel" aria-live="polite">
@for (entry of facade.activity(); track entry.id) {
<li>
<time class="ts-log__time" [attr.datetime]="entry.occurredAt">{{
clock(entry.occurredAt)
}}</time>
<span class="ts-log__who" [attr.title]="entry.actor.name">{{
initialsOf(entry.actor.name)
}}</span>
<span class="ts-log__what">{{ entry.summary }}</span>
@if (entry.issueKey) {
<a class="ts-key" [routerLink]="['../issues', entry.issueKey]">{{
entry.issueKey
}}</a>
} @else {
<span></span>
}
<span class="ts-log__ago">{{ ago(entry.occurredAt) }}</span>
</li>
}
</ol>
} @else {
<pi-empty-state icon="activity" title="Sin actividad todavía" />
}
`,
styles: `
.ts-log-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
.ts-eyebrow {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.ts-muted {
font-size: 0.75rem;
}
}
.ts-live {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: var(--ts-s-done);
animation: ts-pulse 2s ease-out infinite;
}
@keyframes ts-pulse {
0% {
box-shadow: 0 0 0 0
color-mix(in srgb, var(--ts-s-done) 60%, transparent);
}
70%,
100% {
box-shadow: 0 0 0 0.5rem transparent;
}
}
.ts-log {
margin: 0;
padding: 0.25rem 0;
list-style: none;
li {
display: grid;
grid-template-columns: 3.5rem 1.75rem minmax(0, 1fr) auto 8.5rem;
align-items: center;
gap: 0.875rem;
margin: 0;
padding: 0.625rem 1.125rem;
border-bottom: 1px dashed var(--ts-line);
animation: ts-in 0.35s ease-out;
&:last-child {
border-bottom: 0;
}
&:first-child {
animation-name: ts-in, ts-flash;
animation-duration: 0.35s, 1.6s;
}
}
}
.ts-log__time,
.ts-log__ago {
font-family: var(--ts-font-mono);
font-size: 0.75rem;
color: var(--ts-muted);
}
.ts-log__ago {
text-align: right;
white-space: nowrap;
}
.ts-log__who {
display: inline-grid;
place-items: center;
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
background: color-mix(in srgb, var(--ts-brand) 16%, var(--ts-surface));
color: color-mix(in srgb, var(--ts-brand) 70%, var(--ts-ink));
font-family: var(--ts-font-mono);
font-size: 0.625rem;
font-weight: 600;
}
.ts-log__what {
font-size: 0.875rem;
color: var(--ts-ink-soft);
}
a.ts-key {
text-decoration: none;
&:hover {
color: var(--ts-brand);
border-color: var(--ts-brand);
}
}
@keyframes ts-in {
from {
opacity: 0;
transform: translateY(-4px);
}
}
@keyframes ts-flash {
from {
background: color-mix(in srgb, var(--ts-brand) 10%, transparent);
}
}
@media (max-width: 640px) {
.ts-log li {
grid-template-columns: 1.75rem minmax(0, 1fr) auto;
gap: 0.625rem;
padding-inline: 0.875rem;
}
.ts-log__time,
.ts-log__ago {
display: none;
}
}
`,
})
export class ActivityPage {
protected readonly facade = inject(ProjectFacade);
protected readonly ago = timeAgo;
protected readonly initialsOf = initials;
protected readonly clock = clock;
}