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:
@@ -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. */
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user