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" />
|
<img :src="hero.thumbnail_url" alt="hero thumbnail" loading="lazy" />
|
||||||
</button>
|
</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
|
<button
|
||||||
v-for="t in rail" :key="t.image_id" type="button"
|
v-for="t in rail" :key="t.image_id" type="button"
|
||||||
class="fc-post-card__rail-cell"
|
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 plainTitle = computed(() => toPlainText(props.post.post_title))
|
||||||
|
|
||||||
const hero = computed(() => images.value[0])
|
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 visibleCount = computed(() => (images.value.length ? 1 + rail.value.length : 0))
|
||||||
const moreCount = computed(() => {
|
const moreCount = computed(() => {
|
||||||
const more = props.post.thumbnails_more || 0
|
if (!hasOverflow.value) return 0
|
||||||
const extraShown = Math.max(0, images.value.length - visibleCount.value)
|
return serverMore.value + Math.max(0, images.value.length - visibleCount.value)
|
||||||
return more + extraShown
|
|
||||||
})
|
})
|
||||||
|
// 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 sortDateIso = computed(() => props.post.post_date || props.post.downloaded_at)
|
||||||
const absoluteDate = computed(() => new Date(sortDateIso.value).toLocaleString())
|
const absoluteDate = computed(() => new Date(sortDateIso.value).toLocaleString())
|
||||||
@@ -279,11 +298,16 @@ function formatBytes (n) {
|
|||||||
.fc-post-card__hero:hover img,
|
.fc-post-card__hero:hover img,
|
||||||
.fc-post-card__rail-cell:hover img { transform: scale(1.03); filter: brightness(1.08); }
|
.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 {
|
.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 {
|
.fc-post-card__rail-cell {
|
||||||
width: 80px; height: 80px;
|
width: 100%; height: 84px;
|
||||||
overflow: hidden; border-radius: 4px;
|
overflow: hidden; border-radius: 4px;
|
||||||
}
|
}
|
||||||
.fc-post-card__rail-cell img {
|
.fc-post-card__rail-cell img {
|
||||||
@@ -291,7 +315,7 @@ function formatBytes (n) {
|
|||||||
object-fit: cover; display: block;
|
object-fit: cover; display: block;
|
||||||
}
|
}
|
||||||
.fc-post-card__rail-more {
|
.fc-post-card__rail-more {
|
||||||
width: 80px; height: 80px;
|
width: 100%; height: 84px;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
border: 1px dashed rgb(var(--v-theme-on-surface-variant));
|
border: 1px dashed rgb(var(--v-theme-on-surface-variant));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -40,6 +40,30 @@ describe('PostCard', () => {
|
|||||||
expect(full.text()).not.toContain('Show more')
|
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 () => {
|
it('clicking an image opens the post-scoped modal (never expands the card)', async () => {
|
||||||
const pinia = freshPinia()
|
const pinia = freshPinia()
|
||||||
const modal = useModalStore()
|
const modal = useModalStore()
|
||||||
|
|||||||
Reference in New Issue
Block a user