mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-09-09 07:03:43 +00:00
149 lines
5.6 KiB
TypeScript
149 lines
5.6 KiB
TypeScript
import { useState } from "react";
|
|
import { MapStyleSwitcher } from "./MapStyleSwitcher";
|
|
import { SegmentedControl } from "./SegmentedControl";
|
|
import { NODE_TYPE_FILTER_OPTIONS, type NeighborLinesMode } from "./types";
|
|
import { Section } from "../../components/DetailPanel";
|
|
import { CopyLinkButton } from "../../components/CopyLinkButton";
|
|
import { useIsMobile } from "../../hooks/useMediaQuery";
|
|
|
|
// Open/closed state persists across sessions; no click-outside dismiss, so it stays open while you pan.
|
|
const OPEN_STORAGE_KEY = "beacon-map-settings-open";
|
|
|
|
const TYPE_OPTIONS = [{ value: "", label: "All" }, ...NODE_TYPE_FILTER_OPTIONS];
|
|
const CLUSTER_OPTIONS = [
|
|
{ value: "on", label: "On" },
|
|
{ value: "off", label: "Off" },
|
|
];
|
|
const NEIGHBOR_OPTIONS = [
|
|
{ value: "on", label: "On" },
|
|
{ value: "selected", label: "Selected" },
|
|
{ value: "off", label: "Off" },
|
|
];
|
|
|
|
// Legend for a selected node's coloured edges. Gradient stops mirror the map paint's log anchors
|
|
// (red ~1, yellow ~20 at 60%, green ~150+); palette vars keep it in step with the active theme.
|
|
function NeighborLegend() {
|
|
return (
|
|
<div className="mt-2.5">
|
|
<div className="text-[10px] text-text-dim uppercase tracking-wider mb-1">Observations</div>
|
|
<div
|
|
className="h-2 rounded-sm border border-border-subtle"
|
|
style={{ background: "linear-gradient(to right, var(--palette-danger) 0%, var(--palette-warn) 60%, var(--palette-green) 100%)" }}
|
|
/>
|
|
<div className="relative h-3 mt-0.5 text-[9px] text-text-dim tabular-nums">
|
|
<span className="absolute left-0">1</span>
|
|
<span className="absolute -translate-x-1/2" style={{ left: "60%" }}>20</span>
|
|
<span className="absolute right-0">150+</span>
|
|
</div>
|
|
<div className="text-[9px] text-text-dim mt-1">fainter = heard longer ago</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface MapSettingsPanelProps {
|
|
styleId: string;
|
|
onStyleChange: (id: string) => void;
|
|
typeFilter: string;
|
|
onTypeChange: (t: string) => void;
|
|
clustered: boolean;
|
|
onClusteredChange: (c: boolean) => void;
|
|
neighborLines: NeighborLinesMode;
|
|
onNeighborLinesChange: (mode: NeighborLinesMode) => void;
|
|
// builds deep-link params for the current view, evaluated at copy time (reads the live camera)
|
|
buildShareParams: () => Record<string, string | null>;
|
|
}
|
|
|
|
export function MapSettingsPanel({
|
|
styleId,
|
|
onStyleChange,
|
|
typeFilter,
|
|
onTypeChange,
|
|
clustered,
|
|
onClusteredChange,
|
|
neighborLines,
|
|
onNeighborLinesChange,
|
|
buildShareParams,
|
|
}: MapSettingsPanelProps) {
|
|
const isMobile = useIsMobile();
|
|
// collapsed by default on mobile (the card would cover the map); a saved preference still wins
|
|
const [open, setOpen] = useState(() => {
|
|
const stored = localStorage.getItem(OPEN_STORAGE_KEY);
|
|
return stored === null ? !isMobile : stored === "true";
|
|
});
|
|
|
|
const toggle = () => {
|
|
const next = !open;
|
|
setOpen(next);
|
|
try {
|
|
localStorage.setItem(OPEN_STORAGE_KEY, String(next));
|
|
} catch {
|
|
// private mode / quota — the toggle still works, just not persisted
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="absolute top-3 left-3 z-10 w-60 max-w-[calc(100vw-1.5rem)] bg-bg-raised border border-border rounded-md shadow-lg overflow-hidden font-mono">
|
|
<button
|
|
type="button"
|
|
onClick={toggle}
|
|
aria-expanded={open}
|
|
className="flex items-center justify-between w-full px-3 py-2 text-[11px] uppercase tracking-wider text-text-dim hover:text-text-normal transition-colors cursor-pointer"
|
|
>
|
|
<span className="flex items-center gap-1.5">
|
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
<path d="M2 4.5h7M2 11.5h4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
|
|
<circle cx="12" cy="4.5" r="1.7" fill="currentColor" />
|
|
<circle cx="9" cy="11.5" r="1.7" fill="currentColor" />
|
|
<path d="M9 11.5h5M12 4.5h2" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
|
|
</svg>
|
|
Map Settings
|
|
</span>
|
|
<span aria-hidden className="text-text-dim text-[9px]">{open ? "▾" : "▸"}</span>
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="border-t border-border-subtle">
|
|
<Section title="Map Tiles" first>
|
|
<MapStyleSwitcher styleId={styleId} onChange={onStyleChange} className="w-full" />
|
|
</Section>
|
|
<Section title="Node Type">
|
|
<SegmentedControl
|
|
wrap
|
|
ariaLabel="Node type"
|
|
options={TYPE_OPTIONS}
|
|
value={typeFilter}
|
|
onChange={onTypeChange}
|
|
/>
|
|
</Section>
|
|
<Section title="Clustering">
|
|
<SegmentedControl
|
|
ariaLabel="Clustering"
|
|
options={CLUSTER_OPTIONS}
|
|
value={clustered ? "on" : "off"}
|
|
onChange={(v) => onClusteredChange(v === "on")}
|
|
className="w-full"
|
|
/>
|
|
</Section>
|
|
<Section title="Neighbor Lines">
|
|
<SegmentedControl
|
|
ariaLabel="Neighbor lines"
|
|
options={NEIGHBOR_OPTIONS}
|
|
value={neighborLines}
|
|
onChange={(v) => onNeighborLinesChange(v as NeighborLinesMode)}
|
|
className="w-full"
|
|
/>
|
|
{neighborLines === "selected" && <NeighborLegend />}
|
|
</Section>
|
|
<div className="px-3 py-2.5 border-t border-border-subtle flex justify-end">
|
|
<CopyLinkButton
|
|
params={buildShareParams}
|
|
label="Copy map link"
|
|
ariaLabel="Copy a link to this map view"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|