docs: restructure docs/ into guides/ reference/ working/ research/

This commit is contained in:
2026-06-21 12:36:35 +02:00
parent 647f76333d
commit 28008da922
35 changed files with 0 additions and 0 deletions
@@ -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 `<a class="entry-card entry-card--story">`, 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 24 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 `<a>` 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
<article class="journal-post" id="entry-{{ entry.slug }}"
data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
<header class="journal-post-header">
<h2 class="journal-post-title">{{ entry.title }}</h2>
<p class="journal-post-meta">
<a class="journal-post-permalink" href="{{ entry.url }}">
<time datetime="{{ entry.date|date('Y-m-d') }}">{{ entry.date|date('d M Y')|upper }}</time>
</a>
{% if entry.header.location_city or entry.header.location_country %}
<span class="journal-post-location">
· 📍
{%- 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(', ') }}
</span>
{% endif %}
{% if entry.header.weather_desc %}
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
{% endif %}
</p>
</header>
{% set images = entry.media.images %}
{% if images|length > 0 %}
<div class="journal-photo-strip" data-slides="{{ images|length }}">
{% for img in images %}
<div class="journal-photo-slide">
<img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
</div>
{% endfor %}
</div>
{% if images|length > 1 %}
<div class="journal-photo-dots" aria-hidden="true">
{% for img in images %}
<span class="journal-photo-dot{% if loop.first %} is-active{% endif %}"></span>
{% endfor %}
</div>
{% endif %}
{% endif %}
<div class="journal-post-body">{{ entry.content|raw }}</div>
</article>
```
**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 `<article>` root replaces the old `<a class="entry-card">` — 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 `</body>`. It is a no-op on pages with no strips.
```html
<script>
(function () {
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
var dots = strip.nextElementSibling;
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
strip.addEventListener('scroll', function () {
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
}, { passive: true });
});
})();
</script>
```
---
## 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 <a> 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 <article>, not <a>)
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)