diff --git a/CLAUDE.md b/CLAUDE.md index 498cc59..959a77e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,28 @@ The site is structured around Trip entities. Key facts: - GPX route files live as media on the trip page itself, served via leaflet-gpx CDN - Manage GPX files (view/upload/delete) at `/gpx-manager` — requires admin login; filenames are auto-slugified on upload +### Shared feed-map partial + +The mini-map above the feed is shared across two pages via a Twig partial: + +- **Partial:** `user/themes/intotheeast/templates/partials/feed-map.html.twig` +- **Used by:** `dailies.html.twig` and `stories.html.twig` +- **NOT used by:** `trip.html.twig` (uses its own `#trip-map` / `.home-map-col` layout) + +**Parameters (passed via `{% include ... with {...} only %}`):** + +| Parameter | Type | Description | +|---|---|---| +| `map_entries` | array | `[{lat, lng, title, slug, url, type, force_connect, transport_mode}]` | +| `map_id` | string | HTML id for map div: `'feed-map'` or `'stories-map'` | +| `map_var` | string | JS global variable: `'feedMap'` or `'storiesMap'` | +| `link_href` | string\|null | "View full map" link URL; `null` hides it | +| `card_prefix` | string | Scroll-to ID prefix: `'entry-'` (dailies) or `'story-'` (stories) | +| `trip_page` | Page | Trip page object for autoconnect setting | +| `show_journey` | bool | `true` draws the route connector; `false` skips it | + +The partial always: starts attribution collapsed, shows the fullscreen button (mobile-only, CSS `display:none` ≥769px), and on marker click scrolls to `#` + flashes `.is-highlighted`. + ### GPX file management GPX files are stored as page media on the trip page (`user/pages/01.trips//`). They are picked up automatically by `map.html.twig` via `trip_page.media.all`. diff --git a/docs/working/learnings/2026-06-22-mobile-ux-learnings.md b/docs/working/learnings/2026-06-22-mobile-ux-learnings.md new file mode 100644 index 0000000..4289d4c --- /dev/null +++ b/docs/working/learnings/2026-06-22-mobile-ux-learnings.md @@ -0,0 +1,117 @@ +# Mobile UX Session Learnings — 2026-06-22 + +Discoveries from the mobile polish session (stat scaling, map fullscreen, panel toggles, shared partials). + +## MapLibre GL JS v4 — Attribution starts expanded despite compact: true + +**Problem:** `new maplibregl.AttributionControl({ compact: true })` renders a `
` element. In MapLibre v4, this element has `open` set after `map.on('load')` fires, so the attribution panel starts expanded even though `compact: true` was passed. + +**Fix:** In the `load` handler, explicitly remove the `open` attribute: +```js +map.on('load', function () { + var attrib = map.getContainer().querySelector('.maplibregl-ctrl-attrib'); + if (attrib) attrib.removeAttribute('open'); +}); +``` + +**Also:** To avoid the default attribution control conflicting with a custom button in `bottom-right`, disable it in the constructor and add it manually to `bottom-left`: +```js +var map = new maplibregl.Map({ ..., attributionControl: false }); +map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left'); +``` + +## CSS Panel Animation — max-height beats grid-template-rows: 0fr + +**Problem:** `grid-template-rows: 0fr → 1fr` transition fails when the direct grid child has `overflow: hidden`. The child creates a Block Formatting Context (BFC) that prevents `0fr` from collapsing to zero height. + +**Fix:** Use `max-height` transition on the outer container: +```css +.panel { + max-height: 0; + overflow: hidden; + transition: max-height 0.4s ease; +} +.panel.is-open { + max-height: 600px; +} +``` + +## Fluid Font Sizing with clamp() + +```css +.stat-value { + font-size: clamp(2rem, 6vw, var(--text-3xl)); +} +``` + +- `clamp(min, preferred, max)`: scales linearly between min and max +- `6vw` at 333px viewport = 20px = 1.25rem, but floor is 2rem (32px) +- Keep labels at `--text-xs` (0.75rem) intentionally — the contrast makes values pop + +## CSS Grid — Spanning the Lone Last Item in a 2-Column Grid + +```css +@media (max-width: 600px) { + .my-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .my-grid .item:last-child:nth-child(odd) { grid-column: 1 / -1; } +} +``` + +- `minmax(0, 1fr)` — strictly equal columns (bare `1fr` has a hidden `auto` minimum) +- `:last-child:nth-child(odd)` — matches an item that is both last and in an odd position + +## PhotoSwipe v5 — Correct Element for CSS Animations + +**Problem:** `pswp.currSlide.el` is `undefined` in PhotoSwipe v5. + +**Fix:** Use `pswp.currSlide.container` — the DOM wrapper for the current slide: +```js +var el = pswp.currSlide && pswp.currSlide.container; +if (!el) return; +el.classList.add('pswp-key-from-right'); +``` + +## Mobile Fullscreen Map Pattern + +```css +.map-col.is-fullscreen { + position: fixed !important; + inset: 0; + z-index: 9999; + height: 100dvh !important; +} +``` + +```js +fsBtn.addEventListener('click', function() { + var isFs = mapCol.classList.toggle('is-fullscreen'); + document.body.style.overflow = isFs ? 'hidden' : ''; + setTimeout(function() { map.resize(); }, 50); +}); +``` + +**Marker click while fullscreen:** Exit fullscreen first, then scroll after the transition: +```js +if (isFullscreen) { + fsBtn.click(); + setTimeout(scrollAndHighlight, 450); +} else { + scrollAndHighlight(); +} +``` + +## Shared Twig Partial Pattern + +```twig +{% include 'partials/feed-map.html.twig' with { + 'map_entries': map_entries, + 'map_id': 'feed-map', + 'map_var': 'feedMap', + 'link_href': page.parent().url ~ '/map', + 'card_prefix': 'entry-', + 'trip_page': trip_page, + 'show_journey': true +} only %} +``` + +Grav's global Twig functions (`url()`, `theme_var()`) remain available with `only`. Only parent template variables are excluded.