fix(player): the YouTube player keeps its size on a short window and when maximised

Two layout bugs since the player grew a side panel, both reported from prod.

Maximised (F) on a wide window, the video was a thin line across the screen: the side-panel layout
aligns the stage `self-start`, and Chrome applies a flex item's self-alignment to an absolutely
positioned box too, so the fixed stage took its content's height — 0 px. The maximised stage now
stretches between its insets whatever the layout asks of it.

On a short window under 1280 px (a laptop at 100%, a 1080p screen at 175%) only the side strips were
left: the card narrows so the chrome under the video fits, but that chrome grows as the card narrows
(its text wraps), and the loop ran the card down to 2 px. The video now never goes narrower than
36rem to make room; past that the card scrolls.

Measured before/after (localdev, real YouTube iframe): 1097×520 open 0×0 → 610×343; 1920×911 maximised
1920×0 → 1920×911; 1440×760 and 1366×650 maximised fill the window; leaving maximise restores the card.
Held by an e2e test at both sizes (red on the old code: 0 px wide, and 911 ≠ 0), green on Chrome,
WebKit and iPad.
This commit is contained in:
2026-10-05 23:40:13 +02:00
parent e7d2eb8cf6
commit 00886d92fe
4 changed files with 60 additions and 7 deletions
+11 -4
View File
@@ -104,6 +104,8 @@ function canScrollBy(el: HTMLElement | null, dir: -1 | 1): boolean {
// a rem width, so it grows with the text size; the video takes what is left.
const WIDE_PLAYER_PX = 1280;
const SIDE_PANEL = "20rem";
// The narrowest the video gets to make room for the chrome under it (see the card's maxWidth).
const MIN_VIDEO = "36rem";
// The meta line's items each lead with a "·" that parts them from what stands before: the channel name
// on its line. In the wide panel the meta has a line of its own, where the first "·" would lead nothing.
@@ -611,6 +613,10 @@ export default function PlayerModal({
onLoadVideo: loadVideo,
};
// The card's width budget (its maxWidth, below): the video's, and the side strips' gutters.
const videoWidth = `max(${MIN_VIDEO}, (100dvh - 3rem - ${below}px) * 16 / 9)`;
const gutters = hasQueue ? " + 7rem" : "";
// Portaled to <body>, like every other full-screen overlay here. A fixed, full-viewport modal
// must not render inside the page scroller: once that scroller is a stacking context (its
// edge-fade mask makes it one) this z-overlay is scoped inside it, and the nav rail (z-rail) and the
@@ -636,12 +642,13 @@ export default function PlayerModal({
// The design width (63rem with the side strips, else 56rem), narrowed so the 16:9 video plus the
// chrome under it fits the window's height less the backdrop's padding (`p-6` = 3rem).
// Wide: no design cap — the video is as wide as the window's height allows, plus the side panel.
// Never narrower than MIN_VIDEO: the chrome under the video grows as the card narrows (its text
// wraps), so on a short window fitting it was a loop that ran the card down to 2 px (a laptop
// at 100%, a 1080p screen at 175%). Past the floor the card scrolls instead.
style={{
maxWidth: wide
? `calc((100dvh - 3rem - ${below}px) * 16 / 9 + ${SIDE_PANEL}${hasQueue ? " + 7rem" : ""})`
: `min(${hasQueue ? "63rem" : "56rem"}, calc((100dvh - 3rem - ${below}px) * 16 / 9${
hasQueue ? " + 7rem" : ""
}))`,
? `calc(${videoWidth} + ${SIDE_PANEL}${gutters})`
: `min(${hasQueue ? "63rem" : "56rem"}, calc(${videoWidth}${gutters}))`,
}}
>
{hasQueue && (