fix(call): tile sizing polish + cinema-mode chrome suppression + spec/plan docs
Equal-grid cells no longer set aspect-video — on wide chat panels this forced cell height = width × 9/16 (~400px on a 700px panel) which pushed the row past the section's max-h and ate the controls bar below. n>=2 cells now fill grid tracks normally via auto-rows-fr; the solo case (n=1) keeps a 16:9 silhouette via aspect-video + max-w + justify-self- center so a single-user-alone-calling view doesn't stretch into a full-width slab. Same change applied to the fullscreen-grid path plus +16px bottom-padding (pb-28) so audio-only avatars' name chip clears the floating controls bar. Docked stage strip thumbs (focus + bento) switch from aspect-video shrink-0 to flex-1 min-w-[200px] max-w-[460px] so 2-3 thumbs share the row width evenly under the share above, instead of clinging to the left edge with dead space to the right. Fullscreen-cinema strip keeps the small aspect-video thumbs the user explicitly approved. ScreenShareViewer gains a hideFullscreenToggle prop; cinema mode passes it via a new `cinema` prop on TileRender so the in-share fullscreen icon doesn't visually collide with FullscreenCall's strip-hidden toggle at the same top-right corner. docs/superpowers/specs + plans for the Discord-style tile handling workstream are committed alongside the implementation that completed it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -921,6 +921,7 @@ function TileRender({
|
||||
conversationMembers,
|
||||
size,
|
||||
focused,
|
||||
cinema,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onContextMenu,
|
||||
@@ -932,6 +933,10 @@ function TileRender({
|
||||
conversationMembers: StageProps['conversationMembers'];
|
||||
size?: 'default' | 'small';
|
||||
focused?: boolean;
|
||||
/** True when rendered inside FullscreenCall's big-tile slot. Drives
|
||||
* chrome-suppression on the inner ScreenShareViewer so its toggle
|
||||
* doesn't visually collide with the cinema-mode strip-hidden button. */
|
||||
cinema?: boolean;
|
||||
onClick?: () => void;
|
||||
onDoubleClick?: () => void;
|
||||
onContextMenu?: (e: React.MouseEvent) => void;
|
||||
@@ -954,6 +959,7 @@ function TileRender({
|
||||
share={share}
|
||||
avatarUrl={member?.profile?.avatarUrl ?? tile.avatarUrl}
|
||||
displayName={member?.profile?.displayName ?? tile.displayName}
|
||||
hideFullscreenToggle={cinema === true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -1017,7 +1023,11 @@ function CallStage({
|
||||
{others.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="aspect-video h-full shrink-0 [&>div]:h-full [&>div]:w-full"
|
||||
// Docked strip: thumbs grow to share the row width evenly
|
||||
// (flex-1) but stay bounded so 1-2 tiles don't stretch into
|
||||
// 2:1 panoramas. Max-w cap keeps the visual rhythm aligned
|
||||
// with the share above; min-w keeps them readable when many.
|
||||
className="h-full flex-1 min-w-[200px] max-w-[460px] [&>div]:h-full [&>div]:w-full"
|
||||
>
|
||||
<TileRender
|
||||
tile={p}
|
||||
@@ -1076,7 +1086,8 @@ function CallStage({
|
||||
{webcams.map((w) => (
|
||||
<div
|
||||
key={w.id}
|
||||
className="aspect-video h-full shrink-0 [&>div]:h-full [&>div]:w-full"
|
||||
// Same docked-strip sizing as the focus branch above.
|
||||
className="h-full flex-1 min-w-[200px] max-w-[460px] [&>div]:h-full [&>div]:w-full"
|
||||
>
|
||||
<TileRender
|
||||
tile={w}
|
||||
@@ -1100,17 +1111,28 @@ function CallStage({
|
||||
|
||||
// Grid (equal-grid fallthrough)
|
||||
const gridClass = gridColsFor(tiles.length);
|
||||
// aspect-video on every cell pushed the row past the section height on
|
||||
// wide chat panels — a single cell at full width forced height = width
|
||||
// × 9/16 (~400px on a 700px panel), which clipped the controls bar
|
||||
// below. Let cells fill grid tracks normally for n>=2, and only enforce
|
||||
// a 16:9 silhouette (capped width, centred) for the solo-user case.
|
||||
const isSolo = tiles.length === 1;
|
||||
return (
|
||||
<div className={'min-h-0 flex-1 p-3 ' + (compact ? '' : 'p-4')}>
|
||||
<div
|
||||
className={
|
||||
'grid h-full max-h-full gap-2 place-content-center ' + gridClass
|
||||
'grid h-full max-h-full gap-2 auto-rows-fr place-content-center ' +
|
||||
gridClass
|
||||
}
|
||||
>
|
||||
{tiles.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="aspect-video min-h-0 w-full [&>div]:h-full [&>div]:w-full"
|
||||
className={
|
||||
isSolo
|
||||
? 'aspect-video w-full max-w-[480px] justify-self-center [&>div]:h-full [&>div]:w-full'
|
||||
: 'min-h-0 min-w-0 [&>div]:h-full [&>div]:w-full'
|
||||
}
|
||||
>
|
||||
<TileRender
|
||||
tile={p}
|
||||
@@ -1301,9 +1323,12 @@ function FullscreenCall({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[60] flex flex-col overflow-hidden bg-surface">
|
||||
{/* Content area. pb-24 reserves ~96px space at the bottom for the
|
||||
floating controls bar so tiles never sit behind it. */}
|
||||
<div className="relative flex min-h-0 flex-1 flex-col pb-24">
|
||||
{/* Content area. pb-28 reserves ~112px space at the bottom for the
|
||||
floating controls bar plus extra clearance so the tiles' bottom
|
||||
name-chip (positioned `bottom-2` inside each tile) doesn't sit
|
||||
directly underneath the controls — pb-24 was tight enough that
|
||||
on wide screens with audio-only avatars the chip got eclipsed. */}
|
||||
<div className="relative flex min-h-0 flex-1 flex-col pb-28">
|
||||
{hasFocus ? (
|
||||
<>
|
||||
<div
|
||||
@@ -1319,6 +1344,7 @@ function FullscreenCall({
|
||||
remoteScreenShares={remoteScreenShares}
|
||||
conversationMembers={conversationMembers}
|
||||
focused
|
||||
cinema
|
||||
{...(onTileContextMenu
|
||||
? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(speaker!, e) }
|
||||
: {})}
|
||||
@@ -1394,24 +1420,40 @@ function FullscreenCall({
|
||||
<div className="min-h-0 flex-1 p-4">
|
||||
<div
|
||||
className={
|
||||
'grid h-full max-h-full gap-2 place-content-center ' + gridClass
|
||||
'grid h-full max-h-full gap-2 auto-rows-fr place-content-center ' +
|
||||
gridClass
|
||||
}
|
||||
>
|
||||
{visibleTiles.map((p) => (
|
||||
<div key={p.id} className="aspect-video min-h-0 w-full [&>div]:h-full [&>div]:w-full">
|
||||
<TileRender
|
||||
tile={p}
|
||||
activeSpeakers={activeSpeakers}
|
||||
e2ee={e2ee}
|
||||
remoteScreenShares={remoteScreenShares}
|
||||
conversationMembers={conversationMembers}
|
||||
onClick={() => onFocusTile(p.id)}
|
||||
{...(onTileContextMenu
|
||||
? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(p, e) }
|
||||
: {})}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{visibleTiles.map((p) => {
|
||||
const isSolo = visibleTiles.length === 1;
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
// Same fix as docked equal-grid: aspect-video w-full on
|
||||
// wide screens pushed cell height to ~ width × 9/16,
|
||||
// which dragged the tile's bottom name-chip down behind
|
||||
// the floating controls bar. For n>=2 fill the grid
|
||||
// tracks normally; for solo, cap width + centre.
|
||||
className={
|
||||
isSolo
|
||||
? 'aspect-video w-full max-w-[720px] justify-self-center [&>div]:h-full [&>div]:w-full'
|
||||
: 'min-h-0 min-w-0 [&>div]:h-full [&>div]:w-full'
|
||||
}
|
||||
>
|
||||
<TileRender
|
||||
tile={p}
|
||||
activeSpeakers={activeSpeakers}
|
||||
e2ee={e2ee}
|
||||
remoteScreenShares={remoteScreenShares}
|
||||
conversationMembers={conversationMembers}
|
||||
onClick={() => onFocusTile(p.id)}
|
||||
{...(onTileContextMenu
|
||||
? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(p, e) }
|
||||
: {})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{pageCount > 1 && (
|
||||
<div className="mt-2 flex items-center justify-center gap-3 text-xs text-fg-muted">
|
||||
|
||||
@@ -9,6 +9,11 @@ interface ScreenShareViewerProps {
|
||||
share: RemoteScreenShare;
|
||||
avatarUrl: string | null;
|
||||
displayName: string;
|
||||
/** Suppress the in-share fullscreen toggle button. Used inside cinema
|
||||
* mode where (a) the window is already OS-fullscreen and (b) the
|
||||
* toggle collides visually with FullscreenCall's strip-hidden button
|
||||
* at the same top-right corner. */
|
||||
hideFullscreenToggle?: boolean;
|
||||
}
|
||||
|
||||
// Click-to-watch gate, live <video> attach/detach, native fullscreen toggle.
|
||||
@@ -21,6 +26,7 @@ export function ScreenShareViewer({
|
||||
share,
|
||||
avatarUrl,
|
||||
displayName,
|
||||
hideFullscreenToggle = false,
|
||||
}: ScreenShareViewerProps) {
|
||||
const { t } = useTranslation(['app']);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
@@ -76,7 +82,7 @@ export function ScreenShareViewer({
|
||||
defaultValue: displayName + ' teilt den Bildschirm',
|
||||
})}
|
||||
</span>
|
||||
{watching && (
|
||||
{watching && !hideFullscreenToggle && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleFullscreen}
|
||||
|
||||
Reference in New Issue
Block a user