diff --git a/src/plugins/layers/addMinimizeToggle.js b/src/plugins/layers/addMinimizeToggle.js index ad4ffd37..19ebbee1 100644 --- a/src/plugins/layers/addMinimizeToggle.js +++ b/src/plugins/layers/addMinimizeToggle.js @@ -6,7 +6,6 @@ export function addMinimizeToggle(element, storageKey, options = {}) { const { contentClassName = 'panel-content', buttonClassName = 'panel-minimize-btn', - titleColor = '#00b4ff', getIsMinimized, onToggle, persist = true, @@ -25,17 +24,9 @@ export function addMinimizeToggle(element, storageKey, options = {}) { const header = element.firstElementChild; if (!header) return; - const existingTitle = header.querySelector('[data-drag-handle="true"]'); const existingButton = header.querySelector(`.${buttonClassName}`); const existingWrapper = element.querySelector(`.${contentClassName}`); - if (existingTitle) { - existingTitle.style.fontFamily = "'JetBrains Mono', monospace"; - existingTitle.style.fontSize = '13px'; - existingTitle.style.fontWeight = '700'; - existingTitle.style.color = titleColor; - } - const readState = () => { if (typeof getIsMinimized === 'function') return !!getIsMinimized(); return localStorage.getItem(minimizeKey) === 'true'; @@ -49,7 +40,8 @@ export function addMinimizeToggle(element, storageKey, options = {}) { const syncState = (button, wrapper) => { const isMinimized = readState(); wrapper.style.display = isMinimized ? 'none' : 'block'; - button.innerHTML = isMinimized ? '▶' : '▼'; + button.innerHTML = '▶'; + button.style.transform = isMinimized ? 'rotate(0deg)' : 'rotate(90deg)'; element.style.cursor = isMinimized ? 'pointer' : 'default'; }; @@ -68,7 +60,8 @@ export function addMinimizeToggle(element, storageKey, options = {}) { e.stopPropagation(); const next = existingWrapper.style.display !== 'none'; existingWrapper.style.display = next ? 'none' : 'block'; - existingButton.innerHTML = next ? '▶' : '▼'; + existingButton.innerHTML = '▶'; + existingButton.style.transform = next ? 'rotate(90deg)' : 'rotate(0deg)'; element.style.cursor = next ? 'pointer' : 'default'; writeState(next); }, @@ -87,23 +80,14 @@ export function addMinimizeToggle(element, storageKey, options = {}) { const minimizeBtn = document.createElement('button'); minimizeBtn.className = buttonClassName; - minimizeBtn.innerHTML = '▼'; + minimizeBtn.innerHTML = '▶'; minimizeBtn.style.cssText = ` display: inline-flex; align-items: center; justify-content: center; - width: 16px; - min-width: 16px; - height: 16px; - background: none; - border: none; - color: #888; cursor: pointer; user-select: none; - padding: 2px 4px; - margin: 0; - font-size: 10px; - line-height: 1; + transform: rotate(0deg); `; minimizeBtn.title = 'Minimize/Maximize'; minimizeBtn.addEventListener( @@ -122,12 +106,6 @@ export function addMinimizeToggle(element, storageKey, options = {}) { title.textContent = header.textContent.replace(/[▼▶]/g, '').trim(); title.dataset.dragHandle = 'true'; title.style.flex = '1'; - title.style.cursor = 'grab'; - title.style.userSelect = 'none'; - title.style.fontFamily = "'JetBrains Mono', monospace"; - title.style.fontSize = '13px'; - title.style.fontWeight = '700'; - title.style.color = titleColor; header.textContent = ''; header.appendChild(title); @@ -141,7 +119,7 @@ export function addMinimizeToggle(element, storageKey, options = {}) { e.stopPropagation(); const hidden = contentWrapper.style.display === 'none'; contentWrapper.style.display = hidden ? 'block' : 'none'; - minimizeBtn.innerHTML = hidden ? '▼' : '▶'; + minimizeBtn.style.transform = hidden ? 'rotate(90deg)' : 'rotate(0deg)'; element.style.cursor = hidden ? 'default' : 'pointer'; writeState(!hidden); }, diff --git a/src/plugins/layers/makeDraggable.js b/src/plugins/layers/makeDraggable.js index f805bc7f..a8584d0b 100644 --- a/src/plugins/layers/makeDraggable.js +++ b/src/plugins/layers/makeDraggable.js @@ -32,7 +32,22 @@ function clampToViewport(el, margin = 40) { el.style.top = top + 'px'; } -export function makeDraggable(el, storageKey, skipPositionLoad = false) { +/** + * Helper for snapping to a grid + */ +function snapToGrid(value, gridSize) { + if (!gridSize) return value; + return Math.round(value / gridSize) * gridSize; +} + +export function makeDraggable( + el, + storageKey, + { + skipPositionLoad = false, + snap = 0, // pixels; 0 disables snapping + } = {}, +) { if (!el) return; // Cancel any previous listeners for this storageKey (e.g. after layout change) @@ -142,6 +157,7 @@ export function makeDraggable(el, storageKey, skipPositionLoad = false) { (e) => { if (e.button !== 0) return; isDragging = true; + updateCursor(); didDrag = false; startX = e.clientX; startY = e.clientY; @@ -162,11 +178,20 @@ export function makeDraggable(el, storageKey, skipPositionLoad = false) { 'mousemove', (e) => { if (!isDragging) return; + if (!didDrag && (Math.abs(e.clientX - startX) > 2 || Math.abs(e.clientY - startY) > 2)) { didDrag = true; } - el.style.left = startLeft + (e.clientX - startX) + 'px'; - el.style.top = startTop + (e.clientY - startY) + 'px'; + + let nextLeft = startLeft + (e.clientX - startX); + let nextTop = startTop + (e.clientY - startY); + + // Snap to grid if enabled + nextLeft = snapToGrid(nextLeft, snap); + nextTop = snapToGrid(nextTop, snap); + + el.style.left = nextLeft + 'px'; + el.style.top = nextTop + 'px'; }, { signal }, ); @@ -174,7 +199,7 @@ export function makeDraggable(el, storageKey, skipPositionLoad = false) { // --- Mouseup: stop drag, clamp, save --- document.addEventListener( 'mouseup', - () => { + (e) => { if (!isDragging) return; isDragging = false; el.style.opacity = '1'; @@ -182,7 +207,11 @@ export function makeDraggable(el, storageKey, skipPositionLoad = false) { updateCursor(); suppressClick = didDrag; - // Clamp so element can't be lost off-screen + if (snap) { + el.style.left = snapToGrid(el.offsetLeft, snap) + 'px'; + el.style.top = snapToGrid(el.offsetTop, snap) + 'px'; + } + clampToViewport(el); const topPercent = (el.offsetTop / window.innerHeight) * 100; diff --git a/src/plugins/layers/useGrayLine.js b/src/plugins/layers/useGrayLine.js index a7c2503d..9c497646 100644 --- a/src/plugins/layers/useGrayLine.js +++ b/src/plugins/layers/useGrayLine.js @@ -286,24 +286,14 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) { const GrayLineControl = L.Control.extend({ options: { position: 'topright' }, onAdd: function () { - const container = L.DomUtil.create('div', 'grayline-control'); - container.style.cssText = ` - background: var(--bg-panel); - padding: 12px; - border-radius: 5px; - font-family: 'JetBrains Mono', monospace; - font-size: 11px; - color: var(--text-primary); - border: 1px solid var(--border-color); - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - min-width: 200px; - `; + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const container = L.DomUtil.create('div', 'grayline-control', panelWrapper); const now = new Date(); const timeStr = now.toUTCString(); container.innerHTML = ` -
🌅 Gray Line
+
🌅 Gray Line
UTC TIME
@@ -338,7 +328,7 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) { L.DomEvent.disableClickPropagation(container); L.DomEvent.disableScrollPropagation(container); - return container; + return panelWrapper; }, }); @@ -362,7 +352,7 @@ export function useLayer({ enabled = false, opacity = 0.5, map = null }) { } catch (e) {} } - makeDraggable(container, 'grayline-position'); + makeDraggable(container, 'grayline-position', { snap: 5 }); addMinimizeToggle(container, 'grayline-position', { contentClassName: 'grayline-panel-content', buttonClassName: 'grayline-minimize-btn', diff --git a/src/plugins/layers/useLightning.js b/src/plugins/layers/useLightning.js index 152001f4..a751c55a 100644 --- a/src/plugins/layers/useLightning.js +++ b/src/plugins/layers/useLightning.js @@ -444,19 +444,11 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory options: { position: 'topright' }, onAdd: function () { console.log('[Lightning] StatsControl onAdd called'); - const div = L.DomUtil.create('div', 'lightning-stats'); - div.style.cssText = ` - background: var(--bg-panel); - padding: 10px; - border-radius: 8px; - border: 1px solid var(--border-color); - font-family: 'JetBrains Mono', monospace; - font-size: 11px; - color: var(--text-primary); - min-width: 180px; - `; + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const div = L.DomUtil.create('div', 'lightning-stats', panelWrapper); + div.innerHTML = ` -
⚡️ Lightning Activity
+
⚡️ Lightning Activity
Connecting...
`; @@ -465,7 +457,7 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory L.DomEvent.disableScrollPropagation(div); console.log('[Lightning] Stats panel div created'); - return div; + return panelWrapper; }, }); @@ -495,7 +487,7 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory } } - makeDraggable(container, 'lightning-stats-position'); + makeDraggable(container, 'lightning-stats-position', { snap: 5 }); addMinimizeToggle(container, 'lightning-stats-position', { contentClassName: 'lightning-panel-content', buttonClassName: 'lightning-minimize-btn', @@ -692,28 +684,17 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory const ProximityControl = L.Control.extend({ options: { position: 'bottomright' }, onAdd: function () { - const div = L.DomUtil.create('div', 'lightning-proximity'); - div.style.cssText = ` - background: var(--bg-panel); - padding: 10px; - border-radius: 8px; - border: 1px solid var(--border-color); - font-family: 'JetBrains Mono', monospace; - font-size: 11px; - color: var(--text-primary); - min-width: 200px; - max-width: 280px; - `; - div.innerHTML = ` -
📍 Nearby Strikes (30km)
-
No recent strikes
- `; + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const div = L.DomUtil.create('div', 'lightning-proximity', panelWrapper); + + div.innerHTML = + '
📍 Nearby Strikes (30km)
No recent strikes
'; // Prevent map interaction L.DomEvent.disableClickPropagation(div); L.DomEvent.disableScrollPropagation(div); - return div; + return panelWrapper; }, }); @@ -737,9 +718,8 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory // Default to CENTER of screen (not corner!) container.style.position = 'fixed'; - container.style.top = '50%'; - container.style.left = '50%'; - container.style.transform = 'translate(-50%, -50%)'; + container.style.top = '45%'; // NOTE: using 45% instead of 50% with transform: translateX/Y due to dragging issues + container.style.left = '45%'; container.style.right = 'auto'; container.style.bottom = 'auto'; container.style.zIndex = '1001'; // Ensure it's on top @@ -780,7 +760,7 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory positionLoaded = true; console.log('[Lightning] Proximity: Converted pixel to percentage:', { topPercent, leftPercent }); } else { - console.log('[Lightning] Proximity: Saved pixel position off-screen, using center'); + console.log('[Lightning] Proximity: Saved pixel position off-screen, using default'); localStorage.removeItem('lightning-proximity-position'); } } @@ -790,7 +770,7 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory } // Make draggable - pass flag to skip position loading since we already did it - makeDraggable(container, 'lightning-proximity-position', positionLoaded); + makeDraggable(container, 'lightning-proximity-position', { skipPositionLoad: positionLoaded, snap: 5 }); addMinimizeToggle(container, 'lightning-proximity-position', { contentClassName: 'lightning-panel-content', buttonClassName: 'lightning-minimize-btn', @@ -858,8 +838,8 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null, lowMemory if (nearbyStrikes.length === 0) { contentHTML = ` -
- ✅ No strikes within 30km
+
+ ✅ No strikes within 30km
All clear
`; diff --git a/src/plugins/layers/useMUFMap.js b/src/plugins/layers/useMUFMap.js index ad77697e..8dbf041d 100644 --- a/src/plugins/layers/useMUFMap.js +++ b/src/plugins/layers/useMUFMap.js @@ -295,34 +295,16 @@ export function useLayer({ map, enabled, opacity }) { const MUFControl = L.Control.extend({ options: { position: 'topright' }, - onAdd() { - const container = L.DomUtil.create('div', 'muf-map-control'); - L.DomEvent.disableClickPropagation(container); - L.DomEvent.disableScrollPropagation(container); + onAdd: function () { + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const container = L.DomUtil.create('div', 'muf-map-control', panelWrapper); container.innerHTML = ` -
-
- 📡 MUF Map - -
-
+
📡 MUF Map
+
- 3 MHz - 14 - 28 - 40+ + 3 MHz + 14 + 28 + 40+
-
+
${loading ? 'Loading...' : stationCount > 0 ? `${stationCount} ionosondes` : 'Waiting for data...'}
-
-
`; - return container; + + // Prevent map interaction when clicking/dragging on this control + L.DomEvent.disableClickPropagation(container); + L.DomEvent.disableScrollPropagation(container); + + return panelWrapper; }, }); @@ -356,13 +341,14 @@ export function useLayer({ map, enabled, opacity }) { map.addControl(controlRef.current); setTimeout(() => { - const container = controlRef.current?._container; - if (!container) return; - addMinimizeToggle(container, 'muf-map-position', { - contentClassName: 'muf-panel-content', - buttonClassName: 'muf-minimize-btn', - }); - makeDraggable(container, 'muf-map-position'); + const container = document.querySelector('.muf-map-control'); + if (container) { + makeDraggable(container, 'muf-map-position', { snap: 5 }); + addMinimizeToggle(container, 'muf-map-position', { + contentClassName: 'muf-panel-content', + buttonClassName: 'muf-minimize-btn', + }); + } }, 150); }, [enabled, map, stations, loading]); diff --git a/src/plugins/layers/useN3FJPLoggedQSOs.js b/src/plugins/layers/useN3FJPLoggedQSOs.js index 19ad9699..579a3f27 100644 --- a/src/plugins/layers/useN3FJPLoggedQSOs.js +++ b/src/plugins/layers/useN3FJPLoggedQSOs.js @@ -81,20 +81,13 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) { const Control = L.Control.extend({ options: { position: 'topright' }, - onAdd() { - const div = L.DomUtil.create('div', 'n3fjp-control'); - div.style.cssText = ` - background: var(--bg-panel); - padding: 10px; - border-radius: 8px; - border: 1px solid var(--border-color); - font-family: 'JetBrains Mono', monospace; - font-size: 11px; - color: var(--text-primary); - min-width: 190px; - `; + onAdd: function () { + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const div = L.DomUtil.create('div', 'n3fjp-control', panelWrapper); + div.innerHTML = ` -
🗺️ N3FJP Logged QSOs
+
🗺️ N3FJP Logged QSOs
+
QSOs: ${qsos.length}
Display: ${displayMinutes} min
@@ -105,7 +98,7 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) { L.DomEvent.disableClickPropagation(div); L.DomEvent.disableScrollPropagation(div); - return div; + return panelWrapper; }, }); @@ -113,26 +106,26 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) { map.addControl(controlRef.current); setTimeout(() => { - const container = controlRef.current?._container; - if (!container) return; + const container = document.querySelector('.n3fjp-control'); + if (container) { + const saved = localStorage.getItem('n3fjp-position'); + if (saved) { + try { + const { top, left } = JSON.parse(saved); + container.style.position = 'fixed'; + container.style.top = top + 'px'; + container.style.left = left + 'px'; + container.style.right = 'auto'; + container.style.bottom = 'auto'; + } catch {} + } - const saved = localStorage.getItem('n3fjp-position'); - if (saved) { - try { - const { top, left } = JSON.parse(saved); - container.style.position = 'fixed'; - container.style.top = top + 'px'; - container.style.left = left + 'px'; - container.style.right = 'auto'; - container.style.bottom = 'auto'; - } catch {} + makeDraggable(container, 'n3fjp-position', { snap: 5 }); + addMinimizeToggle(container, 'n3fjp-position', { + contentClassName: 'n3fjp-panel-content', + buttonClassName: 'n3fjp-minimize-btn', + }); } - - addMinimizeToggle(container, 'n3fjp-position', { - contentClassName: 'n3fjp-panel-content', - buttonClassName: 'n3fjp-minimize-btn', - }); - makeDraggable(container, 'n3fjp-position'); }, 150); return () => { diff --git a/src/plugins/layers/useRBN.js b/src/plugins/layers/useRBN.js index 2a5ceba2..6e28e2ac 100644 --- a/src/plugins/layers/useRBN.js +++ b/src/plugins/layers/useRBN.js @@ -437,11 +437,11 @@ export function useLayer({ marker.bindPopup(`
- 📡 ${skimmerCall}
- Heard: ${callsign}
- SNR: ${snr} dB
- Band: ${band}
- Freq: ${(freq / 1000).toFixed(1)} kHz
+ 📡 ${skimmerCall}
+ Heard: ${callsign}
+ SNR: ${snr} dB
+ Band: ${band}
+ Freq: ${(freq / 1000).toFixed(1)} kHz
Grid: ${skimmerGrid}
Time: ${timestamp.toLocaleTimeString()}
@@ -454,29 +454,23 @@ export function useLayer({ // Create control panel useEffect(() => { - if (!map || !enabled) return; - - // Create control panel - const control = L.control({ position: 'topright' }); - - control.onAdd = function () { - const div = L.DomUtil.create('div', 'leaflet-bar leaflet-control rbn-control'); - div.style.background = 'var(--bg-panel)'; - div.style.padding = '10px'; - div.style.borderRadius = '8px'; - div.style.minWidth = '250px'; - div.style.color = 'var(--text-primary)'; - div.style.fontFamily = "'JetBrains Mono', monospace"; - div.style.fontSize = '12px'; - div.style.border = '1px solid var(--border-color)'; - - div.innerHTML = ` -
- 📡 RBN: ${callsign} -
-
- Spots: 0 | Skimmers: 0
- Avg SNR: 0 dB + if (!enabled || !map || controlRef.current) return; + + const Control = L.Control.extend({ + options: { position: 'topright' }, + onAdd: function () { + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const div = L.DomUtil.create('div', 'rbn-control', panelWrapper); + div.style.cssText = ` + min-width: 250px; + max-width: 300px; + `; + div.innerHTML = ` +
📡 RBN: ${callsign}
+ +
+ Spots: 0 | Skimmers: 0
+ Avg SNR: 0 dB
@@ -513,88 +507,91 @@ export function useLayer({
Data: reversebeacon.net | Update: 10sec
- `; - - // Add event listeners - setTimeout(() => { - const bandSelect = document.getElementById('rbn-band-select'); - const timeSlider = document.getElementById('rbn-time-slider'); - const timeValue = document.getElementById('rbn-time-value'); - const snrSlider = document.getElementById('rbn-snr-slider'); - const snrValue = document.getElementById('rbn-snr-value'); - const pathsCheck = document.getElementById('rbn-paths-check'); - - if (bandSelect) { - bandSelect.value = selectedBand; - bandSelect.addEventListener('change', (e) => setSelectedBand(e.target.value)); - } - - if (timeSlider && timeValue) { - // Set initial value - timeSlider.value = timeWindow; - if (timeWindow < 1) { - timeValue.textContent = (timeWindow * 60).toFixed(0) + 's'; - } else { - timeValue.textContent = timeWindow.toFixed(1) + 'min'; + `; + + // Add event listeners + setTimeout(() => { + const bandSelect = document.getElementById('rbn-band-select'); + const timeSlider = document.getElementById('rbn-time-slider'); + const timeValue = document.getElementById('rbn-time-value'); + const snrSlider = document.getElementById('rbn-snr-slider'); + const snrValue = document.getElementById('rbn-snr-value'); + const pathsCheck = document.getElementById('rbn-paths-check'); + + if (bandSelect) { + bandSelect.value = selectedBand; + bandSelect.addEventListener('change', (e) => setSelectedBand(e.target.value)); } - timeSlider.addEventListener('input', (e) => { - const val = parseFloat(e.target.value); - // Display as seconds if < 1 minute, otherwise minutes - if (val < 1) { - timeValue.textContent = (val * 60).toFixed(0) + 's'; + if (timeSlider && timeValue) { + // Set initial value + timeSlider.value = timeWindow; + if (timeWindow < 1) { + timeValue.textContent = (timeWindow * 60).toFixed(0) + 's'; } else { - timeValue.textContent = val.toFixed(1) + 'min'; + timeValue.textContent = timeWindow.toFixed(1) + 'min'; } - setTimeWindow(val); - }); - } - if (snrSlider && snrValue) { - snrSlider.value = minSNR; - snrValue.textContent = minSNR; - snrSlider.addEventListener('input', (e) => { - const val = e.target.value; - snrValue.textContent = val; - setMinSNR(parseInt(val)); - }); - } + timeSlider.addEventListener('input', (e) => { + const val = parseFloat(e.target.value); + // Display as seconds if < 1 minute, otherwise minutes + if (val < 1) { + timeValue.textContent = (val * 60).toFixed(0) + 's'; + } else { + timeValue.textContent = val.toFixed(1) + 'min'; + } + setTimeWindow(val); + }); + } - if (pathsCheck) { - pathsCheck.checked = showPaths; - pathsCheck.addEventListener('change', (e) => setShowPaths(e.target.checked)); - } - }, 100); + if (snrSlider && snrValue) { + snrSlider.value = minSNR; + snrValue.textContent = minSNR; + snrSlider.addEventListener('input', (e) => { + const val = e.target.value; + snrValue.textContent = val; + setMinSNR(parseInt(val)); + }); + } - L.DomEvent.disableClickPropagation(div); - L.DomEvent.disableScrollPropagation(div); + if (pathsCheck) { + pathsCheck.checked = showPaths; + pathsCheck.addEventListener('change', (e) => setShowPaths(e.target.checked)); + } + }, 100); - return div; - }; + L.DomEvent.disableClickPropagation(div); + L.DomEvent.disableScrollPropagation(div); + + return panelWrapper; + }, + }); - control.addTo(map); + const control = new Control(); + map.addControl(control); controlRef.current = control; // Make the control draggable and minimizable // Use setTimeout to ensure DOM is ready setTimeout(() => { - const controlElement = control.getContainer(); - if (controlElement) { + // const container = control.getContainer(); + const container = document.querySelector('.rbn-control'); + if (container) { // Apply saved position IMMEDIATELY before making draggable const saved = localStorage.getItem('rbn-panel-position'); if (saved) { try { const { top, left } = JSON.parse(saved); - controlElement.style.position = 'fixed'; - controlElement.style.top = top + 'px'; - controlElement.style.left = left + 'px'; - controlElement.style.right = 'auto'; - controlElement.style.bottom = 'auto'; + container.style.position = 'fixed'; + container.style.top = top + 'px'; + container.style.left = left + 'px'; + container.style.right = 'auto'; + container.style.bottom = 'auto'; } catch (e) {} } - makeDraggable(controlElement, 'rbn-panel-position'); - addMinimizeToggle(controlElement, 'rbn-panel', { + makeDraggable(container, 'rbn-panel-position', { snap: 5 }); + addMinimizeToggle(container, 'rbn-panel-position', { contentClassName: 'rbn-panel-content', buttonClassName: 'rbn-minimize-btn', }); @@ -622,8 +619,8 @@ export function useLayer({ if (statsDisplay) { statsDisplay.innerHTML = ` - Spots: ${stats.total} | Skimmers: ${stats.skimmers}
- Avg SNR: ${stats.avgSNR} dB + Spots: ${stats.total} | Skimmers: ${stats.skimmers}
+ Avg SNR: ${stats.avgSNR} dB `; } }, [enabled, stats]); diff --git a/src/plugins/layers/useVOACAPHeatmap.js b/src/plugins/layers/useVOACAPHeatmap.js index 8b0fd52b..6c9ef77c 100644 --- a/src/plugins/layers/useVOACAPHeatmap.js +++ b/src/plugins/layers/useVOACAPHeatmap.js @@ -179,7 +179,7 @@ export function useLayer({ map, enabled, opacity, locator }) { // Create control panel useEffect(() => { - if (!map || !enabled) return; + if (!enabled || !map || controlRef.current) return; // Avoid duplicate controls if (controlRef.current) { @@ -192,9 +192,8 @@ export function useLayer({ map, enabled, opacity, locator }) { const VOACAPControl = L.Control.extend({ options: { position: 'topright' }, onAdd: function () { - const container = L.DomUtil.create('div', 'voacap-heatmap-control'); - L.DomEvent.disableClickPropagation(container); - L.DomEvent.disableScrollPropagation(container); + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const container = L.DomUtil.create('div', 'voacap-heatmap-control', panelWrapper); const bandOptions = BANDS.map( (b, i) => ``, @@ -213,89 +212,73 @@ export function useLayer({ map, enabled, opacity, locator }) { .join(''); container.innerHTML = ` -
-
- 🌐 VOACAP Heatmap - -
-
+
🌐 VOACAP Heatmap
+
- +
- +
- +
- +
- Poor + Poor - Low + Low - Fair + Fair - Good + Good
-
+
${loading ? 'Loading...' : data ? `${data.mode || 'SSB'} ${data.power || 100}W | SFI: ${data.solarData?.sfi} K: ${data.solarData?.kIndex}` : 'Ready'}
-
-
`; + L.DomEvent.disableClickPropagation(container); + L.DomEvent.disableScrollPropagation(container); - return container; + return panelWrapper; }, }); - controlRef.current = new VOACAPControl(); - map.addControl(controlRef.current); + const control = new VOACAPControl(); + map.addControl(control); + controlRef.current = control; // Helper to update both plugin state AND global config in localStorage const updateGlobalConfig = (mode, power) => { @@ -311,28 +294,28 @@ export function useLayer({ map, enabled, opacity, locator }) { // Wire up event handlers after DOM is ready setTimeout(() => { - const container = controlRef.current?._container; - if (!container) return; + const container = document.querySelector('.voacap-heatmap-control'); + if (container) { + // Apply saved position + const saved = localStorage.getItem('voacap-heatmap-position'); + if (saved) { + try { + const { top, left } = JSON.parse(saved); + container.style.position = 'fixed'; + container.style.top = top + 'px'; + container.style.left = left + 'px'; + container.style.right = 'auto'; + container.style.bottom = 'auto'; + } catch (e) {} + } - // Apply saved position - const saved = localStorage.getItem('voacap-heatmap-position'); - if (saved) { - try { - const { top, left } = JSON.parse(saved); - container.style.position = 'fixed'; - container.style.top = top + 'px'; - container.style.left = left + 'px'; - container.style.right = 'auto'; - container.style.bottom = 'auto'; - } catch (e) {} + makeDraggable(container, 'voacap-heatmap-position', { snap: 5 }); + addMinimizeToggle(container, 'voacap-heatmap-position', { + contentClassName: 'voacap-panel-content', + buttonClassName: 'voacap-minimize-btn', + }); } - addMinimizeToggle(container, 'voacap-heatmap-position', { - contentClassName: 'voacap-panel-content', - buttonClassName: 'voacap-minimize-btn', - }); - makeDraggable(container, 'voacap-heatmap-position'); - const bandSelect = document.getElementById('voacap-band-select'); const gridSelect = document.getElementById('voacap-grid-select'); const modeSelect = document.getElementById('voacap-mode-select'); diff --git a/src/plugins/layers/useWSPR.js b/src/plugins/layers/useWSPR.js index 8800bde6..d1afd6e6 100644 --- a/src/plugins/layers/useWSPR.js +++ b/src/plugins/layers/useWSPR.js @@ -399,21 +399,11 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe const FilterControl = L.Control.extend({ options: { position: 'topright' }, onAdd: function () { - const container = L.DomUtil.create('div', 'wspr-filter-control'); - container.style.cssText = ` - background: var(--bg-panel); - padding: 12px; - border-radius: 5px; - font-family: 'JetBrains Mono', monospace; - font-size: 11px; - color: var(--text-primary); - border: 1px solid var(--border-color); - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - min-width: 180px; - `; + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const container = L.DomUtil.create('div', 'wspr-filter-control', panelWrapper); container.innerHTML = ` -
🎛️ Filters
+
🎛️ Filters
@@ -498,7 +488,7 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe L.DomEvent.disableClickPropagation(container); L.DomEvent.disableScrollPropagation(container); - return container; + return panelWrapper; }, }); @@ -524,7 +514,7 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe } catch (e) {} } - makeDraggable(container, 'wspr-filter-position'); + makeDraggable(container, 'wspr-filter-position', { snap: 5 }); addMinimizeToggle(container, 'wspr-filter-position', { contentClassName: 'wspr-panel-content', buttonClassName: 'wspr-minimize-btn', @@ -594,20 +584,12 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe const StatsControl = L.Control.extend({ options: { position: 'topleft' }, onAdd: function () { - const div = L.DomUtil.create('div', 'wspr-stats'); - div.style.cssText = ` - background: var(--bg-panel); - padding: 12px; - border-radius: 5px; - font-family: 'JetBrains Mono', monospace; - font-size: 11px; - color: var(--text-primary); - border: 1px solid var(--border-color); - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - min-width: 160px; - `; + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const div = L.DomUtil.create('div', 'wspr-stats', panelWrapper); + div.innerHTML = ` -
📊 WSPR Activity
+
📊 WSPR Activity
+
Propagation Score
--/100
@@ -623,7 +605,7 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe L.DomEvent.disableClickPropagation(div); L.DomEvent.disableScrollPropagation(div); - return div; + return panelWrapper; }, }); @@ -648,7 +630,7 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe } catch (e) {} } - makeDraggable(container, 'wspr-stats-position'); + makeDraggable(container, 'wspr-stats-position', { snap: 5 }); addMinimizeToggle(container, 'wspr-stats-position', { contentClassName: 'wspr-panel-content', buttonClassName: 'wspr-minimize-btn', @@ -660,19 +642,12 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe const LegendControl = L.Control.extend({ options: { position: 'bottomright' }, onAdd: function () { - const div = L.DomUtil.create('div', 'wspr-legend'); - div.style.cssText = ` - background: var(--bg-panel); - padding: 10px; - border-radius: 5px; - font-family: 'JetBrains Mono', monospace; - font-size: 11px; - color: var(--text-primary); - border: 1px solid var(--border-color); - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - `; + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const div = L.DomUtil.create('div', 'wspr-legend', panelWrapper); + div.innerHTML = ` -
📡 Signal Strength
+
📡 Signal Strength
+
Excellent (> 5 dB)
Good (0 to 5 dB)
Moderate (-10 to 0 dB)
@@ -682,9 +657,10 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe Best DX Paths
`; - return div; + return panelWrapper; }, }); + const legend = new LegendControl(); map.addControl(legend); legendControlRef.current = legend; @@ -706,7 +682,7 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe } catch (e) {} } - makeDraggable(container, 'wspr-legend-position'); + makeDraggable(container, 'wspr-legend-position', { snap: 5 }); addMinimizeToggle(container, 'wspr-legend-position', { contentClassName: 'wspr-panel-content', buttonClassName: 'wspr-minimize-btn', @@ -718,26 +694,17 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe const ChartControl = L.Control.extend({ options: { position: 'bottomleft' }, onAdd: function () { - const div = L.DomUtil.create('div', 'wspr-chart'); - div.style.cssText = ` - background: var(--bg-panel); - padding: 10px; - border-radius: 5px; - font-family: 'JetBrains Mono', monospace; - font-size: 10px; - color: var(--text-primary); - border: 1px solid var(--border-color); - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - min-width: 160px; - `; + const panelWrapper = L.DomUtil.create('div', 'panel-wrapper'); + const div = L.DomUtil.create('div', 'wspr-chart', panelWrapper); + div.innerHTML = - '
📊 Band Activity
Loading...
'; + '
📊 Band Activity
Loading...
'; // Prevent map interaction when clicking/dragging on this control L.DomEvent.disableClickPropagation(div); L.DomEvent.disableScrollPropagation(div); - return div; + return panelWrapper; }, }); @@ -762,7 +729,7 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe } catch (e) {} } - makeDraggable(container, 'wspr-chart-position'); + makeDraggable(container, 'wspr-chart-position', { snap: 5 }); addMinimizeToggle(container, 'wspr-chart-position', { contentClassName: 'wspr-panel-content', buttonClassName: 'wspr-minimize-btn', @@ -1143,7 +1110,8 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe } else { // Initial render before minimize toggle is added statsContainer.innerHTML = ` -
📊 WSPR Activity
+
📊 WSPR Activity
+ ${contentHTML} `; } @@ -1189,7 +1157,8 @@ export function useLayer({ enabled = false, map = null, callsign, locator, lowMe } else { // Initial render before minimize toggle is added chartContainer.innerHTML = ` -
📊 Band Activity
+
📊 Band Activity
+ ${chartContentHTML} `; } diff --git a/src/styles/main.css b/src/styles/main.css index fe622449..cb0c27d9 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -489,6 +489,51 @@ body::before { letter-spacing: 0.5px; } +/* ============================================ + Leaflet Control Panels + ============================================ */ +.panel-wrapper > div { + transition: all 0.3s ease; + background: var(--bg-panel); + border-radius: 5px; + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + color: var(--text-primary); + border: 1px solid var(--border-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + min-width: 200px; + max-width: 280px; +} + +.panel-wrapper > div:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important; +} + +.panel-wrapper > div .floating-panel-header { + user-select: none; + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + margin: 0; + padding: 10px; + font-size: 13px; + color: var(--accent-cyan); + letter-spacing: 0.5px; +} + +/* minimize toggle */ +.panel-wrapper > div .floating-panel-header button { + color: var(--text-secondary); + width: 1em; + min-width: 1em; + height: 1em; + background: none; + border: none; + padding: 0; + margin: 0; + font-size: 1.1em; + line-height: 1em; +} + /* ============================================ DX CLUSTER MAP TOOLTIPS ============================================ */ @@ -599,7 +644,6 @@ body::before { } /* Theme controls */ - #theme-selector-component .theme-select-button { padding: 10px; background: var(--bg-tertiary); @@ -704,22 +748,6 @@ body::before { animation: wspr-marker-pulse 2s ease-in-out infinite; } -/* Control panel transitions */ -.wspr-filter-control, -.wspr-stats, -.wspr-legend, -.wspr-chart { - transition: all 0.3s ease; -} - -.wspr-filter-control:hover, -.wspr-stats:hover, -.wspr-legend:hover, -.wspr-chart:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important; -} - /* Filter input styles */ .wspr-filter-control select, .wspr-filter-control input[type='range'] { @@ -962,6 +990,17 @@ body::before { z-index: 10000 !important; } +/* content spacing in panels */ +.wspr-panel-content, +.lightning-panel-content, +.grayline-panel-content, +.muf-panel-content, +.n3fjp-panel-content, +.rbn-panel-content, +.voacap-panel-content { + padding: 0 10px 10px 10px; +} + /* ============================================ RESPONSIVE: Phone & Small Screen Overrides ============================================ */