Daily Standup Summary
blockproductivitySlack and Geekbot style async engineering daily standup summary with squad pulse cards, critical blocker alert callout, structured yesterday/today/blockers submission entries, reaction counts, and thread actions.
Installation
$pnpm dlx uipkge-ng@latest add daily-standup-summary$npx uipkge-ng@latest add daily-standup-summary$yarn dlx uipkge-ng@latest add daily-standup-summary$bunx uipkge-ng@latest add daily-standup-summaryManifest:
https://uipkge.dev/r/angular/daily-standup-summary.jsonInstalls to:src/app/components/blocks/daily-standup-summary/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
StandupReactioninterface StandupReaction {
emoji: string
count: number
active?: boolean
}StandupMemberinterface StandupMember {
id: string
name: string
role: string
avatar: string
fallback: string
submittedAt: string
timezone: string
status: StandupStatus
statusLabel: string
yesterday: string[]
today: string[]
blockers?: string | null
blockerSeverity?: 'critical' | 'moderate' | 'low'
reactions: StandupReaction[]
threadCount?: number
}BlockerAlertinterface BlockerAlert {
id: string
title: string
description: string
authorName: string
authorRole: string
reportedAt: string
ticketId?: string
scope: string
assignedSquad: string
}StandupPulseMetricsinterface StandupPulseMetrics {
submissionsCount: number
totalMembers: number
pendingMemberName?: string
activeBlockersCount: number
blockersHighlight?: string
goalsOnTrackCount: number
totalGoals: number
sprintName?: string
deploymentsPlannedCount: number
deploymentsHighlight?: string
}Files installed (2)
src/app/components/blocks/daily-standup-summary/daily-standup-summary.component.ts35.3 kB
// Slack and Geekbot style async engineering daily standup summary with squad pulse cards, // critical blocker alert callout, structured yesterday/today/blockers submission entries, // reaction counts, and thread actions. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { AlertTriangle, ArrowRight, Bot, Calendar, Check, CheckCircle2, Clock, Copy, Flame, Handshake, LucideAngularModule, MessageSquare, Rocket, Send, ShieldAlert, ShieldCheck, Smile, Target, Users, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' export type StandupStatus = 'on-track' | 'blocked' | 'needs-review' export interface StandupReaction { emoji: string count: number active?: boolean } export interface StandupMember { id: string name: string role: string avatar: string fallback: string submittedAt: string timezone: string status: StandupStatus statusLabel: string yesterday: string[] today: string[] blockers?: string | null blockerSeverity?: 'critical' | 'moderate' | 'low' reactions: StandupReaction[] threadCount?: number } export interface BlockerAlert { id: string title: string description: string authorName: string authorRole: string reportedAt: string ticketId?: string scope: string assignedSquad: string } export interface StandupPulseMetrics { submissionsCount: number totalMembers: number pendingMemberName?: string activeBlockersCount: number blockersHighlight?: string goalsOnTrackCount: number totalGoals: number sprintName?: string deploymentsPlannedCount: number deploymentsHighlight?: string } const defaultPulseMetrics: StandupPulseMetrics = { submissionsCount: 5, totalMembers: 6, pendingMemberName: 'Alex Rivera (PTO / Vacation)', activeBlockersCount: 1, blockersHighlight: 'AWS IAM KMS staging rotation', goalsOnTrackCount: 8, totalGoals: 10, sprintName: 'Sprint 42 · 3 days left', deploymentsPlannedCount: 3, deploymentsHighlight: '2 staging · 1 production (v2.4.0)', } const defaultBlockerAlert: BlockerAlert = { id: 'blk-aws-kms-01', title: 'Blocked on AWS IAM permission grant for KMS rotation in staging', description: 'Unable to complete cryptographic secret rotation automated test suite for the v2.4 staging cluster. Ticket SEC-9104 is currently pending approval from the Security Operations squad.', authorName: 'Marcus Vance', authorRole: 'DevOps & Cloud Architect', reportedAt: '42m ago', ticketId: 'SEC-9104', scope: 'Staging Pipeline', assignedSquad: '#sec-ops-triage', } const defaultMembers: StandupMember[] = [ { id: 'mem-1', name: 'Elena Rostova', role: 'Principal Engineer', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&auto=format&fit=crop&q=80', fallback: 'ER', submittedAt: '09:15 AM', timezone: 'UTC+2 · Berlin', status: 'on-track', statusLabel: 'On Track', yesterday: [ 'Merged RFC #142 for headless multi-token theme engine architecture', 'Reviewed and approved 6 PRs across core UI primitive components', 'Mentored frontend team on Reka UI polymorphic asChild slot bindings', ], today: [ 'Finalizing async tree-shaking benchmarking suite for monorepo components (#PERF-892)', 'Kicking off Design Tokens v4 migration RFC discussion thread with design systems team', 'Pairing with Sarah on virtualized combobox keyboard focus trap resolution', ], blockers: null, reactions: [ { emoji: '👍', count: 4, active: true }, { emoji: '🚀', count: 2, active: false }, { emoji: '👀', count: 3, active: false }, { emoji: '🧠', count: 1, active: false }, ], threadCount: 3, }, { id: 'mem-2', name: 'Marcus Vance', role: 'DevOps & Cloud Architect', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&auto=format&fit=crop&q=80', fallback: 'MV', submittedAt: '09:28 AM', timezone: 'UTC-5 · New York', status: 'blocked', statusLabel: 'Blocked', blockerSeverity: 'critical', yesterday: [ 'Provisioned ephemeral preview environments for automated PR test runners', 'Automated Cloudflare Pages edge cache invalidation hooks in GitHub Actions', ], today: [ 'Rolling out canary release v2.4.0 to EU cluster once KMS permissions clear (#OPS-419)', 'Setting up Prometheus edge exporter for SSR latency and cache hit ratios', ], blockers: 'Blocked on AWS IAM permission grant for KMS rotation in staging. Escalated to Security Ops in #sec-ops-triage (Ticket SEC-9104).', reactions: [ { emoji: '🚨', count: 3, active: true }, { emoji: '💬', count: 4, active: false }, { emoji: '🤝', count: 2, active: false }, { emoji: '🙏', count: 1, active: false }, ], threadCount: 4, }, { id: 'mem-3', name: 'David Chen', role: 'Full-Stack Engineer', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150&auto=format&fit=crop&q=80', fallback: 'DC', submittedAt: '09:42 AM', timezone: 'UTC-7 · San Francisco', status: 'on-track', statusLabel: 'On Track', yesterday: [ 'Implemented Nitro rate-limiting telemetry parser and token bucket middleware', 'Wrote 34 unit tests for zero-allocation token stringifier algorithm', ], today: [ 'Implementing HMAC-SHA256 webhook signature validation generator (#CORE-331)', 'Writing OpenAPI 3.1 documentation with interactive curl playground snippets', ], blockers: null, reactions: [ { emoji: '🔥', count: 5, active: false }, { emoji: '👍', count: 3, active: true }, { emoji: '⚡', count: 2, active: false }, ], threadCount: 1, }, { id: 'mem-4', name: 'Sarah Jenkins', role: 'Frontend Specialist', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&auto=format&fit=crop&q=80', fallback: 'SJ', submittedAt: '09:55 AM', timezone: 'UTC+1 · London', status: 'needs-review', statusLabel: 'Needs Review', yesterday: [ 'Completed accessible combobox keyboard navigation and roving tabindex support', 'Audited dark mode OKLCH contrast ratios on table headers and badges', ], today: [ 'Benchmarking spring physics motion transition framerates on mobile viewports (#UI-204)', 'Crafting comprehensive story demos for Daily Standup Summary block', ], blockers: 'Awaiting PR review on PR #884 (Reka roving focus patch) from Elena or David to merge.', reactions: [ { emoji: '✨', count: 6, active: true }, { emoji: '👍', count: 4, active: false }, { emoji: '🚀', count: 1, active: false }, ], threadCount: 2, }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-daily-standup-summary, [ui-daily-standup-summary]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiSeparatorComponent, ], template: ` <div data-slot="daily-standup-summary" [class]="rootClass"> <!-- Header Section --> <header class="bg-card border-border flex flex-col gap-4 rounded-xl border p-5 shadow-xs sm:p-6 lg:flex-row lg:items-center lg:justify-between" > <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Bot" class="size-4.5" aria-hidden="true" /> </div> <h1 class="text-xl font-bold tracking-tight sm:text-2xl">Daily Engineering Standup</h1> <span ui-badge variant="secondary" class="border-info/20 bg-info/10 text-info text-xs font-medium"> <span class="bg-info mr-1.5 inline-block size-1.5 animate-pulse rounded-full"></span> Synced with #eng-standups </span> </div> <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs sm:text-sm"> <span class="text-foreground flex items-center gap-1 font-medium"> <lucide-icon [img]="Calendar" class="text-muted-foreground size-3.5" aria-hidden="true" /> {{ date }} </span> <span class="flex items-center gap-1"> <lucide-icon [img]="Users" class="size-3.5" aria-hidden="true" /> {{ teamName }} · {{ metrics().totalMembers }} Members </span> </div> </div> <div class="flex flex-wrap items-center gap-2.5"> <button ui-button variant="outline" size="sm" class="h-9 gap-1.5 text-xs font-medium" (click)="copySummary()"> @if (copiedSummary()) { <lucide-icon [img]="Check" class="text-success size-3.5" aria-hidden="true" /> } @else { <lucide-icon [img]="Copy" class="text-muted-foreground size-3.5" aria-hidden="true" /> } {{ copiedSummary() ? 'Summary Copied' : 'Copy Summary' }} </button> <button ui-button [variant]="standupSubmitted() ? 'outline' : 'default'" size="sm" class="h-9 gap-1.5 text-xs font-semibold transition" (click)="standupSubmitted.set(!standupSubmitted())" > @if (standupSubmitted()) { <lucide-icon [img]="CheckCircle2" class="text-success size-4" aria-hidden="true" /> } @else { <lucide-icon [img]="Send" class="size-4" aria-hidden="true" /> } {{ standupSubmitted() ? 'Standup Submitted' : 'Submit My Standup' }} </button> </div> </header> <!-- 4 Squad Pulse Summary Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between"> <p ui-card-description class="text-xs font-medium">Total Submissions</p> <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <lucide-icon [img]="CheckCircle2" class="size-3.5" aria-hidden="true" /> </div> </div> <h3 ui-card-title class="mt-1 flex items-baseline gap-2 text-xl font-bold tabular-nums"> {{ metrics().submissionsCount }} / {{ metrics().totalMembers }} <span ui-badge variant="secondary" class="border-success/20 bg-success/10 text-success text-success text-xs font-semibold tabular-nums" > {{ submitPct() }}% In </span> </h3> </div> <div ui-card-content class="pt-0"> <div class="bg-muted mb-2 h-1.5 w-full overflow-hidden rounded-full"> <div class="bg-success h-full transition-[width] duration-500" [style.width.%]="submitPct()"></div> </div> @if (metrics().pendingMemberName) { <p class="text-muted-foreground truncate text-xs">Pending: {{ metrics().pendingMemberName }}</p> } </div> </div> <div ui-card [class]="blockerCardClass"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between"> <p ui-card-description class="text-xs font-medium">Active Blockers</p> <div [class]="blockerIconClass"> <lucide-icon [img]="AlertTriangle" class="size-3.5" aria-hidden="true" /> </div> </div> <h3 ui-card-title class="mt-1 flex items-baseline gap-2 text-xl font-bold tabular-nums"> {{ metrics().activeBlockersCount }} {{ metrics().activeBlockersCount === 1 ? 'Critical' : 'Active' }} @if (metrics().activeBlockersCount > 0) { <span ui-badge variant="destructive" class="text-xs font-semibold"> <span class="mr-1 inline-block size-1.5 rounded-full bg-white"></span> Action Req. </span> } @else { <span ui-badge variant="secondary" class="border-success/20 bg-success/10 text-success text-xs font-semibold" > Clear </span> } </h3> </div> <div ui-card-content class="pt-0"> <p class="text-muted-foreground truncate text-xs"> {{ metrics().blockersHighlight || 'All squad paths currently unblocked' }} </p> </div> </div> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between"> <p ui-card-description class="text-xs font-medium">Sprint Goals On Track</p> <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <lucide-icon [img]="Target" class="size-3.5" aria-hidden="true" /> </div> </div> <h3 ui-card-title class="mt-1 flex items-baseline gap-2 text-xl font-bold tabular-nums"> {{ metrics().goalsOnTrackCount }} / {{ metrics().totalGoals }} <span ui-badge variant="secondary" class="bg-primary/10 text-primary border-primary/20 text-xs font-semibold tabular-nums" > {{ goalsPct() }}% Velocity </span> </h3> </div> <div ui-card-content class="pt-0"> <p class="text-muted-foreground truncate text-xs"> {{ metrics().sprintName || 'Sprint on schedule' }} </p> </div> </div> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between"> <p ui-card-description class="text-xs font-medium">Planned Deployments</p> <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <lucide-icon [img]="Rocket" class="size-3.5" aria-hidden="true" /> </div> </div> <h3 ui-card-title class="mt-1 flex items-baseline gap-2 text-xl font-bold tabular-nums"> {{ metrics().deploymentsPlannedCount }} Releases <span ui-badge variant="outline" class="text-xs font-semibold"> Today </span> </h3> </div> <div ui-card-content class="pt-0"> <p class="text-muted-foreground truncate text-xs"> {{ metrics().deploymentsHighlight || 'Targeting staging & edge production' }} </p> </div> </div> </div> <!-- Active Blockers Alert Card (Highlighted Banner) --> @if (alert(); as blocker) { <div class="border-destructive/30 bg-destructive/5 dark:bg-destructive/10 relative overflow-hidden rounded-xl border p-5 shadow-xs" > <div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between"> <div class="flex items-start gap-3.5"> <div class="bg-destructive/15 text-destructive mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg" > <lucide-icon [img]="ShieldAlert" class="size-5" aria-hidden="true" /> </div> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="destructive" class="text-xs font-semibold"> <span class="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-white"></span> Critical Blocker </span> <span ui-badge variant="outline" class="font-mono text-xs"> {{ blocker.ticketId }} </span> <span class="text-muted-foreground text-xs"> Reported by <strong class="text-foreground font-medium">{{ blocker.authorName }}</strong> ({{ blocker.authorRole }}) · {{ blocker.reportedAt }} </span> </div> <h2 class="text-foreground text-base leading-snug font-semibold"> {{ blocker.title }} </h2> <p class="text-muted-foreground text-xs sm:text-sm"> {{ blocker.description }} </p> <div class="flex flex-wrap items-center gap-3 pt-1 text-xs"> <span class="text-muted-foreground" >Scope: <span class="text-foreground font-medium">{{ blocker.scope }}</span></span > <span class="text-muted-foreground" >Assigned: <span class="text-primary font-medium">{{ blocker.assignedSquad }}</span></span > </div> </div> </div> <div class="flex shrink-0 flex-wrap items-center gap-2 md:flex-col md:items-end"> <button ui-button size="sm" [variant]="escalatedBlocker() ? 'outline' : 'destructive'" class="h-8 text-xs font-semibold" (click)="escalatedBlocker.set(!escalatedBlocker())" > @if (escalatedBlocker()) { <lucide-icon [img]="Check" class="text-success mr-1 size-3.5" /> } @else { <lucide-icon [img]="Flame" class="mr-1 size-3.5" /> } {{ escalatedBlocker() ? 'Escalated to SecOps' : 'Escalate Blocker' }} </button> <button ui-button variant="outline" size="sm" class="h-8 text-xs font-medium"> <lucide-icon [img]="MessageSquare" class="text-muted-foreground mr-1 size-3.5" /> Open Thread (4) </button> </div> </div> </div> } <!-- Standup Submissions Section --> <div class="space-y-4"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-2"> <h2 class="text-base font-semibold tracking-tight">Standup Submissions</h2> <span ui-badge variant="secondary" class="font-mono text-xs tabular-nums"> {{ filteredMembers().length }} </span> </div> <div class="bg-muted border-border flex items-center gap-1 rounded-lg border p-1 text-xs"> <button type="button" [class]="filterPillClass('all')" (click)="activeFilter.set('all')"> All ({{ countAll() }}) </button> <button type="button" [class]="filterPillClass('blocked')" (click)="activeFilter.set('blocked')"> Blocked ({{ countBlocked() }}) </button> <button type="button" [class]="filterPillClass('needs-review')" (click)="activeFilter.set('needs-review')"> Needs Review ({{ countNeedsReview() }}) </button> <button type="button" [class]="filterPillClass('on-track')" (click)="activeFilter.set('on-track')"> On Track ({{ countOnTrack() }}) </button> </div> </div> <div class="space-y-4"> @for (member of filteredMembers(); track member.id; let mIdx = $index) { <div ui-card [class]="memberCardClass(member.status)"> <div ui-card-header class="pb-4"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-3.5"> <ui-avatar class="size-10 border"> <ui-avatar-image [src]="member.avatar" [alt]="member.name" /> <ui-avatar-fallback class="bg-primary/10 text-primary text-xs font-semibold">{{ member.fallback }}</ui-avatar-fallback> </ui-avatar> <div> <div class="flex flex-wrap items-center gap-2"> <h3 class="text-sm font-semibold tracking-tight">{{ member.name }}</h3> <span ui-badge [variant]="member.status === 'blocked' ? 'destructive' : 'secondary'" [class]="statusBadgeClass(member.status)" > {{ member.statusLabel }} </span> </div> <p class="text-muted-foreground text-xs"> {{ member.role }} · <span class="text-muted-foreground/80">{{ member.timezone }}</span> </p> </div> </div> <div class="flex items-center gap-2 self-start text-xs sm:self-auto"> <span class="text-muted-foreground flex items-center gap-1 tabular-nums"> <lucide-icon [img]="Clock" class="text-muted-foreground size-3" aria-hidden="true" /> {{ member.submittedAt }} </span> </div> </div> </div> <div ui-card-content class="space-y-4 pt-0"> <ui-separator /> <div class="grid grid-cols-1 gap-4 md:grid-cols-3"> <div class="bg-muted/30 border-border/60 space-y-2 rounded-lg border p-3.5"> <h4 class="text-muted-foreground flex items-center gap-1.5 text-xs font-semibold tracking-wider uppercase" > <lucide-icon [img]="CheckCircle2" class="text-success size-3.5 shrink-0" aria-hidden="true" /> Yesterday's Output </h4> <ul class="space-y-1.5 text-xs leading-relaxed"> @for (item of member.yesterday; track item) { <li class="flex items-start gap-1.5"> <span class="text-success mt-0.5 font-bold">✓</span> <span class="text-foreground/90">{{ item }}</span> </li> } </ul> </div> <div class="bg-muted/30 border-border/60 space-y-2 rounded-lg border p-3.5"> <h4 class="text-muted-foreground flex items-center gap-1.5 text-xs font-semibold tracking-wider uppercase" > <lucide-icon [img]="ArrowRight" class="text-primary size-3.5 shrink-0" aria-hidden="true" /> Today's Focus </h4> <ul class="space-y-1.5 text-xs leading-relaxed"> @for (item of member.today; track item) { <li class="flex items-start gap-1.5"> <span class="text-primary mt-0.5 font-bold">→</span> <span class="text-foreground/90">{{ item }}</span> </li> } </ul> </div> <div [class]="blockerBoxClass(member)"> <h4 class="text-muted-foreground flex items-center gap-1.5 text-xs font-semibold tracking-wider uppercase" > @if (member.blockers) { <lucide-icon [img]="AlertTriangle" [class]=" member.status === 'blocked' ? 'text-destructive size-3.5 shrink-0' : 'text-warning size-3.5 shrink-0' " aria-hidden="true" /> } @else { <lucide-icon [img]="ShieldCheck" class="text-success size-3.5 shrink-0" aria-hidden="true" /> } Blockers & Risks </h4> @if (member.blockers) { <div class="text-xs leading-relaxed"> <p [class]=" member.status === 'blocked' ? 'text-destructive font-medium' : 'text-warning font-medium' " > {{ member.blockers }} </p> </div> } @else { <div class="text-muted-foreground flex items-center gap-1.5 text-xs"> <span class="text-success font-medium">No blockers</span> <span>— Clear path to sprint goals</span> </div> } </div> </div> <div class="border-border/60 flex flex-wrap items-center justify-between gap-3 border-t pt-3"> <div class="flex flex-wrap items-center gap-1.5"> @for (reaction of member.reactions; track reaction.emoji; let rIdx = $index) { <button type="button" [class]="reactionPillClass(reaction.active ?? false)" (click)="toggleReaction(memberIndexOf(member.id), rIdx)" > <span>{{ reaction.emoji }}</span> <span class="font-semibold tabular-nums">{{ reaction.count }}</span> </button> } <button type="button" class="focus-visible:ring-ring text-muted-foreground hover:text-foreground hover:bg-muted flex size-7 items-center justify-center rounded-full border border-dashed transition focus-visible:ring-2 focus-visible:outline-none" title="Add reaction" aria-label="Add reaction" (click)="toggleReaction(memberIndexOf(member.id), 0)" > <lucide-icon [img]="Smile" class="size-3.5" aria-hidden="true" /> </button> </div> <div class="flex flex-wrap items-center gap-2"> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-8 gap-1.5 text-xs" > <lucide-icon [img]="Handshake" class="size-3.5" aria-hidden="true" /> Offer Help </button> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs font-medium"> <lucide-icon [img]="MessageSquare" class="text-muted-foreground size-3.5" aria-hidden="true" /> <span>Reply in Thread</span> @if (member.threadCount) { <span class="bg-muted text-foreground py-0.2 ml-0.5 rounded px-1.5 font-mono text-xs font-bold tabular-nums" > {{ member.threadCount }} </span> } </button> </div> </div> </div> </div> } </div> </div> </div> `, }) export class UiDailyStandupSummaryComponent { protected readonly AlertTriangle = AlertTriangle protected readonly ArrowRight = ArrowRight protected readonly Bot = Bot protected readonly Calendar = Calendar protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Copy = Copy protected readonly Flame = Flame protected readonly Handshake = Handshake protected readonly MessageSquare = MessageSquare protected readonly Rocket = Rocket protected readonly Send = Send protected readonly ShieldAlert = ShieldAlert protected readonly ShieldCheck = ShieldCheck protected readonly Smile = Smile protected readonly Target = Target protected readonly Users = Users @Input() teamName = 'Core Registry Squad' @Input() date = 'Today · Friday, Aug 21, 2026' @Input() set pulseMetrics(v: StandupPulseMetrics | undefined) { if (v) this.pulseMetricsInput.set(v) } @Input() set blockerAlert(v: BlockerAlert | null | undefined) { // `undefined` (absent input) keeps the default; explicit `null` hides the banner. if (v !== undefined) this.blockerAlertInput.set(v) } @Input() set members(v: StandupMember[] | undefined) { if (v) this.localMembers.set(v.map((m) => ({ ...m, reactions: m.reactions.map((r) => ({ ...r })) }))) } @Input() set initialFilter(v: 'all' | StandupStatus) { if (v) this.activeFilter.set(v) } @Input('class') className?: string private readonly pulseMetricsInput = signal<StandupPulseMetrics | null>(null) private readonly blockerAlertInput = signal<BlockerAlert | null | undefined>(undefined) readonly localMembers = signal<StandupMember[]>( defaultMembers.map((m) => ({ ...m, reactions: m.reactions.map((r) => ({ ...r })) })), ) readonly activeFilter = signal<'all' | StandupStatus>('all') readonly standupSubmitted = signal(false) readonly copiedSummary = signal(false) readonly escalatedBlocker = signal(false) private copyTimer: ReturnType<typeof setTimeout> | undefined readonly metrics = computed(() => this.pulseMetricsInput() ?? defaultPulseMetrics) readonly alert = computed(() => this.blockerAlertInput() !== undefined ? this.blockerAlertInput() : defaultBlockerAlert, ) readonly filteredMembers = computed(() => { const f = this.activeFilter() if (f === 'all') return this.localMembers() return this.localMembers().filter((m) => m.status === f) }) readonly countAll = computed(() => this.localMembers().length) readonly countBlocked = computed(() => this.localMembers().filter((m) => m.status === 'blocked').length) readonly countNeedsReview = computed(() => this.localMembers().filter((m) => m.status === 'needs-review').length) readonly countOnTrack = computed(() => this.localMembers().filter((m) => m.status === 'on-track').length) readonly submitPct = computed(() => { const m = this.metrics() return Math.round((m.submissionsCount / m.totalMembers) * 100) }) readonly goalsPct = computed(() => { const m = this.metrics() return Math.round((m.goalsOnTrackCount / m.totalGoals) * 100) }) get rootClass(): string { return cn('bg-background text-foreground w-full space-y-6', this.className) } get blockerCardClass(): string { return cn( 'border-border shadow-xs', this.metrics().activeBlockersCount > 0 && 'border-destructive/40 bg-destructive/5', ) } get blockerIconClass(): string { // [class] replaces static classes: the base must live here (matches React's cn(base, active)). return cn( 'flex size-7 items-center justify-center rounded-md', this.metrics().activeBlockersCount > 0 ? 'bg-destructive/10 text-destructive' : 'bg-success/10 text-success', ) } filterPillClass(f: 'all' | StandupStatus): string { const active = this.activeFilter() === f const base = 'focus-visible:ring-ring min-h-6 rounded-md px-2.5 py-1 font-medium transition focus-visible:ring-2 focus-visible:outline-none' if (!active) { return cn( base, f === 'blocked' ? 'text-muted-foreground hover:text-destructive' : 'text-muted-foreground hover:text-foreground', ) } if (f === 'blocked') return cn(base, 'bg-background text-destructive font-semibold shadow-xs') if (f === 'needs-review') return cn(base, 'bg-background text-warning text-warning font-semibold shadow-xs') if (f === 'on-track') return cn(base, 'bg-background text-success text-success font-semibold shadow-xs') return cn(base, 'bg-background text-foreground font-semibold shadow-xs') } memberCardClass(status: StandupStatus): string { return cn( 'border-border hover:border-border/80 shadow-xs transition', status === 'blocked' && 'border-destructive/30', status === 'needs-review' && 'border-warning/30', ) } statusBadgeClass(status: StandupStatus): string { return cn( 'text-xs font-medium', status === 'on-track' && 'border-success/20 bg-success/10 text-success', status === 'needs-review' && 'border-warning/20 bg-warning/10 text-warning', ) } blockerBoxClass(member: StandupMember): string { return cn( 'space-y-2 rounded-lg border p-3.5', member.blockers ? member.status === 'blocked' ? 'border-destructive/30 bg-destructive/5 dark:bg-destructive/10' : 'border-warning/30 bg-warning/5' : 'border-border/60 bg-muted/30', ) } reactionPillClass(active: boolean): string { return cn( 'focus-visible:ring-ring flex min-h-6 items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium transition focus-visible:ring-2 focus-visible:outline-none', active ? 'bg-primary/10 border-primary/40 text-primary' : 'bg-muted/40 border-border text-muted-foreground hover:bg-muted hover:text-foreground', ) } memberIndexOf(id: string): number { return this.localMembers().findIndex((m) => m.id === id) } toggleReaction(memberIndex: number, reactionIndex: number): void { this.localMembers.update((prev) => { const next = [...prev] const member = next[memberIndex] if (!member) return prev const reactions = [...member.reactions] const r = reactions[reactionIndex] if (!r) return prev reactions[reactionIndex] = r.active ? { ...r, count: r.count - 1, active: false } : { ...r, count: r.count + 1, active: true } next[memberIndex] = { ...member, reactions } return next }) } async copySummary(): Promise<void> { try { const m = this.metrics() const text = `${this.teamName} Standup Summary — ${this.date}\n\nSubmissions: ${m.submissionsCount}/${m.totalMembers} (${Math.round((m.submissionsCount / m.totalMembers) * 100)}%)\nActive Blockers: ${m.activeBlockersCount}\nGoals on Track: ${m.goalsOnTrackCount}/${m.totalGoals}\nPlanned Releases: ${m.deploymentsPlannedCount}` await navigator.clipboard.writeText(text) this.copiedSummary.set(true) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => this.copiedSummary.set(false), 2000) } catch { // Clipboard unavailable in some contexts. } } }src/app/components/blocks/daily-standup-summary/index.ts0.2 kB
export { UiDailyStandupSummaryComponent, type StandupStatus, type StandupReaction, type StandupMember, type BlockerAlert, type StandupPulseMetrics, } from './daily-standup-summary.component'
Raw manifest:https://uipkge.dev/r/angular/daily-standup-summary.json