Fix UI issues: popup positioning, trail/cluster sync, CSS cleanup
- Remove CSS `bottom: 50px !important` on popups that pushed them too high above markers - Hide trails when markers are clustered (zoom < 11) so trails don't point to invisible markers inside cluster bubbles - Fix locate button overlap with zoom controls and keep it always light mode - Remove duplicate slideover/toggle CSS from layout files (single source of truth in component) - Remove dead CSS classes and standardize breakpoints to 1024px - Add isDestroyed guards on async callbacks to prevent race conditions - Escape callsigns in marker HTML to prevent XSS - Replace Leaflet _container private API with getContainer() - Clean up about page placeholder text
This commit is contained in:
parent
e147a19043
commit
b8d2095346
8 changed files with 71 additions and 446 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}"></div>`;
|
||||
" title="Historical position for ${escapeHtml(data.callsign || "")}"></div>`;
|
||||
|
||||
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}"></div>`;
|
||||
" title="APRS Station: ${escapeHtml(data.callsign || "")}"></div>`;
|
||||
|
||||
return createDivIcon(iconHtml, {
|
||||
iconSize: [12, 12],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<.header dev_mode={Application.get_env(:aprsme, :dev_routes, false)} current_user={@current_user} />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<main>
|
||||
|
|
|
|||
|
|
@ -30,31 +30,11 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">{gettext("Join Our Community")}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
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.
|
||||
</p>
|
||||
<div class="text-center mt-12">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center rounded-md bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
||||
>
|
||||
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m-6 3l6-3"
|
||||
/>
|
||||
</svg>
|
||||
{gettext("Explore the Map")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center text-gray-500 dark:text-gray-400">
|
||||
<p class="text-sm">
|
||||
© 2025 aprs.me. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
© 2026 aprs.me.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
<button
|
||||
phx-click={toggle_slideover_js()}
|
||||
class={[
|
||||
"fixed right-4 top-4 z-40 bg-white rounded-lg shadow-lg p-3",
|
||||
"hover:bg-gray-50 transition-colors lg:hidden",
|
||||
@slideover_open && "hidden"
|
||||
]}
|
||||
>
|
||||
<.icon name="hero-bars-3" class="w-6 h-6 text-gray-700" />
|
||||
</button>
|
||||
"""
|
||||
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 --%>
|
||||
<style>
|
||||
#aprs-map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 100vh;
|
||||
z-index: 1;
|
||||
transition: right 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Desktop styles */
|
||||
@media (min-width: 1024px) {
|
||||
#aprs-map.slideover-open {
|
||||
right: 352px;
|
||||
}
|
||||
|
||||
#aprs-map.slideover-closed {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Slideover panel base styles */
|
||||
.slideover-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 352px;
|
||||
background: white;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slideover-panel.slideover-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.slideover-panel.slideover-closed {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
@media (max-width: 1023px) {
|
||||
#aprs-map {
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
.slideover-panel {
|
||||
width: 90vw !important;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.locate-button {
|
||||
top: 65px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.locate-button {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 80px;
|
||||
top: 100px;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.locate-button {
|
||||
background: rgb(30 41 59); /* slate-800 */
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.locate-button svg {
|
||||
fill: #e2e8f0; /* slate-200 */
|
||||
}
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.locate-button:hover {
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.locate-button:hover {
|
||||
background: rgb(51 65 85); /* slate-700 */
|
||||
/* Mobile: larger touch targets push zoom controls taller */
|
||||
@media (max-width: 1023px) {
|
||||
.locate-button {
|
||||
top: 110px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1415,7 +1327,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.slideover-toggle {
|
||||
.slideover-toggle.slideover-open {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue