feat: the Latest feed uses a wide window — filmstrip cards, a day gutter and a filter rail (407 A, D, E)
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 2s
Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 6s
Build images / build-ml (push) Successful in 6s
CI / frontend-build (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m1s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m10s
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 2s
Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 6s
Build images / build-ml (push) Successful in 6s
CI / frontend-build (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m1s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m10s
On the operator's 3432px window the feed was a 900px column, 26% of the width. They picked three options from a to-scale layout study. A — filmstrip card (PostCard.vue): - A card measures itself with a ResizeObserver. At 1100px or wider its hero gets a fixed height, clamp(260px, 34vh, 460px), and the extra images move into a 2-column grid of squares beside it. The grid cells are sized from the hero height, so the grid ends flush with the hero. - The rail cap is 4 cells in this layout (2×2, the last becoming "+N") and 5 in the narrow layout, which is unchanged. - The description clamp drops to 4 lines, because long reads happen in the expanded view. - The hero has a height, not a width, so a wide card can't grow into a full-screen post. That was the operator's constraint. D — day gutter (PostsView.vue): - The normal feed groups consecutive posts by local day (Today, Yesterday, a weekday, or a date), with post and artist counts for what has loaded. - Runs rather than date buckets, because the sort key includes resurfaced_at, which the payload doesn't carry. A resurfaced grouping gets its own heading where it actually appears, instead of being pulled out of order. E — filter rail (PostsView.vue): - At 1600px and wider, the filters and status ribbon stack in a sticky 280px left rail, and each day's heading sits in a sticky 150px gutter beside its posts. - Below 1600px the layout is exactly the old one, including the 900px column. The in-context (post_id) view gets the wide column but no rail or day grouping, so anchor scrolling is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<v-card class="fc-post-card" variant="outlined">
|
||||
<v-card
|
||||
ref="cardEl" class="fc-post-card" variant="outlined"
|
||||
:class="{ 'fc-post-card--wide': wide }"
|
||||
>
|
||||
<div class="fc-post-card__head">
|
||||
<!-- Posts with no live subscription have source=null (alembic 0030);
|
||||
show a "filesystem import" affordance instead of a platform chip. -->
|
||||
@@ -58,7 +61,7 @@
|
||||
</button>
|
||||
<div
|
||||
v-if="rail.length || moreCount" class="fc-post-card__rail"
|
||||
:style="{ '--fc-rail-cols': railCols }"
|
||||
:style="{ '--fc-rail-cols': railCols, '--fc-grid-cols': Math.min(2, railCols) }"
|
||||
>
|
||||
<button
|
||||
v-for="t in rail" :key="t.image_id" type="button"
|
||||
@@ -221,20 +224,33 @@ const synthesisTitle = computed(() => {
|
||||
})
|
||||
|
||||
const hero = computed(() => images.value[0])
|
||||
// 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
|
||||
|
||||
// Filmstrip layout (milestone #407, option A). On a very wide window a card
|
||||
// that just grew would give one post the whole screen, so a WIDE card instead
|
||||
// pins the hero to a fixed height and moves the extra images into a 2-column
|
||||
// grid BESIDE it. Measured on the card rather than the viewport because the
|
||||
// same card renders in the Latest feed, Browse, and the in-context view, each
|
||||
// at a different width.
|
||||
const WIDE_CARD_PX = 1100
|
||||
const cardEl = ref(null)
|
||||
const wide = ref(false)
|
||||
|
||||
// The narrow layout's strip spans the hero's full width (CSS grid, equal
|
||||
// columns); the wide layout's grid is 2×2. Show up to that many cells; when
|
||||
// there are more images than fit, the last cell becomes a "+N" overflow tile so
|
||||
// the count stays accurate.
|
||||
const RAIL_MAX_NARROW = 5
|
||||
const RAIL_MAX_WIDE = 4
|
||||
const serverMore = computed(() => props.post.thumbnails_more || 0)
|
||||
const afterHero = computed(() => images.value.slice(1))
|
||||
const railMax = computed(() => (wide.value ? RAIL_MAX_WIDE : RAIL_MAX_NARROW))
|
||||
const hasOverflow = computed(
|
||||
() => serverMore.value > 0 || afterHero.value.length > RAIL_MAX,
|
||||
() => serverMore.value > 0 || afterHero.value.length > railMax.value,
|
||||
)
|
||||
const rail = computed(() =>
|
||||
hasOverflow.value
|
||||
? afterHero.value.slice(0, RAIL_MAX - 1)
|
||||
: afterHero.value.slice(0, RAIL_MAX),
|
||||
? afterHero.value.slice(0, railMax.value - 1)
|
||||
: afterHero.value.slice(0, railMax.value),
|
||||
)
|
||||
const visibleCount = computed(() => (images.value.length ? 1 + rail.value.length : 0))
|
||||
const moreCount = computed(() => {
|
||||
@@ -349,8 +365,17 @@ function measureOverflow () {
|
||||
}
|
||||
|
||||
let ro = null
|
||||
let cardRo = null
|
||||
onMounted(() => {
|
||||
nextTick(measureOverflow)
|
||||
const root = cardEl.value?.$el
|
||||
if (typeof ResizeObserver !== 'undefined' && root) {
|
||||
cardRo = new ResizeObserver((entries) => {
|
||||
const w = entries[0]?.contentRect?.width ?? 0
|
||||
wide.value = w >= WIDE_CARD_PX
|
||||
})
|
||||
cardRo.observe(root)
|
||||
}
|
||||
// Re-measure when the card resizes (the container-query clamp differs by
|
||||
// width). Guarded for happy-dom / older runtimes without ResizeObserver.
|
||||
if (typeof ResizeObserver !== 'undefined' && descEl.value) {
|
||||
@@ -358,7 +383,10 @@ onMounted(() => {
|
||||
ro.observe(descEl.value)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => { if (ro) { ro.disconnect(); ro = null } })
|
||||
onBeforeUnmount(() => {
|
||||
if (ro) { ro.disconnect(); ro = null }
|
||||
if (cardRo) { cardRo.disconnect(); cardRo = null }
|
||||
})
|
||||
|
||||
async function toggleDesc () {
|
||||
if (!descExpanded.value) {
|
||||
@@ -495,6 +523,40 @@ function formatBytes (n) {
|
||||
color: rgb(var(--v-theme-accent));
|
||||
}
|
||||
|
||||
/* --- Filmstrip layout (wide cards, #407 A) ---------------------------------
|
||||
The hero has a HEIGHT, not a width: a wide card must not turn into a
|
||||
full-screen post, so its height stays roughly a third of the viewport
|
||||
whatever the window's width. The extra images sit beside it as square cells
|
||||
whose size derives from that same height, so the grid always ends flush with
|
||||
the hero's bottom edge. */
|
||||
.fc-post-card--wide {
|
||||
--fc-hero-h: clamp(260px, 34vh, 460px);
|
||||
--fc-grid-gap: 8px;
|
||||
--fc-cell: calc((var(--fc-hero-h) - var(--fc-grid-gap)) / 2);
|
||||
}
|
||||
.fc-post-card--wide .fc-post-card__body { flex-direction: row; gap: 24px; }
|
||||
.fc-post-card--wide .fc-post-card__media {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: var(--fc-grid-gap);
|
||||
}
|
||||
.fc-post-card--wide .fc-post-card__hero {
|
||||
width: auto;
|
||||
height: var(--fc-hero-h);
|
||||
}
|
||||
.fc-post-card--wide .fc-post-card__rail {
|
||||
margin-top: 0;
|
||||
gap: var(--fc-grid-gap);
|
||||
grid-template-columns: repeat(var(--fc-grid-cols, 2), var(--fc-cell));
|
||||
grid-auto-rows: var(--fc-cell);
|
||||
}
|
||||
.fc-post-card--wide .fc-post-card__rail-cell,
|
||||
.fc-post-card--wide .fc-post-card__rail-more { height: 100%; }
|
||||
.fc-post-card--wide .fc-post-card__text { flex: 1 1 0; min-width: 0; }
|
||||
/* Text is secondary here — long reads happen in the expanded view — so the
|
||||
clamp keeps the text column no taller than the images beside it. */
|
||||
.fc-post-card--wide .fc-post-card__desc--clamped { -webkit-line-clamp: 4; }
|
||||
|
||||
.fc-post-card__title {
|
||||
font-family: 'Fraunces', Georgia, serif;
|
||||
font-size: 18px; font-weight: 700;
|
||||
|
||||
Reference in New Issue
Block a user