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

20 KiB
Raw Blame History

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

  • 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:

/* ── 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); }
  • 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:

/* ── 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).

  • 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:

.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:

/* Before: */
.story-card {
    ...
    transition: box-shadow 0.2s;
}

/* After: */
.story-card {
    ...
    transition: box-shadow 0.2s, background 0.15s;
}
  • Step 4: Add map flash keyframe

At the end of the /* ── Feed ── section (after .entry-card and related rules, around line 210), add:

@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;
}
  • Step 5: Verify no JS errors on the site
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M1:"

Expected: PASS (map page loads without errors — confirms CSS is valid).

  • Step 6: Commit
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

  • Step 1: Write the failing test

Add to tests/ui/stories.spec.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/);
});
  • Step 2: Run test to verify it fails
npx playwright test --project=chromium tests/ui/stories.spec.js -g "S7:"

Expected: FAIL — "locator('.story-footer .back-pill')" found 0 elements.

  • 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:

    <footer class="story-footer">
        <a href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>

Change the <a> to:

    <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:

/* 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;
}
  • Step 4: Run test to verify it passes
npx playwright test --project=chromium tests/ui/stories.spec.js -g "S7:"

Expected: PASS.

  • Step 5: Run full stories suite to check no regressions
npx playwright test --project=chromium tests/ui/stories.spec.js

Expected: All S1S7 pass.

  • Step 6: Commit
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

  • Step 1: Write the failing test

Add to tests/ui/dailies.spec.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/);
});
  • Step 2: Run test to verify it fails
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T6:"

Expected: FAIL — .entry-back-fixed not found.

  • 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:

<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">
  • Step 4: Replace footer teal text link with .back-pill

The current entry footer (around line 124 of entry.html.twig):

    <footer class="entry-footer">
        <a href="{{ page.parent().url }}" onclick="if(history.length>1){event.preventDefault();history.back()}">← Back</a>
    </footer>

Replace with:

    <footer class="entry-footer">
        <a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
    </footer>
  • Step 5: Add .entry-back-fixed positioning to CSS

In style.css, in the /* ── Single entry ── section, add after the existing .entry-hero rules:

.entry-back-fixed {
    position: fixed;
    top: calc(var(--site-header-height) + var(--space-3));
    left: var(--space-4);
    z-index: 100;
}
  • Step 6: Run test to verify it passes
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T6:"

Expected: PASS.

  • Step 7: Run full dailies suite to check no regressions
npx playwright test --project=chromium tests/ui/dailies.spec.js

Expected: T1T6 all pass.

  • Step 8: Commit
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

  • Step 1: Update T2 test selectors before touching the templates

In tests/ui/dailies.spec.js, find the T2 test and replace:

// 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:

// 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);
});
  • Step 2: Run T2 to verify it fails (not yet refactored)
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).

  • Step 3: Refactor journal entry card markup in trip.html.twig

Find the journal card block:

                    {% 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:

                    {% 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:

                        </a>
                    </article>

Replace with:

                    </a>
  • Step 4: Refactor story-in-feed card markup in trip.html.twig

Find the story-in-feed card block:

                    <article class="entry-card entry-card--story" id="entry-{{ entry.slug }}" data-type="story">
                        <a class="entry-card-inner" href="{{ entry.url }}">

Replace with:

                    <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:

                    </a>
  • Step 5: Migrate .entry-card-inner CSS rules to .entry-card

In style.css, find the /* ── Feed ── section. Currently:

.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):

.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:

/* 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); }
/* Before: */
.entry-card-inner:hover .entry-title { color: var(--color-accent); }
/* After:  */
.entry-card:hover .entry-title { color: var(--color-accent); }
  • Step 6: Run T2 to verify it passes
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T2:"

Expected: PASS.

  • Step 7: Run full test suites to check no regressions
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.

  • Step 8: Commit
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

  • Step 1: Write the failing test

Add to tests/ui/maps.spec.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 });
});
  • Step 2: Run test to verify it fails
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:"

Expected: FAIL — .entry-card.is-highlighted not found.

  • Step 3: Update the marker click handler in trip.html.twig

Find the existing marker click handler in trip.html.twig:

        el.addEventListener('click', function () {
            var card = document.getElementById('entry-' + entry.slug);
            if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
        });

Replace with:

        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);
        });
  • Step 4: Run test to verify it passes
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:"

Expected: PASS.

  • Step 5: Run full maps suite
npx playwright test --project=chromium tests/ui/maps.spec.js

Expected: M1M7 all pass.

  • Step 6: Run all affected suites for final check
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.

  • Step 7: Commit
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"