diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 7b454294..f17449b4 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -1015,7 +1015,7 @@ SPDX-License-Identifier: GPL-2.0-or-later -
+
Search
@@ -1034,9 +1034,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
Paths
- - - TRX paths on popup, directed decode paths when target locator is known + + + TRX paths on popup, directed decode paths when target locator is known
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 6642ae58..1adcf86d 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -2556,18 +2556,25 @@ button.map-qso-card:focus-visible { border-color: color-mix(in srgb, var(--accent-blue, #5b9bd5) 35%, transparent); background: color-mix(in srgb, var(--accent-blue, #5b9bd5) 10%, transparent); } +/* A toolbar across the top of the map rather than a panel parked in a corner: + it leaves the map itself unobscured, and the bottom-left band legend keeps + its place. It stops short of the corner controls, which stay outside it -- + the button that hides the filters cannot live inside the thing it hides. */ .map-overlay-panel { position: absolute; - right: 0.7rem; - bottom: 0.7rem; + top: 0.7rem; + /* Clear of Leaflet's zoom buttons, which draw over the top-left corner. */ + left: 3.4rem; + right: 9.6rem; z-index: 410; display: flex; - flex-direction: column; - gap: 0.45rem; - width: min(30rem, calc(100% - 4.9rem)); + flex-flow: row wrap; + align-items: center; + gap: 0.35rem 0.5rem; + width: auto; max-height: calc(100% - 1.4rem); - padding: 0.7rem 0.75rem; - border-radius: 0.8rem; + padding: 0.45rem 0.6rem; + border-radius: 0.7rem; border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent); background: color-mix(in srgb, var(--card-bg) 82%, transparent); box-shadow: 0 16px 30px rgba(0, 0, 0, 0.24); @@ -2577,6 +2584,44 @@ button.map-qso-card:focus-visible { overflow: auto; transition: opacity 140ms ease, transform 140ms ease, visibility 140ms ease; } +.map-overlay-panel .map-locator-filter-group { + flex: 0 1 auto; + /* The label stays beside its control; only the bar itself wraps. */ + flex-wrap: nowrap; + align-items: center; + gap: 0.35rem; + min-width: 0; + padding-right: 0.5rem; + border-right: 1px solid color-mix(in srgb, var(--border-light) 55%, transparent); +} +.map-overlay-panel .map-locator-filter-group:last-child { + padding-right: 0; + border-right: 0; +} +/* The search field takes whatever room the fixed-width groups leave. */ +.map-overlay-panel .map-filter-grow { + flex: 1 1 9rem; +} +.map-overlay-panel .map-locator-filter-label { + min-width: 0; + padding-top: 0; + font-size: 0.72rem; + letter-spacing: 0.02em; + white-space: nowrap; +} +.map-overlay-panel .map-history-select { + flex: 0 0 auto; + width: auto; + max-width: 8rem; +} +.map-overlay-panel .map-search-input { + flex: 1 1 6rem; + min-width: 4.5rem; +} +/* The buttons carry this as a tooltip; a sentence of it would swallow the bar. */ +.map-overlay-panel .map-paths-hint { + display: none; +} .map-overlay-panel.is-hidden { opacity: 0; visibility: hidden; @@ -4119,12 +4164,26 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay { } .map-overlay-panel { top: 0.55rem; - left: 0.55rem; - width: calc(100% - 1.1rem); + left: 3.25rem; + right: 0.55rem; + flex-direction: column; + align-items: stretch; + width: auto; max-height: min(16.5rem, calc(100% - 1.1rem)); padding: 0.6rem 0.65rem; border-radius: 0.7rem; } + .map-overlay-panel .map-locator-filter-group { + flex-wrap: wrap; + align-items: flex-start; + padding-right: 0; + border-right: 0; + } + .map-overlay-panel .map-paths-hint { + display: block; + font-size: 0.75rem; + color: var(--text-muted); + } .map-corner-controls { top: 0.55rem; right: 0.55rem; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/spectrum-layout.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/spectrum-layout.mjs index b9ac0689..ed4251d0 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/spectrum-layout.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/spectrum-layout.mjs @@ -222,3 +222,62 @@ try { await retry.browser.close(); await retryFixture.close(); } + +// The map's filter panel is a bar across the top of the map, not a window +// sitting on it: it has to stay one or two rows tall, span most of the width, +// and keep clear of the things that share the map's corners — Leaflet's zoom +// buttons, the Fullscreen/Hide Filters controls, and the band legend. A panel +// that grew a column would cover the map it filters. +const mapFixture = await startWebFixture({ spectrum: true }); +const mapView = await startBrowser(chromium); +try { + for (const width of [1600, 1200]) { + await mapView.page.setViewportSize({ width, height: 950 }); + await mapView.page.goto(`${mapFixture.origin}/map`, { waitUntil: "domcontentloaded" }); + await mapView.page.locator("#aprs-map .leaflet-pane").first().waitFor({ state: "attached" }); + await mapView.page.waitForTimeout(1200); + + const bar = await mapView.page.evaluate(() => { + const box = (element) => (element ? element.getBoundingClientRect() : null); + const panel = document.querySelector(".map-overlay-panel"); + const stage = box(document.getElementById("map-stage")); + const zoom = box(document.querySelector("#aprs-map .leaflet-control-zoom")); + const corner = box(document.querySelector(".map-corner-controls")); + const legend = box(document.getElementById("map-band-legend")); + const panelBox = box(panel); + const hits = (a, b) => !!a && !!b + && a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom; + return { + widthPct: Math.round((panelBox.width / stage.width) * 100), + height: Math.round(panelBox.height), + outsideStage: panelBox.right > stage.right + 1 || panelBox.bottom > stage.bottom + 1, + hitsZoom: hits(panelBox, zoom), + hitsCorner: hits(panelBox, corner), + hitsLegend: hits(panelBox, legend), + clipped: panel.scrollWidth > panel.clientWidth + 1 || panel.scrollHeight > panel.clientHeight + 1, + }; + }); + assert.ok(bar.widthPct >= 70, `the filter bar covers ${bar.widthPct}% of the map at ${width}px`); + assert.ok(bar.height <= 140, `the filter bar is ${bar.height}px tall at ${width}px, not a bar`); + assert.equal(bar.outsideStage, false, `the filter bar runs off the map at ${width}px`); + assert.equal(bar.hitsZoom, false, `the filter bar covers the zoom buttons at ${width}px`); + assert.equal(bar.hitsCorner, false, `the filter bar covers the map controls at ${width}px`); + assert.equal(bar.hitsLegend, false, `the filter bar covers the band legend at ${width}px`); + assert.equal(bar.clipped, false, `the filter bar is clipping its own controls at ${width}px`); + } + + // Hiding it still works, and gives the whole map back. + await mapView.page.locator("#map-overlay-toggle-btn").click(); + await mapView.page.waitForTimeout(400); + const toggled = await mapView.page.evaluate(() => ({ + hidden: document.querySelector(".map-overlay-panel").classList.contains("is-hidden"), + label: document.getElementById("map-overlay-toggle-btn").textContent.trim(), + })); + assert.equal(toggled.hidden, true, "the filter bar stayed up after Hide Filters"); + assert.equal(toggled.label, "Show Filters", `the toggle still reads "${toggled.label}"`); + + assert.deepEqual(mapView.runtimeErrors, []); +} finally { + await mapView.browser.close(); + await mapFixture.close(); +}