diff --git a/assets/css/app.css b/assets/css/app.css index ab140fb..4d7f59f 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -6,6 +6,15 @@ /* Import mobile-specific styles */ @import "./mobile.css"; +/* + * Z-Index Hierarchy (keep in sync across all CSS files): + * 1 - Map container (#aprs-map) + * 10-15 - Markers (.leaflet-marker-icon, .historical-dot-marker) + * 50 - Slideover panel (.slideover-panel) — set to 1000 in component + * 999 - Slideover toggle (.slideover-toggle) + * 1000 - Slideover panel, marker hover states, Leaflet controls + */ + /* Vendor CSS files are imported via JavaScript/ESBuild */ /* A Tailwind plugin that makes "hero-#{ICON}" classes available. diff --git a/assets/css/mobile.css b/assets/css/mobile.css index 09c9772..bb64504 100644 --- a/assets/css/mobile.css +++ b/assets/css/mobile.css @@ -7,21 +7,13 @@ html, body { } /* Ensure map takes full height on mobile */ -@media (max-width: 768px) { +@media (max-width: 1023px) { #map { - height: calc(100vh - 56px); /* Account for header */ + height: calc(100vh - 56px); height: calc(100dvh - 56px); /* Dynamic viewport height for mobile browsers */ touch-action: pan-x pan-y; } - /* Optimize header for mobile */ - .site-header { - padding: 0.5rem; - position: sticky; - top: 0; - z-index: 1000; - } - /* Larger touch targets for mobile controls */ .leaflet-control-zoom a, .leaflet-control-layers-toggle, @@ -32,55 +24,11 @@ html, body { font-size: 20px !important; } - /* Better popup positioning on mobile */ - .leaflet-popup { - bottom: 50px !important; - max-width: 90vw !important; - } - .leaflet-popup-content { + max-width: 90vw; max-height: 50vh; overflow-y: auto; -webkit-overflow-scrolling: touch; - padding: 1rem; - } - - /* Optimize sidebar for mobile */ - .map-sidebar { - position: fixed; - bottom: 0; - left: 0; - right: 0; - max-height: 40vh; - background: white; - border-top: 2px solid #e5e7eb; - transform: translateY(100%); - transition: transform 0.3s ease; - z-index: 1001; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - } - - .map-sidebar.open { - transform: translateY(0); - } - - /* Mobile-friendly search box */ - .search-container { - position: absolute; - top: 10px; - left: 10px; - right: 60px; - z-index: 1000; - } - - .search-input { - width: 100%; - padding: 12px 16px; - font-size: 16px; /* Prevent zoom on iOS */ - border-radius: 8px; - border: 1px solid #d1d5db; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); } /* Mobile-friendly buttons */ @@ -88,44 +36,6 @@ html, body { min-height: 44px; } - /* Hide non-essential elements on small screens */ - .hide-mobile { - display: none !important; - } - - /* Improve marker touch targets */ - .leaflet-marker-icon { - margin-left: -12px !important; - margin-top: -41px !important; - } - - /* Better clustering on mobile */ - .marker-cluster { - width: 48px !important; - height: 48px !important; - margin-left: -24px !important; - margin-top: -24px !important; - } - - /* Mobile-friendly trail duration controls */ - .trail-controls { - position: fixed; - bottom: 60px; - right: 10px; - display: flex; - flex-direction: column; - gap: 8px; - } - - .trail-controls button { - width: 44px; - height: 44px; - border-radius: 50%; - background: white; - border: 2px solid #e5e7eb; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - } - /* Improve dropdown menus on mobile */ select { font-size: 16px; /* Prevent zoom on iOS */ @@ -133,40 +43,14 @@ html, body { min-height: 44px; } - /* Mobile-friendly tooltips */ - .tooltip { - font-size: 14px; - padding: 8px 12px; - } - - /* Optimize data tables for mobile */ - .table-container { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } - - table { - font-size: 14px; - } - /* Safe area insets for devices with notches */ - .site-header { - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); - padding-top: env(safe-area-inset-top); - } - #map { padding-bottom: env(safe-area-inset-bottom); } } /* Landscape mode optimizations */ -@media (max-width: 768px) and (orientation: landscape) { - .site-header { - padding: 0.25rem 0.5rem; - } - +@media (max-width: 1023px) and (orientation: landscape) { #map { height: calc(100vh - 40px); height: calc(100dvh - 40px); @@ -188,25 +72,12 @@ html, body { /* Disable hover effects on touch devices */ @media (hover: none) { .leaflet-interactive:hover { - stroke-opacity: 1 !important; - fill-opacity: 0.2 !important; - } - - a:hover, button:hover { - background-color: inherit !important; + stroke-opacity: 1; + fill-opacity: 0.2; } } -/* Loading spinner for mobile */ -.mobile-loading { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 9999; -} - -/* Improve form inputs on mobile */ +/* Improve form inputs on mobile - prevent zoom on iOS */ input[type="text"], input[type="email"], input[type="search"], @@ -214,22 +85,14 @@ input[type="tel"], input[type="url"], input[type="password"], textarea { - font-size: 16px; /* Prevent zoom on iOS */ + font-size: 16px; -webkit-appearance: none; -moz-appearance: none; appearance: none; } -/* Better focus states for mobile */ +/* Better focus states for accessibility */ :focus { outline: 3px solid #6366f1; outline-offset: 2px; } - -/* Optimize animations for mobile performance */ -@media (max-width: 768px) { - * { - animation-duration: 0.2s !important; - transition-duration: 0.2s !important; - } -} \ No newline at end of file diff --git a/assets/js/features/trail_manager.ts b/assets/js/features/trail_manager.ts index 033f022..a04ee48 100644 --- a/assets/js/features/trail_manager.ts +++ b/assets/js/features/trail_manager.ts @@ -521,6 +521,12 @@ export class TrailManager { } clearAllTrails() { + // Clear pending hover timer before removing all trails + if (this.trailHoverDebounceTimer) { + clearTimeout(this.trailHoverDebounceTimer); + this.trailHoverDebounceTimer = undefined; + } + this.lastHoveredPath = undefined; this.trails.forEach((_, markerId) => { this.removeTrail(markerId); }); diff --git a/assets/js/map.ts b/assets/js/map.ts index a6c0da7..d96c9c4 100644 --- a/assets/js/map.ts +++ b/assets/js/map.ts @@ -64,11 +64,17 @@ import { saveMapState, safePushEvent, getLiveSocket, + escapeHtml, } from "./map_helpers"; // APRS Map Hook - handles only basic map interaction // All data logic handled by LiveView +// Zoom level at which marker clustering is disabled and individual markers are shown. +// Trails are only visible at this zoom level and above to avoid showing trails +// for markers hidden inside cluster bubbles. +const DISABLE_CLUSTERING_AT_ZOOM = 11; + let MapAPRSMap = { mounted() { const self = this as unknown as LiveViewHookContext; @@ -359,7 +365,7 @@ let MapAPRSMap = { zoomToBoundsOnClick: true, spiderfyOnMaxZoom: true, removeOutsideVisibleBounds: true, - disableClusteringAtZoom: 11, // Show individual markers at zoom 11+ + disableClusteringAtZoom: DISABLE_CLUSTERING_AT_ZOOM, maxClusterRadius: 80, iconCreateFunction: function (cluster: MarkerCluster) { const count = cluster.getChildCount(); @@ -409,7 +415,12 @@ let MapAPRSMap = { } // Create trail layer and manager with RF path hover callbacks - const trailLayer = L.layerGroup().addTo(self.map); + // Only add to map if zoom is high enough (trails hidden when markers are clustered) + const trailLayer = L.layerGroup(); + if (self.map!.getZoom() >= DISABLE_CLUSTERING_AT_ZOOM) { + trailLayer.addTo(self.map!); + } + self.trailLayer = trailLayer; self.trailManager = new TrailManager( trailLayer, 60 * 60 * 1000, @@ -497,6 +508,7 @@ let MapAPRSMap = { // Track when map is ready self.map!.whenReady(() => { + if (self.isDestroyed) return; try { self.lastZoom = self.map!.getZoom(); self.sendMapReadyEvents(); @@ -593,6 +605,20 @@ let MapAPRSMap = { }); } + // Hide trails when markers are clustered, show when unclustered + if (self.trailLayer && self.map) { + if (currentZoom >= DISABLE_CLUSTERING_AT_ZOOM) { + if (!self.map.hasLayer(self.trailLayer)) { + self.trailLayer.addTo(self.map); + self.trailLayer.bringToFront(); + } + } else { + if (self.map.hasLayer(self.trailLayer)) { + self.map.removeLayer(self.trailLayer); + } + } + } + // If zoom changed significantly (more than 2 levels), request reload of normal markers // but preserve historical ones and current position if (zoomDifference > 2) { @@ -770,7 +796,9 @@ let MapAPRSMap = { startY = touch.clientY; longPressTimer = setTimeout(() => { - handleLongPress(e.originalEvent); + if (!self.isDestroyed && self.map) { + handleLongPress(e.originalEvent); + } }, longPressDuration); }); @@ -1646,7 +1674,7 @@ let MapAPRSMap = { // Additional check to ensure map is fully ready if ( !self.map || - !self.map._container || + !self.map.getContainer() || typeof self.map.getZoom !== "function" ) { console.warn("Map not fully initialized, queueing marker:", data.id); @@ -2266,7 +2294,7 @@ let MapAPRSMap = { border-radius: 50%; opacity: 0.8; box-shadow: 0 0 2px rgba(0,0,0,0.3); - " title="Historical position for ${data.callsign}">`; + " title="Historical position for ${escapeHtml(data.callsign || "")}">`; return createDivIcon(iconHtml, { className: "historical-dot-marker", @@ -2289,7 +2317,7 @@ let MapAPRSMap = { border-radius: 50%; opacity: 0.8; box-shadow: 0 0 2px rgba(0,0,0,0.3); - " title="APRS Station: ${data.callsign}">`; + " title="APRS Station: ${escapeHtml(data.callsign || "")}">`; return createDivIcon(iconHtml, { iconSize: [12, 12], diff --git a/lib/aprsme_web/components/layouts/app.html.heex b/lib/aprsme_web/components/layouts/app.html.heex index a795411..2d5d4fe 100644 --- a/lib/aprsme_web/components/layouts/app.html.heex +++ b/lib/aprsme_web/components/layouts/app.html.heex @@ -63,68 +63,6 @@ right: 0 !important; } } - - /* Slideover panel responsive styles */ - body:has(#main-content[data-map-page="true"]) .slideover-panel { - position: fixed; - top: 0; - right: 0; - height: 100vh; - width: 352px; - background: white; - box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15); - z-index: 1000; - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); - overflow-y: auto; - } - - /* Desktop slideover behavior */ - @media (min-width: 1024px) { - body:has(#main-content[data-map-page="true"]) .slideover-panel.slideover-open { - transform: translateX(0); - } - - body:has(#main-content[data-map-page="true"]) .slideover-panel.slideover-closed { - transform: translateX(100%); - } - } - - /* Mobile slideover behavior */ - @media (max-width: 1023px) { - body:has(#main-content[data-map-page="true"]) .slideover-panel { - width: 100vw; - max-width: 400px; - box-shadow: -8px 0 32px rgba(0, 0, 0, 0.2); - } - - body:has(#main-content[data-map-page="true"]) .slideover-panel.slideover-open { - transform: translateX(0); - } - - body:has(#main-content[data-map-page="true"]) .slideover-panel.slideover-closed { - transform: translateX(100%); - } - } - - /* Mobile panel improvements */ - @media (max-width: 1023px) { - body:has(#main-content[data-map-page="true"]) .slideover-panel { - display: flex; - flex-direction: column; - } - } - - /* Improve scrolling on mobile */ - @media (max-width: 1023px) { - body:has(#main-content[data-map-page="true"]) .slideover-panel { - -webkit-overflow-scrolling: touch; - overscroll-behavior: contain; - } - - body:has(#main-content[data-map-page="true"]) .slideover-panel .flex-1 { - min-height: 0; - } - } <.header dev_mode={Application.get_env(:aprsme, :dev_routes, false)} current_user={@current_user} /> diff --git a/lib/aprsme_web/components/layouts/map.html.heex b/lib/aprsme_web/components/layouts/map.html.heex index 51acc5e..71f08f7 100644 --- a/lib/aprsme_web/components/layouts/map.html.heex +++ b/lib/aprsme_web/components/layouts/map.html.heex @@ -29,7 +29,6 @@ right: 0 !important; bottom: 0 !important; height: 100vh !important; - width: 100vw !important; z-index: 1 !important; transition: right 0.3s ease-in-out !important; } @@ -81,116 +80,6 @@ max-width: none; height: 100%; } - - /* Slideover toggle button styles */ - .slideover-toggle { - position: fixed !important; - right: 10px; - top: 10px; - z-index: 1001; - background: white; - border: 2px solid rgba(0, 0, 0, 0.2); - border-radius: 8px; - padding: 10px; - cursor: pointer; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - } - - .slideover-toggle:hover { - background: #f8fafc; - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); - transform: translateY(-1px); - } - - .slideover-toggle:active { - transform: translateY(0); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - } - - /* Adjust toggle position on desktop when slideover is open */ - @media (min-width: 1024px) { - .slideover-toggle.slideover-open { - right: 362px; /* 352px slideover width + 10px gap */ - } - } - - /* Mobile toggle adjustments */ - @media (max-width: 1023px) { - .slideover-toggle { - right: 15px; - top: 15px; - padding: 12px; - border-radius: 50%; - width: 48px; - height: 48px; - } - } - - /* Slideover panel responsive styles */ - .slideover-panel { - position: fixed; - top: 0; - right: 0; - height: 100vh; - width: 352px; - background: white; - box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15); - z-index: 1000; - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); - overflow-y: auto; - } - - /* Desktop slideover behavior */ - @media (min-width: 1024px) { - .slideover-panel.slideover-open { - transform: translateX(0); - } - - .slideover-panel.slideover-closed { - transform: translateX(100%); - } - } - - /* Mobile slideover behavior */ - @media (max-width: 1023px) { - .slideover-panel { - width: 100vw; - max-width: 400px; - box-shadow: -8px 0 32px rgba(0, 0, 0, 0.2); - } - - .slideover-panel.slideover-open { - transform: translateX(0); - } - - .slideover-panel.slideover-closed { - transform: translateX(100%); - } - } - - /* Mobile panel improvements */ - @media (max-width: 1023px) { - .slideover-panel { - display: flex; - flex-direction: column; - } - } - - /* Improve scrolling on mobile */ - @media (max-width: 1023px) { - .slideover-panel { - -webkit-overflow-scrolling: touch; - overscroll-behavior: contain; - } - - .slideover-panel .flex-1 { - min-height: 0; - } - }
diff --git a/lib/aprsme_web/live/about_live.html.heex b/lib/aprsme_web/live/about_live.html.heex index da87f64..602af4f 100644 --- a/lib/aprsme_web/live/about_live.html.heex +++ b/lib/aprsme_web/live/about_live.html.heex @@ -30,31 +30,11 @@ -

{gettext("Join Our Community")}

-

- Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. -

-
- - - - - {gettext("Explore the Map")} - -

- © 2025 aprs.me. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + © 2026 aprs.me.

diff --git a/lib/aprsme_web/live/map_live/index.ex b/lib/aprsme_web/live/map_live/index.ex index 77b7a1f..c30936f 100644 --- a/lib/aprsme_web/live/map_live/index.ex +++ b/lib/aprsme_web/live/map_live/index.ex @@ -1118,8 +1118,6 @@ defmodule AprsmeWeb.MapLive.Index do <.locate_button /> - <.toggle_button slideover_open={@slideover_open} /> - <.bottom_controls {assigns} /> """ end @@ -1139,21 +1137,6 @@ defmodule AprsmeWeb.MapLive.Index do """ end - defp toggle_button(assigns) do - ~H""" - - """ - end - defp toggle_slideover_js do "toggle_slideover" |> JS.push() @@ -1166,103 +1149,32 @@ defmodule AprsmeWeb.MapLive.Index do ~H""" <%!-- Existing bottom controls code will go here --%>