From a8fac52400b2d9e5095a78b2c157e939ae2e54d6 Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Sun, 9 Aug 2026 13:12:43 +0200 Subject: [PATCH] feat(bandmap): drag the width, and remember it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both band maps were pinned to a hardcoded width — 300px docked beside the tables, 260px per card in the Band map tab. On a busy band the map could not be given more room, and on a quiet one the log could not take it back. The docked map becomes a resizable grid column with the grip in the gap between the panes, so the handle costs no space; the tab cards share one width with the grip on their right edge. Side-by-side columns of different widths read as a mistake rather than a choice, which is why the tab has one width and not one per card. Double-click either grip to return to the default. The drag measures from the pointer's START position rather than the container, so the same helper serves both edges — the docked map sits on the left or the right depending on the operator's setting, and the grip is on its inner edge either way. Pointer capture, like the main splitter: without it the map or the grid under the cursor swallows the moves. Both widths are persisted through writeUiPref and registered as portable, so they travel with the data folder like the main splitter and the rest of the layout. --- changelog.json | 10 +++++ frontend/src/App.tsx | 90 +++++++++++++++++++++++++++++++++++--- frontend/src/lib/i18n.tsx | 4 +- frontend/src/lib/uiPref.ts | 2 + 4 files changed, 99 insertions(+), 7 deletions(-) diff --git a/changelog.json b/changelog.json index 8fd423b..3181c65 100644 --- a/changelog.json +++ b/changelog.json @@ -1,4 +1,14 @@ [ + { + "version": "0.24.1", + "date": "", + "en": [ + "Band map: the width can be dragged. Both the map docked beside the tables and the per-band cards in the Band map tab were locked at a fixed width, so an operator watching a busy band could not give the map more room — nor take it back for the log. Grab the edge to resize, double-click it to go back to the default. The width is remembered and travels with your data folder, like the other layout settings. In the tab, one width applies to every card: they sit side by side, and columns of different widths read as a mistake." + ], + "fr": [ + "Band map : la largeur se règle à la souris. La carte ancrée à côté des tableaux et les cartes par bande de l'onglet Band map étaient figées à une largeur fixe : impossible de donner plus de place à la carte sur une bande chargée, ni de la reprendre pour le journal. Attrape le bord pour redimensionner, double-clic pour revenir au défaut. La largeur est mémorisée et voyage avec ton dossier de données, comme les autres réglages de disposition. Dans l'onglet, une seule largeur vaut pour toutes les cartes : elles sont côte à côte, et des colonnes de largeurs différentes se lisent comme une erreur." + ] + }, { "version": "0.24.0", "date": "", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ec1dfb2..eef404e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -907,6 +907,49 @@ export default function App() { return Number.isFinite(n) && n >= 15 && n <= 85 ? n : 50; }); useEffect(() => { writeUiPref('opslog.mainSplit', String(Math.round(mainSplit))); }, [mainSplit]); + + // Band-map widths. Two of them, because they are two different things: the + // docked map sits beside the tables and competes with them for room, while + // the cards in the Band map tab share a scrolling row and want to be uniform. + // Both were hardcoded — 300px and 260px — so an operator watching a busy band + // could not give the map the space it needed, nor claw it back for the log. + const BANDMAP_W_DEFAULT = 300, BANDMAP_W_MIN = 200, BANDMAP_W_MAX = 900; + const BANDMAP_TAB_W_DEFAULT = 260, BANDMAP_TAB_W_MIN = 160, BANDMAP_TAB_W_MAX = 700; + const readWidth = (key: string, def: number, min: number, max: number) => { + const n = parseFloat(localStorage.getItem(key) || ''); + return Number.isFinite(n) && n >= min && n <= max ? n : def; + }; + const [bandMapWidth, setBandMapWidth] = useState( + () => readWidth('opslog.bandMapWidth', BANDMAP_W_DEFAULT, BANDMAP_W_MIN, BANDMAP_W_MAX)); + const [bandMapTabWidth, setBandMapTabWidth] = useState( + () => readWidth('opslog.bandMapTabWidth', BANDMAP_TAB_W_DEFAULT, BANDMAP_TAB_W_MIN, BANDMAP_TAB_W_MAX)); + useEffect(() => { writeUiPref('opslog.bandMapWidth', String(Math.round(bandMapWidth))); }, [bandMapWidth]); + useEffect(() => { writeUiPref('opslog.bandMapTabWidth', String(Math.round(bandMapTabWidth))); }, [bandMapTabWidth]); + + // Drag one edge of a fixed-width column. Measures from the pointer's START + // position rather than the container, so it behaves the same whether the grip + // is on the left or the right edge — the docked map is docked on either side. + const startWidthDrag = ( + e: React.PointerEvent, current: number, edge: 'left' | 'right', + min: number, max: number, apply: (w: number) => void, + ) => { + e.preventDefault(); + // Pointer capture, for the same reason as the main splitter: without it a + // map or a grid under the cursor swallows the moves. + (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); + const x0 = e.clientX; + const onMove = (ev: PointerEvent) => { + const delta = edge === 'right' ? ev.clientX - x0 : x0 - ev.clientX; + apply(Math.min(max, Math.max(min, Math.round(current + delta)))); + }; + const onUp = () => { + window.removeEventListener('pointermove', onMove); + window.removeEventListener('pointerup', onUp); + }; + window.addEventListener('pointermove', onMove); + window.addEventListener('pointerup', onUp); + }; + const mainSplitRef = useRef(null); const startMainSplitDrag = (e: React.PointerEvent) => { e.preventDefault(); @@ -5948,9 +5991,15 @@ export default function App() { {/* ===== LOWER: tabbed table / cluster / band map ===== */} {compact ? null : <> -
-
+ {/* The band map is a fixed-width column with a draggable grip on its inner + edge — the gap between the two panes doubles as the handle, so no room + is spent on it. Same idiom as the Main tab's splitter. */} +
+
{t('tab.main')} @@ -6522,7 +6571,23 @@ export default function App() { Pick one or more bands above to show their band maps side by side.
) : bandMapBands.map((b) => ( -
+
+ {/* One width for every card: they sit side by side in a + scrolling row, and columns of different widths read as a + mistake rather than a choice. Grip on the right edge. */} +
startWidthDrag( + e, bandMapTabWidth, 'right', + BANDMAP_TAB_W_MIN, BANDMAP_TAB_W_MAX, setBandMapTabWidth)} + onDoubleClick={() => setBandMapTabWidth(BANDMAP_TAB_W_DEFAULT)} + className="group absolute inset-y-0 right-0 z-10 w-2 cursor-col-resize flex items-center justify-center" + > + +
s.band === b)} @@ -6541,7 +6606,22 @@ export default function App() {
{showBandMap && ( -
+
startWidthDrag( + e, bandMapWidth, bandMapSide === 'left' ? 'right' : 'left', + BANDMAP_W_MIN, BANDMAP_W_MAX, setBandMapWidth)} + onDoubleClick={() => setBandMapWidth(BANDMAP_W_DEFAULT)} + className="group relative order-2 cursor-col-resize flex items-center justify-center" + > + +
+ )} + {showBandMap && ( +