fix(posts): post-card thumbnail strip spans the full hero width
The hero collage's thumbnail rail hard-capped at 3 fixed-80px cells, left- aligned, so it never reached the edge of the (50%-width) hero. Make the rail a CSS grid of equal columns (1fr) at a fixed height that stretches to the hero's full width: show up to 5 thumbnails, and when a post has more images than fit, the last cell becomes the "+N" overflow tile (count unchanged). Column count is driven by --fc-rail-cols so the strip always reaches the hero edge regardless of image count. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,10 @@
|
||||
>
|
||||
<img :src="hero.thumbnail_url" alt="hero thumbnail" loading="lazy" />
|
||||
</button>
|
||||
<div v-if="rail.length || moreCount" class="fc-post-card__rail">
|
||||
<div
|
||||
v-if="rail.length || moreCount" class="fc-post-card__rail"
|
||||
:style="{ '--fc-rail-cols': railCols }"
|
||||
>
|
||||
<button
|
||||
v-for="t in rail" :key="t.image_id" type="button"
|
||||
class="fc-post-card__rail-cell"
|
||||
@@ -117,13 +120,29 @@ const totalImages = computed(() => images.value.length + (props.post.thumbnails_
|
||||
const plainTitle = computed(() => toPlainText(props.post.post_title))
|
||||
|
||||
const hero = computed(() => images.value[0])
|
||||
const rail = computed(() => images.value.slice(1, 4))
|
||||
// The thumbnail strip spans the hero's full width (CSS grid, equal columns),
|
||||
// rather than a fixed 3-cell cap. Show up to RAIL_MAX cells; when there are
|
||||
// more images than fit, the last cell becomes a "+N" overflow tile so the
|
||||
// count stays accurate.
|
||||
const RAIL_MAX = 5
|
||||
const serverMore = computed(() => props.post.thumbnails_more || 0)
|
||||
const afterHero = computed(() => images.value.slice(1))
|
||||
const hasOverflow = computed(
|
||||
() => serverMore.value > 0 || afterHero.value.length > RAIL_MAX,
|
||||
)
|
||||
const rail = computed(() =>
|
||||
hasOverflow.value
|
||||
? afterHero.value.slice(0, RAIL_MAX - 1)
|
||||
: afterHero.value.slice(0, RAIL_MAX),
|
||||
)
|
||||
const visibleCount = computed(() => (images.value.length ? 1 + rail.value.length : 0))
|
||||
const moreCount = computed(() => {
|
||||
const more = props.post.thumbnails_more || 0
|
||||
const extraShown = Math.max(0, images.value.length - visibleCount.value)
|
||||
return more + extraShown
|
||||
if (!hasOverflow.value) return 0
|
||||
return serverMore.value + Math.max(0, images.value.length - visibleCount.value)
|
||||
})
|
||||
// Columns = thumbnails shown plus the overflow tile, so the strip fills the
|
||||
// hero's full width with equal cells.
|
||||
const railCols = computed(() => rail.value.length + (moreCount.value > 0 ? 1 : 0))
|
||||
|
||||
const sortDateIso = computed(() => props.post.post_date || props.post.downloaded_at)
|
||||
const absoluteDate = computed(() => new Date(sortDateIso.value).toLocaleString())
|
||||
@@ -279,11 +298,16 @@ function formatBytes (n) {
|
||||
.fc-post-card__hero:hover img,
|
||||
.fc-post-card__rail-cell:hover img { transform: scale(1.03); filter: brightness(1.08); }
|
||||
|
||||
/* Equal columns spanning the hero's full width — cells stretch horizontally
|
||||
(1fr) at a fixed height, so the strip always reaches the edge of the hero
|
||||
regardless of how many thumbnails the post has. */
|
||||
.fc-post-card__rail {
|
||||
display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--fc-rail-cols, 3), 1fr);
|
||||
gap: 6px; margin-top: 6px;
|
||||
}
|
||||
.fc-post-card__rail-cell {
|
||||
width: 80px; height: 80px;
|
||||
width: 100%; height: 84px;
|
||||
overflow: hidden; border-radius: 4px;
|
||||
}
|
||||
.fc-post-card__rail-cell img {
|
||||
@@ -291,7 +315,7 @@ function formatBytes (n) {
|
||||
object-fit: cover; display: block;
|
||||
}
|
||||
.fc-post-card__rail-more {
|
||||
width: 80px; height: 80px;
|
||||
width: 100%; height: 84px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: 1px dashed rgb(var(--v-theme-on-surface-variant));
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -40,6 +40,30 @@ describe('PostCard', () => {
|
||||
expect(full.text()).not.toContain('Show more')
|
||||
})
|
||||
|
||||
const thumbs = (n) =>
|
||||
Array.from({ length: n }, (_, i) => ({ image_id: 100 + i, thumbnail_url: `/t${i}` }))
|
||||
|
||||
it('fills the rail to full width (5 cells, no overflow) for a 6-image post', () => {
|
||||
const post = { ...BASE, description_plain: 'd', thumbnails: thumbs(6), thumbnails_more: 0 }
|
||||
const w = mountComponent(PostCard, { props: { post }, pinia: freshPinia() })
|
||||
// hero + 5 rail thumbnails, no "+N" tile.
|
||||
expect(w.findAll('.fc-post-card__rail-cell')).toHaveLength(5)
|
||||
expect(w.find('.fc-post-card__rail-more').exists()).toBe(false)
|
||||
// The grid spans the hero width via a column count == cells shown.
|
||||
expect(w.find('.fc-post-card__rail').attributes('style')).toContain('--fc-rail-cols: 5')
|
||||
})
|
||||
|
||||
it('reserves the last cell for a "+N" overflow tile when there are more images', () => {
|
||||
const post = { ...BASE, description_plain: 'd', thumbnails: thumbs(6), thumbnails_more: 4 }
|
||||
const w = mountComponent(PostCard, { props: { post }, pinia: freshPinia() })
|
||||
// 4 thumbnails + 1 overflow tile == 5 columns; tile counts every hidden image.
|
||||
expect(w.findAll('.fc-post-card__rail-cell')).toHaveLength(4)
|
||||
const more = w.find('.fc-post-card__rail-more')
|
||||
expect(more.exists()).toBe(true)
|
||||
expect(more.text()).toBe('+5')
|
||||
expect(w.find('.fc-post-card__rail').attributes('style')).toContain('--fc-rail-cols: 5')
|
||||
})
|
||||
|
||||
it('clicking an image opens the post-scoped modal (never expands the card)', async () => {
|
||||
const pinia = freshPinia()
|
||||
const modal = useModalStore()
|
||||
|
||||
Reference in New Issue
Block a user