Images Phase 2: uniform brief tiles — photo or topic-colored placeholder

Per review, mixed photo/no-photo rows read as ragged ("the ones without feel
lacking"). Make every brief rest card carry a banner so the grid is uniform: a
compact 16:9 photo when available, otherwise a calm placeholder tinted by the
card's topic accent color (the same per-topic hue as the accent line) with the
topic word set faint in serif. A failed/blocked image falls back to the same
placeholder, so cards never look broken and heights stay identical. Hero remains
the single large image. Light desaturation on photos; no heavy tint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-07 13:13:18 -04:00
parent 403749e26f
commit d19099ee7d
2 changed files with 44 additions and 4 deletions
+43 -3
View File
@@ -2,7 +2,7 @@
import { auth, savedIds, toggleSave } from '$lib/auth.svelte.js';
import { track } from '$lib/analytics.js';
let { article, onaction, onreplace, ontag, onimageerror, onview, hero = false } = $props();
let { article, onaction, onreplace, ontag, onimageerror, onview, hero = false, thumb = false } = $props();
function opened() {
// Records history; the /a page itself fires the summary_viewed event on load.
@@ -38,8 +38,12 @@
failed = false;
});
let hasImg = $derived(!!article.image_url && !failed);
// The hero is the ONLY image slot; every other card is typographic.
let showImage = $derived(hero && hasImg);
// Hero shows a large image. A `thumb` card (brief rest cards) ALWAYS carries a
// banner so the grid stays uniform: a real image when it has one, otherwise a
// calm topic-colored placeholder. Every other card stays typographic.
let showImage = $derived(hasImg && (hero || thumb));
let isThumb = $derived(thumb && !hero);
let usePlaceholder = $derived(isThumb && !hasImg);
let safeHref = $derived(
typeof article.url === 'string' && /^https?:\/\//.test(article.url) ? article.url : '#'
);
@@ -91,6 +95,7 @@
class:hero={hero && hasImg}
class:herotype={hero && !hasImg}
class:tile={!hero}
class:hasthumb={isThumb}
data-topic={article.topic ?? ''}
>
{#if showImage}
@@ -98,6 +103,10 @@
<img src={article.image_url} alt="" loading="lazy" referrerpolicy="no-referrer"
onerror={() => { failed = true; onimageerror?.(); }} />
</a>
{:else if usePlaceholder}
<a class="media placeholder" href={summaryHref} onclick={opened} style="--c:{accentColor}" tabindex="-1" aria-hidden="true">
<span class="ph-word">{humanize(article.topic) || 'upbeat bytes'}</span>
</a>
{/if}
<div class="body">
@@ -277,6 +286,37 @@
article:focus-within .actions .mute { opacity: 1; }
}
/* ---- Compact top image for brief rest cards (thumb) ---- */
/* A modest, fixed-ratio banner — selective image confidence, not a media wall.
Light desaturation for calm cohesion; never a heavy tint. */
.tile .media {
display: block; position: relative; width: 100%;
aspect-ratio: 16 / 9; overflow: hidden; border-bottom: 1px solid var(--line);
background: linear-gradient(135deg, var(--accent-soft), #f1ece0);
}
.tile .media img {
width: 100%; height: 100%; object-fit: cover; display: block;
filter: saturate(0.9);
}
/* Image-less cards get a calm placeholder banner in the card's topic color
(--c), so every card in the grid is the same size — uniform, not lacking. */
.tile .media.placeholder {
display: flex; align-items: center; justify-content: center; text-decoration: none;
background:
radial-gradient(130% 150% at 0% 0%, color-mix(in srgb, var(--c) 26%, transparent), transparent 62%),
linear-gradient(135deg, color-mix(in srgb, var(--c) 13%, var(--surface)), var(--surface));
}
.tile .media.placeholder .ph-word {
font-family: var(--serif); font-size: 2rem; line-height: 1;
color: var(--c); opacity: 0.34; text-transform: lowercase; letter-spacing: -0.01em;
}
/* A thumb card's banner carries the topic identity, so drop the faint corner
watermark there to avoid a doubled word. */
.tile.hasthumb::after { display: none; }
/* Every thumb card carries a banner, so trim the summary to keep total height
compact and uniform across the grid. */
.tile.hasthumb .summary { -webkit-line-clamp: 2; }
/* ---- Typographic editorial tile (every non-hero card) ---- */
.tile { position: relative; }
/* The .cardhead owns the top padding (above the accent); avoid doubling it. */
+1 -1
View File
@@ -350,7 +350,7 @@
{#if restArticles.length}
<div class="grid rest">
{#each restArticles as a (a.id)}
<ArticleCard article={a} onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onview={record} />
<ArticleCard article={a} thumb onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onview={record} />
{/each}
</div>
{/if}