docs: add UI/UX alignment design spec (back pills, card hover, map flash)
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
# UI/UX Alignment — Design Spec
|
||||||
|
|
||||||
|
*2026-06-20*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Unify three disconnected micro-interaction patterns across the site:
|
||||||
|
|
||||||
|
1. **Back navigation** — inconsistent style and position across story and entry pages
|
||||||
|
2. **Card hover** — inconsistent lift behaviour and structural divergence across the three card types
|
||||||
|
3. **Map flash** — no visual feedback after the feed scrolls to a marker-targeted card
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Back pill system
|
||||||
|
|
||||||
|
### Canonical pill component
|
||||||
|
|
||||||
|
The site is dark-themed (`--color-paper: #1A1814`, `--color-ink: #EDE8DF` cream). Two visual variants of a single pill component, chosen by what is behind it:
|
||||||
|
|
||||||
|
**Surface pill** (sits on the dark paper/canvas background):
|
||||||
|
```css
|
||||||
|
background: var(--color-canvas);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
color: var(--color-ink);
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.15s, color 0.15s;
|
||||||
|
```
|
||||||
|
Hover: `border-color: var(--color-accent); color: var(--color-accent)`
|
||||||
|
|
||||||
|
**Overlay pill** (sits on top of a hero photo):
|
||||||
|
```css
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
color: var(--color-ink);
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
```
|
||||||
|
Hover: `color: var(--color-accent)`
|
||||||
|
|
||||||
|
The `.story-totop` button already matches the surface pill tokens (`--color-canvas` bg, `--color-border` border, `--color-ink` text) — it becomes part of this system without visual changes.
|
||||||
|
|
||||||
|
### Pill inventory
|
||||||
|
|
||||||
|
| Element | Page | Variant | Position | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `.story-escape` | story | overlay | `fixed`, top-left | Overlays hero; keep as-is |
|
||||||
|
| `← Back` in story body | story | surface | `static`, below-hero body section | Apply surface pill class |
|
||||||
|
| Entry top back | entry | surface | `fixed`, `top: calc(var(--site-header-height) + var(--space-3))`, left | New element |
|
||||||
|
| Entry footer back | entry | surface | `static`, in `.entry-footer` | Replaces current teal text link |
|
||||||
|
| `.story-totop` | story | surface | `fixed`, bottom-right | Existing; bring into token system |
|
||||||
|
|
||||||
|
### Shared behaviour
|
||||||
|
|
||||||
|
All back pills use the same `onclick` pattern already present on `.story-escape`:
|
||||||
|
```js
|
||||||
|
onclick="if(history.length > 1){ history.back(); return false; }"
|
||||||
|
```
|
||||||
|
Fallback `href` is always `page.parent().url`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Card hover unification
|
||||||
|
|
||||||
|
### Structural fix — entry card markup
|
||||||
|
|
||||||
|
Entry cards currently use a two-level structure (`<article>` wrapping `<a class="entry-card-inner">`), which causes the hover target to differ from trip and story cards. This diverges for no functional reason — `id` and `data-*` attributes are valid on `<a>` elements.
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```html
|
||||||
|
<article class="entry-card" id="entry-{{ entry.slug }}"
|
||||||
|
data-type="journal" data-lat="..." data-lng="...">
|
||||||
|
<a class="entry-card-inner" href="{{ entry.url }}">
|
||||||
|
...
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```html
|
||||||
|
<a class="entry-card" id="entry-{{ entry.slug }}"
|
||||||
|
data-type="journal" data-lat="..." data-lng="..."
|
||||||
|
href="{{ entry.url }}">
|
||||||
|
...
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
The class `.entry-card-inner` is eliminated. All CSS rules previously on `.entry-card-inner` move to `.entry-card`. The map's `document.getElementById('entry-' + slug)` continues to work unchanged.
|
||||||
|
|
||||||
|
The story variant card in the trip feed (`entry-card--story`) follows the same structural change.
|
||||||
|
|
||||||
|
### Hover pattern
|
||||||
|
|
||||||
|
All three card root elements get a uniform background lift:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.trip-card:hover,
|
||||||
|
.entry-card:hover,
|
||||||
|
.story-card:hover {
|
||||||
|
background: var(--color-surface-raised);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing per-card effects are additive on top of the lift:
|
||||||
|
- **Entry card**: photo zoom (`transform: scale(1.04)`) + title tint (`color: var(--color-accent)`) — keep
|
||||||
|
- **Story card**: shadow (`box-shadow: var(--shadow-md)`) — keep
|
||||||
|
- **Trip card**: border accent (`border-color: var(--color-accent)`) — keep
|
||||||
|
|
||||||
|
Transition values align across all three cards: `transition: background 0.15s, border-color 0.15s`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Map flash
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
After clicking a marker on the trip page mini-map, `scrollIntoView({ behavior: 'smooth', block: 'center' })` scrolls the feed but provides no visual confirmation of which card arrived.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
|
||||||
|
A 700ms keyframe animation adds a faint teal wash to the targeted card, delayed 350ms after the click to let the scroll complete first.
|
||||||
|
|
||||||
|
**CSS:**
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**JS (in `trip.html.twig`, marker click handler):**
|
||||||
|
```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);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `is-highlighted` class is removed after the animation so it can re-trigger on repeated clicks of the same marker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Semantics/accessibility audit of feed list containers and landmark roles (logged as backlog)
|
||||||
|
- `<article>` element on full entry/story pages (logged as backlog)
|
||||||
|
- `.story-totop` behaviour changes — visual tokens only
|
||||||
|
- Dark mode variants of the new light pills
|
||||||
Reference in New Issue
Block a user