From 1fe7dc88c6622d1539ed3af15e5f73cb8c7b9d82 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Thu, 12 Mar 2026 19:58:55 +0100 Subject: [PATCH] [feat](trx-frontend-http): add frequency layers display for virtual channels Render virtual channels as absolutely-positioned layer strips inside a shared relative container (#vchan-freq-layers). Layers are sorted by frequency ascending so higher-frequency channels receive a higher z-index and sit on top by default. Hovering any layer temporarily assigns it the maximum z-index to bring it to the front; leaving restores the original stacking order. Each layer is offset by 11 px vertically so all channels remain visible as a staggered card stack. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/index.html | 1 + .../assets/web/plugins/vchan.js | 45 +++++++++++++++++++ .../trx-frontend-http/assets/web/style.css | 38 ++++++++++++++++ 3 files changed, 84 insertions(+) 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 131338e..bc716b1 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 @@ -262,6 +262,7 @@
Signal
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js index 3e64708..c3ea228 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js @@ -49,6 +49,50 @@ function vchanHandleChannels(data) { } } +function vchanRenderLayers() { + const container = document.getElementById("vchan-freq-layers"); + if (!container) return; + container.innerHTML = ""; + + if (vchanChannels.length === 0) { + container.style.height = "0"; + return; + } + + // Sort by frequency ascending so higher-frequency channels get higher z-index. + const sorted = [...vchanChannels].sort((a, b) => a.freq_hz - b.freq_hz); + + const LAYER_H_PX = 32; + const STEP_PX = 11; // vertical offset between layers so each peeks below the next + const totalH = LAYER_H_PX + (sorted.length - 1) * STEP_PX; + container.style.height = totalH + "px"; + + sorted.forEach((ch, i) => { + const layer = document.createElement("div"); + layer.className = "vchan-freq-layer"; + if (ch.id === vchanActiveId) layer.classList.add("active"); + + layer.style.top = (i * STEP_PX) + "px"; + // Higher frequency → higher index → higher z-index (sits on top by default). + const defaultZ = i + 1; + layer.style.zIndex = defaultZ; + + layer.textContent = `${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode}`; + layer.title = `Ch ${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode} · ${ch.subscribers} subscriber${ch.subscribers !== 1 ? "s" : ""}`; + + // Bring hovered layer to the front; restore on leave. + const maxZ = sorted.length + 10; + layer.addEventListener("mouseenter", () => { layer.style.zIndex = maxZ; }); + layer.addEventListener("mouseleave", () => { layer.style.zIndex = defaultZ; }); + + layer.addEventListener("click", () => { + if (ch.id !== vchanActiveId) vchanSubscribe(ch.id); + }); + + container.appendChild(layer); + }); +} + function vchanRender() { const picker = document.getElementById("vchan-picker"); if (!picker) return; @@ -93,6 +137,7 @@ function vchanRender() { addBtn.addEventListener("click", vchanAllocate); picker.appendChild(addBtn); + vchanRenderLayers(); vchanSyncAccentUI(); } 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 81c8e7f..5696ca2 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 @@ -392,6 +392,44 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem; border-color: var(--vchan-color); box-shadow: inset 3px 0 0 var(--vchan-color); } +/* Frequency layers: absolutely-positioned channel strips stacked by frequency */ +.vchan-freq-layers { + position: relative; + margin-top: 0.5rem; + /* height is set dynamically by vchanRenderLayers() */ +} +.vchan-freq-layer { + position: absolute; + left: 0; + right: 0; + height: 2rem; + display: flex; + align-items: center; + padding: 0 0.6rem; + border-radius: 6px; + background: var(--input-bg); + border: 1px solid var(--border-light); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 0.82rem; + color: var(--text-muted); + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: border-color 0.12s, box-shadow 0.12s; +} +.vchan-freq-layer:hover { + border-color: var(--vchan-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); + color: var(--text); +} +.vchan-freq-layer.active { + background: var(--btn-bg); + color: var(--text); + font-weight: 600; + border-color: var(--vchan-color); + box-shadow: inset 3px 0 0 var(--vchan-color); +} /* Applied to #freq and #spectrum-bw-input when on a virtual channel */ .vchan-ch-active { border-color: var(--vchan-color) !important;