From 866558e10082996e0f8c360e092b6e86b14935bc Mon Sep 17 00:00:00 2001 From: Yahya Alhinai Date: Sun, 28 Jun 2026 04:35:24 +0000 Subject: [PATCH] feat: stabilize pod sidebars layout --- frontend/src/components/PodView.tsx | 704 +++++++++++++++------------- scripts/verify-frontend.mjs | 89 ++++ 2 files changed, 479 insertions(+), 314 deletions(-) diff --git a/frontend/src/components/PodView.tsx b/frontend/src/components/PodView.tsx index e056cda..07938cb 100644 --- a/frontend/src/components/PodView.tsx +++ b/frontend/src/components/PodView.tsx @@ -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 ( - - setLeftStreamOpen((open) => !open)} - emptyTitle="No personal signal yet" - emptyDescription="Start the local git watcher or share your IDE screen to populate this lane." - /> +
+
+
+ + + + + Leave pod + +
+
+

{team.name}

+ + {room ? 'live' : 'local'} + +
+

{team.repo}

+
+
+ +
+ + +
+
+
+ - -
-
-
- - - - - - {leftStreamOpen ? 'Collapse my stream' : 'Expand my stream'} - - - - - - - Leave pod - -
-
-

{team.name}

- - {room ? 'live' : 'local'} - -
-

{team.repo}

-
-
- -
- - - -
-
- - {devMode && ( - - - Local mode - LiveKit is not configured for this session. - - )} - - {note && ( - - - Room notice - {note} - - )} - - {room && audioBlocked && ( - -
- -
- Sound is off - - Click to hear PodMan's voice alerts. - -
-
- -
- )} - -
-
-
- - - -
- - - - Room state - People and media currently visible to PodMan. - - - {participants.length === 0 ? ( - - - - - - Connecting - Waiting for LiveKit room state. - - - ) : ( -
- {participants.map((p) => ( - - ))} -
- )} -
- -

- Roster: {team.members.join(', ') || 'No saved members'} -

-
-
- - {activity.error && ( -

{activity.error}

- )} -
- - -
-
-
- 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." /> + + +
+ {devMode && ( + + + Local mode + LiveKit is not configured for this session. + + )} + + {note && ( + + + Room notice + {note} + + )} + + {room && audioBlocked && ( + +
+ +
+ Sound is off + Click to hear PodMan's voice alerts. +
+
+ +
+ )} + +
+
+
+ + + +
+ + + + Room state + + People and media currently visible to PodMan. + + + + {participants.length === 0 ? ( + + + + + + Connecting + Waiting for LiveKit room state. + + + ) : ( +
+ {participants.map((p) => ( + + ))} +
+ )} +
+ +

+ Roster: {team.members.join(', ') || 'No saved members'} +

+
+
+ + {activity.error && ( +

{activity.error}

+ )} +
+ + +
+
+
+ + setRightStreamOpen((open) => !open)} + emptyTitle="No teammate signal yet" + emptyDescription="Waiting for other members' screen, git, or collision events." + /> + - +
); } @@ -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 ( - +
@@ -636,10 +649,14 @@ function ActivitySidebar({

{description}

+
+ + +
- @@ -651,10 +668,17 @@ function ActivitySidebar({
{events.length ? ( -
+
{events.map((event) => ( ))} @@ -718,12 +747,22 @@ function ActivitySidebar({ ); } +function StreamStat({ label, value }: { label: string; value: number }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + function ActivityItem({ event }: { event: PodActivityEvent }) { const Icon = activityIcon(event.kind); + const metadata = activityMetadata(event); return (
-
-

{event.title}

- +
+

+ {event.title} +

+
{event.detail && ( -

+

{event.detail}

)}
- {event.actors?.length ? ( - event.actors.map((actor) => ( - - {actor} - - )) - ) : event.actor ? ( - - {event.actor} - - ) : null} - - {event.source} - - {event.file && ( - - {event.file} - - )} + {metadata.map((item, index) => ( + + {item.label} + + ))}
); } +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 ( + + {children} + + ); +} + function activityIcon(kind: PodActivityKind) { switch (kind) { case 'git': diff --git a/scripts/verify-frontend.mjs b/scripts/verify-frontend.mjs index 964dda4..2586dd1 100644 --- a/scripts/verify-frontend.mjs +++ b/scripts/verify-frontend.mjs @@ -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 =