From 96d9a6971c735649ef79cdef2323c68c2eaff5ad Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 17 Apr 2026 12:49:25 -0400 Subject: [PATCH] feat: split map view into 50/50 map + bounds-filtered photo grid On map view the body container now shows the Leaflet map on one half and a photo grid on the other half. The grid renders only photos whose GPS falls inside the map's current bounds, and re-renders on moveend/zoomend so panning and zooming filters the visible photo set live. Stacks vertically on narrow viewports. Co-Authored-By: Claude Opus 4.7 (1M context) --- search-app/live-search.js | 57 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/search-app/live-search.js b/search-app/live-search.js index a78a394..f685f9d 100644 --- a/search-app/live-search.js +++ b/search-app/live-search.js @@ -135,6 +135,29 @@ flex: 1; overflow-y: auto; padding: 0 20px 20px; max-width: 1440px; width: 100%; margin: 0 auto; } + #live-search-panel .ls-body.ls-split { + display: flex; flex-direction: row; gap: 12px; + overflow: hidden; + } + #live-search-panel .ls-body.ls-split > .ls-map-wrap { + flex: 0 0 50%; height: 100%; min-height: 0; + } + #live-search-panel .ls-body.ls-split > .ls-grid { + flex: 1 1 50%; overflow-y: auto; align-content: start; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + } + @media (max-width: 768px) { + #live-search-panel .ls-body.ls-split { + flex-direction: column; + } + #live-search-panel .ls-body.ls-split > .ls-map-wrap, + #live-search-panel .ls-body.ls-split > .ls-grid { + flex: 1 1 50%; + } + #live-search-panel .ls-body.ls-split > .ls-grid { + grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); + } + } #live-search-panel .ls-grid { display: grid; @@ -235,14 +258,17 @@ const bodyEl = panel.querySelector('.ls-body'); if (view === 'grid') { + bodyEl.classList.remove('ls-split'); gridEl.style.display = ''; mapWrap.style.display = 'none'; bodyEl.style.overflowY = 'auto'; renderGridItems(currentItems); } else { - gridEl.style.display = 'none'; + // Map view = 50/50 split: map on one side, bounds-filtered photos on the other + bodyEl.classList.add('ls-split'); + gridEl.style.display = ''; mapWrap.style.display = ''; - bodyEl.style.overflowY = 'hidden'; + bodyEl.style.overflowY = ''; showMap(currentItems); } } @@ -267,9 +293,10 @@ const mapWrap = panel ? panel.querySelector('.ls-map-wrap') : null; const bodyEl = panel ? panel.querySelector('.ls-body') : null; if (gridEl && mapWrap && bodyEl) { - gridEl.style.display = 'none'; + bodyEl.classList.add('ls-split'); + gridEl.style.display = ''; mapWrap.style.display = ''; - bodyEl.style.overflowY = 'hidden'; + bodyEl.style.overflowY = ''; } } @@ -545,6 +572,28 @@ // Fix map sizing (Leaflet needs this after dynamic show) setTimeout(() => mapInstance.invalidateSize(), 100); + // Bounds-filtered grid on the other half of the split. Updates live + // as the user pans/zooms so "photos taken on that part of the map" + // stay in sync with the visible viewport. + const updateGridFromBounds = () => { + if (currentView !== 'map') return; + const b = mapInstance.getBounds(); + const visible = geoItems.filter(i => + b.contains([i.exifInfo.latitude, i.exifInfo.longitude]) + ); + renderGridItems(visible); + const statusEl = panel.querySelector('.ls-status'); + if (statusEl) { + const totalGeo = geoItems.length; + statusEl.textContent = + visible.length + ' of ' + totalGeo + ' shown in view'; + } + }; + mapInstance.on('moveend', updateGridFromBounds); + mapInstance.on('zoomend', updateGridFromBounds); + // Initial population once bounds settle + setTimeout(updateGridFromBounds, 150); + // Add custom label style if (!document.getElementById('ls-marker-styles')) { const s = document.createElement('style');