Merge pull request 'Keep a post inside the window, and stop letterboxing it' (#2) from media-fits-viewport into main

Reviewed-on: #2
This commit is contained in:
2026-08-26 20:26:41 +00:00
2 changed files with 73 additions and 8 deletions
+64 -6
View File
@@ -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
View File
@@ -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"
> >