feat(main): draggable divider between the two panes
The Main tab was a fixed 50/50 grid, but a map and a cluster list want very different widths and which one deserves the room changes with what the operator is doing. The share is clamped to 15..85: a pane can be made small but never dragged out of existence, because recovering from that would mean grabbing a divider no longer on screen. Double-click restores even. The drag uses pointer capture on the divider — without it Leaflet swallows the moves as soon as the cursor crosses the map. Persisted through writeUiPref, so it travels with data/ like the other UI prefs.
This commit is contained in:
+46
-1
@@ -871,6 +871,36 @@ export default function App() {
|
||||
return ALWAYS_TABS.includes(saved) ? saved : 'recent';
|
||||
});
|
||||
useEffect(() => { writeUiPref('opslog.activeTab', activeTab); }, [activeTab]);
|
||||
|
||||
// Main tab: how much width the LEFT pane gets, in percent. Clamped to 15..85
|
||||
// so a pane can be made small but never dragged out of existence — recovering
|
||||
// from that means finding a divider that is no longer on screen.
|
||||
const [mainSplit, setMainSplit] = useState<number>(() => {
|
||||
const n = parseFloat(localStorage.getItem('opslog.mainSplit') || '');
|
||||
return Number.isFinite(n) && n >= 15 && n <= 85 ? n : 50;
|
||||
});
|
||||
useEffect(() => { writeUiPref('opslog.mainSplit', String(Math.round(mainSplit))); }, [mainSplit]);
|
||||
const mainSplitRef = useRef<HTMLDivElement | null>(null);
|
||||
const startMainSplitDrag = (e: React.PointerEvent) => {
|
||||
e.preventDefault();
|
||||
const host = mainSplitRef.current;
|
||||
if (!host) return;
|
||||
// Pointer capture on the divider, so dragging over a map keeps working —
|
||||
// Leaflet would otherwise swallow the moves the moment the cursor entered it.
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
const onMove = (ev: PointerEvent) => {
|
||||
const r = host.getBoundingClientRect();
|
||||
if (r.width <= 0) return;
|
||||
const pct = ((ev.clientX - r.left) / r.width) * 100;
|
||||
setMainSplit(Math.min(85, Math.max(15, pct)));
|
||||
};
|
||||
const onUp = () => {
|
||||
window.removeEventListener('pointermove', onMove);
|
||||
window.removeEventListener('pointerup', onUp);
|
||||
};
|
||||
window.addEventListener('pointermove', onMove);
|
||||
window.addEventListener('pointerup', onUp);
|
||||
};
|
||||
// QSL Manager is a closable tab opened on demand from Tools → QSL Manager.
|
||||
const [qslTabOpen, setQslTabOpen] = useState(false);
|
||||
const [qslDesignerOpen, setQslDesignerOpen] = useState(false);
|
||||
@@ -6018,8 +6048,23 @@ export default function App() {
|
||||
{/* Two configurable panes (per-profile, Settings → Main view).
|
||||
Each side shows one of: great-circle map, locator map, cluster
|
||||
or worked-before. */}
|
||||
<div className="grid grid-cols-2 grid-rows-1 gap-2 h-full min-h-0 p-2">
|
||||
{/* The divider is draggable: a map and a cluster list want very
|
||||
different widths, and which one deserves the room changes with
|
||||
what the operator is doing. The share is persisted (portable),
|
||||
and a double-click puts it back to even. */}
|
||||
<div ref={mainSplitRef} className="grid grid-rows-1 gap-2 h-full min-h-0 p-2"
|
||||
style={{ gridTemplateColumns: `${mainSplit}fr 6px ${100 - mainSplit}fr` }}>
|
||||
<div className="min-h-0 min-w-0 flex">{renderMainPane(mainPaneLeft)}</div>
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
title={t('main.splitTip')}
|
||||
onPointerDown={startMainSplitDrag}
|
||||
onDoubleClick={() => setMainSplit(50)}
|
||||
className="group relative cursor-col-resize flex items-center justify-center -mx-1 px-1"
|
||||
>
|
||||
<span className="h-10 w-1 rounded-full bg-border group-hover:bg-primary transition-colors" />
|
||||
</div>
|
||||
<div className="min-h-0 min-w-0 flex">{renderMainPane(mainPaneRight)}</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user