[fix](trx-frontend-http): tidy up the map's filter bar
CI / lint (pull_request) Successful in 2m21s
CI / test (pull_request) Successful in 8m7s
CI / frontend (push) Successful in 2m57s
CI / reuse (push) Successful in 3s
CI / frontend (pull_request) Successful in 3m47s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m16s
CI / test (push) Successful in 7m19s

The bar explained itself in prose: "All bands visible by default" sat
between the chips and the next group, taking width the bar could not
spare and reading as a stray line of text. An "All" chip says the same
thing in a chip's width and gives the selection somewhere to be undone.

Band chips also came up dimmed at the very moment every band was on the
map -- an empty selection is no filter at all, so nothing is dimmed
until something is picked. The path toggles drop their "On"/"Off"
suffix, which cost most of a row and only repeated what their own
highlight already said; state moves to aria-pressed and the tooltip.

The rest is alignment. The rule dividing the buttons from the filters
is drawn on the button block's edge, and a centred block left it
floating as a stub beside a two-row bar; stacked, it lay down the left
of a block that sits underneath. The labels sat at their natural
widths, so each row's first control started somewhere different, and
the two pairs of phase buttons differed in width, so the groups after
them missed each other by four pixels. One gutter for every label, one
width for both pairs, and the search field moved last where it can take
the room the fixed-width groups leave.

The map layout test now covers the chips, the divider's height and the
rows' shared start.

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit was merged in pull request #41.
This commit is contained in:
sjg
2026-08-05 21:43:57 +02:00
parent 90ab7781ad
commit 09634eb851
5 changed files with 209 additions and 53 deletions
@@ -62,6 +62,7 @@ var mapWindow = window;
const mapMarkers = /* @__PURE__ */ new Set();
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, sat: false };
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
const MAP_FILTER_ALL_KEY = "__all";
const mapLocatorFilter = { phase: "band", bands: /* @__PURE__ */ new Set() };
let mapSearchFilter = "";
let mapRigFilter = "";
@@ -838,38 +839,36 @@ var mapWindow = window;
container.innerHTML = `<span class="map-locator-empty">No ${kind === "band" ? "bands" : "sources"} available</span>`;
return;
}
let helperText = "";
const noun = kind === "band" ? "bands" : "sources";
const sourceKeys = kind === "source" ? Object.keys(DEFAULT_MAP_SOURCE_FILTER) : [];
const noneSelected = kind === "source" && sourceKeys.every((k) => !mapFilter[k]);
if (kind === "source") {
if (noneSelected) {
helperText = "All sources visible — click to filter";
}
} else if (!(selectedSet instanceof Set) || selectedSet.size === 0) {
helperText = `All ${kind === "band" ? "bands" : "sources"} visible by default`;
}
const showingAll = kind === "source" ? sourceKeys.every((k) => !mapFilter[k]) : !(selectedSet instanceof Set) || selectedSet.size === 0;
const allChip = document.createElement("button");
allChip.type = "button";
allChip.className = "map-locator-chip map-locator-chip-all";
if (showingAll) allChip.classList.add("is-active");
allChip.dataset.filterKind = kind;
allChip.dataset.filterKey = MAP_FILTER_ALL_KEY;
allChip.setAttribute("aria-pressed", showingAll ? "true" : "false");
allChip.title = showingAll ? `All ${noun} shown` : `Show all ${noun}`;
allChip.innerHTML = `<span class="map-locator-chip-text">All</span>`;
container.appendChild(allChip);
for (const item of items) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "map-locator-chip";
const isActive = kind === "source" ? !!mapFilter[item.key] : !!selectedSet?.has(item.key);
if (kind === "source" && noneSelected) {
if (showingAll) {
btn.classList.add("is-default");
} else if (!isActive) {
btn.classList.add("is-inactive");
}
btn.setAttribute("aria-pressed", !showingAll && isActive ? "true" : "false");
btn.dataset.filterKind = kind;
btn.dataset.filterKey = item.key;
btn.style.setProperty("--chip-color", item.color);
btn.innerHTML = `<span class="map-locator-chip-text">${escapeMapHtml(item.label)}</span>`;
container.appendChild(btn);
}
if (helperText) {
const hint = document.createElement("span");
hint.className = "map-locator-empty";
hint.textContent = helperText;
container.appendChild(hint);
}
}
function renderMapLocatorPhaseRow(container, phase) {
if (!container) return;
@@ -977,11 +976,10 @@ var mapWindow = window;
renderMapLocatorLegend(mapLocatorFilter.phase, sourceItems, bandItems);
if (!phaseEl || !choiceEl || !choiceLabelEl) return;
renderMapLocatorPhaseRow(phaseEl, mapLocatorFilter.phase);
choiceLabelEl.textContent = "Show";
if (mapLocatorFilter.phase === "band") {
choiceLabelEl.textContent = "Visible Bands";
renderMapLocatorChipRow(choiceEl, bandItems, mapLocatorFilter.bands, "band");
} else {
choiceLabelEl.textContent = "Visible Sources";
renderMapLocatorChipRow(choiceEl, sourceItems, null, "source");
}
syncLocatorMarkerStyles();
@@ -1533,7 +1531,13 @@ var mapWindow = window;
const kind = String(chip.dataset.filterKind || "");
const key = String(chip.dataset.filterKey || "");
if (!key) return;
if (kind === "source" && Object.prototype.hasOwnProperty.call(mapFilter, key)) {
if (key === MAP_FILTER_ALL_KEY) {
if (kind === "source") {
for (const srcKey of Object.keys(DEFAULT_MAP_SOURCE_FILTER)) mapFilter[srcKey] = false;
} else {
mapLocatorFilter.bands.clear();
}
} else if (kind === "source" && Object.prototype.hasOwnProperty.call(mapFilter, key)) {
const sourceKey = key;
mapFilter[sourceKey] = !mapFilter[sourceKey];
const srcKeys = Object.keys(DEFAULT_MAP_SOURCE_FILTER);
@@ -2148,14 +2152,16 @@ var mapWindow = window;
function updateMapContactPathsToggle() {
const btn = mapEl("map-contact-paths-toggle");
if (!btn) return;
btn.textContent = mapDecodeContactPathsEnabled ? "Contact Paths On" : "Contact Paths Off";
btn.classList.toggle("is-active", mapDecodeContactPathsEnabled);
btn.setAttribute("aria-pressed", mapDecodeContactPathsEnabled ? "true" : "false");
btn.title = mapDecodeContactPathsEnabled ? "Directed decode paths are drawn when the target locator is known" : "Directed decode paths are hidden";
}
function updateMapP2pPathsToggle() {
const btn = mapEl("map-p2p-paths-toggle");
if (!btn) return;
btn.textContent = mapP2pRadioPathsEnabled ? "TRX Paths On" : "TRX Paths Off";
btn.classList.toggle("is-active", mapP2pRadioPathsEnabled);
btn.setAttribute("aria-pressed", mapP2pRadioPathsEnabled ? "true" : "false");
btn.title = mapP2pRadioPathsEnabled ? "TRX paths are drawn from a station popup" : "TRX paths are hidden";
}
function scheduleDecodeMapMaintenance() {
if (C.decodeHistoryMapRenderingDeferred()) {
@@ -1003,7 +1003,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<div class="map-overlay-panel">
<div class="map-overlay-filters">
<div class="map-locator-filter-group">
<span class="map-locator-filter-label">Filter by</span>
<span class="map-locator-filter-label">Filter</span>
<div id="map-locator-phase" class="map-locator-phase-row"></div>
</div>
<div class="map-locator-filter-group">
@@ -1016,10 +1016,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<option value="">All</option>
</select>
</div>
<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>
<div class="map-locator-filter-group">
<span class="map-locator-filter-label">History</span>
<select id="map-history-limit" class="map-history-select" aria-label="Map history limit">
@@ -1035,11 +1031,17 @@ 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" 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>
<button type="button" id="map-p2p-paths-toggle" class="map-locator-phase-btn" aria-pressed="true" title="TRX paths are drawn from a station popup">TRX</button>
<button type="button" id="map-contact-paths-toggle" class="map-locator-phase-btn" aria-pressed="true" title="Directed decode paths are drawn when the target locator is known">Contact</button>
<span class="map-paths-hint">TRX paths on popup, directed decode paths when target locator is known</span>
</div>
</div>
<!-- Last, so the search field takes whatever the fixed-width
groups leave on the bar's final row rather than a sliver. -->
<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>
</div>
<div class="map-overlay-actions">
<button type="button" id="map-fullscreen-btn" class="map-fullscreen-btn">Fullscreen</button>
@@ -2570,7 +2570,10 @@ button.map-qso-card:focus-visible {
z-index: 410;
display: flex;
flex-flow: row nowrap;
align-items: center;
/* Stretched, not centred: the rule dividing the filters from the buttons is
drawn on the button block's edge, and it has to run the height of the bar
rather than float beside it as a stub. */
align-items: stretch;
gap: 0.5rem;
width: auto;
max-height: calc(100% - 1.4rem);
@@ -2590,6 +2593,9 @@ button.map-qso-card:focus-visible {
flex: 1 1 auto;
flex-flow: row wrap;
align-items: center;
/* Rows keep their own height when the bar is taller than they are, so the
groups stay a bar's two rows rather than drifting apart to fill it. */
align-content: center;
gap: 0.35rem 0.5rem;
min-width: 0;
}
@@ -2603,6 +2609,9 @@ button.map-qso-card:focus-visible {
.map-overlay-actions {
display: flex;
flex: 0 0 auto;
/* The block spans the bar so its divider can; the buttons still sit level
with the middle of it. */
align-self: stretch;
align-items: center;
gap: 0.4rem;
}
@@ -2625,17 +2634,29 @@ button.map-qso-card:focus-visible {
padding-right: 0;
border-right: 0;
}
/* The search field takes whatever room the fixed-width groups leave. */
/* The search field takes whatever room the fixed-width groups leave, up to a
width past which a text box spanning the map reads as a mistake. */
.map-overlay-panel .map-filter-grow {
flex: 1 1 9rem;
max-width: 26rem;
}
/* One gutter for every label, so whichever group starts a row starts it in the
same place: at their natural widths the labels staggered each row's first
control by however much the label above it was wider or narrower. */
.map-overlay-panel .map-locator-filter-label {
min-width: 0;
min-width: 3.5rem;
padding-top: 0;
font-size: 0.72rem;
letter-spacing: 0.02em;
white-space: nowrap;
}
/* The two rows of the bar are led by the two pairs of phase buttons, and
SOURCE|BAND is wider than TRX|CONTACT: left to their own widths the groups
after them missed each other by four pixels. One width for both pairs. */
.map-overlay-panel .map-locator-phase-row {
flex: 0 0 auto;
min-width: 9rem;
}
.map-overlay-panel .map-history-select {
flex: 0 0 auto;
width: auto;
@@ -3471,6 +3492,23 @@ body.map-fake-fullscreen-active {
border-color: color-mix(in srgb, var(--border-light) 68%, transparent);
background: color-mix(in srgb, var(--input-bg) 96%, transparent);
}
/* "All" clears the selection rather than naming a band or a source, so it
borrows the phase buttons' look instead of a colour of its own — and says
in one chip's width what a line of helper text used to say in the bar. */
.map-locator-chip-all {
--chip-color: var(--border-light);
color: var(--text-muted);
}
.map-locator-chip-all.is-active {
border-color: var(--accent-green);
background: color-mix(in srgb, var(--accent-green) 10%, var(--input-bg));
color: var(--accent-green);
}
.map-locator-chip-all .map-locator-chip-text {
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.map-locator-chip-text {
font-size: 0.78rem;
font-weight: 600;
@@ -4196,9 +4234,23 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
padding-right: 0;
border-right: 0;
}
/* Stacked in a column the panel scrolls, and the search field is no use at
the bottom of it: it goes back to the top, where it needs no scrolling. */
.map-overlay-panel .map-filter-grow {
order: -1;
max-width: none;
}
.map-overlay-actions {
justify-content: flex-end;
}
/* Stacked, the buttons sit under the filters rather than beside them, so the
rule that divides them has to lie across the panel, not down its left. */
.map-overlay-panel:not(.filters-hidden) .map-overlay-actions {
padding-left: 0;
padding-top: 0.5rem;
border-left: 0;
border-top: 1px solid color-mix(in srgb, var(--border-light) 55%, transparent);
}
.map-overlay-panel .map-paths-hint {
display: block;
font-size: 0.75rem;