[feat](trx-frontend-http): lay the map filters out as a bar across the top
The filters were a 30rem column parked in the bottom-right corner, covering a third of the map they filter. Lay them out horizontally instead: one row per group -- label beside its control, thin rules between -- across the top of the map, spanning ~87% of its width at 1600px and wrapping to a second row as it narrows. It starts clear of Leaflet's zoom buttons and stops short of the corner controls, which stay outside it: the button that hides the filters cannot live inside the thing it hides. The bottom-left band legend keeps its place. The sentence explaining the two path toggles would have swallowed the bar, so it moves to their tooltips and is shown inline only in the stacked narrow-screen layout. Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -1015,7 +1015,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<option value="">All</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="map-locator-filter-group">
|
||||
<div class="map-locator-filter-group map-filter-grow">
|
||||
<span class="map-locator-filter-label">Search</span>
|
||||
<input type="text" id="map-search-filter" class="map-search-input" placeholder="Callsign, MMSI, locator, message..." />
|
||||
</div>
|
||||
@@ -1034,9 +1034,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<div class="map-locator-filter-group">
|
||||
<span class="map-locator-filter-label">Paths</span>
|
||||
<div class="map-locator-phase-row">
|
||||
<button type="button" id="map-p2p-paths-toggle" class="map-locator-phase-btn">TRX Paths On</button>
|
||||
<button type="button" id="map-contact-paths-toggle" class="map-locator-phase-btn">Contact Paths On</button>
|
||||
<span class="map-locator-empty">TRX paths on popup, directed decode paths when target locator is known</span>
|
||||
<button type="button" id="map-p2p-paths-toggle" class="map-locator-phase-btn" title="TRX paths are drawn from a station popup">TRX Paths On</button>
|
||||
<button type="button" id="map-contact-paths-toggle" class="map-locator-phase-btn" title="Directed decode paths are drawn when the target locator is known">Contact Paths On</button>
|
||||
<span class="map-paths-hint">TRX paths on popup, directed decode paths when target locator is known</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user