From 20ed47397b637862194a61291b62e2a57256ec1a Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 2 Apr 2026 12:59:55 -0500 Subject: [PATCH] Improve map page mobile usability Collapsible control panel with hamburger toggle, detail panel as bottom sheet, compact legend moved to top-right, and scaled timeline buttons for narrow viewports. --- assets/js/propagation_map_hook.js | 93 ++++++++++++++---- lib/microwaveprop_web/live/map_live.ex | 131 ++++++++++++++----------- 2 files changed, 149 insertions(+), 75 deletions(-) diff --git a/assets/js/propagation_map_hook.js b/assets/js/propagation_map_hook.js index b68a837a..9350b4db 100644 --- a/assets/js/propagation_map_hook.js +++ b/assets/js/propagation_map_hook.js @@ -190,9 +190,24 @@ function convexHull(points) { return lower.concat(upper) } +function isMobile() { + return window.innerWidth < 768 +} + +function closeButtonHTML() { + if (!isMobile()) return "" + return `` +} + +function mobileHandleHTML() { + if (!isMobile()) return "" + return `
` +} + function buildLoadingHTML(detail) { const tier = scoreTier(detail.score) - return `
+ return `
+ ${mobileHandleHTML()}${closeButtonHTML()}
${detail.score}/100 ${tier.label} @@ -207,7 +222,7 @@ function buildForecastSvg(forecast) { if (!forecast || forecast.length < 2) return "" const marginL = 28, marginR = 6, marginT = 4, marginB = 16 - const w = 260, h = 72 + const w = isMobile() ? Math.min(260, window.innerWidth - 48) : 260, h = 72 const plotW = w - marginL - marginR const plotH = h - marginT - marginB const n = forecast.length @@ -305,7 +320,11 @@ function buildPopupHTML(detail, viewshedLoading) { ? `
Computing terrain coverage…
` : "" - return `
+ const mobile = isMobile() + const minW = mobile ? "auto" : "260px" + + return `
+ ${mobileHandleHTML()}${closeButtonHTML()}
${detail.score}/100 ${tier.label} @@ -550,13 +569,19 @@ export const PropagationMap = { const merged = { ...basic, ...this.bandInfo, factors: null } this.detailPanel.innerHTML = buildLoadingHTML(merged) this.detailPanel.style.display = "block" + this.positionDetailPanel() this.pushEvent("point_detail", { lat: e.latlng.lat, lon: e.latlng.lng }) } }) - // Detail panel below sidebar instead of map popup + // Detail panel — bottom sheet on mobile, positioned next to sidebar on desktop this.detailPanel = document.getElementById("detail-panel") this.viewshedLoading = false + + if (this.detailPanel) { + L.DomEvent.disableClickPropagation(this.detailPanel) + L.DomEvent.disableScrollPropagation(this.detailPanel) + } this.lastDetail = null this.handleEvent("point_detail", (detail) => { @@ -565,6 +590,7 @@ export const PropagationMap = { this.lastDetail = merged this.detailPanel.innerHTML = buildPopupHTML(merged, this.viewshedLoading) this.detailPanel.style.display = "block" + this.positionDetailPanel() // Draw propagation reach polygon based on contiguous good cells if (this.gridLookup && this.clickedLatLng) { @@ -679,27 +705,46 @@ export const PropagationMap = { } }) - // Legend - const legend = L.control({ position: "bottomright" }) + // Legend — compact on mobile, moved to top-right to avoid timeline overlap + const legend = L.control({ position: isMobile() ? "topright" : "bottomright" }) legend.onAdd = () => { const div = L.DomUtil.create("div") - div.style.cssText = "background:#fff;color:#333;padding:10px 14px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:13px;line-height:2;" + const mobile = isMobile() const rows = [ - ["#00ffa3", "Excellent (80-100)"], - ["#7dffd4", "Good (65-79)"], - ["#ffe566", "Marginal (50-64)"], - ["#ff9044", "Poor (33-49)"], - ["#ff4f4f", "Negligible (0-32)"] + ["#00ffa3", "Excellent", "80+"], + ["#7dffd4", "Good", "65"], + ["#ffe566", "Marginal", "50"], + ["#ff9044", "Poor", "33"], + ["#ff4f4f", "Negligible", "0"] ] - div.innerHTML = "Propagation
" + - rows.map(([c, label]) => - `${label}` + if (mobile) { + div.style.cssText = "background:#fff;color:#333;padding:4px 6px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.3);font-size:10px;line-height:1.6;" + div.innerHTML = rows.map(([c, , score]) => + `${score}` ).join("
") + } else { + div.style.cssText = "background:#fff;color:#333;padding:10px 14px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:13px;line-height:2;" + div.innerHTML = "Propagation
" + + rows.map(([c, label, score]) => + `${label} (${score === "80+" ? "80-100" : score === "0" ? "0-32" : score + "-" + (parseInt(score) === 65 ? "79" : parseInt(score) === 50 ? "64" : "49")})` + ).join("
") + } return div } legend.addTo(this.map) }, + positionDetailPanel() { + if (!this.detailPanel || isMobile()) return + // On desktop, position the panel below the control panel + const controls = document.getElementById("map-controls") + if (controls) { + const rect = controls.getBoundingClientRect() + this.detailPanel.style.left = rect.left + "px" + this.detailPanel.style.top = (rect.bottom + 8) + "px" + } + }, + renderScores(scores) { this.scores = scores if (this.scoreOverlay) { @@ -879,6 +924,12 @@ export const PropagationMap = { } }) + const mobile = isMobile() + const btnPad = mobile ? "3px 5px" : "4px 8px" + const btnMinW = mobile ? "32px" : "38px" + const btnFont = mobile ? "10px" : "11px" + const subFont = mobile ? "8px" : "9px" + const buttons = items.map(t => { const bg = t.isSelected ? "#00ffa3" : (t.isPast ? "rgba(255,255,255,0.08)" : "rgba(255,255,255,0.15)") const fg = t.isSelected ? "#000" : (t.isPast ? "rgba(255,255,255,0.4)" : "#fff") @@ -886,17 +937,19 @@ export const PropagationMap = { const weight = t.isSelected ? "700" : "400" const utcLabel = `${t.dt.getUTCHours().toString().padStart(2, "0")}:00` return `` + ">${t.label}
${utcLabel}` }).join("") + const labelText = mobile ? "Forecast" : "Propagation Forecast" + this.timelineEl.style.display = "block" this.timelineEl.innerHTML = ` -
- Propagation Forecast +
+ ${labelText} ${buttons}
` diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index 3281576f..5ff86c08 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -248,7 +248,7 @@ defmodule MicrowavepropWeb.MapLive do @impl true def render(assigns) do ~H""" -
+
<%!-- Full-page map --%>
<%!-- Top-left control panel --%> -
-
-
-
- North Texas Microwave Society +
+
+
+
+ + NTMS
Propagation Map
-
+
+
+
+
- <%!-- Band selector --%> + <%!-- Band selector (always visible) --%> - <%!-- Point detail panel (populated by JS on click, ignored by LiveView patches) --%> - + <%!-- Point detail panel — bottom sheet on mobile, sidebar on desktop --%> + <%!-- Bottom timeline bar --%>