diff --git a/docs/superpowers/specs/2026-06-20-inline-journal-feed-design.md b/docs/superpowers/specs/2026-06-20-inline-journal-feed-design.md new file mode 100644 index 0000000..3082d30 --- /dev/null +++ b/docs/superpowers/specs/2026-06-20-inline-journal-feed-design.md @@ -0,0 +1,306 @@ +# Inline Journal Feed Design Spec + +*2026-06-20* + +--- + +## Goal + +Replace click-through journal entry cards with fully inline posts across the trip page, dailies page, and home page. Each journal entry renders its full content in the feed — title, meta, photo strip, and body text — without requiring navigation to the detail page. + +--- + +## Scope + +**In scope:** +- Journal entry display in `trip.html.twig`, `dailies.html.twig`, `home.html.twig` +- New `.journal-post` CSS component and photo strip styles +- Dot-sync JS for the photo strip (one shared block in `base.html.twig`) +- Map flash animation extended to `.journal-post.is-highlighted` +- Test updates for T1, T2 + +**Out of scope:** +- Story cards in the feed — remain as click-through ``, unchanged +- The journal entry detail page (`entry.html.twig`) — kept as-is; just not linked from the feed +- The post form — photos are already uploaded correctly +- Lightbox on the feed — only on the detail page + +--- + +## Layout + +Each journal entry in the feed renders as: + +``` +Title (DM Serif Display, ~xl) +DATE · 📍 City, Country · ☀️ Weather ← meta row; DATE is the permalink to detail page +┌──────────────────────────────────────┐ +│ │ +│ Photo (full-width, 3:2 ratio) │ ← swipe left/right for 2–4 photos +│ │ +└──────────────────────────────────────┘ + ● ○ ○ ← dots; hidden when only 1 photo +Body text paragraph(s) +──────────────────────────────────────── ← border-bottom separator +``` + +- **Title** sits above the photo, using `var(--font-display)` at `var(--text-xl)` +- **Meta row** (date, location, weather) sits between title and photo; the date is a small `` permalink to the detail page, styled in `var(--color-ink-muted)`. Location and weather are plain text spans +- **Photo strip**: CSS scroll-snap, no JS library required for swipe +- **Dots**: visible only when the entry has 2+ images; update via scroll listener +- **Body**: full entry body text — not truncated, not excerpted +- **Separator**: `border-bottom: 1px solid var(--color-border)` on the post root, matching the current entry card separator + +--- + +## HTML Structure + +```html +
+ +
+

{{ entry.title }}

+
+
+ + {% set images = entry.media.images %} + {% if images|length > 0 %} +
+ {% for img in images %} +
+ {{ entry.title }} +
+ {% endfor %} +
+ {% if images|length > 1 %} + + {% endif %} + {% endif %} + +
{{ entry.content|raw }}
+ +
+``` + +**Key attribute notes:** +- `id="entry-{{ entry.slug }}"` — required for map marker scroll targeting (`document.getElementById`) +- `data-type="journal"` — required for the trip page filter bar (`querySelectorAll('[data-type]')`) +- `data-lat` / `data-lng` — required for map marker rendering +- The `
` root replaces the old `` — the entry is no longer a clickable card + +The `weather_icons` map (currently defined inline in `entry.html.twig`) must also be defined at the top of `trip.html.twig`, `dailies.html.twig`, and `home.html.twig` so the meta row can use it. + +--- + +## Photo Strip: CSS + +```css +/* ── Journal post ──────────────────────────────────────────── */ + +.journal-post { + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-12); + margin-bottom: var(--space-12); +} + +.journal-post-header { + margin-bottom: var(--space-4); +} + +.journal-post-title { + font-family: var(--font-display); + font-size: var(--text-xl); + font-weight: 400; + line-height: var(--leading-snug); + color: var(--color-ink); + margin-bottom: var(--space-2); +} + +.journal-post-meta { + font-size: var(--text-xs); + color: var(--color-ink-muted); + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-2); +} + +.journal-post-permalink { + color: var(--color-ink-muted); + text-decoration: none; + font-weight: 700; + letter-spacing: 0.07em; +} + +.journal-post-permalink:hover { color: var(--color-accent); } + +.journal-post-location, +.journal-post-weather { + color: var(--color-ink-muted); +} + +/* Photo strip */ + +.journal-photo-strip { + display: flex; + overflow-x: scroll; + scroll-snap-type: x mandatory; + scrollbar-width: none; + border-radius: var(--radius-md); + margin-bottom: var(--space-3); +} + +.journal-photo-strip::-webkit-scrollbar { display: none; } + +.journal-photo-slide { + flex: 0 0 100%; + scroll-snap-align: start; + aspect-ratio: 3 / 2; + overflow: hidden; +} + +.journal-photo-slide img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +/* Dot indicators */ + +.journal-photo-dots { + display: flex; + justify-content: center; + gap: var(--space-2); + margin-bottom: var(--space-4); +} + +.journal-photo-dot { + width: 6px; + height: 6px; + border-radius: 9999px; + background: var(--color-border); + transition: background 0.2s; +} + +.journal-photo-dot.is-active { + background: var(--color-ink-muted); +} + +/* Body */ + +.journal-post-body { + font-size: var(--text-base); + line-height: var(--leading-normal); + color: var(--color-ink-2); +} + +.journal-post-body p { margin-bottom: var(--space-4); } +.journal-post-body p:last-child { margin-bottom: 0; } + +/* Map flash — extends existing keyframe */ + +.journal-post.is-highlighted { + animation: card-highlight 0.7s ease-out forwards; +} +``` + +--- + +## Photo Strip: JS + +One shared script block added to `base.html.twig`, just before ``. It is a no-op on pages with no strips. + +```html + +``` + +--- + +## CSS Cleanup + +The following selectors are used exclusively by the old journal entry card and can be removed from `style.css` once the new `.journal-post` component is in place. Story cards in the feed (`entry-card--story`) do **not** use them: + +- `.entry-card-textmeta` and children (`.entry-date-plain`, `.entry-location-plain`) +- `.entry-card-photo-overlay` and children (`.entry-date-overlay`, `.entry-location-overlay`) +- `.entry-excerpt` +- `.entry-read-more` +- `.entry-card .entry-title` — the title rule scoped to `.entry-card`; replace with `.journal-post-title` +- `.entry-card:hover .entry-card-photo img` — photo zoom on hover; journal posts have no hover interaction +- `.entry-card:hover .entry-title` — title tint on hover; same reason +- `.entry-card.is-highlighted` — replaced by `.journal-post.is-highlighted` + +**Keep** the following — they are still used by story cards (`entry-card--story`) or elsewhere: +- `.entry-card` base styles — story cards still use this class +- `.entry-card-photo` and `.entry-card-photo img` — story cards use `.entry-card-photo--story` +- `.entry-card:hover` background lift (in the shared three-card selector) — story cards still hover +- All single-entry-page styles (`.entry-hero`, `.entry-header`, `.entry-body`, etc.) + +--- + +## Test Updates + +**T1** (`tests/ui/dailies.spec.js`): +```js +// OLD +await expect(page.locator('.entry-card').first()).toBeVisible(); +// NEW +await expect(page.locator('.journal-post').first()).toBeVisible(); +``` + +**T2** (`tests/ui/dailies.spec.js`): +```js +// OLD — used href on the root +const newerCard = page.locator(`.entry-card[href*="${NEWER_SLUG}"]`); +const olderCard = page.locator(`.entry-card[href*="${OLDER_SLUG}"]`); +// ... +findIndex(c => c === el) + +// NEW — use id attribute (journal posts are
, not ) +const newerCard = page.locator(`#entry-${NEWER_SLUG}`); +const olderCard = page.locator(`#entry-${OLDER_SLUG}`); +// ... +findIndex(c => c.id === el.id) +``` + +--- + +## Out of scope + +- Swipe velocity / momentum — native browser scroll-snap handles this +- Lightbox on the feed photo strip — photos are not tappable in the feed; the detail page retains the lightbox +- Lazy-load placeholder shimmer +- Image ordering UI — photos appear in filesystem order (same as the detail page gallery)