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 shadcn@latest add https://uipkge.dev/r/react/daily-standup-summary.json$npx shadcn@latest add https://uipkge.dev/r/react/daily-standup-summary.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/daily-standup-summary.json$bunx shadcn@latest add https://uipkge.dev/r/react/daily-standup-summary.jsonNamed registry:
npx shadcn@latest add @uipkge-react/daily-standup-summaryInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
teamName | string | — | optional |
date | string | — | optional |
pulseMetrics | StandupPulseMetrics | — | optional |
blockerAlert | BlockerAlert | null | — | optional |
members | StandupMember[] | — | optional |
initialFilter | 'all' | StandupStatus | — | optional |
className | string | — | optional |
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 (1)
components/blocks/DailyStandupSummary.tsx33.2 kB
'use client' import { useState } from 'react' import { AlertTriangle, ArrowRight, Bot, Calendar, Check, CheckCircle2, Clock, Copy, Flame, Handshake, MessageSquare, Rocket, Send, ShieldAlert, ShieldCheck, Smile, Target, Users, } from 'lucide-react' import { cn } from '@/lib/utils' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' 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 } export interface DailyStandupSummaryProps { teamName?: string date?: string pulseMetrics?: StandupPulseMetrics blockerAlert?: BlockerAlert | null members?: StandupMember[] initialFilter?: 'all' | StandupStatus className?: 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, }, ] export function DailyStandupSummary({ teamName = 'Core Registry Squad', date = 'Today · Friday, Aug 21, 2026', pulseMetrics = defaultPulseMetrics, blockerAlert = defaultBlockerAlert, members = defaultMembers, initialFilter = 'all', className, }: DailyStandupSummaryProps) { const [activeFilter, setActiveFilter] = useState<'all' | StandupStatus>(initialFilter) const [standupSubmitted, setStandupSubmitted] = useState(false) const [copiedSummary, setCopiedSummary] = useState(false) const [escalatedBlocker, setEscalatedBlocker] = useState(false) const [localMembers, setLocalMembers] = useState<StandupMember[]>(members) const filteredMembers = localMembers.filter((m) => { if (activeFilter === 'all') return true return m.status === activeFilter }) const countAll = localMembers.length const countBlocked = localMembers.filter((m) => m.status === 'blocked').length const countNeedsReview = localMembers.filter((m) => m.status === 'needs-review').length const countOnTrack = localMembers.filter((m) => m.status === 'on-track').length const toggleReaction = (memberIndex: number, reactionIndex: number) => { setLocalMembers((prev) => { const next = [...prev] const member = { ...next[memberIndex] } const reactions = [...member.reactions] const r = { ...reactions[reactionIndex] } if (r.active) { r.count -= 1 r.active = false } else { r.count += 1 r.active = true } reactions[reactionIndex] = r member.reactions = reactions next[memberIndex] = member return next }) } const copySummary = async () => { try { const text = `${teamName} Standup Summary — ${date}\n\nSubmissions: ${pulseMetrics.submissionsCount}/${pulseMetrics.totalMembers} (${Math.round((pulseMetrics.submissionsCount / pulseMetrics.totalMembers) * 100)}%)\nActive Blockers: ${pulseMetrics.activeBlockersCount}\nGoals on Track: ${pulseMetrics.goalsOnTrackCount}/${pulseMetrics.totalGoals}\nPlanned Releases: ${pulseMetrics.deploymentsPlannedCount}` await navigator.clipboard.writeText(text) setCopiedSummary(true) setTimeout(() => { setCopiedSummary(false) }, 2000) } catch {} } return ( <div data-slot="daily-standup-summary" className={cn('bg-background text-foreground w-full space-y-6', className)}> {/* Header Section */} <header className="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 className="space-y-1.5"> <div className="flex flex-wrap items-center gap-2.5"> <div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <Bot className="size-4.5" aria-hidden="true" /> </div> <h1 className="text-xl font-bold tracking-tight sm:text-2xl">Daily Engineering Standup</h1> <Badge variant="secondary" className="border-info/20 bg-info/10 text-info text-xs font-medium"> <span className="bg-info mr-1.5 inline-block size-1.5 animate-pulse rounded-full" /> Synced with #eng-standups </Badge> </div> <div className="text-muted-foreground flex flex-wrap items-center gap-2 text-xs sm:text-sm"> <span className="text-foreground flex items-center gap-1 font-medium"> <Calendar className="text-muted-foreground size-3.5" aria-hidden="true" /> {date} </span> <span className="flex items-center gap-1"> <Users className="size-3.5" aria-hidden="true" /> {teamName} · {pulseMetrics.totalMembers} Members </span> </div> </div> {/* Action Controls */} <div className="flex flex-wrap items-center gap-2.5"> <Button variant="outline" size="sm" className="h-9 gap-1.5 text-xs font-medium" onClick={copySummary}> {copiedSummary ? ( <Check className="text-success size-3.5" aria-hidden="true" /> ) : ( <Copy className="text-muted-foreground size-3.5" aria-hidden="true" /> )} {copiedSummary ? 'Summary Copied' : 'Copy Summary'} </Button> <Button variant={standupSubmitted ? 'outline' : 'default'} size="sm" className="h-9 gap-1.5 text-xs font-semibold transition" onClick={() => setStandupSubmitted(!standupSubmitted)} > {standupSubmitted ? ( <CheckCircle2 className="text-success size-4" aria-hidden="true" /> ) : ( <Send className="size-4" aria-hidden="true" /> )} {standupSubmitted ? 'Standup Submitted' : 'Submit My Standup'} </Button> </div> </header> {/* 4 Squad Pulse Summary Cards */} <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> {/* 1. Total Submissions */} <Card className="border-border shadow-xs"> <CardHeader className="pb-2"> <div className="flex items-center justify-between"> <CardDescription className="text-xs font-medium">Total Submissions</CardDescription> <div className="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <CheckCircle2 className="size-3.5" aria-hidden="true" /> </div> </div> <CardTitle className="mt-1 flex items-baseline gap-2 text-xl font-bold tabular-nums"> {pulseMetrics.submissionsCount} / {pulseMetrics.totalMembers} <Badge variant="secondary" className="border-success/20 bg-success/10 text-success text-success text-xs font-semibold tabular-nums" > {Math.round((pulseMetrics.submissionsCount / pulseMetrics.totalMembers) * 100)}% In </Badge> </CardTitle> </CardHeader> <CardContent className="pt-0"> <div className="bg-muted mb-2 h-1.5 w-full overflow-hidden rounded-full"> <div className="bg-success h-full transition-[width] duration-500" style={{ width: `${(pulseMetrics.submissionsCount / pulseMetrics.totalMembers) * 100}%` }} /> </div> {pulseMetrics.pendingMemberName && ( <p className="text-muted-foreground truncate text-xs">Pending: {pulseMetrics.pendingMemberName}</p> )} </CardContent> </Card> {/* 2. Active Blockers */} <Card className={cn( 'border-border shadow-xs', pulseMetrics.activeBlockersCount > 0 && 'border-destructive/40 bg-destructive/5', )} > <CardHeader className="pb-2"> <div className="flex items-center justify-between"> <CardDescription className="text-xs font-medium">Active Blockers</CardDescription> <div className={cn( 'flex size-7 items-center justify-center rounded-md', pulseMetrics.activeBlockersCount > 0 ? 'bg-destructive/10 text-destructive' : 'bg-success/10 text-success', )} > <AlertTriangle className="size-3.5" aria-hidden="true" /> </div> </div> <CardTitle className="mt-1 flex items-baseline gap-2 text-xl font-bold tabular-nums"> {pulseMetrics.activeBlockersCount} {pulseMetrics.activeBlockersCount === 1 ? 'Critical' : 'Active'} {pulseMetrics.activeBlockersCount > 0 ? ( <Badge variant="destructive" className="text-xs font-semibold"> <span className="mr-1 inline-block size-1.5 rounded-full bg-white" /> Action Req. </Badge> ) : ( <Badge variant="secondary" className="border-success/20 bg-success/10 text-success text-xs font-semibold" > Clear </Badge> )} </CardTitle> </CardHeader> <CardContent className="pt-0"> <p className="text-muted-foreground truncate text-xs"> {pulseMetrics.blockersHighlight || 'All squad paths currently unblocked'} </p> </CardContent> </Card> {/* 3. Sprint Goals On Track */} <Card className="border-border shadow-xs"> <CardHeader className="pb-2"> <div className="flex items-center justify-between"> <CardDescription className="text-xs font-medium">Sprint Goals On Track</CardDescription> <div className="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <Target className="size-3.5" aria-hidden="true" /> </div> </div> <CardTitle className="mt-1 flex items-baseline gap-2 text-xl font-bold tabular-nums"> {pulseMetrics.goalsOnTrackCount} / {pulseMetrics.totalGoals} <Badge variant="secondary" className="bg-primary/10 text-primary border-primary/20 text-xs font-semibold tabular-nums" > {Math.round((pulseMetrics.goalsOnTrackCount / pulseMetrics.totalGoals) * 100)}% Velocity </Badge> </CardTitle> </CardHeader> <CardContent className="pt-0"> <p className="text-muted-foreground truncate text-xs">{pulseMetrics.sprintName || 'Sprint on schedule'}</p> </CardContent> </Card> {/* 4. Today's Deployments */} <Card className="border-border shadow-xs"> <CardHeader className="pb-2"> <div className="flex items-center justify-between"> <CardDescription className="text-xs font-medium">Planned Deployments</CardDescription> <div className="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <Rocket className="size-3.5" aria-hidden="true" /> </div> </div> <CardTitle className="mt-1 flex items-baseline gap-2 text-xl font-bold tabular-nums"> {pulseMetrics.deploymentsPlannedCount} Releases <Badge variant="outline" className="text-xs font-semibold"> Today </Badge> </CardTitle> </CardHeader> <CardContent className="pt-0"> <p className="text-muted-foreground truncate text-xs"> {pulseMetrics.deploymentsHighlight || 'Targeting staging & edge production'} </p> </CardContent> </Card> </div> {/* Active Blockers Alert Card (Highlighted Banner) */} {blockerAlert && ( <div className="border-destructive/30 bg-destructive/5 dark:bg-destructive/10 relative overflow-hidden rounded-xl border p-5 shadow-xs"> <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between"> <div className="flex items-start gap-3.5"> <div className="bg-destructive/15 text-destructive mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg"> <ShieldAlert className="size-5" aria-hidden="true" /> </div> <div className="space-y-1.5"> <div className="flex flex-wrap items-center gap-2"> <Badge variant="destructive" className="text-xs font-semibold"> <span className="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-white" /> Critical Blocker </Badge> <Badge variant="outline" className="font-mono text-xs"> {blockerAlert.ticketId} </Badge> <span className="text-muted-foreground text-xs"> Reported by <strong className="text-foreground font-medium">{blockerAlert.authorName}</strong> ( {blockerAlert.authorRole}) · {blockerAlert.reportedAt} </span> </div> <h2 className="text-foreground text-base leading-snug font-semibold">{blockerAlert.title}</h2> <p className="text-muted-foreground text-xs sm:text-sm">{blockerAlert.description}</p> <div className="flex flex-wrap items-center gap-3 pt-1 text-xs"> <span className="text-muted-foreground"> Scope: <span className="text-foreground font-medium">{blockerAlert.scope}</span> </span> <span className="text-muted-foreground"> Assigned: <span className="text-primary font-medium">{blockerAlert.assignedSquad}</span> </span> </div> </div> </div> <div className="flex shrink-0 flex-wrap items-center gap-2 md:flex-col md:items-end"> <Button size="sm" variant={escalatedBlocker ? 'outline' : 'destructive'} className="h-8 text-xs font-semibold" onClick={() => setEscalatedBlocker(!escalatedBlocker)} > {escalatedBlocker ? ( <> <Check className="text-success mr-1 size-3.5" /> Escalated to SecOps </> ) : ( <> <Flame className="mr-1 size-3.5" /> Escalate Blocker </> )} </Button> <Button variant="outline" size="sm" className="h-8 text-xs font-medium"> <MessageSquare className="text-muted-foreground mr-1 size-3.5" /> Open Thread (4) </Button> </div> </div> </div> )} {/* Standup Submissions Section */} <div className="space-y-4"> {/* Section Header & Filter Controls */} <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex items-center gap-2"> <h2 className="text-base font-semibold tracking-tight">Standup Submissions</h2> <Badge variant="secondary" className="font-mono text-xs tabular-nums"> {filteredMembers.length} </Badge> </div> {/* Filter Segmented Pills */} <div className="bg-muted border-border flex items-center gap-1 rounded-lg border p-1 text-xs"> <button type="button" className={cn( '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', activeFilter === 'all' ? 'bg-background text-foreground font-semibold shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setActiveFilter('all')} > All ({countAll}) </button> <button type="button" className={cn( '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', activeFilter === 'blocked' ? 'bg-background text-destructive font-semibold shadow-xs' : 'text-muted-foreground hover:text-destructive', )} onClick={() => setActiveFilter('blocked')} > Blocked ({countBlocked}) </button> <button type="button" className={cn( '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', activeFilter === 'needs-review' ? 'bg-background text-warning text-warning font-semibold shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setActiveFilter('needs-review')} > Needs Review ({countNeedsReview}) </button> <button type="button" className={cn( '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', activeFilter === 'on-track' ? 'bg-background text-success text-success font-semibold shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setActiveFilter('on-track')} > On Track ({countOnTrack}) </button> </div> </div> {/* Member Standup Cards List */} <div className="space-y-4"> {filteredMembers.map((member, mIdx) => ( <Card key={member.id} className={cn( 'border-border hover:border-border/80 shadow-xs transition', member.status === 'blocked' && 'border-destructive/30', member.status === 'needs-review' && 'border-warning/30', )} > {/* Member Card Header */} <CardHeader className="pb-4"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex items-center gap-3.5"> <Avatar className="size-10 border"> <AvatarImage src={member.avatar} alt={member.name} /> <AvatarFallback className="bg-primary/10 text-primary text-xs font-semibold"> {member.fallback} </AvatarFallback> </Avatar> <div> <div className="flex flex-wrap items-center gap-2"> <h3 className="text-sm font-semibold tracking-tight">{member.name}</h3> <Badge variant={member.status === 'blocked' ? 'destructive' : 'secondary'} className={cn( 'text-xs font-medium', member.status === 'on-track' && 'border-success/20 bg-success/10 text-success', member.status === 'needs-review' && 'border-warning/20 bg-warning/10 text-warning', )} > {member.statusLabel} </Badge> </div> <p className="text-muted-foreground text-xs"> {member.role} · <span className="text-muted-foreground/80">{member.timezone}</span> </p> </div> </div> {/* Timestamp */} <div className="flex items-center gap-2 self-start text-xs sm:self-auto"> <span className="text-muted-foreground flex items-center gap-1 tabular-nums"> <Clock className="text-muted-foreground size-3" aria-hidden="true" /> {member.submittedAt} </span> </div> </div> </CardHeader> <CardContent className="space-y-4 pt-0"> <Separator /> {/* Three-Part Standup Structure */} <div className="grid grid-cols-1 gap-4 md:grid-cols-3"> {/* 1. Accomplished Yesterday */} <div className="bg-muted/30 border-border/60 space-y-2 rounded-lg border p-3.5"> <h4 className="text-muted-foreground flex items-center gap-1.5 text-xs font-semibold tracking-wider uppercase"> <CheckCircle2 className="text-success size-3.5 shrink-0" aria-hidden="true" /> Yesterday's Output </h4> <ul className="space-y-1.5 text-xs leading-relaxed"> {member.yesterday.map((item, idx) => ( <li key={idx} className="flex items-start gap-1.5"> <span className="text-success mt-0.5 font-bold">✓</span> <span className="text-foreground/90">{item}</span> </li> ))} </ul> </div> {/* 2. Focusing on Today */} <div className="bg-muted/30 border-border/60 space-y-2 rounded-lg border p-3.5"> <h4 className="text-muted-foreground flex items-center gap-1.5 text-xs font-semibold tracking-wider uppercase"> <ArrowRight className="text-primary size-3.5 shrink-0" aria-hidden="true" /> Today's Focus </h4> <ul className="space-y-1.5 text-xs leading-relaxed"> {member.today.map((item, idx) => ( <li key={idx} className="flex items-start gap-1.5"> <span className="text-primary mt-0.5 font-bold">→</span> <span className="text-foreground/90">{item}</span> </li> ))} </ul> </div> {/* 3. Blockers or Risks */} <div className={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', )} > <h4 className="text-muted-foreground flex items-center gap-1.5 text-xs font-semibold tracking-wider uppercase"> {member.blockers ? ( <AlertTriangle className={cn( 'size-3.5 shrink-0', member.status === 'blocked' ? 'text-destructive' : 'text-warning', )} aria-hidden="true" /> ) : ( <ShieldCheck className="text-success size-3.5 shrink-0" aria-hidden="true" /> )} Blockers & Risks </h4> {member.blockers ? ( <div className="text-xs leading-relaxed"> <p className={ member.status === 'blocked' ? 'text-destructive font-medium' : 'text-warning font-medium' } > {member.blockers} </p> </div> ) : ( <div className="text-muted-foreground flex items-center gap-1.5 text-xs"> <span className="text-success font-medium">No blockers</span> <span>— Clear path to sprint goals</span> </div> )} </div> </div> {/* Footer: Slack-style Reactions & Quick Actions */} <div className="border-border/60 flex flex-wrap items-center justify-between gap-3 border-t pt-3"> {/* Reactions Pills */} <div className="flex flex-wrap items-center gap-1.5"> {member.reactions.map((reaction, rIdx) => ( <button key={rIdx} type="button" className={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', reaction.active ? 'bg-primary/10 border-primary/40 text-primary' : 'bg-muted/40 border-border text-muted-foreground hover:bg-muted hover:text-foreground', )} onClick={() => toggleReaction(mIdx, rIdx)} > <span>{reaction.emoji}</span> <span className="font-semibold tabular-nums">{reaction.count}</span> </button> ))} <button type="button" className="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" onClick={() => toggleReaction(mIdx, 0)} > <Smile className="size-3.5" aria-hidden="true" /> </button> </div> {/* Quick Action Buttons */} <div className="flex flex-wrap items-center gap-2"> <Button variant="ghost" size="sm" className="text-muted-foreground hover:text-foreground h-8 gap-1.5 text-xs" > <Handshake className="size-3.5" aria-hidden="true" /> Offer Help </Button> <Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs font-medium"> <MessageSquare className="text-muted-foreground size-3.5" aria-hidden="true" /> <span>Reply in Thread</span> {member.threadCount !== undefined && member.threadCount > 0 && ( <span className="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> </CardContent> </Card> ))} </div> </div> </div> ) }
Raw manifest:https://uipkge.dev/r/react/daily-standup-summary.json