Files
intotheeast-com/docs/superpowers/plans/2026-06-20-ui-ux-alignment.md
T

627 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UI/UX Alignment 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.
**Status:** ✅ Complete (2026-06-20) — also extended to story map markers (white diamond) and story card flash highlight.
**Goal:** Unify three micro-interaction patterns across the site: back navigation pills, card hover lift, and a map-to-card flash highlight.
**Architecture:** CSS-first — shared `.back-pill` class drives visual consistency; entry card markup is collapsed from a two-level `article > a` to a flat `<a>` to align hover targets across all three card types; map flash is a short CSS keyframe triggered by a JS-added class.
**Tech Stack:** Twig templates, vanilla CSS custom properties, vanilla JS, Playwright for tests.
## Global Constraints
- Dev server: `http://localhost:8081` — must be running (`make start`) before any Playwright run
- Playwright: `npx playwright test --project=chromium tests/ui/<file>.spec.js` — always run the affected spec after changes
- Demo data required for story/map tests: `make demo-load`
- All CSS uses design tokens from `user/themes/intotheeast/css/tokens.css` — never hard-code colours
- `--color-paper: #1A1814`, `--color-canvas: #22201B`, `--color-ink: #EDE8DF` — the site is dark-themed
- `--site-header-height: 60px` — fixed pills must clear the site nav
- Never read `.env` directly
---
## File Map
| File | What changes |
|---|---|
| `user/themes/intotheeast/css/style.css` | Add `.back-pill` class; remove duplicate `.story-escape` block; migrate `.entry-card-inner` hover rules to `.entry-card`; add uniform card hover lift; add `@keyframes card-highlight` |
| `user/themes/intotheeast/templates/story.html.twig` | Add `class="back-pill"` to story-footer back link (line 61) |
| `user/themes/intotheeast/templates/entry.html.twig` | Add fixed top back pill before `<article class="entry">`; replace footer teal link with `.back-pill`; add `.entry-back-fixed` CSS |
| `user/themes/intotheeast/templates/trip.html.twig` | Collapse `<article class="entry-card"><a class="entry-card-inner">` to `<a class="entry-card">` for both card variants; update marker click handler with flash delay |
| `tests/ui/dailies.spec.js` | Update T2 selectors from `.entry-card a[href*="..."]` to `.entry-card[href*="..."]`; add T6 (back pills on entry page) |
| `tests/ui/maps.spec.js` | Add M7 (marker click adds `is-highlighted` class) |
---
## Task 1: CSS foundation — `.back-pill`, card hover lift, flash keyframe, story-escape cleanup
**Files:**
- Modify: `user/themes/intotheeast/css/style.css`
**Interfaces:**
- Produces: `.back-pill` class (surface pill), `.entry-card.is-highlighted` animation, uniform hover lift on `.trip-card:hover`, `.entry-card:hover`, `.story-card:hover`
- [x] **Step 1: Add `.back-pill` surface pill class**
Find the `/* ── Back to top pill ──` section (around line 1217). Insert the following block immediately **before** it:
```css
/* ── Back pill (shared navigation pill component) ───────────────────── */
.back-pill {
display: inline-flex;
align-items: center;
font-family: var(--font-ui);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-ink);
text-decoration: none;
background: var(--color-canvas);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
padding: 0.4rem 0.9rem;
transition: border-color 0.15s, color 0.15s;
cursor: pointer;
}
.back-pill:hover { border-color: var(--color-accent); color: var(--color-accent); }
```
- [x] **Step 2: Remove the duplicate `.story-escape` block**
Around line 958 there is a `/* ── Story page escape link ──` section with a `.story-escape` rule that is overridden later by the story-section block. Remove this entire section:
```css
/* ── Story page escape link ──────────────────────────────────────────────────── */
.story-escape {
position: fixed;
top: var(--space-5);
left: var(--space-5);
z-index: 200;
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-ink);
text-decoration: none;
background: rgba(0,0,0,0.6);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-full);
backdrop-filter: blur(4px);
}
.story-escape:hover { color: var(--color-accent); }
```
The authoritative `.story-escape` definition remains in the `/* ── Story pages ──` section (~line 1056).
- [x] **Step 3: Add uniform card hover lift + fix story-card transition**
Find the `.trip-card:hover` rule (in `/* ── Past trips archive ──`). After the existing `.trip-card:hover` block, add:
```css
.trip-card:hover,
.entry-card:hover,
.story-card:hover {
background: var(--color-surface-raised);
}
```
Then find `.story-card` in the `/* ── Stories listing ──` section and add `background 0.15s` to its existing transition so the lift animates:
```css
/* Before: */
.story-card {
...
transition: box-shadow 0.2s;
}
/* After: */
.story-card {
...
transition: box-shadow 0.2s, background 0.15s;
}
```
- [x] **Step 4: Add map flash keyframe**
At the end of the `/* ── Feed ──` section (after `.entry-card` and related rules, around line 210), add:
```css
@keyframes card-highlight {
0% { background-color: color-mix(in srgb, var(--color-accent) 12%, transparent); }
100% { background-color: transparent; }
}
.entry-card.is-highlighted {
animation: card-highlight 0.7s ease-out forwards;
}
```
- [x] **Step 5: Verify no JS errors on the site**
```bash
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M1:"
```
Expected: PASS (map page loads without errors — confirms CSS is valid).
- [x] **Step 6: Commit**
```bash
git add user/themes/intotheeast/css/style.css
git commit -m "feat: add back-pill class, card hover lift, flash keyframe; remove duplicate story-escape"
```
---
## Task 2: Story template — apply `.back-pill` to body back link
**Files:**
- Modify: `user/themes/intotheeast/templates/story.html.twig`
**Interfaces:**
- Consumes: `.back-pill` class from Task 1
- [x] **Step 1: Write the failing test**
Add to `tests/ui/stories.spec.js`:
```js
// ── S7: Story body back link is styled as a back-pill ────────────────────────
test('S7: story body back link has back-pill class', async ({ page }) => {
await page.goto('/trips/italy-2025/stories/val-dorcia-dawn');
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
// Scroll past the hero to reveal the story body
await page.evaluate(() => window.scrollBy(0, window.innerHeight * 1.5));
await page.waitForTimeout(300);
const bodyBack = page.locator('.story-footer .back-pill');
await expect(bodyBack).toBeAttached();
await expect(bodyBack).toHaveText(/← Back/);
});
```
- [x] **Step 2: Run test to verify it fails**
```bash
npx playwright test --project=chromium tests/ui/stories.spec.js -g "S7:"
```
Expected: FAIL — "locator('.story-footer .back-pill')" found 0 elements.
- [x] **Step 3: Apply `.back-pill` to the story footer back link + fix `.story-footer a` conflict**
In `story.html.twig`, the story footer currently reads:
```twig
<footer class="story-footer">
<a href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
```
Change the `<a>` to:
```twig
<footer class="story-footer">
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
```
Then in `style.css`, find `.story-footer a` and add `:not(.back-pill)` so it no longer overrides the pill colour:
```css
/* Before: */
.story-footer a {
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--color-accent);
text-decoration: none;
}
/* After: */
.story-footer a:not(.back-pill) {
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--color-accent);
text-decoration: none;
}
```
- [x] **Step 4: Run test to verify it passes**
```bash
npx playwright test --project=chromium tests/ui/stories.spec.js -g "S7:"
```
Expected: PASS.
- [x] **Step 5: Run full stories suite to check no regressions**
```bash
npx playwright test --project=chromium tests/ui/stories.spec.js
```
Expected: All S1S7 pass.
- [x] **Step 6: Commit**
```bash
git add user/themes/intotheeast/templates/story.html.twig user/themes/intotheeast/css/style.css tests/ui/stories.spec.js
git commit -m "feat: apply back-pill class to story footer back link"
```
---
## Task 3: Entry page — fixed top back pill + footer back pill
**Files:**
- Modify: `user/themes/intotheeast/templates/entry.html.twig`
- Modify: `user/themes/intotheeast/css/style.css`
- Modify: `tests/ui/dailies.spec.js`
**Interfaces:**
- Consumes: `.back-pill` class from Task 1
- [x] **Step 1: Write the failing test**
Add to `tests/ui/dailies.spec.js`:
```js
const KNOWN_ENTRY = '/trips/japan-korea-2026/dailies/2026-03-25-1540-wheels-down-narita.entry';
// ── T6: Entry page has a fixed top back pill and a footer back pill ───────────
test('T6: entry page has fixed back pill at top and back pill in footer', async ({ page }) => {
await page.goto(KNOWN_ENTRY);
await expect(page.locator('article.entry')).toBeVisible();
// Fixed top pill (outside the article, before it)
const topPill = page.locator('.entry-back-fixed');
await expect(topPill).toBeVisible();
await expect(topPill).toHaveText(/← Back/);
// Footer pill
const footerPill = page.locator('.entry-footer .back-pill');
await expect(footerPill).toBeVisible();
await expect(footerPill).toHaveText(/← Back/);
});
```
- [x] **Step 2: Run test to verify it fails**
```bash
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T6:"
```
Expected: FAIL — `.entry-back-fixed` not found.
- [x] **Step 3: Add fixed top back pill to entry template**
In `entry.html.twig`, the content block currently starts with `<article class="entry">`. Add the fixed pill immediately before it:
```twig
<a class="back-pill entry-back-fixed" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
<article class="entry">
```
- [x] **Step 4: Replace footer teal text link with `.back-pill`**
The current entry footer (around line 124 of entry.html.twig):
```twig
<footer class="entry-footer">
<a href="{{ page.parent().url }}" onclick="if(history.length>1){event.preventDefault();history.back()}">← Back</a>
</footer>
```
Replace with:
```twig
<footer class="entry-footer">
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
</footer>
```
- [x] **Step 5: Add `.entry-back-fixed` positioning to CSS**
In `style.css`, in the `/* ── Single entry ──` section, add after the existing `.entry-hero` rules:
```css
.entry-back-fixed {
position: fixed;
top: calc(var(--site-header-height) + var(--space-3));
left: var(--space-4);
z-index: 100;
}
```
- [x] **Step 6: Run test to verify it passes**
```bash
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T6:"
```
Expected: PASS.
- [x] **Step 7: Run full dailies suite to check no regressions**
```bash
npx playwright test --project=chromium tests/ui/dailies.spec.js
```
Expected: T1T6 all pass.
- [x] **Step 8: Commit**
```bash
git add user/themes/intotheeast/templates/entry.html.twig user/themes/intotheeast/css/style.css tests/ui/dailies.spec.js
git commit -m "feat: add fixed top and footer back pills to entry page"
```
---
## Task 4: Entry card structural refactor + CSS migration
Collapse the two-level `<article class="entry-card"><a class="entry-card-inner">` to a flat `<a class="entry-card">`, matching the structure of trip and story cards.
**Files:**
- Modify: `user/themes/intotheeast/templates/trip.html.twig`
- Modify: `user/themes/intotheeast/css/style.css`
- Modify: `tests/ui/dailies.spec.js`
**Interfaces:**
- Produces: `.entry-card` is now an `<a>` element; `id`, `data-type`, `data-lat`, `data-lng` attributes remain on the card root; `.entry-card-inner` class is eliminated
- [x] **Step 1: Update T2 test selectors before touching the templates**
In `tests/ui/dailies.spec.js`, find the T2 test and replace:
```js
// OLD — inner <a> is nested inside .entry-card
const newerCard = page.locator(`.entry-card a[href*="${NEWER_SLUG}"]`);
const olderCard = page.locator(`.entry-card a[href*="${OLDER_SLUG}"]`);
await expect(newerCard).toBeVisible();
await expect(olderCard).toBeVisible();
const newerIdx = await newerCard.evaluate(el => {
return [...document.querySelectorAll('.entry-card')].findIndex(c => c.contains(el));
});
const olderIdx = await olderCard.evaluate(el => {
return [...document.querySelectorAll('.entry-card')].findIndex(c => c.contains(el));
});
```
With:
```js
// NEW — .entry-card is itself the <a>
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();
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);
});
```
- [x] **Step 2: Run T2 to verify it fails (not yet refactored)**
```bash
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T2:"
```
Expected: FAIL — `.entry-card[href*="..."]` finds 0 elements (the `href` is on the inner `<a>`, not the article).
- [x] **Step 3: Refactor journal entry card markup in `trip.html.twig`**
Find the journal card block:
```twig
{% if item.type == 'journal' %}
<article class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
<a class="entry-card-inner" href="{{ entry.url }}">
```
Replace with:
```twig
{% if item.type == 'journal' %}
<a class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">
```
And close the card with `</a>` instead of `</a></article>`. The closing tags currently are:
```twig
</a>
</article>
```
Replace with:
```twig
</a>
```
- [x] **Step 4: Refactor story-in-feed card markup in `trip.html.twig`**
Find the story-in-feed card block:
```twig
<article class="entry-card entry-card--story" id="entry-{{ entry.slug }}" data-type="story">
<a class="entry-card-inner" href="{{ entry.url }}">
```
Replace with:
```twig
<a class="entry-card entry-card--story" id="entry-{{ entry.slug }}" data-type="story" href="{{ entry.url }}">
```
And its closing tags (currently `</a></article>`) become:
```twig
</a>
```
- [x] **Step 5: Migrate `.entry-card-inner` CSS rules to `.entry-card`**
In `style.css`, find the `/* ── Feed ──` section. Currently:
```css
.entry-card { border-bottom: 1px solid var(--color-border); padding-bottom: var(--space-12); }
.entry-card-inner {
display: block;
text-decoration: none;
color: inherit;
}
```
Replace with (merge inner styles onto card, `.entry-card-inner` is eliminated):
```css
.entry-card {
display: block;
text-decoration: none;
color: inherit;
border-bottom: 1px solid var(--color-border);
padding-bottom: var(--space-12);
transition: background 0.15s;
}
```
Then find the two `.entry-card-inner:hover` rules and rename them to `.entry-card:hover`:
```css
/* Before: */
.entry-card-inner:hover .entry-card-photo img { transform: scale(1.04); }
/* After: */
.entry-card:hover .entry-card-photo img { transform: scale(1.04); }
```
```css
/* Before: */
.entry-card-inner:hover .entry-title { color: var(--color-accent); }
/* After: */
.entry-card:hover .entry-title { color: var(--color-accent); }
```
- [x] **Step 6: Run T2 to verify it passes**
```bash
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T2:"
```
Expected: PASS.
- [x] **Step 7: Run full test suites to check no regressions**
```bash
npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/trip-filter.spec.js tests/ui/maps.spec.js
```
Expected: T1T6, F1F7, M1M6 all pass.
- [x] **Step 8: Commit**
```bash
git add user/themes/intotheeast/templates/trip.html.twig user/themes/intotheeast/css/style.css tests/ui/dailies.spec.js
git commit -m "refactor: collapse entry card article+a to flat <a>, unify hover targets across card types"
```
---
## Task 5: Map flash — JS update + test
**Files:**
- Modify: `user/themes/intotheeast/templates/trip.html.twig`
- Modify: `tests/ui/maps.spec.js`
**Interfaces:**
- Consumes: `.entry-card.is-highlighted` CSS animation from Task 1; `id="entry-{{ slug }}"` on `<a class="entry-card">` from Task 4
- [x] **Step 1: Write the failing test**
Add to `tests/ui/maps.spec.js`:
```js
// ── M7: Clicking a trip-page map marker adds is-highlighted to the entry card ──
test('M7: clicking map marker briefly highlights the corresponding entry card', async ({ page }) => {
await page.goto('/trips/japan-korea-2026');
// Wait for map canvas and at least one marker
await expect(page.locator('#trip-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
await expect(page.locator('.maplibregl-marker').first()).toBeVisible({ timeout: 15000 });
// Click the first marker
await page.locator('.maplibregl-marker').first().click();
// Within 500ms of click + delay, one entry-card should have is-highlighted
await expect(page.locator('.entry-card.is-highlighted')).toBeVisible({ timeout: 1500 });
});
```
- [x] **Step 2: Run test to verify it fails**
```bash
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:"
```
Expected: FAIL — `.entry-card.is-highlighted` not found.
- [x] **Step 3: Update the marker click handler in `trip.html.twig`**
Find the existing marker click handler in `trip.html.twig`:
```js
el.addEventListener('click', function () {
var card = document.getElementById('entry-' + entry.slug);
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
```
Replace with:
```js
el.addEventListener('click', function () {
var card = document.getElementById('entry-' + entry.slug);
if (!card) return;
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(function () {
card.classList.add('is-highlighted');
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
}, 350);
});
```
- [x] **Step 4: Run test to verify it passes**
```bash
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:"
```
Expected: PASS.
- [x] **Step 5: Run full maps suite**
```bash
npx playwright test --project=chromium tests/ui/maps.spec.js
```
Expected: M1M7 all pass.
- [x] **Step 6: Run all affected suites for final check**
```bash
npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/trip-filter.spec.js tests/ui/maps.spec.js tests/ui/stories.spec.js
```
Expected: All pass.
- [x] **Step 7: Commit**
```bash
git add user/themes/intotheeast/templates/trip.html.twig tests/ui/maps.spec.js
git commit -m "feat: add map-to-card flash highlight on marker click"
```