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