docs: add mobile-ux session learnings and shared partial architecture
This commit is contained in:
@@ -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
|
- 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
|
- 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 `#<card_prefix><slug>` + flashes `.is-highlighted`.
|
||||||
|
|
||||||
### GPX file management
|
### GPX file management
|
||||||
|
|
||||||
GPX files are stored as page media on the trip page (`user/pages/01.trips/<slug>/`). They are picked up automatically by `map.html.twig` via `trip_page.media.all`.
|
GPX files are stored as page media on the trip page (`user/pages/01.trips/<slug>/`). They are picked up automatically by `map.html.twig` via `trip_page.media.all`.
|
||||||
|
|||||||
@@ -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 `<details>` 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.
|
||||||
Reference in New Issue
Block a user