docs: restructure docs/ into guides/ reference/ working/ research/
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
# MapLibre GL Migration 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)
|
||||
|
||||
**Goal:** Replace Leaflet JS across all three maps (full map, mini-map on dailies, home page map) with MapLibre GL JS, add an animated journey line, and improve map CSS using our design tokens.
|
||||
|
||||
**Architecture:** A shared JS utility file (`maplibre-utils.js`) provides `animateJourneyLine`, `addJourneyLine`, and `createDotMarker` — reused by all three map templates. Each template loads MapLibre GL + the utility file, then calls these helpers. GPX rendering switches from `leaflet-gpx` to `@mapbox/togeojson` + MapLibre GeoJSON layers.
|
||||
|
||||
**Tech Stack:** MapLibre GL JS 4.x (CDN), `@mapbox/togeojson` 0.16.2 (CDN), CARTO dark-matter vector style (free, no key), vanilla JS (no framework).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- MapLibre GL CDN: `https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js` and `.css`
|
||||
- toGeoJSON CDN: `https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js`
|
||||
- Map tile style URL: `https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json`
|
||||
- Accent colour (journey line, markers): `#2A8C73` — matches `--color-accent` in `tokens.css`
|
||||
- Latest-entry marker accent: `#155244` (same as current Leaflet code)
|
||||
- Animation duration: 5000ms, ease-out cubic
|
||||
- Respect `prefers-reduced-motion: reduce` — skip animation, show full line immediately
|
||||
- `cooperativeGestures` on embedded maps (mini-map, home map); full-page map uses default (free) gestures
|
||||
- No new Grav plugins, no npm — CDN only
|
||||
- Run `make content-push` after changes to sync to production git repo
|
||||
|
||||
---
|
||||
|
||||
### Task 1: CSS — Remove Leaflet override, add MapLibre design-token styles
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/css/style.css` (around line 371)
|
||||
|
||||
**What:** Delete the one Leaflet-specific rule and add a MapLibre CSS block that styles navigation controls, attribution bar, popups, and cursor using design tokens.
|
||||
|
||||
- [x] **Open style.css and find the Leaflet block**
|
||||
|
||||
Locate (around line 371):
|
||||
```css
|
||||
/* match CartoDB dark tile background so no grey flash on load/zoom */
|
||||
.leaflet-container { background: #282828 !important; }
|
||||
```
|
||||
|
||||
- [x] **Delete that rule and replace with the MapLibre block**
|
||||
|
||||
Delete the line above. Immediately after the `.map-empty { ... }` block (around line 381), add:
|
||||
|
||||
```css
|
||||
/* ── MapLibre GL overrides ───────────────────────────────────────────────── */
|
||||
|
||||
/* Navigation controls (zoom +/−) */
|
||||
.maplibregl-ctrl-group {
|
||||
background: var(--color-canvas);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.maplibregl-ctrl-group button {
|
||||
color: var(--color-ink-2);
|
||||
}
|
||||
.maplibregl-ctrl-group button:hover {
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.maplibregl-ctrl-group button + button {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Attribution bar */
|
||||
.maplibregl-ctrl-attrib {
|
||||
background: rgba(26, 24, 20, 0.75) !important;
|
||||
color: var(--color-ink-muted) !important;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.7rem;
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
.maplibregl-ctrl-attrib a {
|
||||
color: var(--color-accent) !important;
|
||||
}
|
||||
|
||||
/* Popup */
|
||||
.maplibregl-popup-content {
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-ui);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
.maplibregl-popup-tip {
|
||||
border-top-color: var(--color-canvas) !important;
|
||||
}
|
||||
.maplibregl-popup-close-button {
|
||||
color: var(--color-ink-muted);
|
||||
font-size: 1.1rem;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
}
|
||||
.maplibregl-popup-close-button:hover {
|
||||
color: var(--color-ink);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Cursor */
|
||||
.maplibregl-canvas-container.maplibregl-interactive { cursor: grab; }
|
||||
.maplibregl-canvas-container.maplibregl-interactive:active { cursor: grabbing; }
|
||||
```
|
||||
|
||||
- [x] **Verify: open `http://localhost:8081/map` in browser**
|
||||
|
||||
If no entries exist, run `make demo-load` first. Check:
|
||||
- No JS errors in console
|
||||
- Page layout unchanged (map still fills viewport below nav)
|
||||
|
||||
- [x] **Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/css/style.css
|
||||
git -C user commit -m "style: swap Leaflet CSS override for MapLibre design-token styles"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Shared JS utilities file
|
||||
|
||||
**Files:**
|
||||
- Create: `user/themes/intotheeast/js/maplibre-utils.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `window.MapUtils.animateJourneyLine(map, coords, sourceId)`, `window.MapUtils.addJourneyLine(map, coords, sourceId)`, `window.MapUtils.createDotMarker(isLatest)`, `window.MapUtils.MAP_STYLE`, `window.MapUtils.ACCENT`
|
||||
- Loaded by: all three map templates via `<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>`
|
||||
|
||||
**What:** Extract the animated journey line logic and marker factory into a single file so all three templates share one implementation.
|
||||
|
||||
- [x] **Create `user/themes/intotheeast/js/maplibre-utils.js`**
|
||||
|
||||
```js
|
||||
/* Shared MapLibre GL utilities — loaded by map.html.twig, dailies.html.twig, home.html.twig */
|
||||
(function (global) {
|
||||
var ACCENT = '#2A8C73';
|
||||
var ACCENT_DIM = '#155244';
|
||||
var MAP_STYLE = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
|
||||
|
||||
/* Build a GeoJSON LineString feature */
|
||||
function lineFeature(coords) {
|
||||
return { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: coords } };
|
||||
}
|
||||
|
||||
/*
|
||||
* Progressively draw the journey line using a requestAnimationFrame loop.
|
||||
* coords: [[lng, lat], ...] in chronological order.
|
||||
* sourceId: the MapLibre source id to update each frame.
|
||||
*/
|
||||
function animateJourneyLine(map, coords, sourceId) {
|
||||
if (coords.length < 2) return;
|
||||
|
||||
/* Cumulative Euclidean distance between waypoints */
|
||||
var segDist = [0];
|
||||
for (var i = 1; i < coords.length; i++) {
|
||||
var dx = coords[i][0] - coords[i - 1][0];
|
||||
var dy = coords[i][1] - coords[i - 1][1];
|
||||
segDist.push(segDist[i - 1] + Math.sqrt(dx * dx + dy * dy));
|
||||
}
|
||||
var totalDist = segDist[segDist.length - 1];
|
||||
var DURATION = 5000;
|
||||
var startTime = performance.now();
|
||||
|
||||
function frame(now) {
|
||||
if (!map.getSource(sourceId)) return; /* map was removed */
|
||||
var t = Math.min((now - startTime) / DURATION, 1);
|
||||
var eased = 1 - Math.pow(1 - t, 3); /* ease-out cubic */
|
||||
var target = eased * totalDist;
|
||||
|
||||
var animCoords = [coords[0]];
|
||||
for (var j = 1; j < coords.length; j++) {
|
||||
if (segDist[j] <= target) {
|
||||
animCoords.push(coords[j]);
|
||||
} else {
|
||||
var frac = (target - segDist[j - 1]) / (segDist[j] - segDist[j - 1]);
|
||||
animCoords.push([
|
||||
coords[j - 1][0] + (coords[j][0] - coords[j - 1][0]) * frac,
|
||||
coords[j - 1][1] + (coords[j][1] - coords[j - 1][1]) * frac
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
map.getSource(sourceId).setData(lineFeature(animCoords));
|
||||
if (t < 1) requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
/*
|
||||
* Add a journey line source + two layers (glow + main) to a loaded map,
|
||||
* then animate or draw instantly based on prefers-reduced-motion.
|
||||
*/
|
||||
function addJourneyLine(map, coords, sourceId) {
|
||||
if (coords.length < 2) return;
|
||||
|
||||
map.addSource(sourceId, { type: 'geojson', data: lineFeature([coords[0]]) });
|
||||
|
||||
map.addLayer({
|
||||
id: sourceId + '-glow', type: 'line', source: sourceId,
|
||||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||||
paint: { 'line-color': ACCENT, 'line-width': 6, 'line-opacity': 0.18 }
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: sourceId + '-line', type: 'line', source: sourceId,
|
||||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||||
paint: { 'line-color': ACCENT, 'line-width': 2.5, 'line-opacity': 0.85 }
|
||||
});
|
||||
|
||||
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (reducedMotion) {
|
||||
map.getSource(sourceId).setData(lineFeature(coords));
|
||||
} else {
|
||||
animateJourneyLine(map, coords, sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Return a styled <div> element for a map marker dot.
|
||||
* isLatest: make it larger with a teal ring.
|
||||
*/
|
||||
function createDotMarker(isLatest) {
|
||||
var el = document.createElement('div');
|
||||
var size = isLatest ? 18 : 12;
|
||||
var bg = isLatest ? ACCENT_DIM : ACCENT;
|
||||
var ring = isLatest ? ',0 0 0 4px rgba(42,140,115,0.25)' : '';
|
||||
el.style.cssText = [
|
||||
'width:' + size + 'px',
|
||||
'height:' + size + 'px',
|
||||
'background:' + bg,
|
||||
'border:2px solid #fff',
|
||||
'border-radius:50%',
|
||||
'box-shadow:0 1px 4px rgba(0,0,0,0.4)' + ring,
|
||||
'cursor:pointer'
|
||||
].join(';');
|
||||
return el;
|
||||
}
|
||||
|
||||
global.MapUtils = { MAP_STYLE: MAP_STYLE, ACCENT: ACCENT, addJourneyLine: addJourneyLine, createDotMarker: createDotMarker };
|
||||
})(window);
|
||||
```
|
||||
|
||||
- [x] **Verify the file parses without syntax errors**
|
||||
|
||||
```bash
|
||||
node --check /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user/themes/intotheeast/js/maplibre-utils.js
|
||||
```
|
||||
|
||||
Expected: no output (clean parse).
|
||||
|
||||
- [x] **Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/js/maplibre-utils.js
|
||||
git -C user commit -m "feat: add shared MapLibre GL utilities (journey line, markers)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Full map page — migrate map.html.twig
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/templates/map.html.twig`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `window.MapUtils` from Task 2 (`MAP_STYLE`, `addJourneyLine`, `createDotMarker`)
|
||||
- Twig data shape consumed unchanged: `map_entries` array with `lat`, `lng`, `title`, `date`, `url`, `hero` keys; `gpx_urls` array of strings
|
||||
|
||||
**What:** Replace the Leaflet map + GPX rendering with MapLibre GL. Keep all Twig data-gathering logic at the top unchanged. Only the HTML/CSS/JS at the bottom changes.
|
||||
|
||||
- [x] **Replace everything from `<div class="map-container"...>` to end of `{% endblock %}`**
|
||||
|
||||
The Twig data-gathering at the top (lines 1–33) is unchanged. Replace from line 35 onwards with:
|
||||
|
||||
```twig
|
||||
<div class="map-container" id="trip-map"></div>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
|
||||
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
|
||||
|
||||
<script>
|
||||
var ENTRIES = {{ map_entries|json_encode|raw }};
|
||||
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
||||
|
||||
var map = new maplibregl.Map({
|
||||
container: 'trip-map',
|
||||
style: MapUtils.MAP_STYLE,
|
||||
center: [20, 20],
|
||||
zoom: 2
|
||||
});
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
|
||||
if (ENTRIES.length === 0) {
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'map-empty';
|
||||
empty.textContent = 'No locations yet — entries with GPS will appear here.';
|
||||
document.getElementById('trip-map').appendChild(empty);
|
||||
}
|
||||
|
||||
map.on('load', function () {
|
||||
/* ── GPX tracks ──────────────────────────────────────────── */
|
||||
GPX_URLS.forEach(function (url, idx) {
|
||||
fetch(url)
|
||||
.then(function (r) { return r.text(); })
|
||||
.then(function (text) {
|
||||
var xml = new DOMParser().parseFromString(text, 'text/xml');
|
||||
var geojson = toGeoJSON.gpx(xml);
|
||||
var sid = 'gpx-' + idx;
|
||||
map.addSource(sid, { type: 'geojson', data: geojson });
|
||||
map.addLayer({
|
||||
id: sid + '-line', type: 'line', source: sid,
|
||||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||||
paint: { 'line-color': MapUtils.ACCENT, 'line-width': 2, 'line-opacity': 0.7 }
|
||||
});
|
||||
})
|
||||
.catch(function (err) { console.warn('GPX load failed:', url, err); });
|
||||
});
|
||||
|
||||
if (ENTRIES.length === 0) return;
|
||||
|
||||
/* ── Markers ─────────────────────────────────────────────── */
|
||||
var bounds = new maplibregl.LngLatBounds();
|
||||
var coords = [];
|
||||
|
||||
ENTRIES.forEach(function (entry, i) {
|
||||
var isLatest = (i === ENTRIES.length - 1);
|
||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
||||
coords.push(lngLat);
|
||||
bounds.extend(lngLat);
|
||||
|
||||
var el = MapUtils.createDotMarker(isLatest);
|
||||
|
||||
var popupHtml = '<div style="min-width:160px;max-width:200px;">';
|
||||
if (entry.hero) {
|
||||
popupHtml += '<img src="' + entry.hero + '" alt="" style="width:100%;height:80px;object-fit:cover;border-radius:4px;display:block;margin-bottom:8px;">';
|
||||
}
|
||||
popupHtml += '<div style="font-size:0.75rem;color:var(--color-ink-muted);margin-bottom:2px;">📅 ' + entry.date + '</div>';
|
||||
popupHtml += '<div style="font-weight:600;font-size:0.9rem;margin-bottom:8px;color:var(--color-ink);">' + entry.title + '</div>';
|
||||
popupHtml += '<a href="' + entry.url + '" style="color:var(--color-accent);font-size:0.85rem;text-decoration:none;">Read entry →</a>';
|
||||
popupHtml += '</div>';
|
||||
|
||||
new maplibregl.Marker({ element: el })
|
||||
.setLngLat(lngLat)
|
||||
.setPopup(new maplibregl.Popup({ offset: 10, maxWidth: '220px' }).setHTML(popupHtml))
|
||||
.addTo(map);
|
||||
});
|
||||
|
||||
/* ── Journey line ────────────────────────────────────────── */
|
||||
MapUtils.addJourneyLine(map, coords, 'journey');
|
||||
|
||||
/* ── Fit bounds ──────────────────────────────────────────── */
|
||||
if (ENTRIES.length === 1) {
|
||||
map.jumpTo({ center: coords[0], zoom: 10 });
|
||||
} else {
|
||||
map.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
- [x] **Verify in browser at `http://localhost:8081/trips/japan-korea-2026/map`**
|
||||
|
||||
With demo data loaded (`make demo-load`):
|
||||
- Dark vector map fills the viewport
|
||||
- 7 teal dot markers visible on Japan→Korea route
|
||||
- Journey line animates in over ~5 seconds on load
|
||||
- Click a marker → popup appears with date, title, "Read entry →" link
|
||||
- Navigate controls (zoom +/−) are styled with dark background (design tokens)
|
||||
- Attribution bar is dark/muted (not white)
|
||||
- No console errors
|
||||
|
||||
- [x] **Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/templates/map.html.twig
|
||||
git -C user commit -m "feat: migrate full map page to MapLibre GL with animated journey line"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Embedded maps — migrate dailies mini-map and home map
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/templates/dailies.html.twig` (mini-map section, around lines 37–78)
|
||||
- Modify: `user/themes/intotheeast/templates/home.html.twig` (map section, around lines 126–168)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `window.MapUtils` from Task 2
|
||||
- Twig data shapes unchanged: `map_entries` (both files) with `lat`, `lng`, `title`, `slug`, `url` keys
|
||||
|
||||
**What:** Both embedded maps follow the same pattern — no GPX, no popup (markers navigate on click), `cooperativeGestures: true` to prevent mobile scroll-trap, animated line via `MapUtils.addJourneyLine`.
|
||||
|
||||
- [x] **Replace the map block in `dailies.html.twig`**
|
||||
|
||||
Find the `{% if map_entries|length > 0 %}` block (around line 31) and replace from there to the closing `{% endif %}` and the script block:
|
||||
|
||||
```twig
|
||||
{% if map_entries|length > 0 %}
|
||||
<div class="feed-map-wrap">
|
||||
<div class="feed-map" id="feed-map"></div>
|
||||
<a class="feed-map-link" href="{{ page.parent().url }}/map">View full map →</a>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
|
||||
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
|
||||
<script>
|
||||
var FEED_ENTRIES = {{ map_entries|json_encode|raw }};
|
||||
|
||||
var feedMap = new maplibregl.Map({
|
||||
container: 'feed-map',
|
||||
style: MapUtils.MAP_STYLE,
|
||||
center: [20, 20],
|
||||
zoom: 2,
|
||||
cooperativeGestures: true
|
||||
});
|
||||
|
||||
feedMap.on('load', function () {
|
||||
var bounds = new maplibregl.LngLatBounds();
|
||||
var coords = [];
|
||||
|
||||
FEED_ENTRIES.forEach(function (entry, i) {
|
||||
var isLatest = (i === FEED_ENTRIES.length - 1);
|
||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
||||
coords.push(lngLat);
|
||||
bounds.extend(lngLat);
|
||||
|
||||
var el = MapUtils.createDotMarker(isLatest);
|
||||
el.addEventListener('click', function () {
|
||||
window.location.href = entry.url;
|
||||
});
|
||||
|
||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(feedMap);
|
||||
});
|
||||
|
||||
MapUtils.addJourneyLine(feedMap, coords, 'feed-journey');
|
||||
|
||||
if (FEED_ENTRIES.length === 1) {
|
||||
feedMap.jumpTo({ center: coords[0], zoom: 10 });
|
||||
} else {
|
||||
feedMap.fitBounds(bounds, { padding: 20, maxZoom: 11 });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
- [x] **Replace the map block in `home.html.twig`**
|
||||
|
||||
Find the `{% if map_entries|length > 0 %}` block (around line 125) and replace from there to end of `{% endblock %}`:
|
||||
|
||||
```twig
|
||||
{% if map_entries|length > 0 %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
|
||||
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
|
||||
<script>
|
||||
var HOME_ENTRIES = {{ map_entries|json_encode|raw }};
|
||||
|
||||
var homeMap = new maplibregl.Map({
|
||||
container: 'home-map',
|
||||
style: MapUtils.MAP_STYLE,
|
||||
center: [20, 20],
|
||||
zoom: 2,
|
||||
cooperativeGestures: true
|
||||
});
|
||||
|
||||
homeMap.on('load', function () {
|
||||
var bounds = new maplibregl.LngLatBounds();
|
||||
var coords = [];
|
||||
|
||||
HOME_ENTRIES.forEach(function (entry, i) {
|
||||
var isLatest = (i === HOME_ENTRIES.length - 1);
|
||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
||||
coords.push(lngLat);
|
||||
bounds.extend(lngLat);
|
||||
|
||||
var el = MapUtils.createDotMarker(isLatest);
|
||||
el.addEventListener('click', function () {
|
||||
var card = document.getElementById('entry-' + entry.slug);
|
||||
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
|
||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
|
||||
});
|
||||
|
||||
MapUtils.addJourneyLine(homeMap, coords, 'home-journey');
|
||||
|
||||
if (HOME_ENTRIES.length === 1) {
|
||||
homeMap.jumpTo({ center: coords[0], zoom: 10 });
|
||||
} else {
|
||||
homeMap.fitBounds(bounds, { padding: 20, maxZoom: 11 });
|
||||
}
|
||||
|
||||
setTimeout(function () { homeMap.resize(); }, 100);
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
- [x] **Verify mini-map at `http://localhost:8081/trips/japan-korea-2026/dailies`**
|
||||
|
||||
- Mini-map appears above journal feed with dark vector tiles
|
||||
- Journey line animates in
|
||||
- Click a marker → navigates to that entry's page (not a popup)
|
||||
- On mobile: pinch-zoom within the mini-map requires two fingers; one finger scrolls the page past it
|
||||
- "View full map →" link works
|
||||
|
||||
- [x] **Verify home map at `http://localhost:8081`**
|
||||
|
||||
- Left column sticky map shows dark vector tiles
|
||||
- Journey line animates in
|
||||
- Click a marker → page scrolls to the matching entry card in the right column
|
||||
- On mobile (< 768px): map collapses to 40vh above the feed, touch-scroll works on page
|
||||
|
||||
- [x] **Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/templates/dailies.html.twig themes/intotheeast/templates/home.html.twig
|
||||
git -C user commit -m "feat: migrate mini-map and home map to MapLibre GL"
|
||||
```
|
||||
|
||||
- [x] **Final sync**
|
||||
|
||||
```bash
|
||||
make content-push
|
||||
```
|
||||
Reference in New Issue
Block a user