[feat](trx-frontend-http): mobile UI improvements
- Fix 5-tab bottom nav (grid was repeat(4) with 5 tabs; About overflowed) - Add SVG icons to each tab; show icon+label on mobile bottom nav - Swipe left/right to switch tabs (excludes jog wheel, spectrum canvas, map, scrollable containers and form inputs to avoid conflicts) - Extract navigateToTab() helper used by both click and swipe handlers - Collapse header subtitles at ≤640px to reclaim vertical space - Bookmark table → 2-column card layout at ≤640px with ::before labels - Audio volume labels switch to horizontal row layout at ≤520px; squelch slider now also spans full width - Controls tray uses overflow-x: auto (not visible) at ≤760px so content wider than viewport scrolls rather than overflowing layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -3340,21 +3340,71 @@ if (spectrumBwSweetBtn) {
|
||||
}
|
||||
|
||||
// --- Tab navigation ---
|
||||
document.querySelector(".tab-bar").addEventListener("click", (e) => {
|
||||
const btn = e.target.closest(".tab[data-tab]");
|
||||
const TAB_ORDER = ["main", "bookmarks", "decoders", "map", "about"];
|
||||
|
||||
function navigateToTab(name) {
|
||||
if (authEnabled && !authRole && name !== "main") return;
|
||||
const btn = document.querySelector(`.tab-bar .tab[data-tab="${name}"]`);
|
||||
if (!btn) return;
|
||||
if (authEnabled && !authRole && btn.dataset.tab !== "main") return;
|
||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
|
||||
document.getElementById(`tab-${btn.dataset.tab}`).style.display = "";
|
||||
document.getElementById(`tab-${name}`).style.display = "";
|
||||
scheduleSpectrumLayout();
|
||||
if (btn.dataset.tab === "map") {
|
||||
if (name === "map") {
|
||||
initAprsMap();
|
||||
sizeAprsMapToViewport();
|
||||
if (aprsMap) setTimeout(() => aprsMap.invalidateSize(), 50);
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector(".tab-bar").addEventListener("click", (e) => {
|
||||
const btn = e.target.closest(".tab[data-tab]");
|
||||
if (!btn) return;
|
||||
navigateToTab(btn.dataset.tab);
|
||||
});
|
||||
|
||||
// Swipe left/right on the main content area to switch tabs (mobile).
|
||||
(function () {
|
||||
let tx = 0, ty = 0;
|
||||
const THRESHOLD = 60; // px horizontal movement required
|
||||
const ANGLE_LIMIT = 1.6; // |dx/dy| ratio — suppress on near-vertical drags
|
||||
|
||||
// Elements where horizontal drag has its own meaning; exclude from swipe.
|
||||
const NO_SWIPE_SELECTORS = [
|
||||
"#jog-wheel", "#spectrum-canvas", "#overview-canvas",
|
||||
"#aprs-map", ".controls-tray-scroll", ".sub-tab-bar",
|
||||
"input[type=range]", "select", "input[type=text]",
|
||||
"input[type=number]", "input[type=search]",
|
||||
];
|
||||
|
||||
function isExcluded(el) {
|
||||
return NO_SWIPE_SELECTORS.some((sel) => el.closest(sel));
|
||||
}
|
||||
|
||||
document.addEventListener("touchstart", (e) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
if (isExcluded(e.target)) return;
|
||||
tx = e.touches[0].clientX;
|
||||
ty = e.touches[0].clientY;
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener("touchend", (e) => {
|
||||
if (e.changedTouches.length !== 1 || tx === 0) return;
|
||||
const dx = e.changedTouches[0].clientX - tx;
|
||||
const dy = e.changedTouches[0].clientY - ty;
|
||||
tx = 0;
|
||||
if (Math.abs(dx) < THRESHOLD) return;
|
||||
if (Math.abs(dy) > 0 && Math.abs(dx) / Math.abs(dy) < ANGLE_LIMIT) return;
|
||||
const activeBtn = document.querySelector(".tab-bar .tab.active");
|
||||
if (!activeBtn) return;
|
||||
const cur = TAB_ORDER.indexOf(activeBtn.dataset.tab);
|
||||
if (cur === -1) return;
|
||||
const next = dx < 0 ? cur + 1 : cur - 1;
|
||||
if (next >= 0 && next < TAB_ORDER.length) navigateToTab(TAB_ORDER[next]);
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
window.addEventListener("resize", () => { scheduleSpectrumLayout(); });
|
||||
|
||||
// --- Auth startup sequence ---
|
||||
|
||||
@@ -30,11 +30,26 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-bar-nav">
|
||||
<button class="tab active" data-tab="main">Main</button>
|
||||
<button class="tab" data-tab="bookmarks">Bookmarks</button>
|
||||
<button class="tab" data-tab="decoders">Decoders</button>
|
||||
<button class="tab" data-tab="map">Map</button>
|
||||
<button class="tab" data-tab="about">About</button>
|
||||
<button class="tab active" data-tab="main">
|
||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M8 14v-4"/><path d="M5 11a4 4 0 0 1 6 0"/><path d="M2.5 8.5a8 8 0 0 1 11 0"/></svg>
|
||||
<span class="tab-label">Main</span>
|
||||
</button>
|
||||
<button class="tab" data-tab="bookmarks">
|
||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 2h8v12l-4-2.5L4 14V2z"/></svg>
|
||||
<span class="tab-label">Bookmarks</span>
|
||||
</button>
|
||||
<button class="tab" data-tab="decoders">
|
||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><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"/></svg>
|
||||
<span class="tab-label">Decoders</span>
|
||||
</button>
|
||||
<button class="tab" data-tab="map">
|
||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><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"/></svg>
|
||||
<span class="tab-label">Map</span>
|
||||
</button>
|
||||
<button class="tab" data-tab="about">
|
||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M8 7v5"/><circle cx="8" cy="5" r="0.5" fill="currentColor" stroke="none"/></svg>
|
||||
<span class="tab-label">About</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-bar-actions">
|
||||
|
||||
@@ -1021,6 +1021,14 @@ small { color: var(--text-muted); }
|
||||
}
|
||||
.tab.active { border-bottom-color: var(--accent-green); color: var(--accent-green); font-weight: 600; }
|
||||
.tab:hover:not(.active) { color: var(--text); }
|
||||
/* Tab icons — hidden on desktop, shown on mobile bottom nav */
|
||||
.tab-icon {
|
||||
display: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab-label { display: block; }
|
||||
.about-table { width: 100%; border-collapse: collapse; }
|
||||
.about-table td { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); }
|
||||
.about-table tr:last-child td { border-bottom: none; }
|
||||
@@ -1825,8 +1833,8 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
button { min-height: 2.8rem; font-size: 0.95rem; }
|
||||
input.status-input, select.status-input { font-size: 1.1rem; }
|
||||
:root { --header-waterfall-overlap: 0rem; }
|
||||
.controls-tray-scroll { overflow-x: visible; }
|
||||
.controls-tray { width: 100%; padding-left: 0.85rem; padding-right: 0.85rem; }
|
||||
.controls-tray-scroll { overflow-x: auto; }
|
||||
.controls-tray { width: 100%; min-width: 0; padding-left: 0.85rem; padding-right: 0.85rem; }
|
||||
.freq-inline { gap: 0.5rem; flex-wrap: wrap; }
|
||||
.header-text { width: auto; min-width: 0; flex: 0 1 auto; }
|
||||
.header-main {
|
||||
@@ -1901,8 +1909,11 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
max-width: 100%;
|
||||
}
|
||||
.tab-bar .header-logo {
|
||||
height: 2.15rem;
|
||||
height: 1.9rem;
|
||||
}
|
||||
/* Collapse verbose subtitles — the About tab has this info */
|
||||
.tab-bar .subtitle { display: none; }
|
||||
.tab-bar .title { font-size: 0.92rem; }
|
||||
.tab-bar-nav {
|
||||
position: fixed;
|
||||
left: calc(0.6rem + env(safe-area-inset-left));
|
||||
@@ -1910,9 +1921,9 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
bottom: calc(0.55rem + env(safe-area-inset-bottom));
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.35rem;
|
||||
padding: 0.42rem;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 0.25rem;
|
||||
padding: 0.38rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border-light) 82%, transparent);
|
||||
border-radius: 1rem;
|
||||
background: color-mix(in srgb, var(--card-bg) 90%, transparent);
|
||||
@@ -1924,18 +1935,22 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
overflow: visible;
|
||||
}
|
||||
.tab {
|
||||
min-height: 3.1rem;
|
||||
padding: 0.45rem 0.25rem;
|
||||
min-height: 3.2rem;
|
||||
padding: 0.35rem 0.1rem 0.3rem;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
border-radius: 0.75rem;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.05;
|
||||
line-height: 1.1;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.tab.active {
|
||||
border-color: color-mix(in srgb, var(--accent-green) 50%, var(--border-light));
|
||||
@@ -1943,6 +1958,10 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
color: var(--text);
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
|
||||
}
|
||||
.tab-icon { display: block; }
|
||||
/* Shorten long tab labels to keep bottom nav compact */
|
||||
.tab[data-tab="bookmarks"] .tab-label { font-size: 0.6rem; }
|
||||
.tab[data-tab="decoders"] .tab-label { font-size: 0.6rem; }
|
||||
.top-bar-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
@@ -2166,6 +2185,52 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
align-items: flex-start;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
/* ── Bookmark card layout ──────────────────────────────────────────── */
|
||||
#bm-table-wrap { overflow-x: hidden; }
|
||||
.bm-table,
|
||||
.bm-table tbody { display: block; }
|
||||
.bm-table thead { display: none; }
|
||||
.bm-table tr {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.22rem 0.5rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
|
||||
border-radius: 0.7rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
margin-bottom: 0.45rem;
|
||||
background: color-mix(in srgb, var(--btn-bg) 35%, transparent);
|
||||
}
|
||||
.bm-table tr:hover td { background: transparent; }
|
||||
.bm-table td {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.05rem;
|
||||
border-bottom: none;
|
||||
padding: 0.08rem 0;
|
||||
font-size: 0.82rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
.bm-table td::before {
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.bm-col-name { grid-column: 1 / -1; font-weight: 600; font-size: 0.9rem; }
|
||||
.bm-col-name::before { content: "Bookmark"; }
|
||||
.bm-col-freq::before { content: "Frequency"; }
|
||||
.bm-col-mode::before { content: "Mode"; }
|
||||
.bm-col-bw::before { content: "Bandwidth"; }
|
||||
.bm-col-loc::before { content: "Locator"; }
|
||||
.bm-col-cat::before { content: "Category"; }
|
||||
.bm-col-dec::before { content: "Decoders"; }
|
||||
.bm-col-cmt { grid-column: 1 / -1; }
|
||||
.bm-col-cmt::before { content: "Comment"; }
|
||||
.bm-col-act { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 0.4rem; padding-top: 0.25rem; }
|
||||
.bm-col-act::before { display: none; }
|
||||
.bm-col-act button { flex: 1 1 auto; min-height: 2.4rem; font-size: 0.8rem; }
|
||||
}
|
||||
|
||||
|
||||
@@ -2543,28 +2608,34 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
#freq { font-size: clamp(1.3rem, 6vw, 2rem); }
|
||||
|
||||
/* Wider volume sliders for touch */
|
||||
.vol-slider { width: 100%; }
|
||||
.vol-slider { width: 100%; flex: 1 1 auto; }
|
||||
.vol-label {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.vol-slider::-webkit-slider-thumb { width: 20px; height: 20px; }
|
||||
.vol-slider::-moz-range-thumb { width: 20px; height: 20px; }
|
||||
.vol-pct { min-width: 2.4rem; text-align: right; }
|
||||
.vol-slider::-webkit-slider-thumb { width: 22px; height: 22px; }
|
||||
.vol-slider::-moz-range-thumb { width: 22px; height: 22px; }
|
||||
|
||||
#audio-row .inline {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.6rem;
|
||||
gap: 0.55rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
#rx-audio-btn,
|
||||
#tx-audio-btn,
|
||||
#audio-level,
|
||||
#audio-status {
|
||||
#audio-status,
|
||||
#sdr-squelch-wrap {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
#audio-level {
|
||||
min-width: 0;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Spectrum control inputs and buttons: meet minimum tap-target size */
|
||||
|
||||
Reference in New Issue
Block a user