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;