fix(embeds): players fill the card and keep their aspect ratio (#228)

Two sizing bugs, both visible the moment a video starts:

1. The video/TikTok embed columns had no width. UrlPreview is a flex ROW,
   so they shrink-to-fit: the facade's <img> supplied the width, but the
   player <iframe> is absolutely positioned and supplies none, so pressing
   play collapsed the whole embed to the iframe's ~200px intrinsic size
   (measured 606x341 -> 204x115 in a 608px card). Both columns are now
   width: 100%.
2. EmbedMediaPortrait's 9:16 came from a 177.78% padding-top, but a
   padding percentage resolves against the CONTAINING BLOCK's width, not
   the element's — so inside a wide card a Short/TikTok rendered
   300x1077 instead of 300x533. Capped with min() so it is exact at
   >= 300px and still correct on narrower phones.

Audited every provider before/after play at 1300 px, 500 px and Pixel 7:
YouTube, Vimeo, Twitch 16:9 (0.56); Shorts, TikTok 9:16 (1.78); Spotify
152 px and SoundCloud 166 px fixed-height — all stable across play.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-23 15:37:57 -04:00
co-authored by Claude Opus 5
parent e393c45f50
commit 08abf5aec8
2 changed files with 16 additions and 6 deletions
@@ -246,11 +246,15 @@ export const EmbedMediaPortrait = style([
DefaultReset, DefaultReset,
{ {
position: 'relative', position: 'relative',
width: toRem(300), // [Gitea #228] `min()` on both axes so the 9:16 ratio holds whatever the
maxWidth: '100%', // parent's width is. A padding-top percentage resolves against the
// 9:16 via the padding-top hack (177.78%) — see EmbedMediaLandscape. // CONTAINING BLOCK's width, not the element's, so a bare `177.78%` inside a
// wide card made the box ~2x too tall (606px card → 1077px instead of 533).
// Capped at the pixel height of a 300px-wide 9:16 box, it is exact when the
// parent is at least 300px and falls back to the percentage below that.
width: `min(${toRem(300)}, 100%)`,
height: 0, height: 0,
paddingTop: '177.78%', paddingTop: `min(${toRem(533)}, 177.78%)`,
margin: '0 auto', margin: '0 auto',
overflow: 'hidden', overflow: 'hidden',
backgroundColor: color.Surface.Container, backgroundColor: color.Surface.Container,
@@ -1203,7 +1203,11 @@ function MediaEmbedCard({
}; };
return ( return (
<Box direction="Column"> // [Gitea #228] `width: 100%` — UrlPreview is a flex ROW, so this column
// shrink-to-fits its content. The facade's <img> supplied that width, but
// the player <iframe> is absolutely positioned and contributes none, so on
// play the whole embed collapsed to the iframe's ~200px intrinsic size.
<Box direction="Column" style={{ width: '100%', minWidth: 0 }}>
{playing && rich ? ( {playing && rich ? (
// Rich post embeds (Instagram/Reddit) are variable-height and self-size. // Rich post embeds (Instagram/Reddit) are variable-height and self-size.
<iframe <iframe
@@ -1380,7 +1384,9 @@ function TikTokEmbedCard({ url, prev }: { url: string; prev: IPreviewUrlResponse
); );
return ( return (
<Box direction="Column"> // [Gitea #228] See the video embed above: this column must fill the card's
// flex row, not shrink to the iframe's intrinsic width.
<Box direction="Column" style={{ width: '100%', minWidth: 0 }}>
<div ref={mediaRef} className={previewCss.EmbedMediaPortrait}> <div ref={mediaRef} className={previewCss.EmbedMediaPortrait}>
{playing && videoId ? ( {playing && videoId ? (
<iframe <iframe