Keep a post inside the window, and stop letterboxing it
CI / Typecheck, test, build (pull_request) Successful in 34s
CI / Typecheck, test, build (pull_request) Successful in 34s
On a desktop a portrait video came with black bars all the way around and sat partly below the fold, so you had to scroll to find the thing you had opened. The bars were the figure, not the video. It carried the media's aspect ratio, so at the full width of the column a 720x1280 reel forced a box 1204px tall inside a 982px window, and the video -- capped at 78vh -- floated in the middle of it with 438px of black above and below and 247px either side. The ratio now goes on the media itself, which sizes to its own proportions instead of being stretched to the column, and the background is the card's rather than black, so what space is left reads as page instead of as a border. The height cap is the window less the chrome around it. Deriving that from the layout is the obvious approach and does not work: the card is sized by its contents, so every percentage height inside resolves to auto and clamps nothing. Measured across the posts this serves, the header, caption and footer come to 270-350px, so the cap subtracts 350 and the whole post fits. Smaller than it might be on a short window, but a video can be full-screened and a post you have to hunt for cannot. Threads keep the looser cap and scroll, which a column of several posts was always going to do. Grid thumbnails were quietly broken and the measuring turned it up: the cell takes its height from an aspect ratio, which a percentage height will not resolve against, and `aspect-ratio: auto` asks for the image's own ratio rather than none, so a portrait thumbnail rendered at twice the height of its cell and spilled out. They fill their cell absolutely now. Verified at 1512x982, 1280x800, 1440x700 and 390x844: no letterboxing anywhere, and the media sits above the fold on every one. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BGkRmLfiWuJHx6tQ12EELY
This commit is contained in:
+64
-6
@@ -86,7 +86,9 @@ main { max-width: 680px; margin: 0 auto; }
|
|||||||
|
|
||||||
/* ---------- media ---------- */
|
/* ---------- media ---------- */
|
||||||
|
|
||||||
.media { border-block: 1px solid var(--line); background: #000; }
|
/* Card-coloured, not black: whatever space the media does not fill should
|
||||||
|
read as part of the page rather than as a border around it. */
|
||||||
|
.media { border-block: 1px solid var(--line); background: var(--card); }
|
||||||
.post__head + .media, .text + .media { border-top: 1px solid var(--line); }
|
.post__head + .media, .text + .media { border-top: 1px solid var(--line); }
|
||||||
|
|
||||||
.media__bar {
|
.media__bar {
|
||||||
@@ -135,14 +137,21 @@ main { max-width: 680px; margin: 0 auto; }
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sized by its own proportions rather than stretched to the column: a
|
||||||
|
portrait video filling the width is mostly empty space either side of it.
|
||||||
|
The cap keeps any single item within the window. */
|
||||||
.item img, .item video {
|
.item img, .item video {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: auto;
|
||||||
max-height: 78vh;
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
max-height: 70dvh;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
background: #000;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item__note {
|
.item__note {
|
||||||
@@ -162,9 +171,31 @@ main { max-width: 680px; margin: 0 auto; }
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media[data-view="grid"] .item { aspect-ratio: 1 / 1 !important; cursor: zoom-in; }
|
.media[data-view="grid"] .item {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
cursor: zoom-in;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Filled absolutely rather than with height: 100%. The cell's height comes
|
||||||
|
* from its aspect-ratio, which a percentage height will not resolve against,
|
||||||
|
* and `aspect-ratio: auto` means "use the image's own" rather than "ignore
|
||||||
|
* ratios" -- between them a portrait thumbnail ends up twice the height of
|
||||||
|
* its cell.
|
||||||
|
*/
|
||||||
.media[data-view="grid"] .item img,
|
.media[data-view="grid"] .item img,
|
||||||
.media[data-view="grid"] .item video { height: 100%; max-height: none; object-fit: cover; }
|
.media[data-view="grid"] .item video {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
min-height: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
aspect-ratio: auto !important;
|
||||||
|
}
|
||||||
.media[data-view="grid"] .item__note { display: none; }
|
.media[data-view="grid"] .item__note { display: none; }
|
||||||
|
|
||||||
/* ---------- footer ---------- */
|
/* ---------- footer ---------- */
|
||||||
@@ -272,3 +303,30 @@ main { max-width: 680px; margin: 0 auto; }
|
|||||||
without hiding the rest. */
|
without hiding the rest. */
|
||||||
background: color-mix(in srgb, var(--accent, #888) 7%, transparent);
|
background: color-mix(in srgb, var(--accent, #888) 7%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- fitting the window ---------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* On a large screen a single post should be readable without scrolling to
|
||||||
|
* find it -- a video especially, which you can full-screen if you want it
|
||||||
|
* bigger.
|
||||||
|
*
|
||||||
|
* The cap is the window less the chrome around the media: header, caption and
|
||||||
|
* footer come to roughly 270-350px on the posts this serves. Deriving it
|
||||||
|
* instead of subtracting a constant is the obvious thing to try and does not
|
||||||
|
* work: the card is sized by its contents, so a percentage height inside it
|
||||||
|
* resolves to auto and clamps nothing.
|
||||||
|
*
|
||||||
|
* A thread is a column of several posts and cannot fit whatever we do, so it
|
||||||
|
* keeps the looser cap above and scrolls.
|
||||||
|
*/
|
||||||
|
@media (min-width: 700px) and (min-height: 560px) {
|
||||||
|
.post:not([data-segments]) .item img,
|
||||||
|
.post:not([data-segments]) .item video {
|
||||||
|
max-height: calc(100vh - 350px);
|
||||||
|
max-height: calc(100dvh - 350px);
|
||||||
|
/* Below that the media would be too small to be worth showing; better to
|
||||||
|
let a very long caption scroll. */
|
||||||
|
min-height: 220px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+9
-2
@@ -4,6 +4,11 @@ import { html, raw, type Raw } from './html.ts';
|
|||||||
import { badge, layout, originalUrlBlock } from './layout.ts';
|
import { badge, layout, originalUrlBlock } from './layout.ts';
|
||||||
import { linkify } from './text.ts';
|
import { linkify } from './text.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Put the ratio on the media itself, never on the figure around it. On the
|
||||||
|
* figure, at the full width of the column, a portrait video forces a box
|
||||||
|
* taller than the window and the rest of the post gets pushed off screen.
|
||||||
|
*/
|
||||||
function aspect(item: Media): string {
|
function aspect(item: Media): string {
|
||||||
return item.width && item.height ? `aspect-ratio: ${item.width} / ${item.height};` : '';
|
return item.width && item.height ? `aspect-ratio: ${item.width} / ${item.height};` : '';
|
||||||
}
|
}
|
||||||
@@ -13,11 +18,12 @@ function renderItem(item: Media, index: number): Raw {
|
|||||||
|
|
||||||
if (item.kind === 'video') {
|
if (item.kind === 'video') {
|
||||||
const poster = item.poster ? proxyUrlFor(item.poster) : undefined;
|
const poster = item.poster ? proxyUrlFor(item.poster) : undefined;
|
||||||
return html`<figure class="item item--video" data-index="${index}" style="${aspect(item)}">
|
return html`<figure class="item item--video" data-index="${index}">
|
||||||
<video
|
<video
|
||||||
controls
|
controls
|
||||||
playsinline
|
playsinline
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
|
style="${aspect(item)}"
|
||||||
${poster ? html`poster="${poster}"` : ''}
|
${poster ? html`poster="${poster}"` : ''}
|
||||||
><source src="${src}"></video>
|
><source src="${src}"></video>
|
||||||
${item.hls
|
${item.hls
|
||||||
@@ -26,10 +32,11 @@ function renderItem(item: Media, index: number): Raw {
|
|||||||
</figure>`;
|
</figure>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<figure class="item item--image" data-index="${index}" style="${aspect(item)}">
|
return html`<figure class="item item--image" data-index="${index}">
|
||||||
<img
|
<img
|
||||||
src="${src}"
|
src="${src}"
|
||||||
alt="${item.alt ?? ''}"
|
alt="${item.alt ?? ''}"
|
||||||
|
style="${aspect(item)}"
|
||||||
loading="${index === 0 ? 'eager' : 'lazy'}"
|
loading="${index === 0 ? 'eager' : 'lazy'}"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user