feat(trip): one-liner, description & retina cover on trip list + page

Give trips an optional one-liner (header.tagline) and description (markdown
content), surface them where they help, and fix the soft cover image — all
editable from admin.

- U1: header.cover_image blueprint field → pagemediaselect media picker.
- U2: new macros/cover.html.twig — single source for cover resolution
  (author-selected → first journal image → none; missing file falls back)
  and retina rendering (1x/2x cropResize + srcset). Merged resolve+render
  into one macro since Twig macros can't return a Medium object.
- U3: trip-list card renders the one-liner (when set) and the retina cover.
- U4: trip-page in-column header gains the one-liner, an expandable
  description, and a thin banner strip — gated behind a trip_header_extras
  partial flag (default off) so the shared home active-trip view is
  unchanged (R12/KTD4).
- U5: styles for the card/header one-liner, collapsible description
  (max-height preview, not line-clamp, so it holds across paragraphs) and
  the banner, with a mobile banner-height reduction.

Covers R1–R15. Verified with new Playwright specs + full trip/home/maps
regression against an isolated worktree dev server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
This commit is contained in:
2026-07-06 00:02:19 +02:00
co-authored by Claude Opus 4.8
parent b317640dd0
commit ad72739b99
6 changed files with 163 additions and 19 deletions
@@ -1,8 +1,16 @@
{% import 'macros/stats.html.twig' as stats_m %}
{% import 'macros/cycling.html.twig' as cycling_m %}
{% import 'macros/cover.html.twig' as cover %}
{# trip_header_extras: gated one-liner + description + banner for the trip-page
caller only. Home's active-trip include omits it (`only`), so it defaults off
and that header renders exactly as before (KTD4 / R12). #}
{% set trip_header_extras = trip_header_extras|default(false) %}
<div class="home-feed-col">
<div class="home-trip-header">
<h1 class="home-trip-name">{{ trip_page.title }}</h1>
{% if trip_header_extras and trip_page.header.tagline %}
<p class="home-trip-tagline">{{ trip_page.header.tagline }}</p>
{% endif %}
{% if trip_page.header.date_start %}
<p class="trip-dates">
{{ trip_page.header.date_start|date('d M Y') }}
@@ -13,6 +21,15 @@
{{ journal_count }} journal {{ journal_count == 1 ? 'entry' : 'entries' }}
{% if story_count > 0 %} · {{ story_count }} {{ story_count == 1 ? 'story' : 'stories' }}{% endif %}
</span>
{% if trip_header_extras and trip_page.content|striptags|trim %}
<div class="trip-header-desc" data-collapsed="true">
<div class="trip-header-desc-body">{{ trip_page.content|raw }}</div>
<button type="button" class="trip-header-desc-toggle" aria-expanded="false">Read more</button>
</div>
{% endif %}
{% if trip_header_extras %}
{{ cover.render(trip_page, trip_page.title, 720, 220, 'trip-header-banner', '100vw') }}
{% endif %}
<div class="trip-filter-bar">
<div class="trip-filter-group">
<button class="trip-filter-btn is-active" data-filter="all" aria-pressed="true">All content</button>
@@ -62,4 +79,27 @@
});
});
</script>
{% if trip_header_extras %}
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.trip-header-desc').forEach(function (desc) {
var body = desc.querySelector('.trip-header-desc-body');
var btn = desc.querySelector('.trip-header-desc-toggle');
if (!body || !btn) return;
// Drop the toggle when the description already fits the collapsed preview.
if (body.scrollHeight <= body.clientHeight + 4) {
btn.hidden = true;
desc.removeAttribute('data-collapsed');
return;
}
btn.addEventListener('click', function () {
var collapsed = desc.getAttribute('data-collapsed') === 'true';
desc.setAttribute('data-collapsed', collapsed ? 'false' : 'true');
btn.setAttribute('aria-expanded', collapsed ? 'true' : 'false');
btn.textContent = collapsed ? 'Show less' : 'Read more';
});
});
});
</script>
{% endif %}
</div>