-
-
- 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();
+}