diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
index e78f4fa..de26d45 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
@@ -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 ---
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
index c26a94a..dd2e98b 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
@@ -30,11 +30,26 @@
-
-
-
-
-
+
+
+
+
+
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 569354a..d6952d5 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
@@ -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 */