feat: stabilize pod sidebars layout
This commit is contained in:
@@ -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,61 +257,16 @@ 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."
|
||||
/>
|
||||
<SidebarProvider
|
||||
open={rightStreamOpen}
|
||||
onOpenChange={setRightStreamOpen}
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': '24rem',
|
||||
'--sidebar-width-icon': '4rem',
|
||||
} as CSSProperties
|
||||
}
|
||||
className="min-h-screen flex-1"
|
||||
<header
|
||||
data-testid="pod-topbar"
|
||||
className="sticky top-0 z-30 border-b bg-background/90 backdrop-blur-xl"
|
||||
>
|
||||
<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="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={() => 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}>
|
||||
@@ -329,10 +288,6 @@ export function PodView({
|
||||
</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'}
|
||||
@@ -342,8 +297,54 @@ export function PodView({
|
||||
{sharing ? 'Stop sharing' : 'Share screen'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<SidebarProvider
|
||||
open={leftStreamOpen}
|
||||
onOpenChange={setLeftStreamOpen}
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': STREAM_SIDEBAR_WIDTH,
|
||||
'--sidebar-width-icon': STREAM_RAIL_WIDTH,
|
||||
minHeight: 'calc(100svh - var(--pod-topbar-height))',
|
||||
} as CSSProperties
|
||||
}
|
||||
className="bg-background text-foreground"
|
||||
>
|
||||
<ActivitySidebar
|
||||
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={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 />
|
||||
@@ -366,9 +367,7 @@ export function PodView({
|
||||
<Volume2Icon />
|
||||
<div>
|
||||
<AlertTitle>Sound is off</AlertTitle>
|
||||
<AlertDescription>
|
||||
Click to hear PodMan's voice alerts.
|
||||
</AlertDescription>
|
||||
<AlertDescription>Click to hear PodMan's voice alerts.</AlertDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => void enableSound()}>
|
||||
@@ -377,7 +376,7 @@ export function PodView({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<main className="grid flex-1 gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<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} />
|
||||
@@ -388,7 +387,9 @@ export function PodView({
|
||||
<Card className="flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Room state</CardTitle>
|
||||
<CardDescription>People and media currently visible to PodMan.</CardDescription>
|
||||
<CardDescription>
|
||||
People and media currently visible to PodMan.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{participants.length === 0 ? (
|
||||
@@ -425,7 +426,9 @@ export function PodView({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Intervention</CardTitle>
|
||||
<CardDescription>Card first, voice only for urgent escalation.</CardDescription>
|
||||
<CardDescription>
|
||||
Card first, voice only for urgent escalation.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant={active ? 'default' : 'secondary'} className="rounded-md">
|
||||
{active ? 'active' : 'clear'}
|
||||
@@ -537,6 +540,7 @@ export function PodView({
|
||||
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}
|
||||
@@ -547,6 +551,7 @@ export function PodView({
|
||||
/>
|
||||
</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]"
|
||||
{metadata.map((item, index) => (
|
||||
<ActivityBadge
|
||||
key={`${item.variant}-${item.label}-${index}`}
|
||||
variant={item.variant}
|
||||
title={item.title}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
{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':
|
||||
|
||||
@@ -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;
|
||||
if (shouldStartPreview) {
|
||||
preview = spawn(
|
||||
@@ -267,6 +288,74 @@ try {
|
||||
}
|
||||
await page.getByRole('heading', { name: 'My 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 hasPodView =
|
||||
|
||||
Reference in New Issue
Block a user