[feat](trx-frontend-http): give digital modes a decoder sidebar
Thirteen decoders in a horizontal strip needed a scroller on anything but a wide window, and the open one was marked by a single underline among thirteen. They are a list down the left now, all visible at once, each keeping the state dot it already carried, with the panel for the selected one filling the rest of the width. No script changed: the sub-tab wiring, the aria roles, the decoder picker and the state-dot observers all work on the same markup, so this is layout only. Below 760px the sidebar gives way to the picker that already existed there. That path needed align-content: the tab panel fills the page height and a grid stretches its rows to match, which handed the picker a 218px row and left a 189px gap under it. The tab icon was the signal-strength bars, which is what the S-meter shows two rows above it; a pulse train says digital modes instead. Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -22,7 +22,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
||||||
<symbol id="icon-home" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.8 7 8 2.9 13.2 7"/><path d="M4.3 5.9V13h7.4V5.9"/><path d="M6.8 13V9.3h2.4V13"/></symbol>
|
<symbol id="icon-home" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.8 7 8 2.9 13.2 7"/><path d="M4.3 5.9V13h7.4V5.9"/><path d="M6.8 13V9.3h2.4V13"/></symbol>
|
||||||
<symbol id="icon-bookmark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2h8v12l-4-2.5L4 14V2z"/></symbol>
|
<symbol id="icon-bookmark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2h8v12l-4-2.5L4 14V2z"/></symbol>
|
||||||
<symbol id="icon-signal" viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="11" width="2.5" height="4" rx="0.5"/><rect x="4.75" y="8" width="2.5" height="7" rx="0.5"/><rect x="8.5" y="5" width="2.5" height="10" rx="0.5"/><rect x="12.25" y="2" width="2.5" height="13" rx="0.5"/></symbol>
|
<symbol id="icon-digital" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 11.5h2.6V4.5h3.2v7h3.2v-7h3.2v7H15"/></symbol>
|
||||||
<symbol id="icon-map" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2a4 4 0 0 1 4 4c0 3-4 8-4 8S4 9 4 6a4 4 0 0 1 4-4z"/><circle cx="8" cy="6" r="1.2" fill="currentColor" stroke="none"/></symbol>
|
<symbol id="icon-map" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2a4 4 0 0 1 4 4c0 3-4 8-4 8S4 9 4 6a4 4 0 0 1 4-4z"/><circle cx="8" cy="6" r="1.2" fill="currentColor" stroke="none"/></symbol>
|
||||||
<symbol id="icon-stats" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2 14h12"/><rect x="3" y="8" width="2" height="6" rx="0.4" fill="currentColor" stroke="none" opacity="0.6"/><rect x="7" y="5" width="2" height="9" rx="0.4" fill="currentColor" stroke="none" opacity="0.75"/><rect x="11" y="2" width="2" height="12" rx="0.4" fill="currentColor" stroke="none" opacity="0.9"/></symbol>
|
<symbol id="icon-stats" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2 14h12"/><rect x="3" y="8" width="2" height="6" rx="0.4" fill="currentColor" stroke="none" opacity="0.6"/><rect x="7" y="5" width="2" height="9" rx="0.4" fill="currentColor" stroke="none" opacity="0.75"/><rect x="11" y="2" width="2" height="12" rx="0.4" fill="currentColor" stroke="none" opacity="0.9"/></symbol>
|
||||||
<symbol id="icon-record" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><circle cx="8" cy="8" r="2.5" fill="currentColor" stroke="none"/></symbol>
|
<symbol id="icon-record" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><circle cx="8" cy="8" r="2.5" fill="currentColor" stroke="none"/></symbol>
|
||||||
@@ -53,7 +53,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<span class="tab-label">Bookmarks</span>
|
<span class="tab-label">Bookmarks</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="digital-modes">
|
<button class="tab" data-tab="digital-modes">
|
||||||
<svg class="tab-icon" aria-hidden="true"><use href="#icon-signal"/></svg>
|
<svg class="tab-icon" aria-hidden="true"><use href="#icon-digital"/></svg>
|
||||||
<span class="tab-label">Digital modes</span>
|
<span class="tab-label">Digital modes</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="map">
|
<button class="tab" data-tab="map">
|
||||||
|
|||||||
@@ -2076,6 +2076,61 @@ small { color: var(--text-muted); }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sub-tab-bar { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 0.75rem; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
.sub-tab-bar { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 0.75rem; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||||
|
/* ── Digital modes: sidebar + panel ───────────────────────────────────
|
||||||
|
Thirteen decoders in a horizontal strip meant a scroller on anything but a
|
||||||
|
wide window, and which one was open was a single underline in a row of
|
||||||
|
thirteen. As a list down the side every decoder is visible at once, each
|
||||||
|
with the state dot it already carried, and the panel gets the full width. */
|
||||||
|
#tab-digital-modes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(8.5rem, 11rem) minmax(0, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
#tab-digital-modes > .sub-tab-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.12rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-right: 0.85rem;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* Stays put while a long decode list scrolls past it. */
|
||||||
|
position: sticky;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
#tab-digital-modes > .sub-tab-bar .sub-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.42rem 0.6rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
transition: color var(--dur-fast) var(--ease-standard),
|
||||||
|
background-color var(--dur-fast) var(--ease-standard),
|
||||||
|
border-color var(--dur-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
#tab-digital-modes > .sub-tab-bar .sub-tab .decoder-state-dot {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
#tab-digital-modes > .sub-tab-bar .sub-tab:hover:not(.active) {
|
||||||
|
color: var(--text);
|
||||||
|
background: color-mix(in srgb, var(--btn-bg) 55%, transparent);
|
||||||
|
}
|
||||||
|
#tab-digital-modes > .sub-tab-bar .sub-tab.active {
|
||||||
|
border-color: color-mix(in srgb, var(--accent-green) 50%, var(--border-light));
|
||||||
|
background: color-mix(in srgb, var(--accent-green) 12%, transparent);
|
||||||
|
color: var(--accent-text);
|
||||||
|
}
|
||||||
|
#tab-digital-modes > .sub-tab-panel {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
.sub-tab { flex-shrink: 0; background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 0; padding: 0.35rem 0.75rem; color: var(--text-muted); cursor: pointer; font-size: 0.85rem; height: auto; }
|
.sub-tab { flex-shrink: 0; background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 0; padding: 0.35rem 0.75rem; color: var(--text-muted); cursor: pointer; font-size: 0.85rem; height: auto; }
|
||||||
.sub-tab.active { border-bottom-color: var(--accent-green); color: var(--accent-green); font-weight: 600; }
|
.sub-tab.active { border-bottom-color: var(--accent-green); color: var(--accent-green); font-weight: 600; }
|
||||||
.sub-tab:hover:not(.active) { color: var(--text); }
|
.sub-tab:hover:not(.active) { color: var(--text); }
|
||||||
@@ -3722,7 +3777,15 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 5%, transparent);
|
box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 5%, transparent);
|
||||||
}
|
}
|
||||||
|
/* The picker replaces the sidebar here, so the panel takes the full width.
|
||||||
|
align-content keeps the two rows at their own height: the panel fills the
|
||||||
|
tab, and stretching would otherwise hand the picker a third of it. */
|
||||||
|
#tab-digital-modes {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
#tab-digital-modes > .sub-tab-bar { display: none; }
|
#tab-digital-modes > .sub-tab-bar { display: none; }
|
||||||
|
#tab-digital-modes > .sub-tab-panel { grid-column: 1; grid-row: auto; }
|
||||||
.toast-region { bottom: calc(5.7rem + env(safe-area-inset-bottom)); }
|
.toast-region { bottom: calc(5.7rem + env(safe-area-inset-bottom)); }
|
||||||
.top-bar-actions {
|
.top-bar-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -23,6 +23,33 @@ try {
|
|||||||
await page.locator("summary", { hasText: "Audio controls" }).click();
|
await page.locator("summary", { hasText: "Audio controls" }).click();
|
||||||
assert.equal(await page.locator("#rx-audio-btn").count(), 1);
|
assert.equal(await page.locator("#rx-audio-btn").count(), 1);
|
||||||
|
|
||||||
|
// Digital modes: the decoders are a list down the side, and the panel for the
|
||||||
|
// selected one sits beside it. A horizontal strip put thirteen decoders in a
|
||||||
|
// scroller and marked the open one with a single underline among them.
|
||||||
|
await page.locator('.tab[data-tab="digital-modes"]').click();
|
||||||
|
await page.locator("#tab-digital-modes").waitFor({ state: "visible" });
|
||||||
|
await page.locator('.sub-tab[data-subtab="ft8"]').click();
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
const digital = await page.evaluate(() => {
|
||||||
|
const bar = document.querySelector("#tab-digital-modes > .sub-tab-bar").getBoundingClientRect();
|
||||||
|
const panel = document.getElementById("subtab-ft8").getBoundingClientRect();
|
||||||
|
const shown = [...document.querySelectorAll("#tab-digital-modes > .sub-tab-panel")]
|
||||||
|
.filter((element) => getComputedStyle(element).display !== "none")
|
||||||
|
.map((element) => element.id);
|
||||||
|
return {
|
||||||
|
sidebarIsColumn: bar.height > bar.width,
|
||||||
|
panelBesideSidebar: Math.round(panel.left) >= Math.round(bar.right),
|
||||||
|
panelsShown: shown,
|
||||||
|
decoders: document.querySelectorAll("#tab-digital-modes > .sub-tab-bar .sub-tab").length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert.ok(digital.sidebarIsColumn, "the decoder list is not a sidebar");
|
||||||
|
assert.ok(digital.panelBesideSidebar, "the decoder panel does not sit beside the sidebar");
|
||||||
|
assert.deepEqual(digital.panelsShown, ["subtab-ft8"], `panels shown: ${JSON.stringify(digital.panelsShown)}`);
|
||||||
|
assert.ok(digital.decoders >= 10, `only ${digital.decoders} decoders in the sidebar`);
|
||||||
|
await page.locator('.tab[data-tab="main"]').click();
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
|
||||||
// Map links from decode rows, before anything has opened the Map tab. The
|
// Map links from decode rows, before anything has opened the Map tab. The
|
||||||
// lazy map module used to install these globals itself, so an AIS pin threw
|
// lazy map module used to install these globals itself, so an AIS pin threw
|
||||||
// "not a function" and an APRS link silently did nothing until the tab had
|
// "not a function" and an APRS link silently did nothing until the tab had
|
||||||
|
|||||||
Reference in New Issue
Block a user