feat: stabilize pod sidebars layout
This commit is contained in:
+390
-314
@@ -55,6 +55,10 @@ import {
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { cn } from '@/lib/utils';
|
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 {
|
interface PInfo {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -253,300 +257,301 @@ export function PodView({
|
|||||||
const podmanPresent = participants.some((p) => p.name.toLowerCase() === 'podman');
|
const podmanPresent = participants.some((p) => p.name.toLowerCase() === 'podman');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<div
|
||||||
open={leftStreamOpen}
|
style={{ '--pod-topbar-height': POD_TOP_BAR_HEIGHT } as CSSProperties}
|
||||||
onOpenChange={setLeftStreamOpen}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
'--sidebar-width': '23rem',
|
|
||||||
'--sidebar-width-icon': '4rem',
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
className="min-h-screen bg-background text-foreground"
|
className="min-h-screen bg-background text-foreground"
|
||||||
>
|
>
|
||||||
<ActivitySidebar
|
<header
|
||||||
side="left"
|
data-testid="pod-topbar"
|
||||||
title="My stream"
|
className="sticky top-0 z-30 border-b bg-background/90 backdrop-blur-xl"
|
||||||
collapsedLabel="Mine"
|
>
|
||||||
description={`${me}'s live screen, git, intervention, and conflict log.`}
|
<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">
|
||||||
events={activity.mine}
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
connected={activity.connected}
|
<Tooltip>
|
||||||
open={leftStreamOpen}
|
<TooltipTrigger asChild>
|
||||||
onToggle={() => setLeftStreamOpen((open) => !open)}
|
<Button variant="ghost" size="icon" onClick={onLeave}>
|
||||||
emptyTitle="No personal signal yet"
|
<ArrowLeftIcon />
|
||||||
emptyDescription="Start the local git watcher or share your IDE screen to populate this lane."
|
<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
|
<SidebarProvider
|
||||||
open={rightStreamOpen}
|
open={leftStreamOpen}
|
||||||
onOpenChange={setRightStreamOpen}
|
onOpenChange={setLeftStreamOpen}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--sidebar-width': '24rem',
|
'--sidebar-width': STREAM_SIDEBAR_WIDTH,
|
||||||
'--sidebar-width-icon': '4rem',
|
'--sidebar-width-icon': STREAM_RAIL_WIDTH,
|
||||||
|
minHeight: 'calc(100svh - var(--pod-topbar-height))',
|
||||||
} as CSSProperties
|
} 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'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
|
<ActivitySidebar
|
||||||
side="right"
|
side="left"
|
||||||
title="Team stream"
|
title="My stream"
|
||||||
collapsedLabel="Team"
|
collapsedLabel="Mine"
|
||||||
description="Everyone else in this pod, merged into one realtime feed."
|
testId="my-stream-sidebar"
|
||||||
events={activity.team}
|
description={`${me}'s live screen, git, intervention, and conflict log.`}
|
||||||
|
events={activity.mine}
|
||||||
connected={activity.connected}
|
connected={activity.connected}
|
||||||
open={rightStreamOpen}
|
open={leftStreamOpen}
|
||||||
onToggle={() => setRightStreamOpen((open) => !open)}
|
onToggle={() => setLeftStreamOpen((open) => !open)}
|
||||||
emptyTitle="No teammate signal yet"
|
emptyTitle="No personal signal yet"
|
||||||
emptyDescription="Waiting for other members' screen, git, or collision events."
|
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'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>
|
||||||
</SidebarProvider>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,6 +607,7 @@ function ActivitySidebar({
|
|||||||
side,
|
side,
|
||||||
title,
|
title,
|
||||||
collapsedLabel,
|
collapsedLabel,
|
||||||
|
testId,
|
||||||
description,
|
description,
|
||||||
events,
|
events,
|
||||||
connected,
|
connected,
|
||||||
@@ -613,6 +619,7 @@ function ActivitySidebar({
|
|||||||
side: 'left' | 'right';
|
side: 'left' | 'right';
|
||||||
title: string;
|
title: string;
|
||||||
collapsedLabel: string;
|
collapsedLabel: string;
|
||||||
|
testId: string;
|
||||||
description: string;
|
description: string;
|
||||||
events: PodActivityEvent[];
|
events: PodActivityEvent[];
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
@@ -623,9 +630,15 @@ function ActivitySidebar({
|
|||||||
}) {
|
}) {
|
||||||
const ToggleIcon = side === 'left' ? PanelLeftIcon : PanelRightIcon;
|
const ToggleIcon = side === 'left' ? PanelLeftIcon : PanelRightIcon;
|
||||||
const critical = events.filter((event) => event.severity === 'critical').length;
|
const critical = events.filter((event) => event.severity === 'critical').length;
|
||||||
|
const toggleTestId = testId.replace('-sidebar', '-toggle');
|
||||||
|
|
||||||
return (
|
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">
|
<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="flex min-w-0 items-start justify-between gap-2 group-data-[collapsible=icon]:hidden">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@@ -636,10 +649,14 @@ function ActivitySidebar({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">{description}</p>
|
<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>
|
</div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon-sm" onClick={onToggle}>
|
<Button variant="ghost" size="icon-sm" onClick={onToggle} data-testid={toggleTestId}>
|
||||||
<ToggleIcon />
|
<ToggleIcon />
|
||||||
<span className="sr-only">{open ? `Collapse ${title}` : `Expand ${title}`}</span>
|
<span className="sr-only">{open ? `Collapse ${title}` : `Expand ${title}`}</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -651,10 +668,17 @@ function ActivitySidebar({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onToggle}
|
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}`}
|
aria-label={`Expand ${title}`}
|
||||||
>
|
>
|
||||||
<ToggleIcon className="size-4" />
|
<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]">
|
<span className="text-[0.68rem] font-medium [writing-mode:vertical-rl]">
|
||||||
{collapsedLabel}
|
{collapsedLabel}
|
||||||
</span>
|
</span>
|
||||||
@@ -664,12 +688,17 @@ function ActivitySidebar({
|
|||||||
>
|
>
|
||||||
{events.length}
|
{events.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{critical > 0 && (
|
||||||
|
<Badge variant="destructive" className="rounded-md px-1.5 py-0">
|
||||||
|
{critical}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent className="px-3 py-3">
|
<SidebarContent className="px-3 py-3">
|
||||||
<div className="group-data-[collapsible=icon]:hidden">
|
<div className="group-data-[collapsible=icon]:hidden">
|
||||||
{events.length ? (
|
{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) => (
|
{events.map((event) => (
|
||||||
<ActivityItem key={event.id} event={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 }) {
|
function ActivityItem({ event }: { event: PodActivityEvent }) {
|
||||||
const Icon = activityIcon(event.kind);
|
const Icon = activityIcon(event.kind);
|
||||||
|
const metadata = activityMetadata(event);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 === 'critical' && 'border-destructive/45 bg-destructive/5',
|
||||||
event.severity === 'warn' && 'border-chart-3/45 bg-chart-3/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" />
|
<Icon className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-start gap-2">
|
||||||
<p className="min-w-0 truncate text-sm font-medium">{event.title}</p>
|
<p className="line-clamp-2 min-w-0 break-words text-sm font-medium leading-5">
|
||||||
<time className="shrink-0 text-xs text-muted-foreground">{timeLabel(event.at)}</time>
|
{event.title}
|
||||||
|
</p>
|
||||||
|
<time className="shrink-0 whitespace-nowrap text-xs text-muted-foreground">
|
||||||
|
{timeLabel(event.at)}
|
||||||
|
</time>
|
||||||
</div>
|
</div>
|
||||||
{event.detail && (
|
{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}
|
{event.detail}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 flex min-w-0 flex-wrap items-center gap-1.5">
|
<div className="mt-2 flex min-w-0 flex-wrap items-center gap-1.5">
|
||||||
{event.actors?.length ? (
|
{metadata.map((item, index) => (
|
||||||
event.actors.map((actor) => (
|
<ActivityBadge
|
||||||
<Badge
|
key={`${item.variant}-${item.label}-${index}`}
|
||||||
key={actor}
|
variant={item.variant}
|
||||||
variant="secondary"
|
title={item.title}
|
||||||
className="rounded-md px-1.5 py-0 text-[0.68rem]"
|
>
|
||||||
>
|
{item.label}
|
||||||
{actor}
|
</ActivityBadge>
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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) {
|
function activityIcon(kind: PodActivityKind) {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'git':
|
case 'git':
|
||||||
|
|||||||
@@ -171,6 +171,27 @@ async function waitForPublishedScreenShare(roomName) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function boxOf(locator, name) {
|
||||||
|
const box = await locator.boundingBox();
|
||||||
|
if (!box) throw new Error(`${name} did not have a visible bounding box`);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNoOverlap(left, main, right, label) {
|
||||||
|
if (left.x + left.width > main.x + 2) {
|
||||||
|
throw new Error(`${label}: left sidebar overlaps main workspace`);
|
||||||
|
}
|
||||||
|
if (main.x + main.width > right.x + 2) {
|
||||||
|
throw new Error(`${label}: right sidebar overlaps main workspace`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertStableTopbar(before, after, label) {
|
||||||
|
if (Math.abs(before.x - after.x) > 2 || Math.abs(before.width - after.width) > 2) {
|
||||||
|
throw new Error(`${label}: top bar moved or resized with sidebar state`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let preview = null;
|
let preview = null;
|
||||||
if (shouldStartPreview) {
|
if (shouldStartPreview) {
|
||||||
preview = spawn(
|
preview = spawn(
|
||||||
@@ -267,6 +288,74 @@ try {
|
|||||||
}
|
}
|
||||||
await page.getByRole('heading', { name: 'My stream' }).waitFor({ timeout: 15_000 });
|
await page.getByRole('heading', { name: 'My stream' }).waitFor({ timeout: 15_000 });
|
||||||
await page.getByRole('heading', { name: 'Team stream' }).waitFor({ timeout: 15_000 });
|
await page.getByRole('heading', { name: 'Team stream' }).waitFor({ timeout: 15_000 });
|
||||||
|
const topbar = page.getByTestId('pod-topbar');
|
||||||
|
const mainWorkspace = page.getByTestId('pod-main-workspace');
|
||||||
|
const mySidebar = page.getByTestId('my-stream-sidebar');
|
||||||
|
const teamSidebar = page.getByTestId('team-stream-sidebar');
|
||||||
|
await topbar.waitFor({ timeout: 15_000 });
|
||||||
|
await mainWorkspace.waitFor({ timeout: 15_000 });
|
||||||
|
await mySidebar.waitFor({ timeout: 15_000 });
|
||||||
|
await teamSidebar.waitFor({ timeout: 15_000 });
|
||||||
|
if ((await topbar.getByRole('button', { name: /stream|team/i }).count()) > 0) {
|
||||||
|
throw new Error('pod top bar contains sidebar stream/team controls');
|
||||||
|
}
|
||||||
|
const expandedLayout = {
|
||||||
|
topbar: await boxOf(topbar, 'top bar expanded'),
|
||||||
|
main: await boxOf(mainWorkspace, 'main workspace expanded'),
|
||||||
|
left: await boxOf(mySidebar, 'my stream sidebar expanded'),
|
||||||
|
right: await boxOf(teamSidebar, 'team stream sidebar expanded'),
|
||||||
|
};
|
||||||
|
assertNoOverlap(
|
||||||
|
expandedLayout.left,
|
||||||
|
expandedLayout.main,
|
||||||
|
expandedLayout.right,
|
||||||
|
'expanded layout',
|
||||||
|
);
|
||||||
|
await page.locator('[data-testid="my-stream-toggle"]:visible').click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const leftCollapsedLayout = {
|
||||||
|
main: await boxOf(mainWorkspace, 'main workspace after left collapse'),
|
||||||
|
left: await boxOf(mySidebar, 'my stream sidebar collapsed'),
|
||||||
|
right: await boxOf(teamSidebar, 'team stream sidebar with left collapsed'),
|
||||||
|
topbar: await boxOf(topbar, 'top bar after left collapse'),
|
||||||
|
};
|
||||||
|
if (leftCollapsedLayout.left.width >= expandedLayout.left.width - 24) {
|
||||||
|
throw new Error('my stream sidebar did not collapse into a compact rail');
|
||||||
|
}
|
||||||
|
if (leftCollapsedLayout.main.width <= expandedLayout.main.width) {
|
||||||
|
throw new Error('main workspace did not expand after my stream collapsed');
|
||||||
|
}
|
||||||
|
assertNoOverlap(
|
||||||
|
leftCollapsedLayout.left,
|
||||||
|
leftCollapsedLayout.main,
|
||||||
|
leftCollapsedLayout.right,
|
||||||
|
'left collapsed layout',
|
||||||
|
);
|
||||||
|
assertStableTopbar(expandedLayout.topbar, leftCollapsedLayout.topbar, 'left collapsed layout');
|
||||||
|
await page.locator('[data-testid="team-stream-toggle"]:visible').click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const bothCollapsedLayout = {
|
||||||
|
main: await boxOf(mainWorkspace, 'main workspace after both collapse'),
|
||||||
|
left: await boxOf(mySidebar, 'my stream sidebar with both collapsed'),
|
||||||
|
right: await boxOf(teamSidebar, 'team stream sidebar collapsed'),
|
||||||
|
topbar: await boxOf(topbar, 'top bar after both collapse'),
|
||||||
|
};
|
||||||
|
if (bothCollapsedLayout.right.width >= expandedLayout.right.width - 24) {
|
||||||
|
throw new Error('team stream sidebar did not collapse into a compact rail');
|
||||||
|
}
|
||||||
|
if (bothCollapsedLayout.main.width <= leftCollapsedLayout.main.width) {
|
||||||
|
throw new Error('main workspace did not expand after team stream collapsed');
|
||||||
|
}
|
||||||
|
assertNoOverlap(
|
||||||
|
bothCollapsedLayout.left,
|
||||||
|
bothCollapsedLayout.main,
|
||||||
|
bothCollapsedLayout.right,
|
||||||
|
'both collapsed layout',
|
||||||
|
);
|
||||||
|
assertStableTopbar(expandedLayout.topbar, bothCollapsedLayout.topbar, 'both collapsed layout');
|
||||||
|
await page.locator('[data-testid="my-stream-toggle"]:visible').click();
|
||||||
|
await page.locator('[data-testid="team-stream-toggle"]:visible').click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
const joinedText = await page.locator('body').innerText();
|
const joinedText = await page.locator('body').innerText();
|
||||||
const hasPodView =
|
const hasPodView =
|
||||||
|
|||||||
Reference in New Issue
Block a user