feat: stabilize pod sidebars layout

This commit is contained in:
Yahya Alhinai
2026-06-28 04:35:24 +00:00
parent 4f012aa54a
commit 866558e100
2 changed files with 479 additions and 314 deletions
+390 -314
View File
@@ -55,6 +55,10 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
const STREAM_SIDEBAR_WIDTH = 'clamp(20rem, 22vw, 23rem)';
const STREAM_RAIL_WIDTH = '4rem';
const POD_TOP_BAR_HEIGHT = '5rem';
interface PInfo {
id: string;
name: string;
@@ -253,300 +257,301 @@ export function PodView({
const podmanPresent = participants.some((p) => p.name.toLowerCase() === 'podman');
return (
<SidebarProvider
open={leftStreamOpen}
onOpenChange={setLeftStreamOpen}
style={
{
'--sidebar-width': '23rem',
'--sidebar-width-icon': '4rem',
} as CSSProperties
}
<div
style={{ '--pod-topbar-height': POD_TOP_BAR_HEIGHT } as CSSProperties}
className="min-h-screen bg-background text-foreground"
>
<ActivitySidebar
side="left"
title="My stream"
collapsedLabel="Mine"
description={`${me}'s live screen, git, intervention, and conflict log.`}
events={activity.mine}
connected={activity.connected}
open={leftStreamOpen}
onToggle={() => setLeftStreamOpen((open) => !open)}
emptyTitle="No personal signal yet"
emptyDescription="Start the local git watcher or share your IDE screen to populate this lane."
/>
<header
data-testid="pod-topbar"
className="sticky top-0 z-30 border-b bg-background/90 backdrop-blur-xl"
>
<div className="mx-auto flex min-h-20 w-full max-w-[1480px] flex-col gap-3 px-4 py-3 sm:px-6 md:flex-row md:items-center md:justify-between lg:px-8">
<div className="flex min-w-0 items-center gap-3">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={onLeave}>
<ArrowLeftIcon />
<span className="sr-only">Leave pod</span>
</Button>
</TooltipTrigger>
<TooltipContent>Leave pod</TooltipContent>
</Tooltip>
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="truncate text-xl font-semibold tracking-tight">{team.name}</h1>
<Badge variant={room ? 'default' : 'secondary'} className="rounded-md">
{room ? 'live' : 'local'}
</Badge>
</div>
<p className="truncate font-mono text-xs text-muted-foreground">{team.repo}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-2 sm:flex sm:items-center">
<Button variant="outline" onClick={toggleBeat} disabled={!room}>
<Volume2Icon data-icon="inline-start" />
{playingBeat ? 'Stop audio' : 'Test audio'}
</Button>
<Button onClick={toggleScreen} disabled={!room}>
<MonitorUpIcon data-icon="inline-start" />
{sharing ? 'Stop sharing' : 'Share screen'}
</Button>
</div>
</div>
</header>
<SidebarProvider
open={rightStreamOpen}
onOpenChange={setRightStreamOpen}
open={leftStreamOpen}
onOpenChange={setLeftStreamOpen}
style={
{
'--sidebar-width': '24rem',
'--sidebar-width-icon': '4rem',
'--sidebar-width': STREAM_SIDEBAR_WIDTH,
'--sidebar-width-icon': STREAM_RAIL_WIDTH,
minHeight: 'calc(100svh - var(--pod-topbar-height))',
} as CSSProperties
}
className="min-h-screen flex-1"
className="bg-background text-foreground"
>
<SidebarInset className="min-h-screen min-w-0 bg-background">
<div className="mx-auto flex min-h-screen w-full max-w-[1240px] flex-col gap-5 px-4 py-4 sm:px-6 lg:px-8">
<header className="sticky top-0 z-10 -mx-4 flex flex-col gap-3 border-b bg-background/86 px-4 pb-4 pt-2 backdrop-blur-xl sm:-mx-6 sm:px-6 md:flex-row md:items-center md:justify-between lg:-mx-8 lg:px-8">
<div className="flex min-w-0 items-center gap-3">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => setLeftStreamOpen((v) => !v)}
>
<PanelLeftIcon />
<span className="sr-only">
{leftStreamOpen ? 'Collapse my stream' : 'Expand my stream'}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{leftStreamOpen ? 'Collapse my stream' : 'Expand my stream'}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={onLeave}>
<ArrowLeftIcon />
<span className="sr-only">Leave pod</span>
</Button>
</TooltipTrigger>
<TooltipContent>Leave pod</TooltipContent>
</Tooltip>
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="truncate text-xl font-semibold tracking-tight">{team.name}</h1>
<Badge variant={room ? 'default' : 'secondary'} className="rounded-md">
{room ? 'live' : 'local'}
</Badge>
</div>
<p className="truncate font-mono text-xs text-muted-foreground">{team.repo}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-2 sm:flex sm:items-center">
<Button variant="outline" onClick={() => setRightStreamOpen((v) => !v)}>
<PanelRightIcon data-icon="inline-start" />
{rightStreamOpen ? 'Hide team' : 'Show team'}
</Button>
<Button variant="outline" onClick={toggleBeat} disabled={!room}>
<Volume2Icon data-icon="inline-start" />
{playingBeat ? 'Stop audio' : 'Test audio'}
</Button>
<Button onClick={toggleScreen} disabled={!room}>
<MonitorUpIcon data-icon="inline-start" />
{sharing ? 'Stop sharing' : 'Share screen'}
</Button>
</div>
</header>
{devMode && (
<Alert>
<RadioTowerIcon />
<AlertTitle>Local mode</AlertTitle>
<AlertDescription>LiveKit is not configured for this session.</AlertDescription>
</Alert>
)}
{note && (
<Alert>
<CircleDotIcon />
<AlertTitle>Room notice</AlertTitle>
<AlertDescription>{note}</AlertDescription>
</Alert>
)}
{room && audioBlocked && (
<Alert className="flex items-center justify-between gap-3">
<div className="flex items-start gap-3">
<Volume2Icon />
<div>
<AlertTitle>Sound is off</AlertTitle>
<AlertDescription>
Click to hear PodMan&apos;s voice alerts.
</AlertDescription>
</div>
</div>
<Button size="sm" onClick={() => void enableSound()}>
Enable sound
</Button>
</Alert>
)}
<main className="grid flex-1 gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
<section className="flex min-w-0 flex-col gap-5">
<div className="grid gap-3 sm:grid-cols-3">
<Metric label="Participants" value={participants.length || 1} />
<Metric label="Screen" value={sharing ? 'sharing' : 'idle'} />
<Metric label="PodMan" value={podmanPresent ? 'online' : 'waiting'} />
</div>
<Card className="flex-1">
<CardHeader>
<CardTitle>Room state</CardTitle>
<CardDescription>People and media currently visible to PodMan.</CardDescription>
</CardHeader>
<CardContent>
{participants.length === 0 ? (
<Empty className="min-h-80">
<EmptyHeader>
<EmptyMedia variant="icon">
<RadioTowerIcon />
</EmptyMedia>
<EmptyTitle>Connecting</EmptyTitle>
<EmptyDescription>Waiting for LiveKit room state.</EmptyDescription>
</EmptyHeader>
</Empty>
) : (
<div className="grid gap-2 md:grid-cols-2">
{participants.map((p) => (
<Participant key={p.id} participant={p} />
))}
</div>
)}
</CardContent>
<CardFooter>
<p className="truncate text-sm text-muted-foreground">
Roster: {team.members.join(', ') || 'No saved members'}
</p>
</CardFooter>
</Card>
{activity.error && (
<p className="text-xs text-muted-foreground">{activity.error}</p>
)}
</section>
<aside className="flex flex-col gap-5">
<Card>
<CardHeader>
<CardTitle>Intervention</CardTitle>
<CardDescription>Card first, voice only for urgent escalation.</CardDescription>
<CardAction>
<Badge variant={active ? 'default' : 'secondary'} className="rounded-md">
{active ? 'active' : 'clear'}
</Badge>
</CardAction>
</CardHeader>
<CardContent>
{active ? (
<div className="flex flex-col gap-4">
<div className="rounded-lg border bg-muted/35 p-3">
<p className="text-sm leading-6">{active.message}</p>
</div>
<div className="flex items-center justify-between gap-3 text-sm">
<span className="text-muted-foreground">Suggested action</span>
<Badge variant="outline">
{active.suggestedAction.kind.replaceAll('_', ' ')}
</Badge>
</div>
{hermes?.interventionId === active.id && (
<div className="rounded-lg border border-dashed p-3">
<div className="mb-1 flex items-center gap-2 text-xs font-medium text-muted-foreground">
<MessageSquareIcon className="size-3.5" />
Hermes message
</div>
<p className="text-sm leading-6">{hermes.text}</p>
</div>
)}
{voiceCue && (
<div className="rounded-lg border border-dashed p-3">
<div className="mb-1 flex items-center gap-2 text-xs font-medium text-muted-foreground">
<Volume2Icon className="size-3.5" />
Voice cue
</div>
<p className="text-sm leading-6">{voiceCue}</p>
</div>
)}
</div>
) : (
<Empty className="min-h-72 border-0 p-0">
<EmptyHeader>
<EmptyMedia variant="icon">
<SparklesIcon />
</EmptyMedia>
<EmptyTitle>No collision detected</EmptyTitle>
<EmptyDescription>
Share your screen when ready. PodMan will stay quiet until there is a
useful signal.
</EmptyDescription>
</EmptyHeader>
</Empty>
)}
{actionUrl && (
<a
href={actionUrl}
target="_blank"
rel="noreferrer"
className="mt-4 flex items-center justify-between gap-3 rounded-lg border bg-muted/30 px-3 py-2 text-sm font-medium hover:bg-muted"
>
Sync PR artifact opened
<ExternalLinkIcon className="size-4" />
</a>
)}
</CardContent>
{active && (
<CardFooter className="justify-end gap-2">
<Button
variant="outline"
onClick={() => void answerIntervention('dismissed', false)}
>
<XIcon data-icon="inline-start" />
Dismiss
</Button>
<Button onClick={() => void answerIntervention('accepted', true)}>
<CheckIcon data-icon="inline-start" />
Accept
</Button>
</CardFooter>
)}
</Card>
<Card>
<CardHeader>
<CardTitle>Status</CardTitle>
<CardDescription>Connection, media, and agent presence.</CardDescription>
</CardHeader>
<CardContent>
<LiveWaveform
processing={!!room}
active={false}
height={32}
barWidth={2}
barGap={3}
className="mb-4 px-3 py-2 text-muted-foreground"
/>
<div className="flex flex-col gap-3">
<StatusLine label="LiveKit" value={room ? 'connected' : 'offline'} />
<StatusLine label="Screen" value={sharing ? 'published' : 'not shared'} />
<StatusLine label="Audio" value={playingBeat ? 'publishing' : 'ready'} />
<StatusLine label="Agent" value={podmanPresent ? 'watching' : 'waiting'} />
</div>
</CardContent>
</Card>
</aside>
</main>
<div ref={audioRef} className="hidden" />
</div>
</SidebarInset>
<ActivitySidebar
side="right"
title="Team stream"
collapsedLabel="Team"
description="Everyone else in this pod, merged into one realtime feed."
events={activity.team}
side="left"
title="My stream"
collapsedLabel="Mine"
testId="my-stream-sidebar"
description={`${me}'s live screen, git, intervention, and conflict log.`}
events={activity.mine}
connected={activity.connected}
open={rightStreamOpen}
onToggle={() => setRightStreamOpen((open) => !open)}
emptyTitle="No teammate signal yet"
emptyDescription="Waiting for other members' screen, git, or collision events."
open={leftStreamOpen}
onToggle={() => setLeftStreamOpen((open) => !open)}
emptyTitle="No personal signal yet"
emptyDescription="Start the local git watcher or share your IDE screen to populate this lane."
/>
<SidebarProvider
open={rightStreamOpen}
onOpenChange={setRightStreamOpen}
style={
{
'--sidebar-width': STREAM_SIDEBAR_WIDTH,
'--sidebar-width-icon': STREAM_RAIL_WIDTH,
} as CSSProperties
}
className="flex-1"
>
<SidebarInset
data-testid="pod-main-workspace"
style={{ minHeight: 'calc(100svh - var(--pod-topbar-height))' }}
className="min-w-0 bg-background"
>
<div
style={{ minHeight: 'calc(100svh - var(--pod-topbar-height))' }}
className="mx-auto flex w-full max-w-[1120px] flex-col gap-5 px-4 py-5 sm:px-6 lg:px-8"
>
{devMode && (
<Alert>
<RadioTowerIcon />
<AlertTitle>Local mode</AlertTitle>
<AlertDescription>LiveKit is not configured for this session.</AlertDescription>
</Alert>
)}
{note && (
<Alert>
<CircleDotIcon />
<AlertTitle>Room notice</AlertTitle>
<AlertDescription>{note}</AlertDescription>
</Alert>
)}
{room && audioBlocked && (
<Alert className="flex items-center justify-between gap-3">
<div className="flex items-start gap-3">
<Volume2Icon />
<div>
<AlertTitle>Sound is off</AlertTitle>
<AlertDescription>Click to hear PodMan&apos;s voice alerts.</AlertDescription>
</div>
</div>
<Button size="sm" onClick={() => void enableSound()}>
Enable sound
</Button>
</Alert>
)}
<main className="grid flex-1 gap-5 min-[1800px]:grid-cols-[minmax(0,1fr)_340px]">
<section className="flex min-w-0 flex-col gap-5">
<div className="grid gap-3 sm:grid-cols-3">
<Metric label="Participants" value={participants.length || 1} />
<Metric label="Screen" value={sharing ? 'sharing' : 'idle'} />
<Metric label="PodMan" value={podmanPresent ? 'online' : 'waiting'} />
</div>
<Card className="flex-1">
<CardHeader>
<CardTitle>Room state</CardTitle>
<CardDescription>
People and media currently visible to PodMan.
</CardDescription>
</CardHeader>
<CardContent>
{participants.length === 0 ? (
<Empty className="min-h-80">
<EmptyHeader>
<EmptyMedia variant="icon">
<RadioTowerIcon />
</EmptyMedia>
<EmptyTitle>Connecting</EmptyTitle>
<EmptyDescription>Waiting for LiveKit room state.</EmptyDescription>
</EmptyHeader>
</Empty>
) : (
<div className="grid gap-2 md:grid-cols-2">
{participants.map((p) => (
<Participant key={p.id} participant={p} />
))}
</div>
)}
</CardContent>
<CardFooter>
<p className="truncate text-sm text-muted-foreground">
Roster: {team.members.join(', ') || 'No saved members'}
</p>
</CardFooter>
</Card>
{activity.error && (
<p className="text-xs text-muted-foreground">{activity.error}</p>
)}
</section>
<aside className="flex flex-col gap-5">
<Card>
<CardHeader>
<CardTitle>Intervention</CardTitle>
<CardDescription>
Card first, voice only for urgent escalation.
</CardDescription>
<CardAction>
<Badge variant={active ? 'default' : 'secondary'} className="rounded-md">
{active ? 'active' : 'clear'}
</Badge>
</CardAction>
</CardHeader>
<CardContent>
{active ? (
<div className="flex flex-col gap-4">
<div className="rounded-lg border bg-muted/35 p-3">
<p className="text-sm leading-6">{active.message}</p>
</div>
<div className="flex items-center justify-between gap-3 text-sm">
<span className="text-muted-foreground">Suggested action</span>
<Badge variant="outline">
{active.suggestedAction.kind.replaceAll('_', ' ')}
</Badge>
</div>
{hermes?.interventionId === active.id && (
<div className="rounded-lg border border-dashed p-3">
<div className="mb-1 flex items-center gap-2 text-xs font-medium text-muted-foreground">
<MessageSquareIcon className="size-3.5" />
Hermes message
</div>
<p className="text-sm leading-6">{hermes.text}</p>
</div>
)}
{voiceCue && (
<div className="rounded-lg border border-dashed p-3">
<div className="mb-1 flex items-center gap-2 text-xs font-medium text-muted-foreground">
<Volume2Icon className="size-3.5" />
Voice cue
</div>
<p className="text-sm leading-6">{voiceCue}</p>
</div>
)}
</div>
) : (
<Empty className="min-h-72 border-0 p-0">
<EmptyHeader>
<EmptyMedia variant="icon">
<SparklesIcon />
</EmptyMedia>
<EmptyTitle>No collision detected</EmptyTitle>
<EmptyDescription>
Share your screen when ready. PodMan will stay quiet until there is a
useful signal.
</EmptyDescription>
</EmptyHeader>
</Empty>
)}
{actionUrl && (
<a
href={actionUrl}
target="_blank"
rel="noreferrer"
className="mt-4 flex items-center justify-between gap-3 rounded-lg border bg-muted/30 px-3 py-2 text-sm font-medium hover:bg-muted"
>
Sync PR artifact opened
<ExternalLinkIcon className="size-4" />
</a>
)}
</CardContent>
{active && (
<CardFooter className="justify-end gap-2">
<Button
variant="outline"
onClick={() => void answerIntervention('dismissed', false)}
>
<XIcon data-icon="inline-start" />
Dismiss
</Button>
<Button onClick={() => void answerIntervention('accepted', true)}>
<CheckIcon data-icon="inline-start" />
Accept
</Button>
</CardFooter>
)}
</Card>
<Card>
<CardHeader>
<CardTitle>Status</CardTitle>
<CardDescription>Connection, media, and agent presence.</CardDescription>
</CardHeader>
<CardContent>
<LiveWaveform
processing={!!room}
active={false}
height={32}
barWidth={2}
barGap={3}
className="mb-4 px-3 py-2 text-muted-foreground"
/>
<div className="flex flex-col gap-3">
<StatusLine label="LiveKit" value={room ? 'connected' : 'offline'} />
<StatusLine label="Screen" value={sharing ? 'published' : 'not shared'} />
<StatusLine label="Audio" value={playingBeat ? 'publishing' : 'ready'} />
<StatusLine label="Agent" value={podmanPresent ? 'watching' : 'waiting'} />
</div>
</CardContent>
</Card>
</aside>
</main>
<div ref={audioRef} className="hidden" />
</div>
</SidebarInset>
<ActivitySidebar
side="right"
title="Team stream"
collapsedLabel="Team"
testId="team-stream-sidebar"
description="Everyone else in this pod, merged into one realtime feed."
events={activity.team}
connected={activity.connected}
open={rightStreamOpen}
onToggle={() => setRightStreamOpen((open) => !open)}
emptyTitle="No teammate signal yet"
emptyDescription="Waiting for other members' screen, git, or collision events."
/>
</SidebarProvider>
</SidebarProvider>
</SidebarProvider>
</div>
);
}
@@ -602,6 +607,7 @@ function ActivitySidebar({
side,
title,
collapsedLabel,
testId,
description,
events,
connected,
@@ -613,6 +619,7 @@ function ActivitySidebar({
side: 'left' | 'right';
title: string;
collapsedLabel: string;
testId: string;
description: string;
events: PodActivityEvent[];
connected: boolean;
@@ -623,9 +630,15 @@ function ActivitySidebar({
}) {
const ToggleIcon = side === 'left' ? PanelLeftIcon : PanelRightIcon;
const critical = events.filter((event) => event.severity === 'critical').length;
const toggleTestId = testId.replace('-sidebar', '-toggle');
return (
<Sidebar side={side} collapsible="icon" className="border-border/80 bg-sidebar">
<Sidebar
side={side}
collapsible="icon"
className="border-border/80 bg-sidebar md:!top-[var(--pod-topbar-height)] md:!h-[calc(100svh-var(--pod-topbar-height))]"
data-testid={testId}
>
<SidebarHeader className="border-b px-3 py-3">
<div className="flex min-w-0 items-start justify-between gap-2 group-data-[collapsible=icon]:hidden">
<div className="min-w-0">
@@ -636,10 +649,14 @@ function ActivitySidebar({
</Badge>
</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">{description}</p>
<div className="mt-3 grid grid-cols-2 gap-2">
<StreamStat label="Events" value={events.length} />
<StreamStat label="Critical" value={critical} />
</div>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon-sm" onClick={onToggle}>
<Button variant="ghost" size="icon-sm" onClick={onToggle} data-testid={toggleTestId}>
<ToggleIcon />
<span className="sr-only">{open ? `Collapse ${title}` : `Expand ${title}`}</span>
</Button>
@@ -651,10 +668,17 @@ function ActivitySidebar({
<button
type="button"
onClick={onToggle}
className="hidden min-h-24 w-full flex-col items-center justify-center gap-2 rounded-md border bg-background text-muted-foreground transition hover:bg-muted group-data-[collapsible=icon]:flex"
data-testid={toggleTestId}
className="hidden min-h-32 w-full flex-col items-center justify-center gap-2 rounded-md border bg-background text-muted-foreground transition hover:bg-muted group-data-[collapsible=icon]:flex"
aria-label={`Expand ${title}`}
>
<ToggleIcon className="size-4" />
<span
className={cn(
'size-2 rounded-full',
connected ? 'bg-chart-2' : 'bg-muted-foreground/35',
)}
/>
<span className="text-[0.68rem] font-medium [writing-mode:vertical-rl]">
{collapsedLabel}
</span>
@@ -664,12 +688,17 @@ function ActivitySidebar({
>
{events.length}
</Badge>
{critical > 0 && (
<Badge variant="destructive" className="rounded-md px-1.5 py-0">
{critical}
</Badge>
)}
</button>
</SidebarHeader>
<SidebarContent className="px-3 py-3">
<div className="group-data-[collapsible=icon]:hidden">
{events.length ? (
<div className="flex max-h-[calc(100svh-9rem)] flex-col gap-2 overflow-y-auto pr-1">
<div className="flex max-h-[calc(100svh-9rem)] flex-col gap-3 overflow-y-auto pr-1">
{events.map((event) => (
<ActivityItem key={event.id} event={event} />
))}
@@ -718,12 +747,22 @@ function ActivitySidebar({
);
}
function StreamStat({ label, value }: { label: string; value: number }) {
return (
<div className="rounded-md border bg-background px-2 py-1.5">
<p className="text-[0.65rem] font-medium uppercase text-muted-foreground">{label}</p>
<p className="mt-0.5 font-mono text-sm font-medium">{value}</p>
</div>
);
}
function ActivityItem({ event }: { event: PodActivityEvent }) {
const Icon = activityIcon(event.kind);
const metadata = activityMetadata(event);
return (
<div
className={cn(
'grid min-h-20 grid-cols-[2.25rem_minmax(0,1fr)] gap-3 rounded-lg border bg-card px-3 py-3 shadow-sm',
'grid min-h-20 shrink-0 grid-cols-[2.25rem_minmax(0,1fr)] gap-3 overflow-hidden rounded-lg border bg-card px-3.5 py-3.5 shadow-sm',
event.severity === 'critical' && 'border-destructive/45 bg-destructive/5',
event.severity === 'warn' && 'border-chart-3/45 bg-chart-3/5',
)}
@@ -738,45 +777,82 @@ function ActivityItem({ event }: { event: PodActivityEvent }) {
<Icon className="size-4" />
</div>
<div className="min-w-0">
<div className="flex min-w-0 items-start justify-between gap-3">
<p className="min-w-0 truncate text-sm font-medium">{event.title}</p>
<time className="shrink-0 text-xs text-muted-foreground">{timeLabel(event.at)}</time>
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-start gap-2">
<p className="line-clamp-2 min-w-0 break-words text-sm font-medium leading-5">
{event.title}
</p>
<time className="shrink-0 whitespace-nowrap text-xs text-muted-foreground">
{timeLabel(event.at)}
</time>
</div>
{event.detail && (
<p className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
<p className="mt-1 line-clamp-2 break-words text-xs leading-5 text-muted-foreground">
{event.detail}
</p>
)}
<div className="mt-2 flex min-w-0 flex-wrap items-center gap-1.5">
{event.actors?.length ? (
event.actors.map((actor) => (
<Badge
key={actor}
variant="secondary"
className="rounded-md px-1.5 py-0 text-[0.68rem]"
>
{actor}
</Badge>
))
) : event.actor ? (
<Badge variant="secondary" className="rounded-md px-1.5 py-0 text-[0.68rem]">
{event.actor}
</Badge>
) : null}
<Badge variant="outline" className="rounded-md px-1.5 py-0 text-[0.68rem]">
{event.source}
</Badge>
{event.file && (
<Badge variant="outline" className="max-w-full rounded-md px-1.5 py-0 text-[0.68rem]">
<span className="truncate">{event.file}</span>
</Badge>
)}
{metadata.map((item, index) => (
<ActivityBadge
key={`${item.variant}-${item.label}-${index}`}
variant={item.variant}
title={item.title}
>
{item.label}
</ActivityBadge>
))}
</div>
</div>
</div>
);
}
function activityMetadata(event: PodActivityEvent): {
label: string;
title?: string;
variant: 'secondary' | 'outline';
}[] {
const actors = event.actors?.length ? event.actors : event.actor ? [event.actor] : [];
const visibleActors = actors.slice(0, 2).map((actor) => ({
label: actor,
variant: 'secondary' as const,
}));
const hiddenActors = actors.length - visibleActors.length;
return [
...visibleActors,
...(hiddenActors > 0
? [
{
label: `+${hiddenActors}`,
title: actors.slice(2).join(', '),
variant: 'secondary' as const,
},
]
: []),
...(event.file ? [{ label: event.file, variant: 'outline' as const }] : []),
{ label: event.source, variant: 'outline' as const },
];
}
function ActivityBadge({
variant,
children,
title,
}: {
variant: 'secondary' | 'outline';
children: string;
title?: string;
}) {
return (
<Badge
variant={variant}
title={title ?? children}
className="min-w-0 max-w-[9rem] rounded-md px-1.5 py-0 text-[0.68rem] leading-4"
>
<span className="truncate">{children}</span>
</Badge>
);
}
function activityIcon(kind: PodActivityKind) {
switch (kind) {
case 'git':