diff --git a/docs/superpowers/plans/2026-06-20-inline-journal-feed.md b/docs/superpowers/plans/2026-06-20-inline-journal-feed.md new file mode 100644 index 0000000..a34250f --- /dev/null +++ b/docs/superpowers/plans/2026-06-20-inline-journal-feed.md @@ -0,0 +1,855 @@ +# Inline Journal Feed Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace click-through journal entry cards with fully inline posts (photo strip + full text) across the trip page, dailies page, and home page. + +**Architecture:** Each journal entry becomes an `
` block that renders all its images in a CSS scroll-snap strip with dot indicators, followed by the full body text. The `id`, `data-type`, `data-lat`, `data-lng` attributes stay on the root so map targeting, filter JS, and flash animation continue to work. Story cards in all three feeds are unchanged. + +**Tech Stack:** Grav 2.0 Twig templates, CSS scroll-snap (no library), vanilla JS IntersectionObserver-free dot sync via scroll event, Playwright tests + +## Global Constraints + +- All CSS values must use design tokens (`var(--...)`) — no hard-coded colours, sizes, or radii +- `id="entry-{{ entry.slug }}"` must remain on the journal post root (map scroll targeting) +- `data-type="journal"` must remain on the journal post root (filter bar JS) +- `data-lat` and `data-lng` must remain on the journal post root (map marker rendering) +- Story cards (``) are not touched by any task +- Two git repos: user content at `/home/mischa/Projects/travel-blog-intotheeast/user/` (separate git repo); outer repo at `/home/mischa/Nextcloud/Projects/travel-blog-intotheeast/`. Templates and CSS commit to the user subrepo; tests commit to the outer repo. Always update the outer repo's `user` submodule pointer in the same commit as the test changes. +- Dev server: http://localhost:8081 + +--- + +## File Map + +| File | Change | +|---|---| +| `user/themes/intotheeast/css/style.css` | Add `.journal-post` component; remove journal-card-only rules; update `.is-highlighted` selector | +| `user/themes/intotheeast/templates/partials/base.html.twig` | Add photo-strip dot-sync JS before `` | +| `user/themes/intotheeast/templates/dailies.html.twig` | Replace journal card block with `.journal-post`; add `weather_icons` | +| `user/themes/intotheeast/templates/trip.html.twig` | Replace journal card block with `.journal-post`; add `weather_icons` | +| `user/themes/intotheeast/templates/home.html.twig` | Replace journal card block with `.journal-post`; add `weather_icons` | +| `tests/ui/dailies.spec.js` | Update T1 selector; update T2 selectors | +| `tests/ui/maps.spec.js` | Update M7 selector | +| `tests/ui/home.spec.js` | New file — H1 test | + +--- + +### Task 1: CSS foundation + dot-sync JS + +Add all new `.journal-post` CSS and the photo-strip dot-sync JS. Remove CSS classes that are only used by the old journal entry card (not by story cards). This task has no template changes — existing tests must still pass at the end. + +**Files:** +- Modify: `user/themes/intotheeast/css/style.css` +- Modify: `user/themes/intotheeast/templates/partials/base.html.twig` + +**Interfaces:** +- Produces: `.journal-post`, `.journal-post-header`, `.journal-post-title`, `.journal-post-meta`, `.journal-post-permalink`, `.journal-post-location`, `.journal-post-weather`, `.journal-photo-strip`, `.journal-photo-slide`, `.journal-photo-dots`, `.journal-photo-dot.is-active`, `.journal-post-body`, `.journal-post.is-highlighted` — all usable by Tasks 2–4 + +- [ ] **Step 1: Add `.journal-post` CSS block to `style.css`** + +In `user/themes/intotheeast/css/style.css`, find the line: + +```css +/* ── Single entry ────────────────────────────────────────────────────────────── */ +``` + +Insert the following block **before** that comment: + +```css +/* ── Journal post (inline feed) ─────────────────────────────────────────────── */ + +.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); +} + +.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; +} + +.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); +} + +.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; } + +.journal-post.is-highlighted { + animation: card-highlight 0.7s ease-out forwards; +} + +``` + +- [ ] **Step 2: Remove journal-card-only CSS rules from `style.css`** + +These rules are only used by the old journal entry card. Story cards do not use them. Remove each block exactly as shown. + +**Remove `.entry-card-photo-overlay` and its children:** + +```css +.entry-card-photo-overlay { + position: absolute; + inset: auto 0 0 0; + padding: var(--space-5) var(--space-4) var(--space-3); + background: linear-gradient(to top, rgba(0,0,0,0.58) 0%, transparent 100%); + display: flex; + align-items: flex-end; + gap: var(--space-3); + flex-wrap: wrap; +} + +.entry-date-overlay { + font-size: var(--text-xs); + font-weight: 700; + letter-spacing: 0.08em; + color: rgba(255,255,255,0.92); +} + +.entry-location-overlay { + font-size: var(--text-xs); + color: rgba(255,255,255,0.85); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 180px; +} +``` + +Replace with nothing (delete the block entirely). + +**Remove the text-only meta block and its comment:** + +```css +/* Card: text-only variant */ + +.entry-card-textmeta { + display: flex; + align-items: center; + gap: var(--space-3); + margin-bottom: var(--space-3); + flex-wrap: wrap; +} + +.entry-date-plain { + font-size: var(--text-xs); + font-weight: 700; + letter-spacing: 0.07em; + color: var(--color-ink-muted); +} + +.entry-location-plain { + font-size: var(--text-xs); + color: var(--color-ink-muted); +} +``` + +Replace with nothing. + +**Remove `.entry-excerpt` and `.entry-read-more`:** + +```css +.entry-excerpt { + font-size: var(--text-base); + line-height: var(--leading-normal); + color: var(--color-ink-2); + margin-bottom: var(--space-3); +} + +.entry-read-more { + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-accent); +} +``` + +Replace with nothing. + +**Replace `.entry-card.is-highlighted` with `.journal-post.is-highlighted`:** + +Find: +```css +.entry-card.is-highlighted { + animation: card-highlight 0.7s ease-out forwards; +} +``` + +Replace with: +```css +.journal-post.is-highlighted { + animation: card-highlight 0.7s ease-out forwards; +} +``` + +- [ ] **Step 3: Add dot-sync JS to `base.html.twig`** + +In `user/themes/intotheeast/templates/partials/base.html.twig`, find: + +```twig + {{ assets.js('bottom')|raw }} + +``` + +Replace with: + +```twig + {{ assets.js('bottom')|raw }} + + +``` + +- [ ] **Step 4: Run existing tests to confirm nothing broke** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js +``` + +Expected: all existing tests pass (M7 still passes because `trip.html.twig` has not changed yet — the JS still adds `is-highlighted` to `.entry-card` elements, and the old M7 selector `.entry-card.is-highlighted` finds the element). + +- [ ] **Step 5: Commit user subrepo** + +```bash +cd /home/mischa/Projects/travel-blog-intotheeast/user +git add themes/intotheeast/css/style.css themes/intotheeast/templates/partials/base.html.twig +git commit -m "feat: add journal-post CSS component and dot-sync JS; remove stale journal-card-only rules" +``` + +--- + +### Task 2: dailies.html.twig + T1/T2 test updates + +Replace the journal entry card in `dailies.html.twig` with the new `.journal-post` inline block. Update T1 and T2 tests to match the new structure. + +**Files:** +- Modify: `user/themes/intotheeast/templates/dailies.html.twig` +- Modify: `tests/ui/dailies.spec.js` + +**Interfaces:** +- Consumes: `.journal-post` CSS from Task 1 +- Produces: `/trips/japan-korea-2026/dailies` renders `.journal-post` blocks; T1 and T2 pass with new selectors + +- [ ] **Step 1: Update T1 and T2 tests to their new selectors** + +In `tests/ui/dailies.spec.js`, make the following changes: + +**T1** — change `.entry-card` to `.journal-post`: + +```js +// OLD +await expect(page.locator('.entry-card').first()).toBeVisible(); +// NEW +await expect(page.locator('.journal-post').first()).toBeVisible(); +``` + +**T2** — replace the entire card locator + index block with id-based selectors: + +Find: +```js + // Both fixture entries must be visible on the page + const newerCard = page.locator(`.entry-card[href*="${NEWER_SLUG}"]`); + const olderCard = page.locator(`.entry-card[href*="${OLDER_SLUG}"]`); + + await expect(newerCard).toBeVisible(); + await expect(olderCard).toBeVisible(); + + // The newer entry should appear higher in the DOM (lower index) + const newerIdx = await newerCard.evaluate(el => { + return [...document.querySelectorAll('.entry-card')].findIndex(c => c === el); + }); + const olderIdx = await olderCard.evaluate(el => { + return [...document.querySelectorAll('.entry-card')].findIndex(c => c === el); + }); +``` + +Replace with: +```js + // Both fixture entries must be visible on the page + const newerCard = page.locator(`#entry-${NEWER_SLUG}`); + const olderCard = page.locator(`#entry-${OLDER_SLUG}`); + + await expect(newerCard).toBeVisible(); + await expect(olderCard).toBeVisible(); + + // The newer entry should appear higher in the DOM (lower index) + const newerIdx = await newerCard.evaluate(el => { + return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id); + }); + const olderIdx = await olderCard.evaluate(el => { + return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id); + }); +``` + +- [ ] **Step 2: Run T1 and T2 to verify they fail** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T1:|T2:" +``` + +Expected: FAIL — `.journal-post` selector finds no elements (the page still renders `.entry-card`). + +- [ ] **Step 3: Add `weather_icons` map and replace journal card in `dailies.html.twig`** + +In `user/themes/intotheeast/templates/dailies.html.twig`, find the line: + +```twig + {% if item.type == 'journal' %} + +``` + +This `{% if item.type == 'journal' %}` block ends at `` before `{% else %}`. Replace the entire journal card block (from `{% if item.type == 'journal' %}` through the closing `` of the journal branch, leaving the `{% else %}` story branch intact) with: + +```twig + {% if item.type == 'journal' %} + {% set weather_icons = { + 'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️', + 'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️', + 'Snow': '❄️', 'Thunderstorm': '⛈️' + } %} +
+
+

{{ 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 }}
+
+``` + +The exact text to find and replace is the old journal branch. The old branch starts with: + +```twig + {% if item.type == 'journal' %} + + {% if hero %} +
+ {{ entry.title }} +
+ + {% if entry.header.location_city or entry.header.location_country %} + + 📍 + {% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %} + {% if entry.header.location_city and entry.header.location_country %}, {% endif %} + {% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %} + + {% endif %} +
+
+ {% else %} +
+ + {% if entry.header.location_city or entry.header.location_country %} + + {%- set _loc = [] -%} + {%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%} + {%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%} + 📍 {{ _loc|join(', ') }} + + {% endif %} +
+ {% endif %} +
+

{{ entry.title }}

+

{{ entry.summary|striptags|slice(0, 250)|trim }}

+ Read entry → +
+
+``` + +- [ ] **Step 4: Run T1 and T2 to verify they pass** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T1:|T2:" +``` + +Expected: PASS. + +- [ ] **Step 5: Run the full suite to check no regressions** + +```bash +npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js +``` + +Expected: all pass. + +- [ ] **Step 6: Commit** + +```bash +cd /home/mischa/Projects/travel-blog-intotheeast/user +git add themes/intotheeast/templates/dailies.html.twig +git commit -m "feat: replace journal entry card with inline journal-post in dailies feed" + +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +git add tests/ui/dailies.spec.js user +git commit -m "test: update T1/T2 selectors for inline journal-post structure" +``` + +--- + +### Task 3: trip.html.twig + M7 test update + +Replace the journal entry card in `trip.html.twig` with the `.journal-post` block. Update M7 which currently tests `.entry-card.is-highlighted` on the trip page. + +**Files:** +- Modify: `user/themes/intotheeast/templates/trip.html.twig` +- Modify: `tests/ui/maps.spec.js` + +**Interfaces:** +- Consumes: `.journal-post` CSS and `.journal-post.is-highlighted` from Task 1; journal-post HTML pattern from Task 2 +- Produces: `/trips/japan-korea-2026` renders `.journal-post` blocks; M7 passes with `.journal-post.is-highlighted` + +- [ ] **Step 1: Update M7 to the new selector** + +In `tests/ui/maps.spec.js`, find: + +```js + // Within 500ms of click + delay, one entry-card should have is-highlighted + await expect(page.locator('.entry-card.is-highlighted')).toBeVisible({ timeout: 1500 }); +``` + +Replace with: + +```js + // Within 500ms of click + delay, one journal-post should have is-highlighted + await expect(page.locator('.journal-post.is-highlighted')).toBeVisible({ timeout: 1500 }); +``` + +- [ ] **Step 2: Run M7 to verify it fails** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:" +``` + +Expected: FAIL — `.journal-post.is-highlighted` not found (trip.html.twig still renders ``). + +- [ ] **Step 3: Replace journal card in `trip.html.twig`** + +In `user/themes/intotheeast/templates/trip.html.twig`, find and replace the journal branch of the `{% if item.type == 'journal' %}` block. The old branch to replace is: + +```twig + {% if item.type == 'journal' %} + + {% if hero %} +
+ {{ entry.title }} +
+ + {% if entry.header.location_city or entry.header.location_country %} + + 📍 + {% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %} + {% if entry.header.location_city and entry.header.location_country %}, {% endif %} + {% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %} + + {% endif %} +
+
+ {% else %} +
+ + {% if entry.header.location_city or entry.header.location_country %} + + {%- set _loc = [] -%} + {%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%} + {%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%} + 📍 {{ _loc|join(', ') }} + + {% endif %} +
+ {% endif %} +
+

{{ entry.title }}

+

{{ entry.summary|striptags|slice(0, 250)|trim }}

+ Read entry → +
+
+``` + +Replace with: + +```twig + {% if item.type == 'journal' %} + {% set weather_icons = { + 'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️', + 'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️', + 'Snow': '❄️', 'Thunderstorm': '⛈️' + } %} +
+
+

{{ 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 }}
+
+``` + +- [ ] **Step 4: Run M7 to verify it passes** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:" +``` + +Expected: PASS. + +- [ ] **Step 5: Run full suite** + +```bash +npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js +``` + +Expected: all pass. + +- [ ] **Step 6: Commit** + +```bash +cd /home/mischa/Projects/travel-blog-intotheeast/user +git add themes/intotheeast/templates/trip.html.twig +git commit -m "feat: replace journal entry card with inline journal-post in trip feed" + +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +git add tests/ui/maps.spec.js user +git commit -m "test: update M7 selector for journal-post.is-highlighted" +``` + +--- + +### Task 4: home.html.twig + H1 test + +Replace the journal entry card in `home.html.twig` and add a minimal home page test. + +**Files:** +- Modify: `user/themes/intotheeast/templates/home.html.twig` +- Create: `tests/ui/home.spec.js` + +**Interfaces:** +- Consumes: `.journal-post` CSS from Task 1; journal-post HTML pattern from Task 2 + +- [ ] **Step 1: Write the failing H1 test** + +Create `tests/ui/home.spec.js`: + +```js +// @ts-check +// Tests: H1 — home page journal feed +const { test, expect } = require('@playwright/test'); + +// ── H1: Home page renders inline journal posts ───────────────────────────────── +test('H1: home page shows at least one inline journal-post block', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('.journal-post').first()).toBeVisible(); + await expect(page.locator('.site-header')).toBeVisible(); +}); +``` + +- [ ] **Step 2: Run H1 to verify it fails** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +npx playwright test --project=chromium tests/ui/home.spec.js +``` + +Expected: FAIL — `.journal-post` not found (home page still renders ``). + +- [ ] **Step 3: Replace journal card in `home.html.twig`** + +In `user/themes/intotheeast/templates/home.html.twig`, find the journal branch: + +```twig + {% if item.type == 'journal' %} + + {% if hero %} +
+ {{ entry.title }} +
+ + {% if entry.header.location_city or entry.header.location_country %} + + 📍 + {% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %} + {% if entry.header.location_city and entry.header.location_country %}, {% endif %} + {% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %} + + {% endif %} +
+
+ {% else %} +
+ + {% if entry.header.location_city or entry.header.location_country %} + + {%- set _loc = [] -%} + {%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%} + {%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%} + 📍 {{ _loc|join(', ') }} + + {% endif %} +
+ {% endif %} +
+

{{ entry.title }}

+

{{ entry.summary|striptags|slice(0, 250)|trim }}

+ Read entry → +
+
+``` + +Replace with: + +```twig + {% if item.type == 'journal' %} + {% set weather_icons = { + 'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️', + 'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️', + 'Snow': '❄️', 'Thunderstorm': '⛈️' + } %} +
+
+

{{ 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 }}
+
+``` + +Note: `home.html.twig` journal posts do **not** include `data-type` (the home page has no filter bar) — this matches the existing `` on home which also had no `data-type`. + +- [ ] **Step 4: Run H1 to verify it passes** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +npx playwright test --project=chromium tests/ui/home.spec.js +``` + +Expected: PASS. + +- [ ] **Step 5: Run the full suite** + +```bash +npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/maps.spec.js tests/ui/trip-filter.spec.js tests/ui/stories.spec.js tests/ui/home.spec.js +``` + +Expected: all pass. + +- [ ] **Step 6: Commit** + +```bash +cd /home/mischa/Projects/travel-blog-intotheeast/user +git add themes/intotheeast/templates/home.html.twig +git commit -m "feat: replace journal entry card with inline journal-post on home page" + +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +git add tests/ui/home.spec.js user +git commit -m "test: add H1 home page journal-post test" +```