diff --git a/frontend/src/components/FilterPanel/FilterPanel.tsx b/frontend/src/components/FilterPanel/FilterPanel.tsx index a46608f..6b7e5a2 100644 --- a/frontend/src/components/FilterPanel/FilterPanel.tsx +++ b/frontend/src/components/FilterPanel/FilterPanel.tsx @@ -16,6 +16,16 @@ interface FilterPanelProps { betaPathConfidence?: number | null; } +export const LinksLegend: React.FC<{ compact?: boolean; muted?: boolean }> = ({ compact = false, muted = false }) => ( +
+
Links Legend
+
Good (≤120 dB)
+
Marginal (121-135 dB)
+
Weak (>135 dB)
+
Unknown (no dB yet)
+
+); + export const FILTER_ROWS: Array<{ key: keyof Filters; label: string; color: string; hollow?: boolean }> = [ { key: 'livePackets', label: 'Live Feed', color: '#00c4ff' }, { key: 'packetPaths', label: 'Packet Paths', color: '#00c4ff', hollow: true }, @@ -30,33 +40,6 @@ export const FilterPanel: React.FC = ({ filters, onChange, bet onChange({ ...filters, [key]: !filters[key] }); }; - const applyPreset = (preset: 'minimal' | 'links' | 'beta') => { - const next: Filters = { ...filters }; - if (preset === 'minimal') { - next.livePackets = true; - next.packetPaths = false; - next.betaPaths = false; - next.links = false; - next.coverage = false; - next.clientNodes = false; - } else if (preset === 'links') { - next.livePackets = true; - next.packetPaths = false; - next.betaPaths = true; - next.links = true; - next.coverage = false; - next.clientNodes = false; - } else { - next.livePackets = true; - next.packetPaths = false; - next.betaPaths = true; - next.links = true; - next.coverage = true; - next.clientNodes = false; - } - onChange(next); - }; - const confidenceLabel = betaPathConfidence == null ? 'N/A' : betaPathConfidence >= 0.75 @@ -68,11 +51,6 @@ export const FilterPanel: React.FC = ({ filters, onChange, bet return (
Layers
-
- - - -
{filters.betaPaths && (
Beta Confidence: {confidenceLabel} @@ -123,6 +101,7 @@ export const FilterPanel: React.FC = ({ filters, onChange, bet )} ))} +
); }; diff --git a/frontend/src/components/Map/MapView.tsx b/frontend/src/components/Map/MapView.tsx index 93d5243..523793d 100644 --- a/frontend/src/components/Map/MapView.tsx +++ b/frontend/src/components/Map/MapView.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react'; -import { MapContainer, TileLayer, useMap, Pane, Polygon, Polyline, Tooltip } from 'react-leaflet'; +import { MapContainer, TileLayer, useMap, Pane, Polygon, Polyline } from 'react-leaflet'; import type { LatLngExpression, Map as LeafletMap, Polyline as LeafletPolyline } from 'leaflet'; import type { MeshNode, PacketArc } from '../../hooks/useNodes.js'; import type { NodeCoverage } from '../../hooks/useCoverage.js'; @@ -294,16 +294,8 @@ export const MapView: React.FC = ({ weight, opacity, }} - interactive - > - -
-
Seen{line.observedCount}
-
Path Loss{line.pathLossDb == null ? 'unknown' : `${Math.round(line.pathLossDb)} dB`}
-
A→B / B→A{line.countAToB} / {line.countBToA}
-
-
- + interactive={false} + /> ); })} @@ -358,16 +350,6 @@ export const MapView: React.FC = ({ )} - {showLinks && ( -
-
Links Legend
-
Good (≤120 dB)
-
Marginal (121-135 dB)
-
Weak (>135 dB)
-
Unknown (no dB yet)
-
- )} - {/* deck.gl overlay — arcs only */} = { @@ -21,27 +21,14 @@ interface Props { pinnedPacketId?: string | null; } -const ROW_HEIGHT = 32; const VISIBLE_ROWS = 8; -const OVERSCAN = 4; export const PacketFeed: React.FC = React.memo(({ packets, nodes, onPacketClick, pinnedPacketId }) => { - const [scrollTop, setScrollTop] = useState(0); - const total = packets.length; - const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN); - const end = Math.min(total, start + VISIBLE_ROWS + OVERSCAN * 2); - const visible = useMemo( - () => packets.slice(start, end).map((packet, idx) => ({ packet, index: start + idx })), - [packets, start, end], - ); + const visible = useMemo(() => packets.slice(0, VISIBLE_ROWS), [packets]); return ( -
= VISIBLE_ROWS ? ' packet-feed--overflow' : ''}`} - onScroll={(e) => setScrollTop((e.target as HTMLDivElement).scrollTop)} - > -
- {visible.map(({ packet: p, index }) => { +
+ {visible.map((p) => { const typeLabel = p.packetType !== undefined ? (TYPE_LABELS[p.packetType] ?? `T${p.packetType}`) : '???'; @@ -63,7 +50,6 @@ export const PacketFeed: React.FC = React.memo(({ packets, nodes, onPacke
onPacketClick?.(p)} role="button" tabIndex={0} @@ -87,7 +73,6 @@ export const PacketFeed: React.FC = React.memo(({ packets, nodes, onPacke
); })} -
); }); diff --git a/frontend/src/components/app/MobileControls.tsx b/frontend/src/components/app/MobileControls.tsx index dc7e52d..d5c9ad9 100644 --- a/frontend/src/components/app/MobileControls.tsx +++ b/frontend/src/components/app/MobileControls.tsx @@ -1,7 +1,7 @@ import React from 'react'; import type { Map as LeafletMap } from 'leaflet'; import { NodeSearch } from '../Map/NodeSearch.js'; -import { FILTER_ROWS, type Filters } from '../FilterPanel/FilterPanel.js'; +import { FILTER_ROWS, LinksLegend, type Filters } from '../FilterPanel/FilterPanel.js'; import type { MeshNode } from '../../hooks/useNodes.js'; type MobileControlsProps = { @@ -42,9 +42,11 @@ export const MobileControls: React.FC = ({
))}
+
+ +
); - diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 48f2d7a..ea32b9c 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -327,33 +327,6 @@ html, body, #root { cursor: pointer; } -.filter-presets { - display: grid; - grid-template-columns: 1fr; - gap: 6px; - margin-bottom: 8px; -} - -.filter-preset-btn { - width: 100%; - text-align: left; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--bg-panel-alt); - color: var(--text-secondary); - font-size: 11px; - font-family: var(--font-mono); - padding: 6px 8px; - cursor: pointer; - transition: background var(--transition), color var(--transition), border-color var(--transition); -} - -.filter-preset-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); - border-color: var(--border-bright); -} - .filter-beta-note { font-size: 10px; color: var(--text-muted); @@ -544,48 +517,39 @@ html, body, #root { flex-shrink: 0; } -.link-tooltip { - min-width: 180px; - font-size: 11px; - color: var(--text-primary); -} - -.link-tooltip__row { - display: flex; - justify-content: space-between; - gap: 10px; -} - -.links-legend { - position: absolute; - left: 16px; - top: 70px; - z-index: 820; +.links-legend-inline { background: var(--bg-panel); border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 10px 12px; - min-width: 180px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); - pointer-events: none; + border-radius: var(--radius); + padding: 8px 10px; + margin-top: 8px; } -.links-legend__title { +.links-legend-inline--muted { + opacity: 0.6; +} + +.links-legend-inline--compact { + margin: 6px 8px 0; + padding: 7px 9px; +} + +.links-legend-inline__title { font-family: var(--font-mono); font-size: 10px; color: var(--text-muted); letter-spacing: 0.08em; text-transform: uppercase; - margin-bottom: 8px; + margin-bottom: 6px; } -.links-legend__row { +.links-legend-inline__row { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text-secondary); - margin-bottom: 4px; + margin-bottom: 3px; } .links-legend__swatch { @@ -603,16 +567,12 @@ html, body, #root { z-index: 800; width: 340px; height: 220px; - overflow-y: auto; + overflow: hidden; overflow-x: hidden; pointer-events: auto; - padding-right: 2px; -} - -/* Fade the oldest (topmost) item only when the feed is full */ -.packet-feed--overflow { - -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 40px, black 100%); - mask-image: linear-gradient(to bottom, transparent 0%, black 40px, black 100%); + display: flex; + flex-direction: column; + justify-content: flex-start; } .packet-item { @@ -1758,11 +1718,15 @@ html, body, #root { .mobile-filter-grid .filter-row { padding: 8px 6px; } /* ── Mobile search: shown below filter grid ──────────────────────────── */ - .mobile-search { +.mobile-search { display: block; padding: 8px 12px; border-top: 1px solid var(--border); } + .mobile-links-legend-wrap { + border-top: 1px solid var(--border); + padding-top: 2px; + } .mobile-search .node-search { position: static; transform: none;